From e3c50b343dd7bb82e65d17a0602d33cbe3a6eeed Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 6 Feb 2026 15:56:30 +0100 Subject: [PATCH 01/11] refactor: remove helmchart installation and update logic - Deleted the install.go and update.go files along with their associated test files. - Removed the postrenderer.go file which handled post-rendering of Helm charts. - Introduced a new processor package to handle release decoding and hashing. - Added processor tests and benchmarks for performance evaluation. - Implemented a tracer for HTTP requests to log resource interactions. - Updated main.go to integrate the new dynamic tools and REST mapper. --- go.mod | 99 +- go.sum | 254 ++-- internal/composition/composition.go | 283 ++-- internal/composition/composition_test.go | 106 +- internal/composition/support.go | 39 +- internal/composition/support_test.go | 64 +- internal/helm/getter/getter.go | 170 --- internal/helm/getter/getter_test.go | 188 --- internal/helm/getter/http.go | 79 -- internal/helm/getter/oci.go | 141 -- internal/helm/getter/tgz.go | 28 - internal/helm/repo/helpers.go | 24 - internal/helm/repo/index.go | 189 --- internal/helm/repo/load.go | 39 - internal/helm/repo/types.go | 105 -- internal/helmclient/client.go | 1231 ----------------- internal/helmclient/client_getter.go | 179 --- internal/helmclient/client_test.go | 345 ----- internal/helmclient/doc.go | 3 - internal/helmclient/interface.go | 39 - internal/helmclient/mock/interface.go | 324 ----- internal/helmclient/mock/mock_test.go | 64 - internal/helmclient/spec.go | 27 - internal/helmclient/types.go | 231 ---- internal/helmclient/values/options.go | 124 -- internal/helmclient/values/options_test.go | 72 - .../tools/{helmchart => }/archive/getter.go | 21 +- .../{helmchart => }/archive/getter_test.go | 17 +- internal/tools/dynamic/dynamic.go | 49 + internal/tools/hasher/hasher.go | 33 +- internal/tools/hasher/hasher_test.go | 49 + internal/tools/helmchart/helmchart.go | 269 ---- internal/tools/helmchart/install.go | 136 -- internal/tools/helmchart/install_test.go | 154 --- internal/tools/helmchart/postrenderer.go | 49 - internal/tools/helmchart/update.go | 127 -- internal/tools/helmchart/update_test.go | 37 - internal/tools/processor/processor.go | 88 ++ .../tools/processor/processor_bench_test.go | 79 ++ internal/tools/processor/processor_test.go | 179 +++ internal/tools/processor/types.go | 61 + .../{helmclient => tools}/tracer/tracer.go | 0 .../tracer/tracer_test.go | 0 main.go | 11 +- 44 files changed, 988 insertions(+), 4818 deletions(-) delete mode 100644 internal/helm/getter/getter.go delete mode 100644 internal/helm/getter/getter_test.go delete mode 100644 internal/helm/getter/http.go delete mode 100644 internal/helm/getter/oci.go delete mode 100644 internal/helm/getter/tgz.go delete mode 100644 internal/helm/repo/helpers.go delete mode 100644 internal/helm/repo/index.go delete mode 100644 internal/helm/repo/load.go delete mode 100644 internal/helm/repo/types.go delete mode 100644 internal/helmclient/client.go delete mode 100644 internal/helmclient/client_getter.go delete mode 100644 internal/helmclient/client_test.go delete mode 100644 internal/helmclient/doc.go delete mode 100644 internal/helmclient/interface.go delete mode 100644 internal/helmclient/mock/interface.go delete mode 100644 internal/helmclient/mock/mock_test.go delete mode 100644 internal/helmclient/spec.go delete mode 100644 internal/helmclient/types.go delete mode 100644 internal/helmclient/values/options.go delete mode 100644 internal/helmclient/values/options_test.go rename internal/tools/{helmchart => }/archive/getter.go (96%) rename internal/tools/{helmchart => }/archive/getter_test.go (97%) create mode 100644 internal/tools/dynamic/dynamic.go delete mode 100644 internal/tools/helmchart/helmchart.go delete mode 100644 internal/tools/helmchart/install.go delete mode 100644 internal/tools/helmchart/install_test.go delete mode 100644 internal/tools/helmchart/postrenderer.go delete mode 100644 internal/tools/helmchart/update.go delete mode 100644 internal/tools/helmchart/update_test.go create mode 100644 internal/tools/processor/processor.go create mode 100644 internal/tools/processor/processor_bench_test.go create mode 100644 internal/tools/processor/processor_test.go create mode 100644 internal/tools/processor/types.go rename internal/{helmclient => tools}/tracer/tracer.go (100%) rename internal/{helmclient => tools}/tracer/tracer_test.go (100%) diff --git a/go.mod b/go.mod index c1ba12a..75d2bba 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,28 @@ module github.com/krateoplatformops/composition-dynamic-controller -go 1.25.0 +go 1.25.3 require ( - github.com/Masterminds/semver v1.5.0 - github.com/Masterminds/semver/v3 v3.4.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/go-logr/logr v1.4.3 github.com/gobuffalo/flect v1.0.3 - github.com/golang/mock v1.6.0 github.com/krateoplatformops/plumbing v0.7.2 github.com/krateoplatformops/unstructured-runtime v0.3.1 - github.com/pkg/errors v0.9.1 - github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - helm.sh/helm/v3 v3.19.2 - k8s.io/api v0.34.2 - k8s.io/apiextensions-apiserver v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/cli-runtime v0.34.2 - k8s.io/client-go v0.34.2 + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/client-go v0.35.0 sigs.k8s.io/e2e-framework v0.6.0 - sigs.k8s.io/kustomize/kyaml v0.20.1 - sigs.k8s.io/yaml v1.6.0 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -38,25 +30,24 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.29 // indirect + github.com/containerd/containerd v1.7.30 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -77,7 +68,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -94,52 +85,60 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twmb/murmur3 v1.1.8 // indirect github.com/vladimirvivien/gexe v0.4.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.12.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.34.2 // indirect - k8s.io/component-base v0.34.2 // indirect + helm.sh/helm/v3 v3.20.0 // indirect + k8s.io/apiserver v0.35.0 // indirect + k8s.io/cli-runtime v0.35.0 // indirect + k8s.io/component-base v0.35.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/kubectl v0.34.0 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kubectl v0.35.0 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect oras.land/oras-go/v2 v2.6.0 // indirect - sigs.k8s.io/controller-runtime v0.20.0 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/controller-runtime v0.22.3 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) + +replace github.com/krateoplatformops/plumbing => /Users/matteogastaldello/Documents/plumbing diff --git a/go.sum b/go.sum index b902a35..25f5438 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,19 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -38,8 +36,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= -github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= +github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -51,8 +49,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= -github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -75,16 +73,16 @@ github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -101,12 +99,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= -github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -115,12 +113,8 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -132,8 +126,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -167,16 +161,12 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/krateoplatformops/plumbing v0.7.2 h1:4UuWy9747p9ligMtNEiOOQGsuK6d9lczg7R1no8ERsE= -github.com/krateoplatformops/plumbing v0.7.2/go.mod h1:mQ/sm0viyKgfR2ARzHuwCpY0rcyMKqCv8a8SOu52yYQ= github.com/krateoplatformops/unstructured-runtime v0.3.1 h1:tQMH19YEJ7+La5283a4FOQlCeBBhS9cqwYzBPW59srs= github.com/krateoplatformops/unstructured-runtime v0.3.1/go.mod h1:19uT87wZzRSjrfk3731Xhdt8ww7vnsXhljy4jk0cuWA= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -189,8 +179,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -224,10 +214,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -243,14 +233,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= @@ -260,10 +250,10 @@ github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRl github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= -github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -276,8 +266,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -303,19 +293,16 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= @@ -340,16 +327,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -358,81 +345,48 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -440,38 +394,38 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= -helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= -k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.2 h1:2/yu8suwkmES7IzwlehAovo8dDE07cFRC7KMDb1+MAE= -k8s.io/apiserver v0.34.2/go.mod h1:gqJQy2yDOB50R3JUReHSFr+cwJnL8G1dzTA0YLEqAPI= -k8s.io/cli-runtime v0.34.2 h1:cct1GEuWc3IyVT8MSCoIWzRGw9HJ/C5rgP32H60H6aE= -k8s.io/cli-runtime v0.34.2/go.mod h1:X13tsrYexYUCIq8MarCBy8lrm0k0weFPTpcaNo7lms4= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= +helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= +helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= +k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= -k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= +k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= -sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= +sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= diff --git a/internal/composition/composition.go b/internal/composition/composition.go index fd23a5c..f1e1c77 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -3,9 +3,8 @@ package composition import ( "context" "fmt" + "log/slog" "net/http" - "os" - "path/filepath" "time" "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured/condition" @@ -16,20 +15,23 @@ import ( compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" unstructuredtools "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured" - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient/tracer" "github.com/krateoplatformops/composition-dynamic-controller/internal/rbacgen" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart/archive" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/archive" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/processor" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/tracer" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/rbac" "github.com/krateoplatformops/plumbing/env" + helmconfig "github.com/krateoplatformops/plumbing/helm" + "github.com/krateoplatformops/plumbing/helm/v3" "github.com/krateoplatformops/plumbing/kubeutil/event" "github.com/krateoplatformops/plumbing/maps" "github.com/krateoplatformops/unstructured-runtime/pkg/controller" "github.com/krateoplatformops/unstructured-runtime/pkg/meta" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" "github.com/krateoplatformops/unstructured-runtime/pkg/tools" - "helm.sh/helm/v3/pkg/registry" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" @@ -37,10 +39,10 @@ import ( ) var ( - helmRegistryConfigPath = env.String(helmRegistryConfigPathEnvVar, helmclient.DefaultRegistryConfigPath) - krateoNamespace = env.String(krateoNamespaceEnvVar, krateoNamespaceDefault) - helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) - helmMaxHistory = env.Int(helmMaxHistoryEnvvar, 3) + // helmRegistryConfigPath = env.String(helmRegistryConfigPathEnvVar, helmclient.DefaultRegistryConfigPath) + krateoNamespace = env.String(krateoNamespaceEnvVar, krateoNamespaceDefault) + // helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) + helmMaxHistory = env.Int(helmMaxHistoryEnvvar, 3) ) const ( @@ -65,13 +67,20 @@ const ( var _ controller.ExternalClient = (*handler)(nil) -func NewHandler(cfg *rest.Config, pig archive.Getter, event event.APIRecorder, pluralizer pluralizer.PluralizerInterface, chartInspectorUrl string, saName string, saNamespace string) controller.ExternalClient { - val, ok := os.LookupEnv(helmRegistryConfigPathEnvVar) - if ok { - helmRegistryConfigPath = val - } - - helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) +func NewHandler(cfg *rest.Config, + pig archive.Getter, + event event.APIRecorder, + pluralizer pluralizer.PluralizerInterface, + mapper apimeta.RESTMapper, + chartInspectorUrl string, + saName string, + saNamespace string) controller.ExternalClient { + // val, ok := os.LookupEnv(helmRegistryConfigPathEnvVar) + // if ok { + // helmRegistryConfigPath = val + // } + + // helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) return &handler{ kubeconfig: cfg, @@ -85,10 +94,12 @@ func NewHandler(cfg *rest.Config, pig archive.Getter, event event.APIRecorder, p } type handler struct { - kubeconfig *rest.Config - pluralizer pluralizer.PluralizerInterface + kubeconfig *rest.Config + pluralizer pluralizer.PluralizerInterface + eventRecorder event.APIRecorder + mapper apimeta.RESTMapper + packageInfoGetter archive.Getter - eventRecorder event.APIRecorder chartInspectorUrl string saName string @@ -97,6 +108,7 @@ type handler struct { func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (controller.ExternalObservation, error) { mg = mg.DeepCopy() + releaseName := compositionMeta.GetReleaseName(mg) log := xcontext.Logger(ctx) @@ -151,12 +163,12 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, fmt.Errorf("updating cr with values: %w", err) } - hc, _, err := h.helmClientForResource(ctx, mg, pkg.RegistryAuth) + hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) if err != nil { - return controller.ExternalObservation{}, fmt.Errorf("getting helm client: %w", err) + return controller.ExternalObservation{}, fmt.Errorf("creating helm client: %w", err) } - rel, err := helmchart.FindRelease(hc, compositionMeta.GetReleaseName(mg)) + rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("finding helm release: %w", err) } @@ -168,16 +180,11 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c }, nil } - if rel.Info.Status.IsPending() { + if rel.Status == helmconfig.StatusPendingInstall || rel.Status == helmconfig.StatusPendingUpgrade { log.Debug("Composition stuck install or upgrade in progress. Rolling back to previous release before re-attempting.") // Rollback to previous release - err = hc.RollbackRelease(&helmclient.ChartSpec{ - ReleaseName: compositionMeta.GetReleaseName(mg), - Namespace: mg.GetNamespace(), - Repo: pkg.Repo, - ChartName: pkg.URL, - Version: pkg.Version, - MaxHistory: helmMaxHistory, + rel, err = hc.Rollback(ctx, releaseName, &helmconfig.RollbackConfig{ + MaxHistory: helmMaxHistory, }) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("rolling back release: %w", err) @@ -193,7 +200,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c rbgen := rbacgen.NewRBACGen(h.saName, h.saNamespace, &chartInspector) // Get Resources and generate RBAC generated, err := rbgen. - WithBaseName(compositionMeta.GetReleaseName(mg)). + WithBaseName(releaseName). Generate(rbacgen.Parameters{ CompositionName: mg.GetName(), CompositionNamespace: mg.GetNamespace(), @@ -228,36 +235,34 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c } tracer := tracer.NewTracer(ctx, meta.IsVerbose(mg)) - hc, clientset, err := h.helmClientForResourceWithTransportWrapper(mg, pkg.RegistryAuth, func(rt http.RoundTripper) http.RoundTripper { + cfg := rest.CopyConfig(h.kubeconfig) + cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { return tracer.WithRoundTripper(rt) - }) + } + hc, err = helm.NewClient(cfg, mg.GetNamespace(), slog.Default().Handler()) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("getting helm client: %w", err) } - opts := helmchart.UpdateOptions{ - CheckResourceOptions: helmchart.CheckResourceOptions{ - DynamicClient: dyn, - Pluralizer: h.pluralizer, - }, - HelmClient: hc, - ChartName: pkg.URL, - Resource: mg, - Repo: pkg.Repo, - Version: pkg.Version, - KrateoNamespace: krateoNamespace, - MaxHistory: helmMaxHistory, - } - if pkg.RegistryAuth != nil { - opts.Credentials = &helmchart.Credentials{ - Username: pkg.RegistryAuth.Username, - Password: pkg.RegistryAuth.Password, - } + values, ok, err := maps.NestedMap(mg.Object, "spec") + if err != nil { + return controller.ExternalObservation{}, fmt.Errorf("getting spec values: %w", err) } - - upgradedRel, err := helmchart.Update(ctx, opts) + if !ok { + values = map[string]interface{}{} + } + upgradedRel, err := hc.Upgrade(ctx, releaseName, pkg.URL, &helmconfig.UpgradeConfig{ + ActionConfig: &helmconfig.ActionConfig{ + ChartVersion: pkg.Version, + ChartName: pkg.Repo, + Username: pkg.Auth.Username, + Password: pkg.Auth.Password, + Values: values, + }, + MaxHistory: helmMaxHistory, + }) if err != nil { - retErr := fmt.Errorf("updating helm chart: %w", err) + retErr := fmt.Errorf("upgrading helm chart: %w", err) condition := condition.Unavailable() condition.Message = retErr.Error() unstructuredtools.SetConditions(mg, condition) @@ -268,10 +273,11 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, retErr } - _, digest, err := helmchart.GetResourcesRefFromRelease(upgradedRel, mg.GetNamespace(), clientset) + digest, err := processor.ComputeReleaseDigest(upgradedRel) if err != nil { - return controller.ExternalObservation{}, fmt.Errorf("getting resources from release: %w", err) + return controller.ExternalObservation{}, fmt.Errorf("computing release digest: %w", err) } + previousDigest, err := maps.NestedString(mg.Object, "status", "digest") if err != nil { return controller.ExternalObservation{}, fmt.Errorf("getting previous digest from status: %w", err) @@ -279,9 +285,9 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c if previousDigest == "" { // Calculate the digest from the previous release if not present in status log.Debug("Previous digest not found in status, calculating from previous release") - _, previousDigest, err = helmchart.GetResourcesRefFromRelease(rel, mg.GetNamespace(), clientset) + previousDigest, err = processor.ComputeReleaseDigest(rel) if err != nil { - return controller.ExternalObservation{}, fmt.Errorf("getting resources from previous release: %w", err) + return controller.ExternalObservation{}, fmt.Errorf("computing release digest from previous release: %w", err) } } if digest != previousDigest { @@ -292,16 +298,15 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c }, nil } - if rel.Chart.Metadata.Version != upgradedRel.Chart.Metadata.Version { - log.Debug("Composition package version mismatch.", "package", pkg.URL, "installed", rel.Chart.Metadata.Version, "expected", pkg.Version) + if rel.ChartVersion != upgradedRel.ChartVersion { + log.Debug("Composition package version mismatch.", "package", pkg.URL, "installed", rel.ChartVersion, "expected", pkg.Version) return controller.ExternalObservation{ ResourceExists: true, ResourceUpToDate: false, }, nil } - err = setStatus(mg, &statusManagerOpts{ - pluralizer: h.pluralizer, + err = h.setStatus(mg, &statusManagerOpts{ force: false, resources: nil, // we don't need to set resources here as they are already set when a resource is created/updated previousDigest: previousDigest, @@ -395,45 +400,29 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("installing rbac: %w", err) } - // Install the helm chart - hc, clientset, err := h.helmClientForResource(ctx, mg, pkg.RegistryAuth) + hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) if err != nil { - return fmt.Errorf("getting helm client: %w", err) + return fmt.Errorf("creating helm client: %w", err) } - opts := helmchart.InstallOptions{ - CheckResourceOptions: helmchart.CheckResourceOptions{ - DynamicClient: dyn, - Pluralizer: h.pluralizer, + rel, err := hc.Install(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.InstallConfig{ + ActionConfig: &helmconfig.ActionConfig{ + ChartVersion: pkg.Version, + ChartName: pkg.Repo, }, - HelmClient: hc, - ChartName: pkg.URL, - Resource: mg, - Repo: pkg.Repo, - Version: pkg.Version, - KrateoNamespace: krateoNamespace, - MaxHistory: helmMaxHistory, - } - if pkg.RegistryAuth != nil { - opts.Credentials = &helmchart.Credentials{ - Username: pkg.RegistryAuth.Username, - Password: pkg.RegistryAuth.Password, - } - } + }) - rel, _, err := helmchart.Install(ctx, opts) if err != nil { return fmt.Errorf("installing helm chart: %w", err) } log.Debug("Installing composition package", "package", pkg.URL) - all, digest, err := helmchart.GetResourcesRefFromRelease(rel, mg.GetNamespace(), clientset) + all, digest, err := processor.DecodeMinRelease(rel) if err != nil { - return fmt.Errorf("getting resources from release: %w", err) + return fmt.Errorf("decoding release: %w", err) } - err = setStatus(mg, &statusManagerOpts{ - pluralizer: h.pluralizer, + err = h.setStatus(mg, &statusManagerOpts{ force: true, resources: all, previousDigest: "", @@ -466,6 +455,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) error { mg = mg.DeepCopy() + releaseName := compositionMeta.GetReleaseName(mg) log := xcontext.Logger(ctx) @@ -503,12 +493,12 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err } // Update the helm chart - hc, clientset, err := h.helmClientForResource(ctx, mg, pkg.RegistryAuth) + hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) if err != nil { - return fmt.Errorf("getting helm client: %w", err) + return fmt.Errorf("creating helm client: %w", err) } - rel, err := helmchart.FindRelease(hc, compositionMeta.GetReleaseName(mg)) + rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return fmt.Errorf("finding helm release: %w", err) } @@ -518,12 +508,12 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("getting previous digest from status: %w", err) } - all, digest, err := helmchart.GetResourcesRefFromRelease(rel, mg.GetNamespace(), clientset) + all, digest, err := processor.DecodeMinRelease(rel) if err != nil { - return fmt.Errorf("getting resources from release: %w", err) + return fmt.Errorf("decoding release: %w", err) } - managed, err := populateManagedResources(h.pluralizer, all) + managed, err := h.populateManagedResources(all) if err != nil { return fmt.Errorf("populating managed resources: %w", err) } @@ -534,7 +524,6 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err h.eventRecorder.Event(mg, event.Normal(reasonUpdated, "Update", fmt.Sprintf("Updated composition: %s", mg.GetName()))) statusOpts := &statusManagerOpts{ - pluralizer: h.pluralizer, force: false, resources: all, digest: digest, @@ -544,7 +533,7 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err chartVersion: pkg.Version, conditionType: ConditionTypeAvailable, } - err = setStatus(mg, statusOpts) + err = h.setStatus(mg, statusOpts) if err != nil { return fmt.Errorf("setting status: %w", err) } @@ -578,6 +567,8 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) error { mg = mg.DeepCopy() + releaseName := compositionMeta.GetReleaseName(mg) + log := xcontext.Logger(ctx) log = log.WithValues("op", "Delete"). @@ -606,9 +597,9 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("helm chart package info getter must be specified") } - hc, _, err := h.helmClientForResource(ctx, mg, nil) + hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) if err != nil { - return fmt.Errorf("getting helm client: %w", err) + return fmt.Errorf("creating helm client: %w", err) } pkg, err := h.packageInfoGetter.WithLogger(log).Get(mg) @@ -616,16 +607,8 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("getting package info: %w", err) } - chartSpec := helmclient.ChartSpec{ - ReleaseName: compositionMeta.GetReleaseName(mg), - Namespace: mg.GetNamespace(), - ChartName: pkg.URL, - Version: pkg.Version, - Wait: false, - } - // Check if the release exists before uninstalling - rel, err := helmchart.FindRelease(hc, compositionMeta.GetReleaseName(mg)) + rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return fmt.Errorf("finding helm release: %w", err) } @@ -635,17 +618,17 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return nil } - err = hc.UninstallRelease(&chartSpec) + err = hc.Uninstall(ctx, releaseName, &helmconfig.UninstallConfig{}) if err != nil { return fmt.Errorf("uninstalling helm chart: %w", err) } - rel, err = helmchart.FindRelease(hc, compositionMeta.GetReleaseName(mg)) + rel, err = hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return fmt.Errorf("finding helm release: %w", err) } if rel != nil { - return fmt.Errorf("composition not deleted, release %s still exists", compositionMeta.GetReleaseName(mg)) + return fmt.Errorf("composition not deleted, release %s still exists", releaseName) } log.Debug("Uninstalling RBAC", "package", pkg.URL) @@ -656,6 +639,7 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err } chartInspector := chartinspector.NewChartInspector(h.chartInspectorUrl) rbgen := rbacgen.NewRBACGen(h.saName, h.saNamespace, &chartInspector) + // Get Resources and generate RBAC generated, err := rbgen. WithBaseName(compositionMeta.GetReleaseName(mg)). @@ -687,78 +671,3 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return nil } - -func (h *handler) helmClientForResource(ctx context.Context, mg *unstructured.Unstructured, registryAuth *helmclient.RegistryAuth) (helmclient.Client, helmclient.CachedClientsInterface, error) { - - log := xcontext.Logger(ctx) - - log = log.WithValues("apiVersion", mg.GetAPIVersion()). - WithValues("kind", mg.GetKind()). - WithValues("name", mg.GetName()). - WithValues("namespace", mg.GetNamespace()) - - clientSet, err := helmclient.NewCachedClients(h.kubeconfig) - if err != nil { - return nil, nil, fmt.Errorf("creating cached helm client set: %w", err) - } - - opts := &helmclient.Options{ - Namespace: mg.GetNamespace(), - RepositoryCache: "/tmp/.helmcache", - RepositoryConfig: "/tmp/.helmrepo", - RegistryConfig: helmRegistryConfigFile, - Debug: true, - Linting: false, - DebugLog: func(format string, v ...interface{}) { - if !meta.IsVerbose(mg) { - return - } - - if len(v) > 0 { - log.Info(fmt.Sprintf(format, v)) - } else { - log.Info(format) - } - }, - RegistryAuth: (registryAuth), - } - - hc, err := helmclient.NewCachedClientFromRestConf( - &helmclient.RestConfClientOptions{ - Options: opts, - RestConfig: h.kubeconfig, - }, - clientSet, - ) - return hc, clientSet, err -} - -func (h *handler) helmClientForResourceWithTransportWrapper(mg *unstructured.Unstructured, registryAuth *helmclient.RegistryAuth, transportWrapper func(http.RoundTripper) http.RoundTripper) (helmclient.Client, helmclient.CachedClientsInterface, error) { - opts := &helmclient.Options{ - Namespace: mg.GetNamespace(), - RepositoryCache: "/tmp/.helmcache", - RepositoryConfig: "/tmp/.helmrepo", - RegistryConfig: helmRegistryConfigFile, - Debug: true, - Linting: false, - DebugLog: func(format string, v ...interface{}) {}, - RegistryAuth: registryAuth, - } - - clientSet, err := helmclient.NewCachedClients(h.kubeconfig) - if err != nil { - return nil, nil, err - } - - cfg := rest.CopyConfig(h.kubeconfig) - cfg.WrapTransport = transportWrapper - - hc, err := helmclient.NewCachedClientFromRestConf( - &helmclient.RestConfClientOptions{ - Options: opts, - RestConfig: cfg, - }, - clientSet, - ) - return hc, clientSet, err -} diff --git a/internal/composition/composition_test.go b/internal/composition/composition_test.go index 5411241..eef6756 100644 --- a/internal/composition/composition_test.go +++ b/internal/composition/composition_test.go @@ -27,12 +27,13 @@ import ( "github.com/gobuffalo/flect" compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart/archive" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/archive" "github.com/krateoplatformops/plumbing/kubeutil/event" "github.com/krateoplatformops/plumbing/kubeutil/eventrecorder" "github.com/krateoplatformops/unstructured-runtime/pkg/controller" "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" + mapper "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/dynamic" "github.com/krateoplatformops/plumbing/e2e" xenv "github.com/krateoplatformops/plumbing/env" "k8s.io/apimachinery/pkg/runtime/schema" @@ -189,7 +190,12 @@ func TestController(t *testing.T) { return ctx } - handler = NewHandler(cfg.Client().RESTConfig(), pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorMockURL, "test-sa", altNamespace) + mapper, err := mapper.NewRESTMapper(cfg.Client().RESTConfig()) + if err != nil { + t.Error("Creating REST mapper.", "error", err) + return ctx + } + handler = NewHandler(cfg.Client().RESTConfig(), pig, *event.NewAPIRecorder(rec), pluralizer, mapper, chartInspectorMockURL, "test-sa", altNamespace) // handler = NewHandler(cfg.Client().RESTConfig(), log, pig, *event.NewAPIRecorder(rec), pluralizer, chartInspectorUrl, "test-sa", altNamespace) @@ -419,6 +425,97 @@ func TestController(t *testing.T) { t.Error("Managed resources not updated.") } + return ctx + }).Assess("Break: Verify Observe Side Effects", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + dy := dynamic.NewForConfigOrDie(c) + var obj unstructured.Unstructured + if err := decoder.DecodeFile(os.DirFS(filepath.Join(testdataPath, "compositions")), "focus.yaml", &obj); err != nil { + t.Error("Decoding composition manifests.", "error", err) + return ctx + } + + // 1. Get the Composition + version := obj.GetLabels()["krateo.io/composition-version"] + gvr := schema.GroupVersionResource{ + Group: "composition.krateo.io", + Version: version, + Resource: flect.Pluralize(strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind)), + } + + cli := dy.Resource(gvr).Namespace(obj.GetNamespace()) + u, err := cli.Get(ctx, obj.GetName(), metav1.GetOptions{}) + if err != nil { + t.Error("Getting composition.", "error", err) + return ctx + } + + // 2. Inspect the underlying Helm Secret to get the INITIAL Revision number + // We look for secrets with owner=helm and name=release-name + releaseName := compositionMeta.GetReleaseName(u) + // secretClient := cfg.Client().Resources().GetControllerRuntimeClient() + + // Helper to count revisions + countRevisions := func() (int, error) { + // var secrets corev1.SecretList + // // Helm stores releases in secrets with type 'helm.sh/release.v1' + // // We verify by label "name" which equals the release name + // labels := map[string]string{ + // "name": releaseName, + // "owner": "helm", + // } + // Note: In e2e-framework, we might need to use the dynamic client or typed client for listing with selectors + // Using standard client-go for list here to be safe within the test closure + clientset, _ := kubernetes.NewForConfig(c) + list, err := clientset.CoreV1().Secrets(u.GetNamespace()).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("name=%s,owner=helm", releaseName), + }) + if err != nil { + return 0, err + } + return len(list.Items), nil + } + + initialRevisionCount, err := countRevisions() + if err != nil { + t.Error("Failed to count helm revisions", err) + return ctx + } + t.Logf("Initial Helm Revision Count: %d", initialRevisionCount) + + // 3. The "Stress" Loop + // We call Observe multiple times. In a correct controller, Observe is Read-Only. + // It should NOT trigger a new Helm Release. + t.Log("Starting Observe Loop (Simulating controller reconciliation)...") + for i := 0; i < 5; i++ { + // Refetch to ensure we have latest resourceVersion for internal logic + u, _ = cli.Get(ctx, obj.GetName(), metav1.GetOptions{}) + + // CALL OBSERVE + _, err := handler.Observe(ctx, u) + if err != nil { + t.Logf("Observe iteration %d failed: %v", i, err) + } + } + + // 4. Check Revisions again + finalRevisionCount, err := countRevisions() + if err != nil { + t.Error("Failed to count helm revisions after loop", err) + return ctx + } + t.Logf("Final Helm Revision Count: %d", finalRevisionCount) + + // 5. ASSERTION - This is where the code "Breaks" + if finalRevisionCount > initialRevisionCount { + t.Errorf("CRITICAL FAILURE: The Observe method is not idempotent! "+ + "Helm revisions increased from %d to %d without any Spec changes. "+ + "This implies 'hc.Upgrade' is running on every reconciliation loop.", + initialRevisionCount, finalRevisionCount) + + // Fail immediately to prevent cleaning up evidence if debugging + t.FailNow() + } + return ctx }).Assess("Delete", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { dy := dynamic.NewForConfigOrDie(c) @@ -640,6 +737,11 @@ func SetSAToken(ctx context.Context, t *testing.T, cfg *envconf.Config) *rest.Co Resources: []string{"roles", "rolebindings", "clusterroles", "clusterrolebindings"}, Verbs: []string{"*"}, }, + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"*"}, + }, }, }, metav1.CreateOptions{}) if err != nil { diff --git a/internal/composition/support.go b/internal/composition/support.go index d8b7780..f069527 100644 --- a/internal/composition/support.go +++ b/internal/composition/support.go @@ -4,10 +4,10 @@ import ( "fmt" compositionCondition "github.com/krateoplatformops/composition-dynamic-controller/internal/condition" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/dynamic" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/processor" "github.com/krateoplatformops/plumbing/maps" - "github.com/krateoplatformops/unstructured-runtime/pkg/controller/objectref" - "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" unstructuredtools "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured" "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured/condition" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -67,23 +67,22 @@ const ( type statusManagerOpts struct { force bool - pluralizer pluralizer.PluralizerInterface chartURL string chartVersion string - resources []objectref.ObjectRef + resources []processor.MinimalMetadata previousDigest string digest string message string conditionType ConditionType } -func setStatus(mg *unstructured.Unstructured, opts *statusManagerOpts) error { +func (h *handler) setStatus(mg *unstructured.Unstructured, opts *statusManagerOpts) error { if opts == nil { return fmt.Errorf("status manager options are nil") } if len(opts.resources) > 0 { - managed, err := populateManagedResources(opts.pluralizer, opts.resources) + managed, err := h.populateManagedResources(opts.resources) if err != nil { return fmt.Errorf("populating managed resources: %w", err) } @@ -131,12 +130,21 @@ func setManagedResources(mg *unstructured.Unstructured, managed []any) { mg.Object["status"] = mapstatus } -func populateManagedResources(pluralizer pluralizer.PluralizerInterface, resources []objectref.ObjectRef) ([]any, error) { +func (h *handler) populateManagedResources(resources []processor.MinimalMetadata) ([]any, error) { var managed []interface{} for _, ref := range resources { - gvr, err := pluralizer.GVKtoGVR(schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)) + gvr, err := h.pluralizer.GVKtoGVR(schema.FromAPIVersionAndKind(ref.GetAPIVersion(), ref.GetKind())) if err != nil { - return nil, fmt.Errorf("getting GVR for %s: %w", ref.String(), err) + return nil, fmt.Errorf("getting GVR for %s/%s with name %s and namespace %s: %w", ref.GetAPIVersion(), ref.GetKind(), ref.GetName(), ref.GetNamespace(), err) + } + + gvk := schema.FromAPIVersionAndKind(ref.GetAPIVersion(), ref.GetKind()) + isNamespaced, err := dynamic.IsNamespaced(h.mapper, gvk) + if err != nil { + return nil, fmt.Errorf("getting REST mapping for %s: %w", gvk.String(), err) + } + if !isNamespaced { + ref.SetNamespace("") } buildpath := func() string { @@ -146,21 +154,22 @@ func populateManagedResources(pluralizer pluralizer.PluralizerInterface, resourc prefix = "/api/" + gvr.Version } - suffix := "/namespaces/" + ref.Namespace + "/" + gvr.Resource + "/" + ref.Name + suffix := "/namespaces/" + ref.GetNamespace() + "/" + gvr.Resource + "/" + ref.GetName() // Cluster scoped resources - if len(ref.Namespace) == 0 { - suffix = "/" + gvr.Resource + "/" + ref.Name + if len(ref.GetNamespace()) == 0 { + suffix = "/" + gvr.Resource + "/" + ref.GetName() } if len(gvr.Group) == 0 { return prefix + suffix } return prefix + suffix } + managed = append(managed, ManagedResource{ - APIVersion: ref.APIVersion, + APIVersion: ref.GetAPIVersion(), Resource: gvr.Resource, - Name: ref.Name, - Namespace: ref.Namespace, + Name: ref.GetName(), + Namespace: ref.GetNamespace(), Path: buildpath(), }) } diff --git a/internal/composition/support_test.go b/internal/composition/support_test.go index b12fdd9..15f0e0b 100644 --- a/internal/composition/support_test.go +++ b/internal/composition/support_test.go @@ -3,8 +3,9 @@ package composition import ( "testing" - "github.com/krateoplatformops/unstructured-runtime/pkg/controller/objectref" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/processor" "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -23,11 +24,33 @@ func (m *mockPluralizer) GVKtoGVR(gvk schema.GroupVersionKind) (schema.GroupVers return schema.GroupVersionResource{}, nil } +func newTestMapper() *meta.DefaultRESTMapper { + // Define the versions the mapper should recognize + gvks := []schema.GroupVersion{ + {Group: "apps", Version: "v1"}, + {Group: "rbac.authorization.k8s.io", Version: "v1"}, + {Group: "", Version: "v1"}, + } + + mapper := meta.NewDefaultRESTMapper(gvks) + + // Add Namespaced resources + mapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace) + mapper.Add(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, meta.RESTScopeNamespace) + mapper.Add(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, meta.RESTScopeNamespace) + + // Add Cluster-scoped resources + mapper.Add(schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, meta.RESTScopeRoot) + mapper.Add(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}, meta.RESTScopeRoot) + + return mapper +} + func TestPopulateManagedResources(t *testing.T) { tests := []struct { name string pluralizer pluralizer.PluralizerInterface - resources []objectref.ObjectRef + resources []processor.MinimalMetadata expected []interface{} expectError bool }{ @@ -37,7 +60,7 @@ func TestPopulateManagedResources(t *testing.T) { gvrMap: make(map[schema.GroupVersionKind]schema.GroupVersionResource), errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{}, + resources: []processor.MinimalMetadata{}, expected: []interface{}{}, }, { @@ -48,8 +71,15 @@ func TestPopulateManagedResources(t *testing.T) { }, errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{ - {APIVersion: "apps/v1", Kind: "Deployment", Name: "test-deployment", Namespace: "default"}, + resources: []processor.MinimalMetadata{ + { + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: processor.Metadata{ + Name: "test-deployment", + Namespace: "default", + }, + }, }, expected: []interface{}{ ManagedResource{ @@ -69,8 +99,8 @@ func TestPopulateManagedResources(t *testing.T) { }, errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{ - {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole", Name: "test-clusterrole", Namespace: ""}, + resources: []processor.MinimalMetadata{ + {APIVersion: "rbac.authorization.k8s.io/v1", Kind: "ClusterRole", Metadata: processor.Metadata{Name: "test-clusterrole", Namespace: ""}}, }, expected: []interface{}{ ManagedResource{ @@ -90,8 +120,8 @@ func TestPopulateManagedResources(t *testing.T) { }, errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{ - {APIVersion: "v1", Kind: "Pod", Name: "test-pod", Namespace: "default"}, + resources: []processor.MinimalMetadata{ + {APIVersion: "v1", Kind: "Pod", Metadata: processor.Metadata{Name: "test-pod", Namespace: "default"}}, }, expected: []interface{}{ ManagedResource{ @@ -111,8 +141,8 @@ func TestPopulateManagedResources(t *testing.T) { }, errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{ - {APIVersion: "v1", Kind: "Node", Name: "test-node", Namespace: ""}, + resources: []processor.MinimalMetadata{ + {APIVersion: "v1", Kind: "Node", Metadata: processor.Metadata{Name: "test-node", Namespace: ""}}, }, expected: []interface{}{ ManagedResource{ @@ -133,9 +163,9 @@ func TestPopulateManagedResources(t *testing.T) { }, errMap: make(map[schema.GroupVersionKind]error), }, - resources: []objectref.ObjectRef{ - {APIVersion: "apps/v1", Kind: "Deployment", Name: "test-deployment", Namespace: "default"}, - {APIVersion: "v1", Kind: "Service", Name: "test-service", Namespace: "default"}, + resources: []processor.MinimalMetadata{ + {APIVersion: "apps/v1", Kind: "Deployment", Metadata: processor.Metadata{Name: "test-deployment", Namespace: "default"}}, + {APIVersion: "v1", Kind: "Service", Metadata: processor.Metadata{Name: "test-service", Namespace: "default"}}, }, expected: []interface{}{ ManagedResource{ @@ -158,7 +188,11 @@ func TestPopulateManagedResources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := populateManagedResources(tt.pluralizer, tt.resources) + h := &handler{ + pluralizer: tt.pluralizer, + mapper: newTestMapper(), + } + result, err := h.populateManagedResources(tt.resources) if tt.expectError && err == nil { t.Fatal("expected error but got none") diff --git a/internal/helm/getter/getter.go b/internal/helm/getter/getter.go deleted file mode 100644 index 6c796d5..0000000 --- a/internal/helm/getter/getter.go +++ /dev/null @@ -1,170 +0,0 @@ -package getter - -import ( - "crypto/sha256" - "crypto/tls" - "encoding/hex" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/krateoplatformops/unstructured-runtime/pkg/logging" -) - -type GetOptions struct { - Logging logging.Logger - URI string - Version string - Repo string - InsecureSkipVerifyTLS bool - Username string - Password string - PassCredentialsAll bool -} - -// Getter is an interface to support GET to the specified URI. -type Getter interface { - // Get file content by url string - Get(opts GetOptions) (io.ReadCloser, string, error) -} - -func Get(opts GetOptions) (io.ReadCloser, string, error) { - // Simple disk cache: env HELM_CHART_CACHE_DIR or /tmp/helmchart-cache - cacheDir := func() string { - if v := os.Getenv("HELM_CHART_CACHE_DIR"); v != "" { - return v - } - return "/tmp/helmchart-cache" - }() - - if err := os.MkdirAll(cacheDir, 0o755); err != nil { - // non-blocking: log and continue to fetch from network - } - - // cache key = sha256(uri|version|repo) - h := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s", opts.URI, opts.Version, opts.Repo))) - cacheFile := filepath.Join(cacheDir, hex.EncodeToString(h[:])+".tgz") - - // if cached file exists, open and return it (caller must Close) - if fi, err := os.Stat(cacheFile); err == nil && fi.Mode().IsRegular() && fi.Size() > 0 { - f, err := os.Open(cacheFile) - if err == nil { - return f, cacheFile, nil - } - // if error opening, fallthrough to refetch - } - - // fallback: call the appropriate getter and stream to cache file - var ( - rc io.ReadCloser - uri string - err error - ) - - // delegate to specific getters - if isOCI(opts.URI) { - g, errNew := newOCIGetter() - if errNew != nil { - return nil, "", errNew - } - rc, uri, err = g.Get(opts) - } else if isTGZ(opts.URI) { - g := &tgzGetter{} - rc, uri, err = g.Get(opts) - } else if isHTTP(opts.URI) { - g := &repoGetter{} - rc, uri, err = g.Get(opts) - } else { - return nil, "", fmt.Errorf("no handler found for url: %s", opts.URI) - } - if err != nil { - return nil, "", err - } - // ensure rc is closed on error / after copy - defer func() { - // if we return success we'll re-open cached file and return that handle instead - }() - - // write stream -> tmp file in cache dir - tmpf, err := os.CreateTemp(cacheDir, "chart-*.tmp") - if err != nil { - rc.Close() - return nil, "", err - } - _, err = io.Copy(tmpf, rc) - // free original stream - _ = rc.Close() - // close tmp - if cerr := tmpf.Close(); cerr != nil && err == nil { - err = cerr - } - if err != nil { - os.Remove(tmpf.Name()) - return nil, "", err - } - - // atomic move to final cache path - if err := os.Rename(tmpf.Name(), cacheFile); err != nil { - // if rename fails, try to remove tmp and return file directly - os.Remove(tmpf.Name()) - return nil, "", err - } - - // open cached file for reading and return it - f, err := os.Open(cacheFile) - if err != nil { - return nil, "", err - } - return f, uri, nil -} - -func fetchStream(opts GetOptions) (io.ReadCloser, error) { - req, err := http.NewRequest(http.MethodGet, opts.URI, nil) - if err != nil { - return nil, err - } - if opts.PassCredentialsAll { - if opts.Username != "" && opts.Password != "" { - req.SetBasicAuth(opts.Username, opts.Password) - } - } - resp, err := newHTTPClient(opts).Do(req) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - return nil, fmt.Errorf("failed to fetch %s: %s", opts.URI, resp.Status) - } - // return the body stream directly; caller is responsible for closing it - return resp.Body, nil -} - -func newHTTPClient(opts GetOptions) *http.Client { - transport := &http.Transport{ - DisableCompression: true, - Proxy: http.ProxyFromEnvironment, - } - - if opts.InsecureSkipVerifyTLS { - if transport.TLSClientConfig == nil { - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - } else { - transport.TLSClientConfig.InsecureSkipVerify = true - } - } - - return &http.Client{ - Transport: transport, - Timeout: 1 * time.Minute, - // CheckRedirect: func(req *http.Request, via []*http.Request) error { - // fmt.Printf("redir: %v\n", via) - // return nil - // }, - } -} diff --git a/internal/helm/getter/getter_test.go b/internal/helm/getter/getter_test.go deleted file mode 100644 index d147d80..0000000 --- a/internal/helm/getter/getter_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package getter - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGet(t *testing.T) { - tests := []struct { - name string - opts GetOptions - wantErr bool - }{ - { - name: "OCI URI", - opts: GetOptions{ - URI: "oci://registry-1.docker.io/bitnamicharts/nginx", - }, - wantErr: false, - }, - { - name: "TGZ URI", - opts: GetOptions{ - URI: "https://raw.githubusercontent.com/krateoplatformops/helm-charts/refs/heads/gh-pages/api-1.0.0.tgz", - }, - wantErr: false, - }, - { - name: "HTTP URI", - opts: GetOptions{ - URI: "https://charts.krateo.io", - Repo: "fireworks-app", - Version: "1.1.10", - }, - wantErr: false, - }, - { - name: "Invalid URI", - opts: GetOptions{ - URI: "invalid://example.com/chart", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Delete tmp folder after each test - cacheDir := func() string { - if v := os.Getenv("HELM_CHART_CACHE_DIR"); v != "" { - return v - } - return "/tmp/helmchart-cache" - }() - _, _, err := Get(tt.opts) - if (err != nil) != tt.wantErr { - t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) - } - os.RemoveAll(cacheDir) - }) - } -} - -func TestTGZGetter(t *testing.T) { - opts := GetOptions{ - URI: "https://raw.githubusercontent.com/krateoplatformops/helm-charts/refs/heads/gh-pages/api-1.0.0.tgz", - } - - g := &tgzGetter{} - _, _, err := g.Get(opts) - assert.NoError(t, err) -} - -func TestOCIGetter(t *testing.T) { - opts := GetOptions{ - URI: "oci://registry-1.docker.io/bitnamicharts/nginx", - } - - g, err := newOCIGetter() - assert.NoError(t, err) - - _, _, err = g.Get(opts) - assert.NoError(t, err) -} - -func TestRepoGetter(t *testing.T) { - opts := GetOptions{ - URI: "https://charts.krateo.io", - Repo: "fireworks-app", - Version: "1.1.10", - } - - g := &repoGetter{} - _, _, err := g.Get(opts) - assert.NoError(t, err) -} - -func TestFetch(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("test data")) - })) - defer server.Close() - - opts := GetOptions{ - URI: server.URL, - } - - data, err := fetchStream(opts) - assert.NoError(t, err) - b, err := io.ReadAll(data) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "test data", string(b)) -} - -// ...existing code... - -// Test that Get uses the disk cache and does not re-download when the cache file exists -func TestGet_CacheAvoidsRedownload(t *testing.T) { - // create a temp dir for cache and force the library to use it - tmpdir := t.TempDir() - if err := os.Setenv("HELM_CHART_CACHE_DIR", tmpdir); err != nil { - t.Fatalf("failed to set env: %v", err) - } - defer os.Unsetenv("HELM_CHART_CACHE_DIR") - - var mu sync.Mutex - requests := 0 - - // server returns some data and counts requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mu.Lock() - requests++ - mu.Unlock() - w.WriteHeader(http.StatusOK) - w.Write([]byte("chart-data")) - })) - defer server.Close() - - uri := server.URL + "/test.tgz" - opts := GetOptions{URI: uri} - - // First call should hit the server and populate cache - _, _, err := Get(opts) - if err != nil { - t.Fatalf("first Get failed: %v", err) - } - - // check server was called exactly once - mu.Lock() - if requests != 1 { - mu.Unlock() - t.Fatalf("expected 1 request after first Get, got %d", requests) - } - mu.Unlock() - - // compute expected cache file path (same logic as Get()) - h := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s", opts.URI, opts.Version, opts.Repo))) - cacheFile := filepath.Join(tmpdir, hex.EncodeToString(h[:])+".tgz") - if _, err := os.Stat(cacheFile); err != nil { - t.Fatalf("expected cache file at %s, stat error: %v", cacheFile, err) - } - - // Second call should read from cache and NOT call server again - _, _, err = Get(opts) - if err != nil { - t.Fatalf("second Get failed: %v", err) - } - - mu.Lock() - if requests != 1 { - mu.Unlock() - t.Fatalf("expected cached access, server should have been called once, got %d", requests) - } - mu.Unlock() -} diff --git a/internal/helm/getter/http.go b/internal/helm/getter/http.go deleted file mode 100644 index 3712733..0000000 --- a/internal/helm/getter/http.go +++ /dev/null @@ -1,79 +0,0 @@ -package getter - -import ( - "fmt" - "io" - "net/url" - "strings" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helm/repo" -) - -var _ Getter = (*repoGetter)(nil) - -type repoGetter struct{} - -func (g *repoGetter) Get(opts GetOptions) (io.ReadCloser, string, error) { - if !isHTTP(opts.URI) { - return nil, "", fmt.Errorf("uri '%s' is not a valid Repo ref", opts.URI) - } - - buf, err := fetchStream(GetOptions{ - URI: fmt.Sprintf("%s/index.yaml", opts.URI), - InsecureSkipVerifyTLS: opts.InsecureSkipVerifyTLS, - Username: opts.Username, - Password: opts.Password, - PassCredentialsAll: opts.PassCredentialsAll, - }) - if err != nil { - return nil, "", err - } - bufb, err := io.ReadAll(buf) - if err != nil { - return nil, "", err - } - - idx, err := repo.Load(bufb, opts.URI, opts.Logging) - if err != nil { - return nil, "", err - } - - res, err := idx.Get(opts.Repo, opts.Version) - if err != nil { - return nil, "", err - } - if len(res.URLs) == 0 { - return nil, "", fmt.Errorf("no package url found in index @ %s/%s", res.Name, res.Version) - } - - chartUrlStr := res.URLs[0] - _, err = url.ParseRequestURI(chartUrlStr) - if err != nil { - chartUrlStr = fmt.Sprintf("%s/%s", opts.URI, chartUrlStr) - _, err = url.ParseRequestURI(chartUrlStr) - if err != nil { - return nil, "", fmt.Errorf("invalid chart url: %s", chartUrlStr) - } - } - - newopts := GetOptions{ - URI: chartUrlStr, - Version: res.Version, - Repo: res.Name, - InsecureSkipVerifyTLS: opts.InsecureSkipVerifyTLS, - Username: opts.Username, - Password: opts.Password, - PassCredentialsAll: opts.PassCredentialsAll, - } - - dat, err := fetchStream(newopts) - if err != nil { - return nil, "", err - } - - return dat, newopts.URI, err -} - -func isHTTP(uri string) bool { - return strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "https://") -} diff --git a/internal/helm/getter/oci.go b/internal/helm/getter/oci.go deleted file mode 100644 index 5b74ebd..0000000 --- a/internal/helm/getter/oci.go +++ /dev/null @@ -1,141 +0,0 @@ -package getter - -import ( - "bytes" - "fmt" - "io" - "net" - "net/http" - "net/url" - "strings" - - "time" - - "github.com/Masterminds/semver" - "github.com/davecgh/go-spew/spew" - "helm.sh/helm/v3/pkg/registry" -) - -var _ Getter = (*ociGetter)(nil) - -func newOCIGetter() (Getter, error) { - transport := &http.Transport{ - // From https://github.com/google/go-containerregistry/blob/31786c6cbb82d6ec4fb8eb79cd9387905130534e/pkg/v1/remote/options.go#L87 - DisableCompression: true, - DialContext: (&net.Dialer{ - // By default we wrap the transport in retries, so reduce the - // default dial timeout to 5s to avoid 5x 30s of connection - // timeouts when doing the "ping" on certain http registries. - Timeout: 5 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 3 * time.Second, - } - - client, err := registry.NewClient( - registry.ClientOptDebug(true), - registry.ClientOptHTTPClient(&http.Client{ - Transport: transport, - //Timeout: g.opts.timeout, - }), - ) - if err != nil { - return nil, err - } - - return &ociGetter{ - client: client, - }, nil -} - -type ociGetter struct { - client *registry.Client -} - -func (g *ociGetter) Get(opts GetOptions) (io.ReadCloser, string, error) { - if !isOCI(opts.URI) { - return nil, "", fmt.Errorf("uri '%s' is not a valid OCI ref", opts.URI) - } - - ref := strings.TrimPrefix(opts.URI, "oci://") - if len(opts.Repo) > 0 { - ref = fmt.Sprintf("%s/%s", ref, opts.Repo) - } - u, err := g.resolveURI(ref, opts.Version) - if err != nil { - return nil, "", err - } - - if opts.PassCredentialsAll { - if opts.Username != "" && opts.Password != "" { - host := strings.Split(ref, "/")[0] - loginopts := []registry.LoginOption{ - registry.LoginOptBasicAuth(opts.Username, opts.Password), - registry.LoginOptInsecure(opts.InsecureSkipVerifyTLS), - } - err := g.client.Login(host, loginopts...) - if err != nil { - return nil, "", fmt.Errorf("failed to login: %w", err) - } - defer g.client.Logout(host) - } - } - - pullOpts := []registry.PullOption{ - registry.PullOptWithChart(true), - registry.PullOptIgnoreMissingProv(true), - } - - result, err := g.client.Pull(u.String(), pullOpts...) - if err != nil { - return nil, "", err - } - - return io.NopCloser(bytes.NewReader(result.Chart.Data)), opts.URI, nil -} - -func (g *ociGetter) resolveURI(ref, version string) (*url.URL, error) { - var tag string - var err error - - // Evaluate whether an explicit version has been provided. Otherwise, determine version to use - _, errSemVer := semver.NewVersion(version) - if errSemVer == nil { - tag = version - } else { - // Retrieve list of repository tags - tags, err := g.client.Tags(ref) - if err != nil { - return nil, err - } - if len(tags) == 0 { - return nil, fmt.Errorf("no tags found in provided repository: %s", ref) - } - - spew.Dump(tags) - // Determine if version provided - // If empty, try to get the highest available tag - // If exact version, try to find it - // If semver constraint string, try to find a match - tag, err = registry.GetTagMatchingVersionOrConstraint(tags, version) - if err != nil { - return nil, err - } - } - - u, err := url.Parse(ref) - if err != nil { - return nil, err - } - u.Path = fmt.Sprintf("%s:%s", u.Path, tag) - - return u, err -} - -func isOCI(url string) bool { - return strings.HasPrefix(url, "oci://") -} diff --git a/internal/helm/getter/tgz.go b/internal/helm/getter/tgz.go deleted file mode 100644 index 1601cdf..0000000 --- a/internal/helm/getter/tgz.go +++ /dev/null @@ -1,28 +0,0 @@ -package getter - -import ( - "fmt" - "io" - "strings" -) - -var _ Getter = (*tgzGetter)(nil) - -type tgzGetter struct{} - -func (g *tgzGetter) Get(opts GetOptions) (io.ReadCloser, string, error) { - if !isTGZ(opts.URI) { - return nil, "", fmt.Errorf("uri '%s' is not a valid .tgz ref", opts.URI) - } - - dat, err := fetchStream(opts) - if err != nil { - return nil, "", err - } - - return dat, opts.URI, nil -} - -func isTGZ(url string) bool { - return strings.HasSuffix(url, ".tgz") || strings.HasSuffix(url, ".tar.gz") -} diff --git a/internal/helm/repo/helpers.go b/internal/helm/repo/helpers.go deleted file mode 100644 index 7ca8917..0000000 --- a/internal/helm/repo/helpers.go +++ /dev/null @@ -1,24 +0,0 @@ -package repo - -import ( - "net/url" - "path" -) - -// URLJoin joins a base URL to one or more path components. -// -// It's like filepath.Join for URLs. If the baseURL is pathish, this will still -// perform a join. -// -// If the URL is unparsable, this returns an error. -func URLJoin(baseURL string, paths ...string) (string, error) { - u, err := url.Parse(baseURL) - if err != nil { - return "", err - } - // We want path instead of filepath because path always uses /. - all := []string{u.Path} - all = append(all, paths...) - u.Path = path.Join(all...) - return u.String(), nil -} diff --git a/internal/helm/repo/index.go b/internal/helm/repo/index.go deleted file mode 100644 index 7cfe5ec..0000000 --- a/internal/helm/repo/index.go +++ /dev/null @@ -1,189 +0,0 @@ -package repo - -import ( - "fmt" - "path" - "path/filepath" - "sort" - "time" - - "github.com/Masterminds/semver/v3" -) - -var indexPath = "index.yaml" - -// APIVersionV1 is the v1 API version for index and repository files. -const APIVersionV1 = "v1" - -var ( - // ErrNoAPIVersion indicates that an API version was not specified. - ErrNoAPIVersion = fmt.Errorf("no API version specified") - // ErrNoChartVersion indicates that a chart with the given version is not found. - ErrNoChartVersion = fmt.Errorf("no chart version found") - // ErrNoChartName indicates that a chart with the given name is not found. - ErrNoChartName = fmt.Errorf("no chart name found") - // ErrEmptyIndexYaml indicates that the content of index.yaml is empty. - ErrEmptyIndexYaml = fmt.Errorf("empty index.yaml file") -) - -// ChartVersions is a list of versioned chart references. -// Implements a sorter on Version. -type ChartVersions []*ChartVersion - -// Len returns the length. -func (c ChartVersions) Len() int { return len(c) } - -// Swap swaps the position of two items in the versions slice. -func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] } - -// Less returns true if the version of entry a is less than the version of entry b. -func (c ChartVersions) Less(a, b int) bool { - // Failed parse pushes to the back. - i, err := semver.NewVersion(c[a].Version) - if err != nil { - return true - } - j, err := semver.NewVersion(c[b].Version) - if err != nil { - return false - } - return i.LessThan(j) -} - -// IndexFile represents the index file in a chart repository -type IndexFile struct { - // This is used ONLY for validation against chartmuseum's index files and is discarded after validation. - ServerInfo map[string]interface{} `json:"serverInfo,omitempty"` - APIVersion string `json:"apiVersion"` - Generated time.Time `json:"generated"` - Entries map[string]ChartVersions `json:"entries"` - PublicKeys []string `json:"publicKeys,omitempty"` - - // Annotations are additional mappings uninterpreted by Helm. They are made available for - // other applications to add information to the index file. - Annotations map[string]string `json:"annotations,omitempty"` -} - -// NewIndexFile initializes an index. -func NewIndexFile() *IndexFile { - return &IndexFile{ - APIVersion: APIVersionV1, - Generated: time.Now(), - Entries: map[string]ChartVersions{}, - PublicKeys: []string{}, - } -} - -// MustAdd adds a file to the index -// This can leave the index in an unsorted state -func (i IndexFile) MustAdd(md *Metadata, filename, baseURL, digest string) error { - if i.Entries == nil { - return fmt.Errorf("entries not initialized") - } - - if md.APIVersion == "" { - md.APIVersion = APIVersionV1 - } - - u := filename - if baseURL != "" { - _, file := filepath.Split(filename) - var err error - u, err = URLJoin(baseURL, file) - if err != nil { - u = path.Join(baseURL, file) - } - } - cr := &ChartVersion{ - URLs: []string{u}, - Metadata: md, - Digest: digest, - Created: time.Now(), - } - ee := i.Entries[md.Name] - i.Entries[md.Name] = append(ee, cr) - return nil -} - -// Has returns true if the index has an entry for a chart with the given name and exact version. -func (i IndexFile) Has(name, version string) bool { - _, err := i.Get(name, version) - return err == nil -} - -// SortEntries sorts the entries by version in descending order. -// -// In canonical form, the individual version records should be sorted so that -// the most recent release for every version is in the 0th slot in the -// Entries.ChartVersions array. That way, tooling can predict the newest -// version without needing to parse SemVers. -func (i IndexFile) SortEntries() { - for _, versions := range i.Entries { - sort.Sort(sort.Reverse(versions)) - } -} - -// Get returns the ChartVersion for the given name. -// -// If version is empty, this will return the chart with the latest stable version, -// prerelease versions will be skipped. -func (i IndexFile) Get(name, version string) (*ChartVersion, error) { - vs, ok := i.Entries[name] - if !ok { - return nil, ErrNoChartName - } - if len(vs) == 0 { - return nil, ErrNoChartVersion - } - - var constraint *semver.Constraints - if version == "" { - constraint, _ = semver.NewConstraint("*") - } else { - var err error - constraint, err = semver.NewConstraint(version) - if err != nil { - return nil, err - } - } - - // when customer input exact version, check whether have exact match one first - if len(version) != 0 { - for _, ver := range vs { - if version == ver.Version { - return ver, nil - } - } - } - - for _, ver := range vs { - test, err := semver.NewVersion(ver.Version) - if err != nil { - continue - } - - if constraint.Check(test) { - return ver, nil - } - } - return nil, fmt.Errorf("no chart version found for %s-%s", name, version) -} - -// Merge merges the given index file into this index. -// -// This merges by name and version. -// -// If one of the entries in the given index does _not_ already exist, it is added. -// In all other cases, the existing record is preserved. -// -// This can leave the index in an unsorted state -func (i *IndexFile) Merge(f *IndexFile) { - for _, cvs := range f.Entries { - for _, cv := range cvs { - if !i.Has(cv.Name, cv.Version) { - e := i.Entries[cv.Name] - i.Entries[cv.Name] = append(e, cv) - } - } - } -} diff --git a/internal/helm/repo/load.go b/internal/helm/repo/load.go deleted file mode 100644 index e2868fd..0000000 --- a/internal/helm/repo/load.go +++ /dev/null @@ -1,39 +0,0 @@ -package repo - -import ( - "github.com/krateoplatformops/unstructured-runtime/pkg/logging" - "sigs.k8s.io/yaml" -) - -// loadIndex loads an index file and does minimal validity checking. -// -// The source parameter is only used for logging. -// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. -func Load(data []byte, source string, log logging.Logger) (*IndexFile, error) { - i := &IndexFile{} - - if len(data) == 0 { - return i, ErrEmptyIndexYaml - } - - if err := yaml.UnmarshalStrict(data, i); err != nil { - return i, err - } - - for name, cvs := range i.Entries { - for idx := len(cvs) - 1; idx >= 0; idx-- { - if cvs[idx] == nil { - log.Debug("skipping invalid entry", "chart", name, "source", source, "reason", "empty entry") - continue - } - if cvs[idx].APIVersion == "" { - cvs[idx].APIVersion = APIVersionV1 - } - } - } - i.SortEntries() - if i.APIVersion == "" { - return i, ErrNoAPIVersion - } - return i, nil -} diff --git a/internal/helm/repo/types.go b/internal/helm/repo/types.go deleted file mode 100644 index 098f9bd..0000000 --- a/internal/helm/repo/types.go +++ /dev/null @@ -1,105 +0,0 @@ -package repo - -import "time" - -// Maintainer describes a Chart maintainer. -type Maintainer struct { - // Name is a user name or organization name - Name string `json:"name,omitempty"` - // Email is an optional email address to contact the named maintainer - Email string `json:"email,omitempty"` - // URL is an optional URL to an address for the named maintainer - URL string `json:"url,omitempty"` -} - -// Metadata for a Chart file. This models the structure of a Chart.yaml file. -type Metadata struct { - // The name of the chart. Required. - Name string `json:"name,omitempty"` - // The URL to a relevant project page, git repo, or contact person - Home string `json:"home,omitempty"` - // Source is the URL to the source code of this chart - Sources []string `json:"sources,omitempty"` - // A SemVer 2 conformant version string of the chart. Required. - Version string `json:"version,omitempty"` - // A one-sentence description of the chart - Description string `json:"description,omitempty"` - // A list of string keywords - Keywords []string `json:"keywords,omitempty"` - // A list of name and URL/email address combinations for the maintainer(s) - Maintainers []*Maintainer `json:"maintainers,omitempty"` - // The URL to an icon file. - Icon string `json:"icon,omitempty"` - // The API Version of this chart. Required. - APIVersion string `json:"apiVersion,omitempty"` - // The condition to check to enable chart - Condition string `json:"condition,omitempty"` - // The tags to check to enable chart - Tags string `json:"tags,omitempty"` - // The version of the application enclosed inside of this chart. - AppVersion string `json:"appVersion,omitempty"` - // Whether or not this chart is deprecated - Deprecated bool `json:"deprecated,omitempty"` - // Annotations are additional mappings uninterpreted by Helm, - // made available for inspection by other applications. - Annotations map[string]string `json:"annotations,omitempty"` - // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - KubeVersion string `json:"kubeVersion,omitempty"` - // Dependencies are a list of dependencies for a chart. - Dependencies []*Dependency `json:"dependencies,omitempty"` - // Specifies the chart type: application or library - Type string `json:"type,omitempty"` -} - -// Dependency describes a chart upon which another chart depends. -// -// Dependencies can be used to express developer intent, or to capture the state -// of a chart. -type Dependency struct { - // Name is the name of the dependency. - // - // This must mach the name in the dependency's Chart.yaml. - Name string `json:"name"` - // Version is the version (range) of this chart. - // - // A lock file will always produce a single version, while a dependency - // may contain a semantic version range. - Version string `json:"version,omitempty"` - // The URL to the repository. - // - // Appending `index.yaml` to this string should result in a URL that can be - // used to fetch the repository index. - Repository string `json:"repository"` - // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) - Condition string `json:"condition,omitempty"` - // Tags can be used to group charts for enabling/disabling together - Tags []string `json:"tags,omitempty"` - // Enabled bool determines if chart should be loaded - Enabled bool `json:"enabled,omitempty"` - // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a - // string or pair of child/parent sublist items. - ImportValues []interface{} `json:"import-values,omitempty"` - // Alias usable alias to be used for the chart - Alias string `json:"alias,omitempty"` -} - -// Lock is a lock file for dependencies. -// -// It represents the state that the dependencies should be in. -type Lock struct { - // Generated is the date the lock file was last generated. - Generated time.Time `json:"generated"` - // Digest is a hash of the dependencies in Chart.yaml. - Digest string `json:"digest"` - // Dependencies is the list of dependencies that this lock file has locked. - Dependencies []*Dependency `json:"dependencies"` -} - -// ChartVersion represents a chart entry in the IndexFile -type ChartVersion struct { - *Metadata - URLs []string `json:"urls"` - Created time.Time `json:"created,omitempty"` - Removed bool `json:"removed,omitempty"` - Digest string `json:"digest,omitempty"` -} diff --git a/internal/helmclient/client.go b/internal/helmclient/client.go deleted file mode 100644 index 2f2cc0e..0000000 --- a/internal/helmclient/client.go +++ /dev/null @@ -1,1231 +0,0 @@ -package helmclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "reflect" - "strings" - - "helm.sh/helm/v3/pkg/storage/driver" - "k8s.io/apimachinery/pkg/api/errors" - - "github.com/spf13/pflag" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/downloader" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/registry" - "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/repo" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - - helmgetter "github.com/krateoplatformops/composition-dynamic-controller/internal/helm/getter" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -var storage = repo.File{} - -const ( - defaultCachePath = "/tmp/.helmcache" - defaultRepositoryConfigPath = "/tmp/.helmrepo" - defaultConfigPath = "/tmp/.helmconfig" - DefaultRegistryConfigPath = "/tmp" -) - -// New returns a new Helm client with the provided options -func New(options *Options) (Client, error) { - settings := cli.New() - - err := setEnvSettings(&options, settings) - if err != nil { - return nil, err - } - - return newClient(options, settings.RESTClientGetter(), settings) -} - -// NewClientFromKubeConf returns a new Helm client constructed with the provided kubeconfig & RESTClient (optional) options. -func NewClientFromKubeConf(options *KubeConfClientOptions, restClientOpts ...RESTClientOption) (Client, error) { - settings := cli.New() - if options.KubeConfig == nil { - return nil, fmt.Errorf("kubeconfig missing") - } - - clientGetter := NewRESTClientGetter(options.Namespace, options.KubeConfig, nil, restClientOpts...) - - if options.KubeContext != "" { - settings.KubeContext = options.KubeContext - } - - return newClient(options.Options, clientGetter, settings) -} - -// NewClientFromRestConf returns a new Helm client constructed with the provided REST config options. -func NewClientFromRestConf(options *RestConfClientOptions) (Client, error) { - settings := cli.New() - - clientGetter := NewRESTClientGetter(options.Namespace, nil, options.RestConfig) - - return newClient(options.Options, clientGetter, settings) -} - -func NewCachedClientFromRestConf(options *RestConfClientOptions, clientset CachedClientsInterface) (Client, error) { - settings := cli.New() - - clientGetter := NewCachedRESTClientGetter(options.Namespace, nil, options.RestConfig, clientset) - - return newClient(options.Options, clientGetter, settings) -} - -// newClient is used by both NewClientFromKubeConf and NewClientFromRestConf -// and returns a new Helm client via the provided options and REST config. -func newClient(options *Options, clientGetter genericclioptions.RESTClientGetter, settings *cli.EnvSettings) (Client, error) { - err := setEnvSettings(&options, settings) - if err != nil { - return nil, err - } - - debugLog := options.DebugLog - if debugLog == nil { - debugLog = func(format string, v ...interface{}) { - log.Printf(format, v...) - } - } - - if options.Output == nil { - options.Output = os.Stdout - } - - actionConfig := new(action.Configuration) - err = actionConfig.Init( - clientGetter, - settings.Namespace(), - os.Getenv("HELM_DRIVER"), - debugLog, - ) - if err != nil { - return nil, err - } - - registryClient, err := registry.NewClient( - registry.ClientOptDebug(settings.Debug), - registry.ClientOptCredentialsFile(settings.RegistryConfig), - ) - - if err != nil { - return nil, err - } - actionConfig.RegistryClient = registryClient - - return &HelmClient{ - Settings: settings, - Providers: getter.All(settings), - storage: &storage, - ActionConfig: actionConfig, - linting: options.Linting, - DebugLog: debugLog, - output: options.Output, - RegistryAuth: options.RegistryAuth, - }, nil -} - -// setEnvSettings sets the client's environment settings based on the provided client configuration. -func setEnvSettings(ppOptions **Options, settings *cli.EnvSettings) error { - if *ppOptions == nil { - *ppOptions = &Options{ - RepositoryConfig: defaultRepositoryConfigPath, - RepositoryCache: defaultCachePath, - RegistryConfig: filepath.Join(DefaultRegistryConfigPath, registry.CredentialsFileBasename), - Linting: true, - } - } - - options := *ppOptions - - // set the namespace with this ugly workaround because cli.EnvSettings.namespace is private - // thank you helm! - if options.Namespace != "" { - pflags := pflag.NewFlagSet("", pflag.ContinueOnError) - settings.AddFlags(pflags) - err := pflags.Parse([]string{"-n", options.Namespace}) - if err != nil { - return err - } - } - - if options.RepositoryConfig == "" { - options.RepositoryConfig = defaultRepositoryConfigPath - } - - if options.RepositoryCache == "" { - options.RepositoryCache = defaultCachePath - } - - settings.RepositoryCache = options.RepositoryCache - settings.RepositoryConfig = options.RepositoryConfig - settings.Debug = options.Debug - - if options.RegistryConfig != "" { - settings.RegistryConfig = options.RegistryConfig - } - - return nil -} - -// AddOrUpdateChartRepo adds or updates the provided helm chart repository. -func (c *HelmClient) AddOrUpdateChartRepo(entry repo.Entry) error { - chartRepo, err := repo.NewChartRepository(&entry, c.Providers) - if err != nil { - return err - } - - chartRepo.CachePath = c.Settings.RepositoryCache - - _, err = chartRepo.DownloadIndexFile() - if err != nil { - return err - } - - if c.storage.Has(entry.Name) { - c.DebugLog("WARNING: repository name %q already exists", entry.Name) - return nil - } - - c.storage.Update(&entry) - err = c.storage.WriteFile(c.Settings.RepositoryConfig, 0o644) - if err != nil { - return err - } - - return nil -} - -// UpdateChartRepos updates the list of chart repositories stored in the client's cache. -func (c *HelmClient) UpdateChartRepos() error { - for _, entry := range c.storage.Repositories { - chartRepo, err := repo.NewChartRepository(entry, c.Providers) - if err != nil { - return err - } - - chartRepo.CachePath = c.Settings.RepositoryCache - _, err = chartRepo.DownloadIndexFile() - if err != nil { - return err - } - - c.storage.Update(entry) - } - - return c.storage.WriteFile(c.Settings.RepositoryConfig, 0o644) -} - -// InstallOrUpgradeChart installs or upgrades the provided chart and returns the corresponding release. -// Namespace and other context is provided via the helmclient.Options struct when instantiating a client. -func (c *HelmClient) InstallOrUpgradeChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) { - // exists, err := c.chartExists(spec) - // if err != nil { - // return nil, err - // } - - // if exists { - // return c.upgrade(ctx, spec, opts) - // } - - // Fixes #7002 - Support reading values from STDIN for `upgrade` command - // Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice - - // If a release does not exist, install it. - histClient := action.NewHistory(c.ActionConfig) - histClient.Max = 1 - versions, err := histClient.Run(spec.ReleaseName) - if err == driver.ErrReleaseNotFound || isReleaseUninstalled(versions) { - // fmt.Println("Release not present. Installing it now.") - // Only print this to stdout for table output - instClient := action.NewInstall(c.ActionConfig) - instClient.CreateNamespace = spec.CreateNamespace - instClient.Force = spec.Force - instClient.DryRun = spec.DryRun - instClient.DisableHooks = spec.DisableHooks - instClient.SkipCRDs = spec.SkipCRDs - instClient.Timeout = spec.Timeout - instClient.Wait = spec.Wait - instClient.WaitForJobs = spec.WaitForJobs - instClient.Namespace = spec.Namespace - instClient.Atomic = spec.Atomic - instClient.SubNotes = spec.SubNotes - instClient.Description = spec.Description - instClient.DependencyUpdate = spec.DependencyUpdate - - if isReleaseUninstalled(versions) { - instClient.Replace = true - } - - // rel, err := runInstall(args, instClient, valueOpts, out) - rel, err := c.install(ctx, spec, opts) - if err != nil { - return nil, err - } - return rel, nil - } else if err != nil { - return nil, err - } - - return c.upgrade(ctx, spec, opts) -} - -// InstallChart installs the provided chart and returns the corresponding release. -// Namespace and other context is provided via the helmclient.Options struct when instantiating a client. -func (c *HelmClient) InstallChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) { - return c.install(ctx, spec, opts) -} - -func isReleaseUninstalled(versions []*release.Release) bool { - return len(versions) > 0 && versions[len(versions)-1].Info.Status == release.StatusUninstalled -} - -// UpgradeChart upgrades the provided chart and returns the corresponding release. -// Namespace and other context is provided via the helmclient.Options struct when instantiating a client. -func (c *HelmClient) UpgradeChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) { - // fmt.Println("UpgradeChart called") - // Fixes #7002 - Support reading values from STDIN for `upgrade` command - // Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice - if spec.Install { - // If a release does not exist, install it. - histClient := action.NewHistory(c.ActionConfig) - histClient.Max = 1 - versions, err := histClient.Run(spec.ReleaseName) - if err == driver.ErrReleaseNotFound || isReleaseUninstalled(versions) { - // fmt.Println("Release not present. Installing it now.") - // Only print this to stdout for table output - instClient := action.NewInstall(c.ActionConfig) - instClient.CreateNamespace = spec.CreateNamespace - instClient.Force = spec.Force - instClient.DryRun = spec.DryRun - instClient.DisableHooks = spec.DisableHooks - instClient.SkipCRDs = spec.SkipCRDs - instClient.Timeout = spec.Timeout - instClient.Wait = spec.Wait - instClient.WaitForJobs = spec.WaitForJobs - instClient.Namespace = spec.Namespace - instClient.Atomic = spec.Atomic - instClient.SubNotes = spec.SubNotes - instClient.Description = spec.Description - instClient.DependencyUpdate = spec.DependencyUpdate - - if isReleaseUninstalled(versions) { - instClient.Replace = true - } - - // rel, err := runInstall(args, instClient, valueOpts, out) - rel, err := c.install(ctx, spec, opts) - if err != nil { - return nil, err - } - return rel, nil - } else if err != nil { - return nil, err - } - } - - return c.upgrade(ctx, spec, opts) -} - -// ListDeployedReleases lists all deployed releases. -// Namespace and other context is provided via the helmclient.Options struct when instantiating a client. -func (c *HelmClient) ListDeployedReleases() ([]*release.Release, error) { - return c.listReleases(action.ListDeployed) -} - -// ListReleasesByStateMask lists all releases filtered by stateMask. -// Namespace and other context is provided via the helmclient.Options struct when instantiating a client. -func (c *HelmClient) ListReleasesByStateMask(states action.ListStates) ([]*release.Release, error) { - return c.listReleases(states) -} - -// GetReleaseValues returns the (optionally, all computed) values for the specified release. -func (c *HelmClient) GetReleaseValues(name string, allValues bool) (map[string]interface{}, error) { - return c.getReleaseValues(name, allValues) -} - -// GetRelease returns a release specified by name. -func (c *HelmClient) GetRelease(name string) (*release.Release, error) { - return c.getRelease(name) -} - -// RollbackRelease implicitly rolls back a release to the last revision. -func (c *HelmClient) RollbackRelease(spec *ChartSpec) error { - return c.rollbackRelease(spec) -} - -// UninstallRelease uninstalls the provided release -func (c *HelmClient) UninstallRelease(spec *ChartSpec) error { - return c.uninstallRelease(spec) -} - -// UninstallReleaseByName uninstalls a release identified by the provided 'name'. -func (c *HelmClient) UninstallReleaseByName(name string) error { - return c.uninstallReleaseByName(name) -} - -// install installs the provided chart. -// Optionally lints the chart if the linting flag is set. -func (c *HelmClient) install(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) { - client := action.NewInstall(c.ActionConfig) - mergeInstallOptions(spec, client) - - // NameAndChart returns either the TemplateName if set, - // the ReleaseName if set or the generatedName as the first return value. - releaseName, _, err := client.NameAndChart([]string{spec.ReleaseName}) - if err != nil { - return nil, err - } - client.ReleaseName = releaseName - - if client.Version == "" { - client.Version = ">0.0.0-0" - } - - if opts != nil { - if opts.PostRenderer != nil { - client.PostRenderer = opts.PostRenderer - } - } - - helmChart, chartPath, err := c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: &Credentials{ - Username: spec.Username, - Password: spec.Password, - }, - }) - if err != nil { - return nil, err - } - - // helmChart, chartPath, err := c.GetChart(spec.ChartName, &client.ChartPathOptions) - // if err != nil { - // return nil, err - // } - - if helmChart.Metadata.Type != "" && helmChart.Metadata.Type != "application" { - return nil, fmt.Errorf( - "chart %q has an unsupported type and is not installable: %q", - helmChart.Metadata.Name, - helmChart.Metadata.Type, - ) - } - - helmChart, err = updateDependencies(helmChart, &client.ChartPathOptions, chartPath, c, client.DependencyUpdate, spec) - if err != nil { - return nil, err - } - - p := getter.All(c.Settings) - values, err := spec.GetValuesMap(p) - if err != nil { - return nil, err - } - - if c.linting { - err = c.lint(chartPath, values) - if err != nil { - return nil, err - } - } - - rel, err := client.RunWithContext(ctx, helmChart, values) - if err != nil { - return rel, err - } - - c.DebugLog("release installed successfully: %s/%s-%s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version) - - return rel, nil -} - -// upgrade upgrades a chart and CRDs. -// Optionally lints the chart if the linting flag is set. -func (c *HelmClient) upgrade(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) { - client := action.NewUpgrade(c.ActionConfig) - mergeUpgradeOptions(spec, client) - client.Install = true - - if client.Version == "" { - client.Version = ">0.0.0-0" - } - - if opts != nil { - if opts.PostRenderer != nil { - client.PostRenderer = opts.PostRenderer - } - } - - helmChart, chartPath, err := c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: &Credentials{ - Username: spec.Username, - Password: spec.Password, - }, - }) - if err != nil { - return nil, err - } - - // helmChart, chartPath, err := c.GetChart(spec.ChartName, &client.ChartPathOptions) - // if err != nil { - // return nil, err - // } - - helmChart, err = updateDependencies(helmChart, &client.ChartPathOptions, chartPath, c, client.DependencyUpdate, spec) - if err != nil { - return nil, err - } - - p := getter.All(c.Settings) - values, err := spec.GetValuesMap(p) - if err != nil { - return nil, err - } - - if c.linting { - err = c.lint(chartPath, values) - if err != nil { - return nil, err - } - } - - if !spec.SkipCRDs && spec.UpgradeCRDs { - c.DebugLog("upgrading crds") - err = c.upgradeCRDs(ctx, helmChart) - if err != nil { - return nil, err - } - } - - upgradedRelease, upgradeErr := client.RunWithContext(ctx, spec.ReleaseName, helmChart, values) - if upgradeErr != nil { - resultErr := upgradeErr - if upgradedRelease == nil && opts != nil && opts.RollBack != nil { - rollbackErr := opts.RollBack.RollbackRelease(spec) - if rollbackErr != nil { - resultErr = fmt.Errorf("release failed, rollback failed: release error: %w, rollback error: %v", upgradeErr, rollbackErr) - } else { - resultErr = fmt.Errorf("release failed, rollback succeeded: release error: %w", upgradeErr) - } - } - c.DebugLog("release upgrade failed: %s", resultErr) - return nil, resultErr - } - - c.DebugLog("release upgraded successfully: %s/%s-%s", upgradedRelease.Name, upgradedRelease.Chart.Metadata.Name, upgradedRelease.Chart.Metadata.Version) - - return upgradedRelease, nil -} - -// uninstallRelease uninstalls the provided release. -func (c *HelmClient) uninstallRelease(spec *ChartSpec) error { - client := action.NewUninstall(c.ActionConfig) - - mergeUninstallReleaseOptions(spec, client) - client.IgnoreNotFound = true - - resp, err := client.Run(spec.ReleaseName) - - if err != nil { - return err - } - - c.DebugLog("release uninstalled, response: %v", resp) - - return nil -} - -// uninstallReleaseByName uninstalls a release identified by the provided 'name'. -func (c *HelmClient) uninstallReleaseByName(name string) error { - client := action.NewUninstall(c.ActionConfig) - - resp, err := client.Run(name) - if err != nil { - return err - } - - c.DebugLog("release uninstalled, response: %v", resp) - - return nil -} - -// lint lints a chart's values. -func (c *HelmClient) lint(chartPath string, values map[string]interface{}) error { - client := action.NewLint() - - result := client.Run([]string{chartPath}, values) - - for _, err := range result.Errors { - c.DebugLog("Error %s", err) - } - - if len(result.Errors) > 0 { - return fmt.Errorf("linting for chartpath %q failed", chartPath) - } - - return nil -} - -// TemplateChart returns a rendered version of the provided ChartSpec 'spec' by performing a "dry-run" install. -func (c *HelmClient) TemplateChart(spec *ChartSpec, options *HelmTemplateOptions) ([]byte, error) { - client := action.NewInstall(c.ActionConfig) - mergeInstallOptions(spec, client) - - client.DryRun = true - client.ReleaseName = spec.ReleaseName - client.Replace = true // Skip the name check - client.ClientOnly = false - client.IncludeCRDs = true - client.DryRunOption = "server" - - if options != nil { - client.KubeVersion = options.KubeVersion - client.APIVersions = options.APIVersions - } - - // NameAndChart returns either the TemplateName if set, - // the ReleaseName if set or the generatedName as the first return value. - releaseName, _, err := client.NameAndChart([]string{spec.ReleaseName}) - if err != nil { - return nil, err - } - client.ReleaseName = releaseName - - if client.Version == "" { - client.Version = ">0.0.0-0" - } - - var creds *Credentials - - if spec.Username != "" && spec.Password != "" { - creds = &Credentials{ - Username: spec.Username, - Password: spec.Password, - } - } - - helmChart, chartPath, err := c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: creds, - }) - if err != nil { - return nil, err - } - - // helmChart, chartPath, err := c.GetChart(spec.ChartName, &client.ChartPathOptions) - // if err != nil { - // return nil, err - // } - - if helmChart.Metadata.Type != "" && helmChart.Metadata.Type != "application" { - return nil, fmt.Errorf( - "chart %q has an unsupported type and is not installable: %q", - helmChart.Metadata.Name, - helmChart.Metadata.Type, - ) - } - - helmChart, err = updateDependencies(helmChart, &client.ChartPathOptions, chartPath, c, client.DependencyUpdate, spec) - if err != nil { - return nil, err - } - - p := getter.All(c.Settings) - values, err := spec.GetValuesMap(p) - if err != nil { - return nil, err - } - - out := new(bytes.Buffer) - rel, err := client.Run(helmChart, values) - - // We ignore a potential error here because, when the --debug flag was specified, - // we always want to print the YAML, even if it is not valid. The error is still returned afterwards. - if rel != nil { - var manifests bytes.Buffer - fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) - if !client.DisableHooks { - for _, m := range rel.Hooks { - fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest) - } - } - - // if we have a list of files to render, then check that each of the - // provided files exists in the chart. - fmt.Fprintf(out, "%s", manifests.String()) - } - - return out.Bytes(), err -} - -// TemplateChartRaw returns a rendered version of the provided ChartSpec 'spec' by performing a "dry-run" install. -func (c *HelmClient) TemplateChartRaw(spec *ChartSpec, options *HelmTemplateOptions) (*release.Release, error) { - client := action.NewInstall(c.ActionConfig) - mergeInstallOptions(spec, client) - - client.DryRun = true - client.ReleaseName = spec.ReleaseName - client.Replace = true // Skip the name check - client.ClientOnly = false - client.IncludeCRDs = true - client.DryRunOption = "server" - - if options != nil { - client.KubeVersion = options.KubeVersion - client.APIVersions = options.APIVersions - } - - // NameAndChart returns either the TemplateName if set, - // the ReleaseName if set or the generatedName as the first return value. - releaseName, _, err := client.NameAndChart([]string{spec.ReleaseName}) - if err != nil { - return nil, err - } - client.ReleaseName = releaseName - - if client.Version == "" { - client.Version = ">0.0.0-0" - } - - var creds *Credentials - creds = nil - if spec.Username != "" && spec.Password != "" { - creds = &Credentials{ - Username: spec.Username, - Password: spec.Password, - } - } - - helmChart, chartPath, err := c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: creds, - }) - if err != nil { - return nil, err - } - - if helmChart.Metadata.Type != "" && helmChart.Metadata.Type != "application" { - return nil, fmt.Errorf( - "chart %q has an unsupported type and is not installable: %q", - helmChart.Metadata.Name, - helmChart.Metadata.Type, - ) - } - - helmChart, err = updateDependencies(helmChart, &client.ChartPathOptions, chartPath, c, client.DependencyUpdate, spec) - if err != nil { - return nil, err - } - - p := getter.All(c.Settings) - values, err := spec.GetValuesMap(p) - if err != nil { - return nil, err - } - - rel, err := client.Run(helmChart, values) - - return rel, err -} - -// LintChart fetches a chart using the provided ChartSpec 'spec' and lints it's values. -func (c *HelmClient) LintChart(spec *ChartSpec) error { - _, chartPath, err := c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: &Credentials{ - Username: spec.Username, - Password: spec.Password, - }, - }) - if err != nil { - return err - } - - // _, chartPath, err := c.GetChart(spec.ChartName, &action.ChartPathOptions{ - // Version: spec.Version, - // }) - // if err != nil { - // return err - // } - - p := getter.All(c.Settings) - values, err := spec.GetValuesMap(p) - if err != nil { - return err - } - - return c.lint(chartPath, values) -} - -// SetDebugLog set's a Helm client's DebugLog to the desired 'debugLog'. -func (c *HelmClient) SetDebugLog(debugLog action.DebugLog) { - c.DebugLog = debugLog -} - -// ListReleaseHistory lists the last 'max' number of entries -// in the history of the release identified by 'name'. -func (c *HelmClient) ListReleaseHistory(name string, max int) ([]*release.Release, error) { - client := action.NewHistory(c.ActionConfig) - - client.Max = max - - return client.Run(name) -} - -// upgradeCRDs upgrades the CRDs of the provided chart. -func (c *HelmClient) upgradeCRDs(ctx context.Context, chartInstance *chart.Chart) error { - cfg, err := c.ActionConfig.RESTClientGetter.ToRESTConfig() - if err != nil { - return err - } - - k8sClient, err := clientset.NewForConfig(cfg) - if err != nil { - return err - } - - for _, crd := range chartInstance.CRDObjects() { - if err := c.upgradeCRD(ctx, k8sClient, crd); err != nil { - return err - } - c.DebugLog("CRD %s upgraded successfully for chart: %s", crd.Name, chartInstance.Metadata.Name) - } - - return nil -} - -// upgradeCRD upgrades the CRD 'crd' using the provided k8s client. -func (c *HelmClient) upgradeCRD(ctx context.Context, k8sClient *clientset.Clientset, crd chart.CRD) error { - // use this ugly detour to parse the crdYaml to a CustomResourceDefinitions-Object because direct - // yaml-unmarshalling does not find the correct keys - jsonCRD, err := yaml.ToJSON(crd.File.Data) - if err != nil { - return err - } - - var typeMeta metav1.TypeMeta - err = json.Unmarshal(jsonCRD, &typeMeta) - if err != nil { - return err - } - - switch typeMeta.APIVersion { - default: - return fmt.Errorf("WARNING: failed to upgrade CRD %q: unsupported api-version %q", crd.Name, typeMeta.APIVersion) - case "apiextensions.k8s.io/v1beta1": - return c.upgradeCRDV1Beta1(ctx, k8sClient, jsonCRD) - case "apiextensions.k8s.io/v1": - return c.upgradeCRDV1(ctx, k8sClient, jsonCRD) - } -} - -func (c *HelmClient) createCRDV1(ctx context.Context, cl *clientset.Clientset, crd *v1.CustomResourceDefinition) error { - if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}); err != nil { - return err - } - - c.DebugLog("create ran successful for CRD: %s", crd.Name) - return nil -} - -func (c *HelmClient) createCRDV1Beta1(ctx context.Context, cl *clientset.Clientset, crd *v1beta1.CustomResourceDefinition) error { - if _, err := cl.ApiextensionsV1beta1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}); err != nil { - return err - } - - c.DebugLog("create ran successful for CRD: %s", crd.Name) - return nil -} - -// upgradeCRDV1Beta1 upgrades a CRD of the v1beta1 API version using the provided k8s client and CRD yaml. -func (c *HelmClient) upgradeCRDV1Beta1(ctx context.Context, cl *clientset.Clientset, rawCRD []byte) error { - var crdObj v1beta1.CustomResourceDefinition - if err := json.Unmarshal(rawCRD, &crdObj); err != nil { - return err - } - - existingCRDObj, err := cl.ApiextensionsV1beta1().CustomResourceDefinitions().Get(ctx, crdObj.Name, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return c.createCRDV1Beta1(ctx, cl, &crdObj) - } - - return err - } - - // Check that the storage version does not change through the update. - oldStorageVersion := v1beta1.CustomResourceDefinitionVersion{} - - for _, oldVersion := range existingCRDObj.Spec.Versions { - if oldVersion.Storage { - oldStorageVersion = oldVersion - } - } - - i := 0 - - for _, newVersion := range crdObj.Spec.Versions { - if newVersion.Storage { - i++ - if newVersion.Name != oldStorageVersion.Name { - return fmt.Errorf("ERROR: storage version of CRD %q changed, aborting upgrade", crdObj.Name) - } - } - if i > 1 { - return fmt.Errorf("ERROR: more than one storage version set on CRD %q, aborting upgrade", crdObj.Name) - } - } - - if reflect.DeepEqual(existingCRDObj.Spec.Versions, crdObj.Spec.Versions) { - c.DebugLog("INFO: new version of CRD %q contains no changes, skipping upgrade", crdObj.Name) - return nil - } - - crdObj.ResourceVersion = existingCRDObj.ResourceVersion - if _, err := cl.ApiextensionsV1beta1().CustomResourceDefinitions().Update(ctx, &crdObj, metav1.UpdateOptions{DryRun: []string{"All"}}); err != nil { - return err - } - c.DebugLog("upgrade ran successful for CRD (dry run): %s", crdObj.Name) - - if _, err = cl.ApiextensionsV1beta1().CustomResourceDefinitions().Update(ctx, &crdObj, metav1.UpdateOptions{}); err != nil { - return err - } - c.DebugLog("upgrade ran successful for CRD: %s", crdObj.Name) - - return nil -} - -// upgradeCRDV1Beta1 upgrades a CRD of the v1 API version using the provided k8s client and CRD yaml. -func (c *HelmClient) upgradeCRDV1(ctx context.Context, cl *clientset.Clientset, rawCRD []byte) error { - var crdObj v1.CustomResourceDefinition - if err := json.Unmarshal(rawCRD, &crdObj); err != nil { - return err - } - - existingCRDObj, err := cl.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crdObj.Name, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return c.createCRDV1(ctx, cl, &crdObj) - } - - return err - } - - // Check to ensure that no previously existing API version is deleted through the upgrade. - if len(existingCRDObj.Spec.Versions) > len(crdObj.Spec.Versions) { - c.DebugLog("WARNING: new version of CRD %q would remove an existing API version, skipping upgrade", crdObj.Name) - return nil - } - - // Check that the storage version does not change through the update. - oldStorageVersion := v1.CustomResourceDefinitionVersion{} - - for _, oldVersion := range existingCRDObj.Spec.Versions { - if oldVersion.Storage { - oldStorageVersion = oldVersion - } - } - - i := 0 - - for _, newVersion := range crdObj.Spec.Versions { - if newVersion.Storage { - i++ - if newVersion.Name != oldStorageVersion.Name { - return fmt.Errorf("ERROR: storage version of CRD %q changed, aborting upgrade", crdObj.Name) - } - } - if i > 1 { - return fmt.Errorf("ERROR: more than one storage version set on CRD %q, aborting upgrade", crdObj.Name) - } - } - - if reflect.DeepEqual(existingCRDObj.Spec.Versions, crdObj.Spec.Versions) { - c.DebugLog("INFO: new version of CRD %q contains no changes, skipping upgrade", crdObj.Name) - return nil - } - - crdObj.ResourceVersion = existingCRDObj.ResourceVersion - if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crdObj, metav1.UpdateOptions{DryRun: []string{"All"}}); err != nil { - return err - } - c.DebugLog("upgrade ran successful for CRD (dry run): %s", crdObj.Name) - - if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, &crdObj, metav1.UpdateOptions{}); err != nil { - return err - } - c.DebugLog("upgrade ran successful for CRD: %s", crdObj.Name) - - return nil -} - -func isOci(chartName string) bool { - return strings.HasPrefix(chartName, "oci://") -} - -func (c *HelmClient) buildLoginOpts() []registry.LoginOption { - if c.RegistryAuth != nil { - return []registry.LoginOption{ - registry.LoginOptBasicAuth(c.RegistryAuth.Username, c.RegistryAuth.Password), - registry.LoginOptInsecure(c.RegistryAuth.InsecureSkipTLSverify), - } - } - return nil -} - -type Credentials struct { - Username string `json:"username"` - Password string `json:"password"` -} - -type ChartInfo struct { - // Url: oci or tgz full url - // +immutable - Url string `json:"url"` - // Version: desired chart version - // +kubebuilder:validation:Optional - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Version is immutable" - // +kubebuilder:validation:MaxLength=20 - Version string `json:"version,omitempty"` - // Repo: helm repo name (for helm repo urls only) - // +kubebuilder:validation:Optional - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Repo is immutable" - // +kubebuilder:validation:MaxLength=256 - Repo string `json:"repo,omitempty"` - - // InsecureSkipVerifyTLS: skip tls verification - // +optional - InsecureSkipVerifyTLS bool `json:"insecureSkipVerifyTLS,omitempty"` - - // Credentials: credentials for private repos - // +optional - Credentials *Credentials `json:"credentials,omitempty"` -} - -func (c *HelmClient) GetChartV2(spec *ChartInfo) (*chart.Chart, string, error) { - - opts := helmgetter.GetOptions{ - URI: spec.Url, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipVerifyTLS, - } - - if spec.Credentials != nil { - opts.Username = spec.Credentials.Username - opts.Password = spec.Credentials.Password - opts.PassCredentialsAll = true - } - - bChart, chartPath, err := helmgetter.Get(opts) - if err != nil { - return nil, "", fmt.Errorf("failed to get chart %q: %w", spec.Url, err) - } - - helmChart, err := loader.LoadArchive(bChart) - if err != nil { - return nil, "", err - } - - if helmChart.Metadata.Deprecated { - c.DebugLog("WARNING: This chart (%q) is deprecated", helmChart.Metadata.Name) - } - - return helmChart, chartPath, err -} - -// chartExists checks whether a chart is already installed -// in a namespace or not based on the provided chart spec. -// Note that this function only considers the contained chart name and namespace. -func (c *HelmClient) chartExists(spec *ChartSpec) (bool, error) { - releases, err := c.listReleases(action.ListAll) - if err != nil { - return false, err - } - - for _, r := range releases { - if r.Name == spec.ReleaseName && r.Namespace == spec.Namespace { - return true, nil - } - } - - return false, nil -} - -// listReleases lists all releases that match the given state. -func (c *HelmClient) listReleases(state action.ListStates) ([]*release.Release, error) { - listClient := action.NewList(c.ActionConfig) - listClient.StateMask = state - - return listClient.Run() -} - -// getReleaseValues returns the values for the provided release 'name'. -// If allValues = true is specified, all computed values are returned. -func (c *HelmClient) getReleaseValues(name string, allValues bool) (map[string]interface{}, error) { - getReleaseValuesClient := action.NewGetValues(c.ActionConfig) - - getReleaseValuesClient.AllValues = allValues - - return getReleaseValuesClient.Run(name) -} - -// getRelease returns a release matching the provided 'name'. -func (c *HelmClient) getRelease(name string) (*release.Release, error) { - getReleaseClient := action.NewGet(c.ActionConfig) - - return getReleaseClient.Run(name) -} - -// rollbackRelease implicitly rolls back a release to the last revision. -func (c *HelmClient) rollbackRelease(spec *ChartSpec) error { - client := action.NewRollback(c.ActionConfig) - - mergeRollbackOptions(spec, client) - - return client.Run(spec.ReleaseName) -} - -// updateDependencies checks dependencies for given helmChart and updates dependencies with metadata if dependencyUpdate is true. returns updated HelmChart -func updateDependencies(helmChart *chart.Chart, chartPathOptions *action.ChartPathOptions, chartPath string, c *HelmClient, dependencyUpdate bool, spec *ChartSpec) (*chart.Chart, error) { - if req := helmChart.Metadata.Dependencies; req != nil { - if err := action.CheckDependencies(helmChart, req); err != nil { - if dependencyUpdate { - man := &downloader.Manager{ - ChartPath: chartPath, - Keyring: chartPathOptions.Keyring, - SkipUpdate: false, - Getters: c.Providers, - RepositoryConfig: c.Settings.RepositoryConfig, - RepositoryCache: c.Settings.RepositoryCache, - Out: c.output, - } - if err := man.Update(); err != nil { - return nil, err - } - - helmChart, _, err = c.GetChartV2(&ChartInfo{ - Url: spec.ChartName, - Version: spec.Version, - Repo: spec.Repo, - InsecureSkipVerifyTLS: spec.InsecureSkipTLSverify, - Credentials: &Credentials{ - Username: spec.Username, - Password: spec.Password, - }, - }) - if err != nil { - return nil, err - } - - // helmChart, _, err = c.GetChart(spec.ChartName, chartPathOptions) - // if err != nil { - // return nil, err - // } - - } else { - return nil, err - } - } - } - return helmChart, nil -} - -// mergeRollbackOptions merges values of the provided chart to helm rollback options used by the client. -func mergeRollbackOptions(chartSpec *ChartSpec, rollbackOptions *action.Rollback) { - rollbackOptions.DisableHooks = chartSpec.DisableHooks - rollbackOptions.DryRun = chartSpec.DryRun - rollbackOptions.Timeout = chartSpec.Timeout - rollbackOptions.CleanupOnFail = chartSpec.CleanupOnFail - rollbackOptions.Force = chartSpec.Force - rollbackOptions.MaxHistory = chartSpec.MaxHistory - rollbackOptions.Recreate = chartSpec.Recreate - rollbackOptions.Wait = chartSpec.Wait - rollbackOptions.WaitForJobs = chartSpec.WaitForJobs -} - -// mergeInstallOptions merges values of the provided chart to helm install options used by the client. -func mergeInstallOptions(chartSpec *ChartSpec, installOptions *action.Install) { - installOptions.CreateNamespace = chartSpec.CreateNamespace - installOptions.DisableHooks = chartSpec.DisableHooks - installOptions.Replace = chartSpec.Replace - installOptions.Wait = chartSpec.Wait - installOptions.DependencyUpdate = chartSpec.DependencyUpdate - installOptions.Timeout = chartSpec.Timeout - installOptions.Namespace = chartSpec.Namespace - installOptions.ReleaseName = chartSpec.ReleaseName - installOptions.Version = chartSpec.Version - installOptions.GenerateName = chartSpec.GenerateName - installOptions.NameTemplate = chartSpec.NameTemplate - installOptions.Atomic = chartSpec.Atomic - installOptions.SkipCRDs = chartSpec.SkipCRDs - installOptions.DryRun = chartSpec.DryRun - installOptions.SubNotes = chartSpec.SubNotes - installOptions.WaitForJobs = chartSpec.WaitForJobs - installOptions.DisableOpenAPIValidation = true -} - -// mergeUpgradeOptions merges values of the provided chart to helm upgrade options used by the client. -func mergeUpgradeOptions(chartSpec *ChartSpec, upgradeOptions *action.Upgrade) { - upgradeOptions.Version = chartSpec.Version - upgradeOptions.Namespace = chartSpec.Namespace - upgradeOptions.Timeout = chartSpec.Timeout - upgradeOptions.Wait = chartSpec.Wait - upgradeOptions.DependencyUpdate = chartSpec.DependencyUpdate - upgradeOptions.DisableHooks = chartSpec.DisableHooks - upgradeOptions.Force = chartSpec.Force - upgradeOptions.ResetValues = chartSpec.ResetValues - upgradeOptions.ReuseValues = chartSpec.ReuseValues - upgradeOptions.Recreate = chartSpec.Recreate - upgradeOptions.MaxHistory = chartSpec.MaxHistory - upgradeOptions.Atomic = chartSpec.Atomic - upgradeOptions.CleanupOnFail = chartSpec.CleanupOnFail - upgradeOptions.DryRun = chartSpec.DryRun - upgradeOptions.DryRunOption = chartSpec.DryRunOption - upgradeOptions.SubNotes = chartSpec.SubNotes - upgradeOptions.WaitForJobs = chartSpec.WaitForJobs - upgradeOptions.Install = chartSpec.Install - upgradeOptions.DisableOpenAPIValidation = true -} - -// mergeUninstallReleaseOptions merges values of the provided chart to helm uninstall options used by the client. -func mergeUninstallReleaseOptions(chartSpec *ChartSpec, uninstallReleaseOptions *action.Uninstall) { - uninstallReleaseOptions.DisableHooks = chartSpec.DisableHooks - uninstallReleaseOptions.Timeout = chartSpec.Timeout - uninstallReleaseOptions.DryRun = chartSpec.DryRun - uninstallReleaseOptions.Description = chartSpec.Description - uninstallReleaseOptions.KeepHistory = chartSpec.KeepHistory - uninstallReleaseOptions.Wait = chartSpec.Wait -} diff --git a/internal/helmclient/client_getter.go b/internal/helmclient/client_getter.go deleted file mode 100644 index a7a7926..0000000 --- a/internal/helmclient/client_getter.go +++ /dev/null @@ -1,179 +0,0 @@ -package helmclient - -import ( - "os" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/client-go/discovery" - "k8s.io/client-go/discovery/cached/disk" - "k8s.io/client-go/discovery/cached/memory" - "k8s.io/client-go/rest" - "k8s.io/client-go/restmapper" - "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" -) - -// NewRESTClientGetter returns a RESTClientGetter using the provided 'namespace', 'kubeConfig' and 'restConfig'. -// -// source: https://github.com/helm/helm/issues/6910#issuecomment-601277026 -func NewRESTClientGetter(namespace string, kubeConfig []byte, restConfig *rest.Config, opts ...RESTClientOption) *RESTClientGetter { - return &RESTClientGetter{ - namespace: namespace, - kubeConfig: kubeConfig, - restConfig: restConfig, - opts: opts, - } -} - -// ToRESTConfig returns a REST config build from a given kubeconfig -func (c *RESTClientGetter) ToRESTConfig() (*rest.Config, error) { - if c.restConfig != nil { - return c.restConfig, nil - } - - return clientcmd.RESTConfigFromKubeConfig(c.kubeConfig) - -} - -// ToDiscoveryClient returns a CachedDiscoveryInterface that can be used as a discovery client. -func (c *RESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { - config, err := c.ToRESTConfig() - if err != nil { - return nil, err - } - - // The more API groups exist, the more discovery requests need to be made. - // Given 25 API groups with about one version each, discovery needs to make 50 requests. - // This setting is only used for discovery. - config.Burst = 100 - - for _, fn := range c.opts { - fn(config) - } - - discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config) - return memory.NewMemCacheClient(discoveryClient), nil -} - -func (c *RESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) { - discoveryClient, err := c.ToDiscoveryClient() - if err != nil { - return nil, err - } - - mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) - expander := restmapper.NewShortcutExpander(mapper, discoveryClient, nil) - return expander, nil -} - -func (c *RESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - // use the standard defaults for this client command - // DEPRECATED: remove and replace with something more accurate - loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig - - overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} - overrides.Context.Namespace = c.namespace - - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) -} - -type CachedClientsInterface interface { - DiscoveryClient() discovery.CachedDiscoveryInterface - RESTMapper() meta.RESTMapper -} - -var _ CachedClientsInterface = &cachedClients{} - -type cachedClients struct { - discoveryClient discovery.CachedDiscoveryInterface - _RESTMapper meta.RESTMapper -} - -func NewCachedClients(cfg *rest.Config) (CachedClientsInterface, error) { - dir, err := os.MkdirTemp("", "helmclient") - if err != nil { - return cachedClients{}, err - } - cachedDiscovery, err := disk.NewCachedDiscoveryClientForConfig(cfg, dir, "", 0) - if err != nil { - return cachedClients{}, err - } - - cachedDiscovery.Invalidate() - - mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery) - mapper.Reset() - expander := restmapper.NewShortcutExpander(mapper, cachedDiscovery, nil) - - return cachedClients{ - discoveryClient: cachedDiscovery, - _RESTMapper: expander, - }, nil -} - -func (c cachedClients) DiscoveryClient() discovery.CachedDiscoveryInterface { - return c.discoveryClient -} - -func (c cachedClients) RESTMapper() meta.RESTMapper { - return c._RESTMapper -} - -var _ clientcmd.ClientConfig = &namespaceClientConfig{} - -type namespaceClientConfig struct { - namespace string -} - -func (c namespaceClientConfig) RawConfig() (clientcmdapi.Config, error) { - return clientcmdapi.Config{}, nil -} - -func (c namespaceClientConfig) ClientConfig() (*rest.Config, error) { - return nil, nil -} - -func (c namespaceClientConfig) Namespace() (string, bool, error) { - return c.namespace, false, nil -} - -func (c namespaceClientConfig) ConfigAccess() clientcmd.ConfigAccess { - return nil -} - -// NewCachedRESTClientGetter returns a RESTClientGetter using the provided 'namespace', 'kubeConfig', and 'restConfig', -// and uses cached clients for discovery and REST mapping to improve performance and reduce API server load. -func NewCachedRESTClientGetter(namespace string, kubeConfig []byte, restConfig *rest.Config, clients CachedClientsInterface, opts ...RESTClientOption) *CachedRESTClientGetter { - return &CachedRESTClientGetter{ - kubeConfig: kubeConfig, - restConfig: restConfig, - discoveryClient: clients.DiscoveryClient(), - restMapper: clients.RESTMapper(), - namespaceConfig: &namespaceClientConfig{namespace: namespace}, - opts: opts, - } -} - -// ToRESTConfig returns a REST config build from a given kubeconfig -func (c *CachedRESTClientGetter) ToRESTConfig() (*rest.Config, error) { - if c.restConfig != nil { - return c.restConfig, nil - } - - return clientcmd.RESTConfigFromKubeConfig(c.kubeConfig) - -} - -// ToDiscoveryClient returns a CachedDiscoveryInterface that can be used as a discovery client. -func (c *CachedRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { - return c.discoveryClient, nil -} - -func (c *CachedRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) { - return c.restMapper, nil -} - -func (c *CachedRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { - return c.namespaceConfig -} diff --git a/internal/helmclient/client_test.go b/internal/helmclient/client_test.go deleted file mode 100644 index eb2fdd9..0000000 --- a/internal/helmclient/client_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package helmclient - -import ( - "bytes" - "context" - "helm.sh/helm/v3/pkg/chartutil" - - "helm.sh/helm/v3/pkg/action" - - "helm.sh/helm/v3/pkg/repo" - "k8s.io/client-go/rest" -) - -func ExampleNew() { - var outputBuffer bytes.Buffer - - opt := &Options{ - Namespace: "default", // Change this to the namespace you wish the client to operate in. - RepositoryCache: "/tmp/.helmcache", - RepositoryConfig: "/tmp/.helmrepo", - Debug: true, - Linting: true, - DebugLog: func(format string, v ...interface{}) {}, - Output: &outputBuffer, // Not mandatory, leave open for default os.Stdout - } - - helmClient, err := New(opt) - if err != nil { - panic(err) - } - _ = helmClient -} - -func ExampleNewClientFromRestConf() { - opt := &RestConfClientOptions{ - Options: &Options{ - Namespace: "default", // Change this to the namespace you wish the client to operate in. - RepositoryCache: "/tmp/.helmcache", - RepositoryConfig: "/tmp/.helmrepo", - Debug: true, - Linting: true, // Change this to false if you don't want linting. - DebugLog: func(format string, v ...interface{}) { - // Change this to your own logger. Default is 'log.Printf(format, v...)'. - }, - }, - RestConfig: &rest.Config{}, - } - - helmClient, err := NewClientFromRestConf(opt) - if err != nil { - panic(err) - } - _ = helmClient -} - -func ExampleNewClientFromKubeConf() { - opt := &KubeConfClientOptions{ - Options: &Options{ - Namespace: "default", // Change this to the namespace you wish to install the chart in. - RepositoryCache: "/tmp/.helmcache", - RepositoryConfig: "/tmp/.helmrepo", - Debug: true, - Linting: true, // Change this to false if you don't want linting. - DebugLog: func(format string, v ...interface{}) { - // Change this to your own logger. Default is 'log.Printf(format, v...)'. - }, - }, - KubeContext: "", - KubeConfig: []byte{}, - } - - helmClient, err := NewClientFromKubeConf(opt, Burst(100), Timeout(10e9)) - if err != nil { - panic(err) - } - _ = helmClient -} - -func ExampleHelmClient_AddOrUpdateChartRepo_public() { - // Define a public chart repository. - chartRepo := repo.Entry{ - Name: "stable", - URL: "https://charts.helm.sh/stable", - } - - // Add a chart-repository to the client. - if err := helmClient.AddOrUpdateChartRepo(chartRepo); err != nil { - panic(err) - } -} - -func ExampleHelmClient_AddOrUpdateChartRepo_private() { - // Define a private chart repository - chartRepo := repo.Entry{ - Name: "stable", - URL: "https://private-chartrepo.somedomain.com", - Username: "foo", - Password: "bar", - // Since helm 3.6.1 it is necessary to pass 'PassCredentialsAll = true'. - PassCredentialsAll: true, - } - - // Add a chart-repository to the client. - if err := helmClient.AddOrUpdateChartRepo(chartRepo); err != nil { - panic(err) - } -} - -func ExampleHelmClient_InstallOrUpgradeChart() { - // Define the chart to be installed - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - // Install a chart release. - // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { - panic(err) - } -} - -func ExampleHelmClient_InstallOrUpgradeChart_useChartDirectory() { - // Use an unpacked chart directory. - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "/path/to/stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { - panic(err) - } -} - -func ExampleHelmClient_InstallOrUpgradeChart_useLocalChartArchive() { - // Use an archived chart directory. - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "/path/to/stable/etcd-operator.tar.gz", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { - panic(err) - } -} - -func ExampleHelmClient_InstallOrUpgradeChart_useURL() { - // Use an archived chart directory via URL. - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "http://helm.whatever.com/repo/etcd-operator.tar.gz", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, nil); err != nil { - panic(err) - } -} - -func ExampleHelmClient_InstallOrUpgradeChart_useDefaultRollBackStrategy() { - // Define the chart to be installed - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - // Use the default rollback strategy offer by HelmClient (revert to the previous version). - opts := GenericHelmOptions{ - RollBack: helmClient, - } - - // Install a chart release. - // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, &opts); err != nil { - panic(err) - } -} - -type customRollBack struct { - HelmClient -} - -var _ RollBack = &customRollBack{} - -func (c customRollBack) RollbackRelease(spec *ChartSpec) error { - client := action.NewRollback(c.ActionConfig) - - client.Force = true - - return client.Run(spec.ReleaseName) -} - -func ExampleHelmClient_InstallOrUpgradeChart_useCustomRollBackStrategy() { - // Define the chart to be installed - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - // Use a custom rollback strategy (customRollBack needs to implement RollBack). - rollBacker := customRollBack{} - - opts := GenericHelmOptions{ - RollBack: rollBacker, - } - - // Install a chart release. - // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. - if _, err := helmClient.InstallOrUpgradeChart(context.Background(), &chartSpec, &opts); err != nil { - panic(err) - } -} - -func ExampleHelmClient_LintChart() { - // Define a chart with custom values to be tested. - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - ValuesYaml: `deployments: - etcdOperator: true - backupOperator: false`, - } - - if err := helmClient.LintChart(&chartSpec); err != nil { - panic(err) - } -} - -func ExampleHelmClient_TemplateChart() { - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - ValuesYaml: `deployments: - etcdOperator: true - backupOperator: false`, - } - - options := &HelmTemplateOptions{ - KubeVersion: &chartutil.KubeVersion{ - Version: "v1.23.10", - Major: "1", - Minor: "23", - }, - APIVersions: []string{ - "helm.sh/v1/Test", - }, - } - - _, err := helmClient.TemplateChart(&chartSpec, options) - if err != nil { - panic(err) - } -} - -func ExampleHelmClient_UpdateChartRepos() { - // Update the list of chart repositories. - if err := helmClient.UpdateChartRepos(); err != nil { - panic(err) - } -} - -func ExampleHelmClient_UninstallRelease() { - // Define the released chart to be installed. - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - Wait: true, - DryRun: true, - KeepHistory: true, - } - - // Uninstall the chart release. - // Note that helmclient.Options.Namespace should ideally match the namespace in chartSpec.Namespace. - if err := helmClient.UninstallRelease(&chartSpec); err != nil { - panic(err) - } -} - -func ExampleHelmClient_UninstallReleaseByName() { - // Uninstall a release by name. - if err := helmClient.UninstallReleaseByName("etcd-operator"); err != nil { - panic(err) - } -} - -func ExampleHelmClient_ListDeployedReleases() { - // List all deployed releases. - if _, err := helmClient.ListDeployedReleases(); err != nil { - panic(err) - } -} - -func ExampleHelmClient_GetReleaseValues() { - // Get the values of a deployed release. - if _, err := helmClient.GetReleaseValues("etcd-operator", true); err != nil { - panic(err) - } -} - -func ExampleHelmClient_GetRelease() { - // Get specific details of a deployed release. - if _, err := helmClient.GetRelease("etcd-operator"); err != nil { - panic(err) - } -} - -func ExampleHelmClient_RollbackRelease() { - // Define the released chart to be installed - chartSpec := ChartSpec{ - ReleaseName: "etcd-operator", - ChartName: "stable/etcd-operator", - Namespace: "default", - UpgradeCRDs: true, - Wait: true, - } - - // Rollback to the previous version of the release. - if err := helmClient.RollbackRelease(&chartSpec); err != nil { - return - } -} diff --git a/internal/helmclient/doc.go b/internal/helmclient/doc.go deleted file mode 100644 index c87b84c..0000000 --- a/internal/helmclient/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -package helmclient - -var helmClient *HelmClient diff --git a/internal/helmclient/interface.go b/internal/helmclient/interface.go deleted file mode 100644 index aa30f4c..0000000 --- a/internal/helmclient/interface.go +++ /dev/null @@ -1,39 +0,0 @@ -package helmclient - -import ( - "context" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/repo" -) - -// Client holds the method signatures for a Helm client. -// NOTE: This is an interface to allow for mocking in tests. -type Client interface { - AddOrUpdateChartRepo(entry repo.Entry) error - UpdateChartRepos() error - InstallOrUpgradeChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) - InstallChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) - UpgradeChart(ctx context.Context, spec *ChartSpec, opts *GenericHelmOptions) (*release.Release, error) - ListDeployedReleases() ([]*release.Release, error) - ListReleasesByStateMask(action.ListStates) ([]*release.Release, error) - GetRelease(name string) (*release.Release, error) - // RollBack is an interface to abstract a rollback action. - RollBack - GetReleaseValues(name string, allValues bool) (map[string]interface{}, error) - UninstallRelease(spec *ChartSpec) error - UninstallReleaseByName(name string) error - TemplateChart(spec *ChartSpec, options *HelmTemplateOptions) ([]byte, error) - TemplateChartRaw(spec *ChartSpec, options *HelmTemplateOptions) (*release.Release, error) - LintChart(spec *ChartSpec) error - SetDebugLog(debugLog action.DebugLog) - ListReleaseHistory(name string, max int) ([]*release.Release, error) - // GetChart(chartName string, chartPathOptions *action.ChartPathOptions) (*chart.Chart, string, error) - GetChartV2(spec *ChartInfo) (*chart.Chart, string, error) //adds authentication and support for tgz and non oci compositions. -} - -type RollBack interface { - RollbackRelease(spec *ChartSpec) error -} diff --git a/internal/helmclient/mock/interface.go b/internal/helmclient/mock/interface.go deleted file mode 100644 index c7b0588..0000000 --- a/internal/helmclient/mock/interface.go +++ /dev/null @@ -1,324 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: interface.go - -// Package mockhelmclient is a generated GoMock package. -package mockhelmclient - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - helmclient "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - action "helm.sh/helm/v3/pkg/action" - chart "helm.sh/helm/v3/pkg/chart" - release "helm.sh/helm/v3/pkg/release" - repo "helm.sh/helm/v3/pkg/repo" -) - -// MockClient is a mock of Client interface. -type MockClient struct { - ctrl *gomock.Controller - recorder *MockClientMockRecorder -} - -// MockClientMockRecorder is the mock recorder for MockClient. -type MockClientMockRecorder struct { - mock *MockClient -} - -// NewMockClient creates a new mock instance. -func NewMockClient(ctrl *gomock.Controller) *MockClient { - mock := &MockClient{ctrl: ctrl} - mock.recorder = &MockClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockClient) EXPECT() *MockClientMockRecorder { - return m.recorder -} - -// AddOrUpdateChartRepo mocks base method. -func (m *MockClient) AddOrUpdateChartRepo(entry repo.Entry) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddOrUpdateChartRepo", entry) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddOrUpdateChartRepo indicates an expected call of AddOrUpdateChartRepo. -func (mr *MockClientMockRecorder) AddOrUpdateChartRepo(entry interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateChartRepo", reflect.TypeOf((*MockClient)(nil).AddOrUpdateChartRepo), entry) -} - -// GetChart mocks base method. -func (m *MockClient) GetChart(chartName string, chartPathOptions *action.ChartPathOptions) (*chart.Chart, string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetChart", chartName, chartPathOptions) - ret0, _ := ret[0].(*chart.Chart) - ret1, _ := ret[1].(string) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// GetChart indicates an expected call of GetChart. -func (mr *MockClientMockRecorder) GetChart(chartName, chartPathOptions interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChart", reflect.TypeOf((*MockClient)(nil).GetChart), chartName, chartPathOptions) -} - -// GetRelease mocks base method. -func (m *MockClient) GetRelease(name string) (*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetRelease", name) - ret0, _ := ret[0].(*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetRelease indicates an expected call of GetRelease. -func (mr *MockClientMockRecorder) GetRelease(name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRelease", reflect.TypeOf((*MockClient)(nil).GetRelease), name) -} - -// GetReleaseValues mocks base method. -func (m *MockClient) GetReleaseValues(name string, allValues bool) (map[string]interface{}, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetReleaseValues", name, allValues) - ret0, _ := ret[0].(map[string]interface{}) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetReleaseValues indicates an expected call of GetReleaseValues. -func (mr *MockClientMockRecorder) GetReleaseValues(name, allValues interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReleaseValues", reflect.TypeOf((*MockClient)(nil).GetReleaseValues), name, allValues) -} - -// InstallChart mocks base method. -func (m *MockClient) InstallChart(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstallChart", ctx, spec, opts) - ret0, _ := ret[0].(*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InstallChart indicates an expected call of InstallChart. -func (mr *MockClientMockRecorder) InstallChart(ctx, spec, opts interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallChart", reflect.TypeOf((*MockClient)(nil).InstallChart), ctx, spec, opts) -} - -// InstallOrUpgradeChart mocks base method. -func (m *MockClient) InstallOrUpgradeChart(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstallOrUpgradeChart", ctx, spec, opts) - ret0, _ := ret[0].(*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InstallOrUpgradeChart indicates an expected call of InstallOrUpgradeChart. -func (mr *MockClientMockRecorder) InstallOrUpgradeChart(ctx, spec, opts interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallOrUpgradeChart", reflect.TypeOf((*MockClient)(nil).InstallOrUpgradeChart), ctx, spec, opts) -} - -// LintChart mocks base method. -func (m *MockClient) LintChart(spec *helmclient.ChartSpec) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LintChart", spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// LintChart indicates an expected call of LintChart. -func (mr *MockClientMockRecorder) LintChart(spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LintChart", reflect.TypeOf((*MockClient)(nil).LintChart), spec) -} - -// ListDeployedReleases mocks base method. -func (m *MockClient) ListDeployedReleases() ([]*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListDeployedReleases") - ret0, _ := ret[0].([]*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListDeployedReleases indicates an expected call of ListDeployedReleases. -func (mr *MockClientMockRecorder) ListDeployedReleases() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDeployedReleases", reflect.TypeOf((*MockClient)(nil).ListDeployedReleases)) -} - -// ListReleaseHistory mocks base method. -func (m *MockClient) ListReleaseHistory(name string, max int) ([]*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListReleaseHistory", name, max) - ret0, _ := ret[0].([]*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListReleaseHistory indicates an expected call of ListReleaseHistory. -func (mr *MockClientMockRecorder) ListReleaseHistory(name, max interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListReleaseHistory", reflect.TypeOf((*MockClient)(nil).ListReleaseHistory), name, max) -} - -// ListReleasesByStateMask mocks base method. -func (m *MockClient) ListReleasesByStateMask(arg0 action.ListStates) ([]*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListReleasesByStateMask", arg0) - ret0, _ := ret[0].([]*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListReleasesByStateMask indicates an expected call of ListReleasesByStateMask. -func (mr *MockClientMockRecorder) ListReleasesByStateMask(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListReleasesByStateMask", reflect.TypeOf((*MockClient)(nil).ListReleasesByStateMask), arg0) -} - -// RollbackRelease mocks base method. -func (m *MockClient) RollbackRelease(spec *helmclient.ChartSpec) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RollbackRelease", spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// RollbackRelease indicates an expected call of RollbackRelease. -func (mr *MockClientMockRecorder) RollbackRelease(spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackRelease", reflect.TypeOf((*MockClient)(nil).RollbackRelease), spec) -} - -// SetDebugLog mocks base method. -func (m *MockClient) SetDebugLog(debugLog action.DebugLog) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetDebugLog", debugLog) -} - -// SetDebugLog indicates an expected call of SetDebugLog. -func (mr *MockClientMockRecorder) SetDebugLog(debugLog interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDebugLog", reflect.TypeOf((*MockClient)(nil).SetDebugLog), debugLog) -} - -// TemplateChart mocks base method. -func (m *MockClient) TemplateChart(spec *helmclient.ChartSpec, options *helmclient.HelmTemplateOptions) ([]byte, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "TemplateChart", spec, options) - ret0, _ := ret[0].([]byte) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// TemplateChart indicates an expected call of TemplateChart. -func (mr *MockClientMockRecorder) TemplateChart(spec, options interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TemplateChart", reflect.TypeOf((*MockClient)(nil).TemplateChart), spec, options) -} - -// UninstallRelease mocks base method. -func (m *MockClient) UninstallRelease(spec *helmclient.ChartSpec) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UninstallRelease", spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// UninstallRelease indicates an expected call of UninstallRelease. -func (mr *MockClientMockRecorder) UninstallRelease(spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallRelease", reflect.TypeOf((*MockClient)(nil).UninstallRelease), spec) -} - -// UninstallReleaseByName mocks base method. -func (m *MockClient) UninstallReleaseByName(name string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UninstallReleaseByName", name) - ret0, _ := ret[0].(error) - return ret0 -} - -// UninstallReleaseByName indicates an expected call of UninstallReleaseByName. -func (mr *MockClientMockRecorder) UninstallReleaseByName(name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallReleaseByName", reflect.TypeOf((*MockClient)(nil).UninstallReleaseByName), name) -} - -// UpdateChartRepos mocks base method. -func (m *MockClient) UpdateChartRepos() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateChartRepos") - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateChartRepos indicates an expected call of UpdateChartRepos. -func (mr *MockClientMockRecorder) UpdateChartRepos() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChartRepos", reflect.TypeOf((*MockClient)(nil).UpdateChartRepos)) -} - -// UpgradeChart mocks base method. -func (m *MockClient) UpgradeChart(ctx context.Context, spec *helmclient.ChartSpec, opts *helmclient.GenericHelmOptions) (*release.Release, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpgradeChart", ctx, spec, opts) - ret0, _ := ret[0].(*release.Release) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpgradeChart indicates an expected call of UpgradeChart. -func (mr *MockClientMockRecorder) UpgradeChart(ctx, spec, opts interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeChart", reflect.TypeOf((*MockClient)(nil).UpgradeChart), ctx, spec, opts) -} - -// MockRollBack is a mock of RollBack interface. -type MockRollBack struct { - ctrl *gomock.Controller - recorder *MockRollBackMockRecorder -} - -// MockRollBackMockRecorder is the mock recorder for MockRollBack. -type MockRollBackMockRecorder struct { - mock *MockRollBack -} - -// NewMockRollBack creates a new mock instance. -func NewMockRollBack(ctrl *gomock.Controller) *MockRollBack { - mock := &MockRollBack{ctrl: ctrl} - mock.recorder = &MockRollBackMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRollBack) EXPECT() *MockRollBackMockRecorder { - return m.recorder -} - -// RollbackRelease mocks base method. -func (m *MockRollBack) RollbackRelease(spec *helmclient.ChartSpec) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RollbackRelease", spec) - ret0, _ := ret[0].(error) - return ret0 -} - -// RollbackRelease indicates an expected call of RollbackRelease. -func (mr *MockRollBackMockRecorder) RollbackRelease(spec interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackRelease", reflect.TypeOf((*MockRollBack)(nil).RollbackRelease), spec) -} diff --git a/internal/helmclient/mock/mock_test.go b/internal/helmclient/mock/mock_test.go deleted file mode 100644 index 6cdfca6..0000000 --- a/internal/helmclient/mock/mock_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package mockhelmclient - -import ( - "testing" - - "github.com/golang/mock/gomock" - "helm.sh/helm/v3/pkg/release" -) - -var mockedRelease = release.Release{Name: "test"} - -// TestHelmClientInterfaces performs checks on the clients interface functions. -func TestHelmClientInterfaces(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockClient := NewMockClient(ctrl) - if mockClient == nil { - t.Fail() - } - - t.Run("UpdateChartRepos", func(t *testing.T) { - mockClient.EXPECT().UpdateChartRepos().Return(nil) - err := mockClient.UpdateChartRepos() - if err != nil { - panic(err) - } - }) - - t.Run("ListReleases", func(t *testing.T) { - mockClient.EXPECT().ListDeployedReleases().Return([]*release.Release{&mockedRelease}, nil) - r, err := mockClient.ListDeployedReleases() - if err != nil { - panic(err) - } - if len(r) == 0 { - panic(err) - } - }) - - t.Run("GetRelease", func(t *testing.T) { - mockClient.EXPECT().GetRelease(mockedRelease.Name).Return(&release.Release{Name: mockedRelease.Name}, nil) - r, err := mockClient.GetRelease(mockedRelease.Name) - if err != nil { - panic(err) - } - if r == nil { - panic(err) - } - }) - - t.Run("GetReleaseValues", func(t *testing.T) { - m := make(map[string]interface{}) - mockClient.EXPECT().GetReleaseValues(mockedRelease.Name, true). - Return(m, nil) - rv, err := mockClient.GetReleaseValues(mockedRelease.Name, true) - if err != nil { - panic(err) - } - if rv == nil { - panic(err) - } - }) -} diff --git a/internal/helmclient/spec.go b/internal/helmclient/spec.go deleted file mode 100644 index b253d07..0000000 --- a/internal/helmclient/spec.go +++ /dev/null @@ -1,27 +0,0 @@ -package helmclient - -import ( - "github.com/pkg/errors" - "helm.sh/helm/v3/pkg/getter" - "sigs.k8s.io/yaml" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient/values" -) - -// GetValuesMap returns the merged mapped out values of a chart, -// using both ValuesYaml and ValuesOptions -func (spec *ChartSpec) GetValuesMap(p getter.Providers) (map[string]interface{}, error) { - valuesYaml := map[string]interface{}{} - - err := yaml.Unmarshal([]byte(spec.ValuesYaml), &valuesYaml) - if err != nil { - return nil, errors.Wrap(err, "Failed to Parse ValuesYaml") - } - - valuesOptions, err := spec.ValuesOptions.MergeValues(p) - if err != nil { - return nil, errors.Wrap(err, "Failed to Parse ValuesOptions") - } - - return values.MergeMaps(valuesYaml, valuesOptions), nil -} diff --git a/internal/helmclient/types.go b/internal/helmclient/types.go deleted file mode 100644 index 949e435..0000000 --- a/internal/helmclient/types.go +++ /dev/null @@ -1,231 +0,0 @@ -package helmclient - -import ( - "io" - "time" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/postrender" - "helm.sh/helm/v3/pkg/repo" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient/values" -) - -// Type Guard asserting that HelmClient satisfies the HelmClient interface. -var _ Client = &HelmClient{} - -// KubeConfClientOptions defines the options used for constructing a client via kubeconfig. -type KubeConfClientOptions struct { - *Options - KubeContext string - KubeConfig []byte -} - -// RestConfClientOptions defines the options used for constructing a client via REST config. -type RestConfClientOptions struct { - *Options - RestConfig *rest.Config -} - -type RegistryAuth struct { - Username string - Password string - InsecureSkipTLSverify bool -} - -// Options defines the options of a client. If Output is not set, os.Stdout will be used. -type Options struct { - Namespace string - RepositoryConfig string - RepositoryCache string - Debug bool - Linting bool - DebugLog action.DebugLog - RegistryConfig string - Output io.Writer - RegistryAuth *RegistryAuth -} - -// RESTClientOption is a function that can be used to set the RESTClientOptions of a HelmClient. -type RESTClientOption func(*rest.Config) - -// Timeout specifies the timeout for a RESTClient as a RESTClientOption. -// The default (if unspecified) is 32 seconds. -// See [1] for reference. -// [^1]: https://github.com/kubernetes/client-go/blob/c6bd30b9ec5f668df191bc268c6f550c37726edb/discovery/discovery_client.go#L52 -func Timeout(d time.Duration) RESTClientOption { - return func(r *rest.Config) { - r.Timeout = d - } -} - -// Maximum burst for throttle -// the created RESTClient will use DefaultBurst: 100. -func Burst(v int) RESTClientOption { - return func(r *rest.Config) { - r.Burst = v - } -} - -// RESTClientGetter defines the values of a helm REST client. -type RESTClientGetter struct { - namespace string - kubeConfig []byte - restConfig *rest.Config - - opts []RESTClientOption -} - -// RESTClientGetter defines the values of a helm REST client. -type CachedRESTClientGetter struct { - kubeConfig []byte - restConfig *rest.Config - discoveryClient discovery.CachedDiscoveryInterface - restMapper meta.RESTMapper - namespaceConfig clientcmd.ClientConfig - - opts []RESTClientOption -} - -// HelmClient Client defines the values of a helm client. -type HelmClient struct { - // Settings defines the environment settings of a client. - Settings *cli.EnvSettings - Providers getter.Providers - storage *repo.File - // ActionConfig is the helm action configuration. - ActionConfig *action.Configuration - linting bool - output io.Writer - DebugLog action.DebugLog - RegistryAuth *RegistryAuth -} - -type GenericHelmOptions struct { - PostRenderer postrender.PostRenderer - RollBack RollBack -} - -type HelmTemplateOptions struct { - KubeVersion *chartutil.KubeVersion - // APIVersions defined here will be appended to the default list helm provides - APIVersions chartutil.VersionSet -} - -// ChartSpec defines the values of a helm chart -type ChartSpec struct { - ReleaseName string `json:"release"` - ChartName string `json:"chart"` - Repo string `json:"repo"` - - // Namespace where the chart release is deployed. - // Note that helmclient.Options.Namespace should ideally match the namespace configured here. - Namespace string `json:"namespace"` - // ValuesYaml is the values.yaml content. - // use string instead of map[string]interface{} - // https://github.com/kubernetes-sigs/kubebuilder/issues/528#issuecomment-466449483 - // and https://github.com/kubernetes-sigs/controller-tools/pull/317 - // +optional - ValuesYaml string `json:"valuesYaml,omitempty"` - // Specify values similar to the cli - // +optional - ValuesOptions values.Options `json:"valuesOptions,omitempty"` - // Version of the chart release. - // +optional - Version string `json:"version,omitempty"` - // CreateNamespace indicates whether to create the namespace if it does not exist. - // +optional - CreateNamespace bool `json:"createNamespace,omitempty"` - // DisableHooks indicates whether to disable hooks. - // +optional - DisableHooks bool `json:"disableHooks,omitempty"` - // Replace indicates whether to replace the chart release if it already exists. - // +optional - Replace bool `json:"replace,omitempty"` - // Wait indicates whether to wait for the release to be deployed or not. - // +optional - Wait bool `json:"wait,omitempty"` - // WaitForJobs indicates whether to wait for completion of release Jobs before marking the release as successful. - // 'Wait' has to be specified for this to take effect. - // The timeout may be specified via the 'Timeout' field. - WaitForJobs bool `json:"waitForJobs,omitempty"` - // DependencyUpdate indicates whether to update the chart release if the dependencies have changed. - // +optional - DependencyUpdate bool `json:"dependencyUpdate,omitempty"` - // Timeout configures the time to wait for any individual Kubernetes operation (like Jobs for hooks). - // +optional - Timeout time.Duration `json:"timeout,omitempty"` - // GenerateName indicates that the release name should be generated. - // +optional - GenerateName bool `json:"generateName,omitempty"` - // NameTemplate is the template used to generate the release name if GenerateName is configured. - // +optional - NameTemplate string `json:"nameTemplate,omitempty"` - // Atomic indicates whether to install resources atomically. - // 'Wait' will automatically be set to true when using Atomic. - // +optional - Atomic bool `json:"atomic,omitempty"` - // SkipCRDs indicates whether to skip CRDs during installation. - // +optional - SkipCRDs bool `json:"skipCRDs,omitempty"` - // Upgrade indicates whether to perform a CRD upgrade during installation. - // +optional - UpgradeCRDs bool `json:"upgradeCRDs,omitempty"` - // SubNotes indicates whether to print sub-notes. - // +optional - SubNotes bool `json:"subNotes,omitempty"` - // Force indicates whether to force the operation. - // +optional - Force bool `json:"force,omitempty"` - // ResetValues indicates whether to reset the values.yaml file during installation. - // +optional - ResetValues bool `json:"resetValues,omitempty"` - // ReuseValues indicates whether to reuse the values.yaml file during installation. - // +optional - ReuseValues bool `json:"reuseValues,omitempty"` - // Recreate indicates whether to recreate the release if it already exists. - // +optional - Recreate bool `json:"recreate,omitempty"` - // MaxHistory limits the maximum number of revisions saved per release. - // +optional - MaxHistory int `json:"maxHistory,omitempty"` - // CleanupOnFail indicates whether to cleanup the release on failure. - // +optional - CleanupOnFail bool `json:"cleanupOnFail,omitempty"` - // DryRun indicates whether to perform a dry run. - // +optional - DryRun bool `json:"dryRun,omitempty"` - // DryRunOptions specifies the options for a dry run. - // +optional - DryRunOption string `json:"dryRunOptions,omitempty"` - // Description specifies a custom description for the uninstalled release - // +optional - Description string `json:"description,omitempty"` - // KeepHistory indicates whether to retain or purge the release history during uninstall - // +optional - KeepHistory bool `json:"keepHistory,omitempty"` - // Install is a purely informative flag that indicates whether this upgrade was done in "install" mode. - // - // Applications may use this to determine whether this Upgrade operation was done as part of a - // pure upgrade (Upgrade.Install == false) or as part of an install-or-upgrade operation - // (Upgrade.Install == true). - // - // Setting this to `true` will NOT cause `Upgrade` to perform an install if the release does not exist. - // That process must be handled by creating an Install action directly. See cmd/upgrade.go for an - // example of how this flag is used. - Install bool `json:"install,omitempty"` - - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - - // +optional - InsecureSkipTLSverify bool `json:"insecureSkipTLSverify,omitempty"` -} diff --git a/internal/helmclient/values/options.go b/internal/helmclient/values/options.go deleted file mode 100644 index f489976..0000000 --- a/internal/helmclient/values/options.go +++ /dev/null @@ -1,124 +0,0 @@ -package values - -import ( - "io" - "net/url" - "os" - "strings" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/strvals" -) - -// Options captures the different ways to specify values -// +kubebuilder:object:generate:=true -type Options struct { - ValueFiles []string // -f/--values - StringValues []string // --set-string - Values []string // --set - FileValues []string // --set-file - JSONValues []string // --set-json -} - -// MergeValues merges values from files specified via -f/--values and directly -// via --set-json, --set, --set-string, or --set-file, marshaling them to YAML -func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range opts.ValueFiles { - currentMap := map[string]interface{}{} - - bytes, err := readFile(filePath, p) - if err != nil { - return nil, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return nil, errors.Wrapf(err, "failed to parse %s", filePath) - } - // Merge with the previous map - base = MergeMaps(base, currentMap) - } - - // User specified a value via --set-json - for _, value := range opts.JSONValues { - if err := strvals.ParseJSON(value, base); err != nil { - return nil, errors.Errorf("failed parsing --set-json data %s", value) - } - } - - // User specified a value via --set - for _, value := range opts.Values { - if err := strvals.ParseInto(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set data") - } - } - - // User specified a value via --set-string - for _, value := range opts.StringValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-string data") - } - } - - // User specified a value via --set-file - for _, value := range opts.FileValues { - reader := func(rs []rune) (interface{}, error) { - bytes, err := readFile(string(rs), p) - if err != nil { - return nil, err - } - return string(bytes), err - } - if err := strvals.ParseIntoFile(value, base, reader); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-file data") - } - } - - return base, nil -} - -func MergeMaps(a, b map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}, len(a)) - for k, v := range a { - out[k] = v - } - for k, v := range b { - if v, ok := v.(map[string]interface{}); ok { - if bv, ok := out[k]; ok { - if bv, ok := bv.(map[string]interface{}); ok { - out[k] = MergeMaps(bv, v) - continue - } - } - } - out[k] = v - } - return out -} - -// readFile load a file from stdin, the local directory, or a remote file with a url. -func readFile(filePath string, p getter.Providers) ([]byte, error) { - if strings.TrimSpace(filePath) == "-" { - return io.ReadAll(os.Stdin) - } - u, err := url.Parse(filePath) - if err != nil { - return nil, err - } - - // FIXME: maybe someone handle other protocols like ftp. - g, err := p.ByScheme(u.Scheme) - if err != nil { - return os.ReadFile(filePath) - } - data, err := g.Get(filePath, getter.WithURL(filePath)) - if err != nil { - return nil, err - } - return data.Bytes(), err -} diff --git a/internal/helmclient/values/options_test.go b/internal/helmclient/values/options_test.go deleted file mode 100644 index b8a9fa9..0000000 --- a/internal/helmclient/values/options_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package values - -import ( - "reflect" - "testing" - - "helm.sh/helm/v3/pkg/getter" -) - -func TestMergeValues(t *testing.T) { - nestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "stuff", - }, - } - anotherNestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", - }, - } - flatMap := map[string]interface{}{ - "foo": "bar", - "baz": "stuff", - } - anotherFlatMap := map[string]interface{}{ - "testing": "fun", - } - - testMap := MergeMaps(flatMap, nestedMap) - equal := reflect.DeepEqual(testMap, nestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) - } - - testMap = MergeMaps(nestedMap, flatMap) - equal = reflect.DeepEqual(testMap, flatMap) - if !equal { - t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) - } - - testMap = MergeMaps(nestedMap, anotherNestedMap) - equal = reflect.DeepEqual(testMap, anotherNestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) - } - - testMap = MergeMaps(anotherFlatMap, anotherNestedMap) - expectedMap := map[string]interface{}{ - "testing": "fun", - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", - }, - } - equal = reflect.DeepEqual(testMap, expectedMap) - if !equal { - t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) - } -} - -func TestReadFile(t *testing.T) { - var p getter.Providers - filePath := "%a.txt" - _, err := readFile(filePath, p) - if err == nil { - t.Errorf("Expected error when has special strings") - } -} diff --git a/internal/tools/helmchart/archive/getter.go b/internal/tools/archive/getter.go similarity index 96% rename from internal/tools/helmchart/archive/getter.go rename to internal/tools/archive/getter.go index ef8668a..5805439 100644 --- a/internal/tools/helmchart/archive/getter.go +++ b/internal/tools/archive/getter.go @@ -8,7 +8,6 @@ import ( compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" "github.com/krateoplatformops/unstructured-runtime/pkg/logging" "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,6 +23,11 @@ type CompositionDefinitionInfo struct { GVR schema.GroupVersionResource } +type Auth struct { + Username string + Password string +} + type Info struct { // URL of the helm chart package that is being requested. URL string `json:"url"` @@ -34,8 +38,11 @@ type Info struct { // Repo is the repository name. Repo string `json:"repo,omitempty"` - // RegistryAuth is the credentials to access the registry. - RegistryAuth *helmclient.RegistryAuth `json:"registryAuth,omitempty"` + // Auth is the credentials to access the chart registry, if needed. + Auth *Auth `json:"auth,omitempty"` + + // InsecureSkipTLSverify indicates whether to skip TLS verification. + InsecureSkipTLSverify bool `json:"insecureSkipTLSverify,omitempty"` // CompositionDefinitionInfo is the information about the composition definition. CompositionDefinitionInfo *CompositionDefinitionInfo `json:"compositionDefinitionInfo,omitempty"` @@ -246,11 +253,11 @@ func (g *dynamicGetter) Get(uns *unstructured.Unstructured) (*Info, error) { URL: packageUrl, Version: packageVersion, Repo: repo, - RegistryAuth: &helmclient.RegistryAuth{ - Username: username, - Password: password, - InsecureSkipTLSverify: insecureSkipTLSverify, + Auth: &Auth{ + Username: username, + Password: password, }, + InsecureSkipTLSverify: insecureSkipTLSverify, CompositionDefinitionInfo: &CompositionDefinitionInfo{ Name: compositionDefinition.GetName(), Namespace: compositionDefinition.GetNamespace(), diff --git a/internal/tools/helmchart/archive/getter_test.go b/internal/tools/archive/getter_test.go similarity index 97% rename from internal/tools/helmchart/archive/getter_test.go rename to internal/tools/archive/getter_test.go index ae0f44e..a8a0ae2 100644 --- a/internal/tools/helmchart/archive/getter_test.go +++ b/internal/tools/archive/getter_test.go @@ -1,7 +1,7 @@ //go:build integration // +build integration -package archive_test +package archive import ( "context" @@ -14,7 +14,8 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/gobuffalo/flect" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart/archive" + + // "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/archive" "github.com/krateoplatformops/plumbing/e2e" xenv "github.com/krateoplatformops/plumbing/env" "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" @@ -262,7 +263,7 @@ func testGetterFunctionality(ctx context.Context, t *testing.T, c *envconf.Confi // Test dynamic getter pluralizer := FakePluralizer{} - gt, err := archive.Dynamic(c.Client().RESTConfig(), pluralizer) + gt, err := Dynamic(c.Client().RESTConfig(), pluralizer) require.NoError(t, err, "Should be able to create dynamic getter") // Test the getter @@ -338,7 +339,7 @@ func testStaticGetter(ctx context.Context, t *testing.T, c *envconf.Config) cont for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - getter := archive.Static(tc.chartURL) + getter := Static(tc.chartURL) require.NotNil(t, getter, "Static getter should not be nil") // Test with empty unstructured (static getter ignores input) @@ -349,7 +350,7 @@ func testStaticGetter(ctx context.Context, t *testing.T, c *envconf.Config) cont assert.Equal(t, tc.chartURL, info.URL, "URL should match input") assert.Empty(t, info.Version, "Static getter should not have version") assert.Empty(t, info.Repo, "Static getter should not have repo") - assert.Nil(t, info.RegistryAuth, "Static getter should not have registry auth") + assert.Nil(t, info.Auth, "Static getter should not have registry auth") assert.Nil(t, info.CompositionDefinitionInfo, "Static getter should not have composition definition info") // Test URL type detection @@ -369,7 +370,7 @@ func testStaticGetter(ctx context.Context, t *testing.T, c *envconf.Config) cont func testErrorConditions(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { pluralizer := FakePluralizer{} - getter, err := archive.Dynamic(c.Client().RESTConfig(), pluralizer) + getter, err := Dynamic(c.Client().RESTConfig(), pluralizer) require.NoError(t, err) testCases := []struct { @@ -438,7 +439,7 @@ func testErrorConditions(ctx context.Context, t *testing.T, c *envconf.Config) c func testEdgeCases(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { pluralizer := FakePluralizer{} - getter, err := archive.Dynamic(c.Client().RESTConfig(), pluralizer) + getter, err := Dynamic(c.Client().RESTConfig(), pluralizer) require.NoError(t, err) // Test with nil input @@ -475,7 +476,7 @@ func testEdgeCases(ctx context.Context, t *testing.T, c *envconf.Config) context } for _, tc := range testCases { - info := &archive.Info{URL: tc.url} + info := &Info{URL: tc.url} assert.Equal(t, tc.isOCI, info.IsOCI(), "IsOCI mismatch for %s", tc.url) assert.Equal(t, tc.isTGZ, info.IsTGZ(), "IsTGZ mismatch for %s", tc.url) assert.Equal(t, tc.isHTTP, info.IsHTTP(), "IsHTTP mismatch for %s", tc.url) diff --git a/internal/tools/dynamic/dynamic.go b/internal/tools/dynamic/dynamic.go new file mode 100644 index 0000000..06d9933 --- /dev/null +++ b/internal/tools/dynamic/dynamic.go @@ -0,0 +1,49 @@ +package dynamic + +import ( + xenv "github.com/krateoplatformops/plumbing/env" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/rest" + + "k8s.io/client-go/restmapper" +) + +func NewRESTMapper(rc *rest.Config) (meta.RESTMapper, error) { + if rc == nil && !xenv.TestMode() { + var err error + rc, err = rest.InClusterConfig() + if err != nil { + return nil, err + } + } + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(rc) + if err != nil { + return nil, err + } + + mapper := restmapper.NewDeferredDiscoveryRESTMapper( + memory.NewMemCacheClient(discoveryClient), + ) + + return mapper, nil +} + +func IsNamespaced(mapper meta.RESTMapper, gvk schema.GroupVersionKind) (bool, error) { + if mapper == nil { + return false, nil + } + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return false, err + } + + if mapping.Scope.Name() == meta.RESTScopeNameRoot { + return false, nil + } + + return true, nil +} diff --git a/internal/tools/hasher/hasher.go b/internal/tools/hasher/hasher.go index f80e309..9be432f 100644 --- a/internal/tools/hasher/hasher.go +++ b/internal/tools/hasher/hasher.go @@ -5,12 +5,29 @@ import ( "fmt" "hash" "hash/fnv" + "unsafe" ) type ObjectHash struct { hash.Hash64 } +// Optimization: Zero-copy conversion from string to []byte. +// This prevents allocating a copy of the string just to read it. +func stringToBytes(s string) []byte { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +func (h *ObjectHash) SumHashStrings(a ...string) error { + for _, v := range a { + // Use zero-copy conversion here + if _, err := h.Write(stringToBytes(v)); err != nil { + return err + } + } + return nil +} + // the hash is cumulative, so you can call Hash() multiple times // with different values and the hash will be updated func (h *ObjectHash) SumHash(a ...any) error { @@ -26,14 +43,14 @@ func (h *ObjectHash) SumHash(a ...any) error { return nil } -func (h *ObjectHash) SumHashStrings(a ...string) error { - for _, v := range a { - if _, err := h.Write([]byte(v)); err != nil { - return err - } - } - return nil -} +// func (h *ObjectHash) SumHashStrings(a ...string) error { +// for _, v := range a { +// if _, err := h.Write([]byte(v)); err != nil { +// return err +// } +// } +// return nil +// } func (h *ObjectHash) Reset() { h.Hash64.Reset() diff --git a/internal/tools/hasher/hasher_test.go b/internal/tools/hasher/hasher_test.go index 7eb99ae..72009b6 100644 --- a/internal/tools/hasher/hasher_test.go +++ b/internal/tools/hasher/hasher_test.go @@ -6,6 +6,32 @@ import ( "testing" ) +func BenchmarkHashManySmallStrings(b *testing.B) { + b.ReportAllocs() + + const numStrings = 100000 + const strSize = 64 + + // Prepariamo i dati fuori dal timer + data := make([]string, numStrings) + for i := range data { + data[i] = strings.Repeat("y", strSize) + } + + b.SetBytes(int64(numStrings * strSize)) + + h := NewFNVObjectHash() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h.Reset() + // Testiamo specificamente la funzione ottimizzata per varargs di stringhe + if err := h.SumHashStrings(data...); err != nil { + b.Fatalf("SumHashStrings failed: %v", err) + } + } +} + func BenchmarkHashLargeMap(b *testing.B) { b.ReportAllocs() @@ -51,6 +77,29 @@ func BenchmarkHashLargeMap(b *testing.B) { } } +func BenchmarkHashLargeString(b *testing.B) { + b.ReportAllocs() + + // Configurazione: una singola stringa enorme (es. 100MB) + const strSize = 100 * 1024 * 1024 // 100 MB + largeStr := strings.Repeat("x", strSize) + + // Impostiamo i bytes processati per calcolare il throughput (MB/s) + b.SetBytes(int64(strSize)) + + h := NewFNVObjectHash() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h.Reset() + // Qui testiamo SumHash con una stringa diretta. + // Grazie all'ottimizzazione, questo dovrebbe evitare json.Marshal + // e usare la conversione zero-copy. + if err := h.SumHash(largeStr); err != nil { + b.Fatalf("SumHash failed: %v", err) + } + } +} func BenchmarkHash(b *testing.B) { input := []any{"test", 123, true, "another string", 456.78} diff --git a/internal/tools/helmchart/helmchart.go b/internal/tools/helmchart/helmchart.go deleted file mode 100644 index 515db6c..0000000 --- a/internal/tools/helmchart/helmchart.go +++ /dev/null @@ -1,269 +0,0 @@ -package helmchart - -import ( - "context" - "fmt" - "io" - "strings" - - "k8s.io/apimachinery/pkg/api/meta" - - "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" - unstructuredtools "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/hasher" - "github.com/krateoplatformops/unstructured-runtime/pkg/controller/objectref" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/release" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - sigsyaml "sigs.k8s.io/yaml" - - yamlutil "k8s.io/apimachinery/pkg/util/yaml" -) - -func ExtractValuesFromSpec(un *unstructured.Unstructured) ([]byte, error) { - if un == nil { - return nil, nil - } - - spec, ok, err := unstructured.NestedMap(un.UnstructuredContent(), "spec") - if err != nil { - return nil, err - } - if !ok { - return nil, nil - } - - return sigsyaml.Marshal(spec) -} - -func AddOrUpdateFieldInValues(values []byte, value interface{}, fields ...string) ([]byte, error) { - var valuesMap map[string]interface{} - if err := sigsyaml.Unmarshal(values, &valuesMap); err != nil { - return nil, err - } - - // Recursive function to add the value to the map creating nested maps if needed - var addOrUpdateField func(map[string]interface{}, []string, interface{}) error - addOrUpdateField = func(m map[string]interface{}, fields []string, value interface{}) error { - if len(fields) == 1 { - m[fields[0]] = value - return nil - } - - if _, ok := m[fields[0]]; !ok { - m[fields[0]] = map[string]interface{}{} - } - - if nestedMap, ok := m[fields[0]].(map[string]interface{}); ok { - return addOrUpdateField(nestedMap, fields[1:], value) - } else { - return fmt.Errorf("field %s is not a map", fields[0]) - } - } - - if err := addOrUpdateField(valuesMap, fields, value); err != nil { - return nil, err - } - - return sigsyaml.Marshal(valuesMap) -} - -type RenderTemplateOptions struct { - HelmClient helmclient.Client - PackageUrl string - PackageVersion string - Resource *unstructured.Unstructured - Repo string - Credentials *Credentials - Pluralizer pluralizer.PluralizerInterface -} - -// MinimalMetadata holds only the necessary fields for reference extraction. -// Decoding into this struct is significantly cheaper than map[string]any. -type minimalMetadata struct { - APIVersion string `json:"apiVersion"` - Kind string `json:"kind"` - Metadata struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - // Use the map to capture annotations, including the helm hook - Annotations map[string]string `json:"annotations"` - } `json:"metadata"` -} - -func GetResourcesRefFromRelease(rel *release.Release, defaultNamespace string, clientset helmclient.CachedClientsInterface) ([]objectref.ObjectRef, string, error) { - - // The hasher must implement io.Writer to allow incremental hashing. - var hasher = hasher.NewFNVObjectHash() - // build an io.Reader that streams manifest + hooks without concatenating into a single []byte - var readers []io.Reader - if rel != nil { - if strings.TrimSpace(rel.Manifest) != "" { - readers = append(readers, strings.NewReader(rel.Manifest)) - } - for _, h := range rel.Hooks { - // include hook marker to preserve document boundary - hdr := fmt.Sprintf("\n---\n# Source: %s\n", h.Path) - readers = append(readers, strings.NewReader(hdr+h.Manifest)) - } - } - - all := []objectref.ObjectRef{} - if len(readers) == 0 { - return all, "", nil - } - - combined := io.MultiReader(readers...) - - // 1. Set up io.Pipe for concurrent stream processing. - // pr is the Reader for the decoder, pw is the Writer fed by the goroutine. - pr, pw := io.Pipe() - - // 2. Use io.MultiWriter to pipe the stream to both: - // a) The PipeWriter (pw), which feeds the decoder in the main routine. - // b) The Hasher, which calculates the hash incrementally. - mw := io.MultiWriter(pw, hasher) - - // 3. Start a goroutine to copy the combined manifest into the MultiWriter concurrently. - go func() { - // io.Copy reads from 'combined' (source) and writes to 'mw' (destinations). - // It's crucial to close the PipeWriter (pw) when finished, or the decoder will hang. - _, err := io.Copy(mw, combined) - pw.CloseWithError(err) - }() - - // The decoder now reads from the PipeReader (pr). - decoder := yamlutil.NewYAMLOrJSONDecoder(pr, 4096) - for { - var doc minimalMetadata - if err := decoder.Decode(&doc); err != nil { - if err == io.EOF { - break - } - // skip invalid doc but continue processing others - continue - } - - apiVersion := doc.APIVersion - kind := doc.Kind - name := doc.Metadata.Name - namespace := doc.Metadata.Namespace - hook := doc.Metadata.Annotations["helm.sh/hook"] - - if namespace == "" { - namespace = defaultNamespace - - // Check if the resource is cluster-scoped - gvk := schema.FromAPIVersionAndKind(apiVersion, kind) - mapping, err := clientset.RESTMapper().RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - // Close the reader pipe on error to ensure the goroutine doesn't hang - pr.Close() - return nil, "", fmt.Errorf("failed to get REST mapping for %s: %w", gvk.String(), err) - } - if mapping.Scope.Name() == meta.RESTScopeNameRoot { - namespace = "" - } - } - // skip helm hooks if present - if hook != "" { - continue - } - // skip empty documents - if apiVersion == "" && kind == "" && name == "" { - continue - } - - all = append(all, objectref.ObjectRef{ - APIVersion: apiVersion, - Kind: kind, - Name: name, - Namespace: namespace, - }) - } - - // Ensure the PipeReader is closed after the loop to clean up the pipe resources. - pr.Close() - - // The hasher already holds the final hash digest, calculated incrementally. - // We use GetHash() to retrieve the final hash string. - return all, hasher.GetHash(), nil -} - -type CheckResourceOptions struct { - DynamicClient dynamic.Interface - Pluralizer pluralizer.PluralizerInterface -} - -func CheckResource(ctx context.Context, ref objectref.ObjectRef, opts CheckResourceOptions) (*objectref.ObjectRef, error) { - gvr, err := opts.Pluralizer.GVKtoGVR(schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind)) - if err != nil { - return nil, err - } - - un, err := opts.DynamicClient.Resource(gvr). - Namespace(ref.Namespace). - Get(ctx, ref.Name, metav1.GetOptions{}) - if un == nil { - // Try to get the resource without the namespace. This is useful for cluster-scoped resources. - un, err = opts.DynamicClient.Resource(gvr). - Get(ctx, ref.Name, metav1.GetOptions{}) - } - if err != nil { - return nil, err - } - - _, err = unstructuredtools.IsAvailable(un) - if err != nil { - if ex, ok := err.(*unstructuredtools.NotAvailableError); ok { - return ex.FailedObjectRef, ex.Err - } - } - - return &ref, err -} - -func FindRelease(hc helmclient.Client, name string) (*release.Release, error) { - rel, err := hc.GetRelease(name) - if err != nil { - if strings.Contains(err.Error(), "release: not found") { - return nil, nil - } - return nil, err - } - return rel, nil -} - -func FindAllReleases(hc helmclient.Client) ([]*release.Release, error) { - all, err := hc.ListReleasesByStateMask(action.ListAll) - if err != nil { - return nil, err - } - - res := make([]*release.Release, 0, len(all)) - res = append(res, all...) - - return res, nil -} - -func FindAnyRelease(hc helmclient.Client, name string) (*release.Release, error) { - all, err := FindAllReleases(hc) - if err != nil { - return nil, fmt.Errorf("failed to list releases: %w", err) - } - - for _, el := range all { - if name == el.Name { - return el, nil - } - } - - return nil, nil -} diff --git a/internal/tools/helmchart/install.go b/internal/tools/helmchart/install.go deleted file mode 100644 index 649b001..0000000 --- a/internal/tools/helmchart/install.go +++ /dev/null @@ -1,136 +0,0 @@ -package helmchart - -import ( - "context" - "fmt" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - "helm.sh/helm/v3/pkg/release" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -type Credentials struct { - Username string - Password string -} - -type InstallOptions struct { - CheckResourceOptions - HelmClient helmclient.Client - ChartName string - Resource *unstructured.Unstructured - Repo string - Version string - Credentials *Credentials - KrateoNamespace string - MaxHistory int -} - -func Install(ctx context.Context, opts InstallOptions) (*release.Release, int64, error) { - chartSpec := helmclient.ChartSpec{ - ReleaseName: compositionMeta.GetReleaseName(opts.Resource), - Namespace: opts.Resource.GetNamespace(), - Version: opts.Version, - Repo: opts.Repo, - ChartName: opts.ChartName, - CreateNamespace: true, - UpgradeCRDs: true, - Wait: false, - MaxHistory: opts.MaxHistory, - } - if opts.Credentials != nil { - chartSpec.Username = opts.Credentials.Username - chartSpec.Password = opts.Credentials.Password - } - - dat, err := ExtractValuesFromSpec(opts.Resource) - if err != nil { - return nil, 0, err - } - if len(dat) == 0 { - return nil, 0, nil - } - uid := opts.Resource.GetUID() - - gvr, err := opts.Pluralizer.GVKtoGVR(opts.Resource.GetObjectKind().GroupVersionKind()) - if err != nil { - return nil, 0, fmt.Errorf("failed to get GVR: %w", err) - } - - gracefullyPaused := "false" - if compositionMeta.IsGracefullyPaused(opts.Resource) { - gracefullyPaused = "true" - } else { - gracefullyPaused = "false" - } - dat, err = AddOrUpdateFieldInValues(dat, gracefullyPaused, "global", "gracefullyPaused") - if err != nil { - return nil, 0, fmt.Errorf("failed to add gracefullyPaused to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetNamespace(), "global", "compositionNamespace") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionNamespace to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetName(), "global", "compositionName") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionName to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.KrateoNamespace, "global", "krateoNamespace") - if err != nil { - return nil, 0, fmt.Errorf("failed to add krateoNamespace to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, uid, "global", "compositionId") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionId to values: %w", err) - } - // DEPRECATED: Remove in future versions in favor of compositionGroup and compositionInstalledVersion - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetAPIVersion(), "global", "compositionApiVersion") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionApiVersion to values: %w", err) - } - // END DEPRECATED - dat, err = AddOrUpdateFieldInValues(dat, gvr.Group, "global", "compositionGroup") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionGroup to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, gvr.Version, "global", "compositionInstalledVersion") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionInstalledVersion to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, gvr.Resource, "global", "compositionResource") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionResource to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetObjectKind().GroupVersionKind().Kind, "global", "compositionKind") - if err != nil { - return nil, 0, fmt.Errorf("failed to add compositionKind to values: %w", err) - } - - claimGen := opts.Resource.GetGeneration() - chartSpec.ValuesYaml = string(dat) - helmOpts := &helmclient.GenericHelmOptions{ - PostRenderer: &labelsPostRender{ - UID: uid, - CompositionName: opts.Resource.GetName(), - CompositionNamespace: opts.Resource.GetNamespace(), - CompositionGVR: gvr, - CompositionGVK: opts.Resource.GetObjectKind().GroupVersionKind(), - KrateoNamespace: opts.KrateoNamespace, - }, - } - rel, err := opts.HelmClient.InstallOrUpgradeChart(ctx, &chartSpec, helmOpts) - return rel, claimGen, err -} - -/* -compositionId: the UID of the composition resource -compositionGroup: the group of the composition resource -compositionInstalledVersion: the version of the composition resource. It changes when the composition version changes. (eg. an update in the chart version) -compositionResource: the resource of the composition resource -compositionName: the name of the composition resource -compositionNamespace: the namespace of the composition resource -compositionKind: the kind of the composition resource -krateoNamespace: the namespace of the Krateo installation -*/ diff --git a/internal/tools/helmchart/install_test.go b/internal/tools/helmchart/install_test.go deleted file mode 100644 index fabee57..0000000 --- a/internal/tools/helmchart/install_test.go +++ /dev/null @@ -1,154 +0,0 @@ -//go:build integration -// +build integration - -package helmchart - -import ( - "context" - "os" - "strings" - "testing" - - "github.com/gobuffalo/flect" - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - "github.com/krateoplatformops/plumbing/e2e" - xenv "github.com/krateoplatformops/plumbing/env" - "github.com/krateoplatformops/unstructured-runtime/pkg/meta" - "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" - "helm.sh/helm/v3/pkg/release" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "sigs.k8s.io/e2e-framework/klient/k8s/resources" - "sigs.k8s.io/e2e-framework/pkg/env" - "sigs.k8s.io/e2e-framework/pkg/envconf" - "sigs.k8s.io/e2e-framework/pkg/envfuncs" - "sigs.k8s.io/e2e-framework/pkg/features" - "sigs.k8s.io/e2e-framework/support/kind" -) - -type FakePluralizer struct { -} - -var _ pluralizer.PluralizerInterface = &FakePluralizer{} - -func (p FakePluralizer) GVKtoGVR(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { - return schema.GroupVersionResource{ - Group: gvk.Group, - Version: gvk.Version, - Resource: flect.Pluralize(strings.ToLower(gvk.Kind)), - }, nil -} - -var ( - testenv env.Environment - clusterName string - namespace string -) - -const ( - testdataPath = "../../../testdata" -) - -func TestMain(m *testing.M) { - xenv.SetTestMode(true) - - namespace = "demo-system" - altNamespace := "krateo-system" - clusterName = "krateo" - testenv = env.New() - - testenv.Setup( - envfuncs.CreateCluster(kind.NewProvider(), clusterName), - e2e.CreateNamespace(namespace), - e2e.CreateNamespace(altNamespace), - - func(ctx context.Context, cfg *envconf.Config) (context.Context, error) { - r, err := resources.New(cfg.Client().RESTConfig()) - if err != nil { - return ctx, err - } - - r.WithNamespace(namespace) - - return ctx, nil - }, - ).Finish( - envfuncs.DeleteNamespace(namespace), - envfuncs.DestroyCluster(clusterName), - ) - - os.Exit(testenv.Run(m)) -} - -func TestInstall(t *testing.T) { - - f := features.New("Setup"). - Setup(e2e.Logger("test")). - Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - return ctx - }).Assess("Install", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - - helmClient, _ := helmclient.NewClientFromRestConf(&helmclient.RestConfClientOptions{ - RestConfig: cfg.Client().RESTConfig(), - Options: &helmclient.Options{ - Namespace: namespace, - }, - }) - - // Create a dummy resource - res := createDummyResource() - - ls := res.GetLabels() - if ls == nil { - ls = make(map[string]string) - } - ls[meta.ReleaseNameLabel] = "test" - res.SetLabels(ls) - - res.SetName("12") - - res.SetUID("12345678-1234-1234-1234-123456789012") - - dynamicClient, err := dynamic.NewForConfig(cfg.Client().RESTConfig()) - - // Set up the install options - opts := InstallOptions{ - HelmClient: helmClient, - ChartName: "https://charts.bitnami.com/bitnami", - Resource: res, - Repo: "postgresql", - Version: "12.8.3", - CheckResourceOptions: CheckResourceOptions{ - DynamicClient: dynamicClient, - Pluralizer: FakePluralizer{}, - }, - KrateoNamespace: namespace, - } - - // Call the Install function - rel, _, err := Install(ctx, opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // Check the returned release - expectedRelease := &release.Release{ - Name: "test", - Namespace: "demo-system", - Version: 1, - } - - rel, err = helmClient.GetRelease(expectedRelease.Name) - if err != nil { - t.Fatalf("failed to get release: %v", err) - } - - if rel.Name != expectedRelease.Name || rel.Namespace != expectedRelease.Namespace || rel.Version != expectedRelease.Version { - t.Fatalf("expected release name: %s, namespace: %s, version: %d, got name: %s, namespace: %s, version: %d", - expectedRelease.Name, expectedRelease.Namespace, expectedRelease.Version, - rel.Name, rel.Namespace, rel.Version) - } - return ctx - }).Feature() - - testenv.Test(t, f) -} diff --git a/internal/tools/helmchart/postrenderer.go b/internal/tools/helmchart/postrenderer.go deleted file mode 100644 index 4895523..0000000 --- a/internal/tools/helmchart/postrenderer.go +++ /dev/null @@ -1,49 +0,0 @@ -package helmchart - -import ( - "bytes" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/kustomize/kyaml/kio" -) - -type labelsPostRender struct { - UID types.UID - CompositionGVR schema.GroupVersionResource - CompositionName string - CompositionNamespace string - CompositionGVK schema.GroupVersionKind - KrateoNamespace string -} - -func (r *labelsPostRender) Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) { - nodes, err := kio.FromBytes(renderedManifests.Bytes()) - if err != nil { - return renderedManifests, errors.Wrap(err, "parse rendered manifests failed") - } - for _, v := range nodes { - labels := v.GetLabels() - if labels == nil { - labels = make(map[string]string) - } - // your labels - labels["krateo.io/composition-id"] = string(r.UID) - labels["krateo.io/composition-group"] = r.CompositionGVR.Group - labels["krateo.io/composition-installed-version"] = r.CompositionGVR.Version - labels["krateo.io/composition-resource"] = r.CompositionGVR.Resource - labels["krateo.io/composition-name"] = r.CompositionName - labels["krateo.io/composition-namespace"] = r.CompositionNamespace - labels["krateo.io/composition-kind"] = r.CompositionGVK.Kind - labels["krateo.io/krateo-namespace"] = r.KrateoNamespace - v.SetLabels(labels) - } - - str, err := kio.StringAll(nodes) - if err != nil { - return renderedManifests, errors.Wrap(err, "string all nodes failed") - } - - return bytes.NewBufferString(str), nil -} diff --git a/internal/tools/helmchart/update.go b/internal/tools/helmchart/update.go deleted file mode 100644 index f3a2b75..0000000 --- a/internal/tools/helmchart/update.go +++ /dev/null @@ -1,127 +0,0 @@ -package helmchart - -import ( - "context" - "fmt" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - "helm.sh/helm/v3/pkg/release" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -type UpdateOptions struct { - CheckResourceOptions - HelmClient helmclient.Client - ChartName string - Version string - Resource *unstructured.Unstructured - Repo string - Credentials *Credentials - KrateoNamespace string - MaxHistory int -} - -func Update(ctx context.Context, opts UpdateOptions) (*release.Release, error) { - chartSpec := helmclient.ChartSpec{ - ReleaseName: compositionMeta.GetReleaseName(opts.Resource), - Namespace: opts.Resource.GetNamespace(), - Repo: opts.Repo, - ChartName: opts.ChartName, - Version: opts.Version, - CreateNamespace: true, - UpgradeCRDs: true, - Replace: true, - CleanupOnFail: true, - Install: true, - MaxHistory: opts.MaxHistory, - } - if opts.Credentials != nil { - chartSpec.Username = opts.Credentials.Username - chartSpec.Password = opts.Credentials.Password - } - - dat, err := ExtractValuesFromSpec(opts.Resource) - if err != nil { - return nil, err - } - if len(dat) > 0 { - chartSpec.ResetValues = true - chartSpec.ValuesYaml = string(dat) - } - - uid := opts.Resource.GetUID() - - gvr, err := opts.Pluralizer.GVKtoGVR(opts.Resource.GetObjectKind().GroupVersionKind()) - if err != nil { - return nil, fmt.Errorf("failed to get GVR: %w", err) - } - - gracefullyPaused := "false" - if compositionMeta.IsGracefullyPaused(opts.Resource) { - gracefullyPaused = "true" - } else { - gracefullyPaused = "false" - } - dat, err = AddOrUpdateFieldInValues(dat, gracefullyPaused, "global", "gracefullyPaused") - if err != nil { - return nil, fmt.Errorf("failed to add gracefullyPaused to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetNamespace(), "global", "compositionNamespace") - if err != nil { - return nil, fmt.Errorf("failed to add compositionNamespace to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetName(), "global", "compositionName") - if err != nil { - return nil, fmt.Errorf("failed to add compositionName to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, opts.KrateoNamespace, "global", "krateoNamespace") - if err != nil { - return nil, fmt.Errorf("failed to add krateoNamespace to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, uid, "global", "compositionId") - if err != nil { - return nil, fmt.Errorf("failed to add compositionId to values: %w", err) - } - // DEPRECATED: Remove in future versions in favor of compositionGroup and compositionInstalledVersion - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetAPIVersion(), "global", "compositionApiVersion") - if err != nil { - return nil, fmt.Errorf("failed to add compositionApiVersion to values: %w", err) - } - // END DEPRECATED - dat, err = AddOrUpdateFieldInValues(dat, gvr.Group, "global", "compositionGroup") - if err != nil { - return nil, fmt.Errorf("failed to add compositionGroup to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, gvr.Version, "global", "compositionInstalledVersion") - if err != nil { - return nil, fmt.Errorf("failed to add compositionInstalledVersion to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, gvr.Resource, "global", "compositionResource") - if err != nil { - return nil, fmt.Errorf("failed to add compositionResource to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.Resource.GetObjectKind().GroupVersionKind().Kind, "global", "compositionKind") - if err != nil { - return nil, fmt.Errorf("failed to add compositionKind to values: %w", err) - } - - chartSpec.ValuesYaml = string(dat) - helmOpts := &helmclient.GenericHelmOptions{ - PostRenderer: &labelsPostRender{ - UID: uid, - CompositionName: opts.Resource.GetName(), - CompositionNamespace: opts.Resource.GetNamespace(), - CompositionGVR: gvr, - CompositionGVK: opts.Resource.GetObjectKind().GroupVersionKind(), - KrateoNamespace: opts.KrateoNamespace, - }, - } - - rel, err := opts.HelmClient.UpgradeChart(ctx, &chartSpec, helmOpts) - return rel, err -} diff --git a/internal/tools/helmchart/update_test.go b/internal/tools/helmchart/update_test.go deleted file mode 100644 index cb925a8..0000000 --- a/internal/tools/helmchart/update_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package helmchart - -// import ( -// "context" -// "testing" -// ) - -// func TestUpdate(t *testing.T) { -// helmClient := newHelmClient() -// ctx := context.TODO() - -// // Create a dummy resource -// res := createDummyResource() - -// Install(ctx, InstallOptions{ -// HelmClient: helmClient, -// ChartName: "https://charts.bitnami.com/bitnami", -// Resource: res, -// Repo: "postgresql", -// Version: "12.8.3", -// }) - -// // Set up the update options -// opts := UpdateOptions{ -// HelmClient: helmClient, -// ChartName: "https://charts.bitnami.com/bitnami", -// Resource: res, -// Repo: "postgresql", -// Version: "12.8.3", -// } - -// // Call the Update function -// err := Update(ctx, opts) -// if err != nil { -// t.Fatalf("unexpected error: %v", err) -// } -// } diff --git a/internal/tools/processor/processor.go b/internal/tools/processor/processor.go new file mode 100644 index 0000000..3931ce2 --- /dev/null +++ b/internal/tools/processor/processor.go @@ -0,0 +1,88 @@ +package processor + +import ( + "io" + "strings" + + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/hasher" + "github.com/krateoplatformops/plumbing/helm" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/yaml" +) + +func DecodeMinRelease(rel *helm.Release) ([]MinimalMetadata, string, error) { + return DecodeRelease[MinimalMetadata](rel) +} + +func DecodeUnstructuredRelease(rel *helm.Release) ([]unstructured.Unstructured, string, error) { + return DecodeRelease[unstructured.Unstructured](rel) +} + +// DecodeRelease parses the release manifest, computes its hash, and returns a slice of objects. +// Optimization: Uses io.TeeReader to decode and hash in a single synchronous pass. +// DecodeRelease parses the release manifest, computes its hash, and returns a slice of objects. +func DecodeRelease[T any, PT interface { + *T + MinimalMetaObject +}](rel *helm.Release) ([]T, string, error) { + // 1. Fast path: Empty manifest + if len(rel.Manifest) == 0 { + return nil, "", nil + } + + h := hasher.NewFNVObjectHash() + + // 2. OPTIMIZATION: Zero-Copy Reader & TeeReader + // strings.NewReader creates a read-only view of the string (no allocation). + // TeeReader streams bytes to the hasher 'h' automatically as the decoder reads them. + tr := io.TeeReader(strings.NewReader(rel.Manifest), h) + + // 3. Decoder Setup + // We use a larger buffer (4096) to reduce read syscalls for large objects + decoder := yaml.NewYAMLOrJSONDecoder(tr, 4096) + + // Pre-allocate slice. Heuristic: 10 objects prevents resizing for most charts. + objects := make([]T, 0, 10) + + for { + // 4. OPTIMIZATION: Direct Decode + // Instead of decoding to RawExtension (buffer) -> json.Unmarshal (struct), + // we decode directly into the struct. This saves one massive allocation per object. + var obj T + err := decoder.Decode(&obj) + + if err != nil { + if err == io.EOF { + break + } + return nil, "", err + } + + // 5. Filter Empty/Null Objects + // Since we skipped RawExtension check, we validate the object itself. + // If APIVersion is empty, it's likely a "---" separator or empty doc. + p := PT(&obj) + if p.GetAPIVersion() == "" { + continue + } + + objects = append(objects, obj) + } + + return objects, h.GetHash(), nil +} + +// ComputeReleaseDigest calculates the hash of the release manifest without decoding objects. +func ComputeReleaseDigest(rel *helm.Release) (string, error) { + if strings.TrimSpace(rel.Manifest) == "" { + return "", nil + } + + h := hasher.NewFNVObjectHash() + err := h.SumHashStrings(rel.Manifest) + if err != nil { + return "", err + } + + return h.GetHash(), nil +} diff --git a/internal/tools/processor/processor_bench_test.go b/internal/tools/processor/processor_bench_test.go new file mode 100644 index 0000000..3803e28 --- /dev/null +++ b/internal/tools/processor/processor_bench_test.go @@ -0,0 +1,79 @@ +package processor + +import ( + "fmt" + "strings" + "testing" + + "github.com/krateoplatformops/plumbing/helm" +) + +// --- Benchmarks --- + +// Helper to generate consistent large manifests for benchmarking +func generateLargeManifest(kbSize int) string { + sb := strings.Builder{} + chunk := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: chunk-%d +data: + payload: "%s" +--- +` + // 1KB of payload data per object + payload := strings.Repeat("x", 1024) + + // Build the manifest until we reach the desired size roughly + for i := 0; i < kbSize; i++ { + sb.WriteString(fmt.Sprintf(chunk, i, payload)) + } + return sb.String() +} + +func BenchmarkReleaseProcessing(b *testing.B) { + // We test 3 different scales to see how the performance gap widens + sizes := []struct { + name string + kb int + }{ + {"Small_10KB", 10}, // Typical chart + {"Medium_500KB", 500}, // Large chart + {"Large_5MB", 5000}, // Huge chart / CRDs + } + + for _, tc := range sizes { + manifest := generateLargeManifest(tc.kb) + rel := &helm.Release{Manifest: manifest} + + // Benchmark 1: The expensive "Decode" way (simulating discard) + b.Run("DecodeRelease_"+tc.name, func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(int64(len(manifest))) + + for i := 0; i < b.N; i++ { + _, _, err := DecodeRelease[MinimalMetadata](rel) + if err != nil { + b.Fatal(err) + } + } + }) + + // Benchmark 2: The optimized "Hash Only" way + b.Run("ComputeDigest_"+tc.name, func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(int64(len(manifest))) + + for i := 0; i < b.N; i++ { + digest, err := ComputeReleaseDigest(rel) + if err != nil { + b.Fatal(err) + } + if digest == "" { + b.Fatal("digest shouldn't be empty") + } + } + }) + } +} diff --git a/internal/tools/processor/processor_test.go b/internal/tools/processor/processor_test.go new file mode 100644 index 0000000..7ce2c7a --- /dev/null +++ b/internal/tools/processor/processor_test.go @@ -0,0 +1,179 @@ +package processor + +import ( + "strings" + "testing" + + "github.com/krateoplatformops/plumbing/helm" + "github.com/stretchr/testify/assert" +) + +func TestDecodeRelease(t *testing.T) { + tests := []struct { + name string + manifest string + expectedCount int + expectError bool + }{ + { + name: "Valid Single Document", + manifest: ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value +`, + expectedCount: 1, + expectError: false, + }, + { + name: "Multiple Documents with Separators", + manifest: ` +apiVersion: v1 +kind: Service +metadata: + name: svc-1 +--- +apiVersion: v1 +kind: Service +metadata: + name: svc-2 +`, + expectedCount: 2, + expectError: false, + }, + { + name: "Empty Manifest", + manifest: "", + expectedCount: 0, + expectError: false, + }, + { + name: "Whitespace and Comments Only", + manifest: ` +# This is a comment + +--- +# Another empty doc +`, + expectedCount: 0, + expectError: false, + }, + { + name: "Malformed YAML Document", + manifest: ` +apiVersion: v1 +kind: Pod +metadata: + name: [unclosed-bracket +`, + expectedCount: 0, + expectError: true, + }, + { + name: "Document with Leading/Trailing Separators", + manifest: ` +--- +apiVersion: v1 +kind: Namespace +metadata: + name: test +--- +`, + expectedCount: 1, + expectError: false, + }, + { + name: "Large Manifest (Stream Stress)", + manifest: strings.Repeat("---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: cm\n", 100), + expectedCount: 100, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rel := &helm.Release{ + Manifest: tt.manifest, + } + + // We test with MinimalMetadata to ensure partial unmarshaling works + objs, hash, err := DecodeRelease[MinimalMetadata](rel) + + if (err != nil) != tt.expectError { + t.Fatalf("expected error: %v, got: %v", tt.expectError, err) + } + + if err == nil { + if len(objs) != tt.expectedCount { + t.Errorf("expected %d objects, got %d", tt.expectedCount, len(objs)) + } + + if tt.manifest != "" && hash == "" { + t.Error("expected a hash for non-empty manifest, got empty string") + } + } + }) + } +} + +func TestHashConsistency(t *testing.T) { + manifest := "apiVersion: v1\nkind: Pod\nmetadata:\n name: foo" + rel := &helm.Release{Manifest: manifest} + + _, hash1, _ := DecodeRelease[MinimalMetadata](rel) + _, hash2, _ := DecodeRelease[MinimalMetadata](rel) + + if hash1 != hash2 { + t.Errorf("hashes are not consistent: %s vs %s", hash1, hash2) + } +} + +func TestDigestConsistency(t *testing.T) { + // 1. Create a realistic Helm Release manifest with multiple documents + manifest := ` +apiVersion: v1 +kind: Service +metadata: + name: test-service +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + replicas: 3 +` + rel := &helm.Release{ + Manifest: manifest, + } + + // 2. Run the old heavy function + // We use mockMetadata to satisfy the generic constraints + _, digestFull, err := DecodeRelease[MinimalMetadata](rel) + assert.NoError(t, err) + + // 3. Run the new light function + digestFast, err := ComputeReleaseDigest(rel) + assert.NoError(t, err) + + // 4. Verify they are identical + assert.NotEmpty(t, digestFull, "Digest should not be empty") + assert.Equal(t, digestFull, digestFast, "Optimized digest MUST match the decoded digest exactly") +} + +func TestDigestConsistency_Empty(t *testing.T) { + // Test handling of whitespace-only strings + rel := &helm.Release{Manifest: " \n "} + + _, digestFull, err := DecodeRelease[MinimalMetadata](rel) + assert.NoError(t, err) + + digestFast, err := ComputeReleaseDigest(rel) + assert.NoError(t, err) + + assert.Equal(t, "", digestFull) + assert.Equal(t, "", digestFast) +} diff --git a/internal/tools/processor/types.go b/internal/tools/processor/types.go new file mode 100644 index 0000000..756cc03 --- /dev/null +++ b/internal/tools/processor/types.go @@ -0,0 +1,61 @@ +package processor + +var _ MinimalMetaObject = &MinimalMetadata{} + +type MinimalMetaObject interface { + GetAnnotations() map[string]string + GetNamespace() string + GetName() string + GetKind() string + GetAPIVersion() string + + SetNamespace(namespace string) + SetName(name string) + SetAnnotations(annotations map[string]string) +} + +type Metadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + // Use the map to capture annotations, including the helm hook + Annotations map[string]string `json:"annotations"` +} + +// MinimalMetadata holds only the necessary fields for reference extraction. +// Decoding into this struct is significantly cheaper than map[string]any. +type MinimalMetadata struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata Metadata `json:"metadata"` +} + +func (m *MinimalMetadata) GetAnnotations() map[string]string { + return m.Metadata.Annotations +} + +func (m *MinimalMetadata) GetNamespace() string { + return m.Metadata.Namespace +} + +func (m *MinimalMetadata) GetName() string { + return m.Metadata.Name +} + +func (m *MinimalMetadata) GetKind() string { + return m.Kind +} + +func (m *MinimalMetadata) GetAPIVersion() string { + return m.APIVersion +} + +func (m *MinimalMetadata) SetNamespace(namespace string) { + m.Metadata.Namespace = namespace +} + +func (m *MinimalMetadata) SetName(name string) { + m.Metadata.Name = name +} +func (m *MinimalMetadata) SetAnnotations(annotations map[string]string) { + m.Metadata.Annotations = annotations +} diff --git a/internal/helmclient/tracer/tracer.go b/internal/tools/tracer/tracer.go similarity index 100% rename from internal/helmclient/tracer/tracer.go rename to internal/tools/tracer/tracer.go diff --git a/internal/helmclient/tracer/tracer_test.go b/internal/tools/tracer/tracer_test.go similarity index 100% rename from internal/helmclient/tracer/tracer_test.go rename to internal/tools/tracer/tracer_test.go diff --git a/main.go b/main.go index 730ec5d..dad739b 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,8 @@ import ( "github.com/go-logr/logr" "github.com/krateoplatformops/composition-dynamic-controller/internal/composition" "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/helmchart/archive" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/archive" + "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/dynamic" "github.com/krateoplatformops/plumbing/env" "github.com/krateoplatformops/plumbing/kubeutil/event" "github.com/krateoplatformops/plumbing/kubeutil/eventrecorder" @@ -165,7 +166,13 @@ func main() { } labelselector := labels.NewSelector().Add(*labelreq) - handler := composition.NewHandler(cfg, pig, *event.NewAPIRecorder(rec), *pluralizer, *urlChartInspector, *saName, *saNamespace) + mapper, err := dynamic.NewRESTMapper(cfg) + if err != nil { + log.Error(err, "Creating RESTMapper.") + os.Exit(1) + } + + handler := composition.NewHandler(cfg, pig, *event.NewAPIRecorder(rec), *pluralizer, mapper, *urlChartInspector, *saName, *saNamespace) opts := []builder.FuncOption{ builder.WithLogger(log), From c0e61bf999404c6384e281766eaecfdbbac570b4 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 9 Feb 2026 17:00:23 +0100 Subject: [PATCH 02/11] chore: update .gitignore, go.mod, and go.sum; refactor composition logic and tests --- .gitignore | 2 + go.mod | 2 + go.sum | 2 - internal/composition/composition.go | 60 +++- internal/composition/composition_test.go | 1 - internal/tools/dynamic/dynamic_test.go | 302 +++++++++++++++++ internal/tools/hasher/hasher.go | 9 - internal/tools/helmchart/helmchart_test.go | 364 --------------------- 8 files changed, 357 insertions(+), 385 deletions(-) create mode 100644 internal/tools/dynamic/dynamic_test.go delete mode 100644 internal/tools/helmchart/helmchart_test.go diff --git a/.gitignore b/.gitignore index 09478c6..5b239b6 100755 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,6 @@ cover.out .tool-versions **/*.xpkg +.github/skills + .vscode \ No newline at end of file diff --git a/go.mod b/go.mod index 75d2bba..507501d 100644 --- a/go.mod +++ b/go.mod @@ -142,3 +142,5 @@ require ( ) replace github.com/krateoplatformops/plumbing => /Users/matteogastaldello/Documents/plumbing + +replace github.com/krateoplatformops/unstructured-runtime => /Users/matteogastaldello/Documents/unstructured-runtime diff --git a/go.sum b/go.sum index 25f5438..bf27f16 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/krateoplatformops/unstructured-runtime v0.3.1 h1:tQMH19YEJ7+La5283a4FOQlCeBBhS9cqwYzBPW59srs= -github.com/krateoplatformops/unstructured-runtime v0.3.1/go.mod h1:19uT87wZzRSjrfk3731Xhdt8ww7vnsXhljy4jk0cuWA= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= diff --git a/internal/composition/composition.go b/internal/composition/composition.go index f1e1c77..1debbfb 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -320,10 +320,10 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, err } - _, err = tools.UpdateStatus(ctx, mg, updateOpts) - if err != nil { - return controller.ExternalObservation{}, err - } + // _, err = tools.UpdateStatus(ctx, mg, updateOpts) + // if err != nil { + // return controller.ExternalObservation{}, err + // } log.Debug("Composition Observed - installed", "package", pkg.URL) @@ -405,10 +405,18 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("creating helm client: %w", err) } + values, ok, err := maps.NestedMap(mg.Object, "spec") + if err != nil { + return fmt.Errorf("getting spec values: %w", err) + } + if !ok { + values = map[string]interface{}{} + } rel, err := hc.Install(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.InstallConfig{ ActionConfig: &helmconfig.ActionConfig{ ChartVersion: pkg.Version, ChartName: pkg.Repo, + Values: values, }, }) @@ -481,7 +489,7 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return nil } - log.Debug("Handling composition values update.") + log.Debug("Handling composition update") if h.packageInfoGetter == nil { return fmt.Errorf("helm chart package info getter must be specified") @@ -498,9 +506,42 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("creating helm client: %w", err) } - rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) + // values, ok, err := maps.NestedMap(mg.Object, "spec") + // if err != nil { + // return fmt.Errorf("getting spec values: %w", err) + // } + // if !ok { + // values = map[string]interface{}{} + // } + // upgradedRel, err := hc.Upgrade(ctx, releaseName, pkg.URL, &helmconfig.UpgradeConfig{ + // ActionConfig: &helmconfig.ActionConfig{ + // ChartVersion: pkg.Version, + // ChartName: pkg.Repo, + // Username: pkg.Auth.Username, + // Password: pkg.Auth.Password, + // Values: values, + // }, + // MaxHistory: helmMaxHistory, + // }) + // if err != nil { + // retErr := fmt.Errorf("upgrading helm chart: %w", err) + // condition := condition.Unavailable() + // condition.Message = retErr.Error() + // unstructuredtools.SetConditions(mg, condition) + // _, err = tools.UpdateStatus(ctx, mg, updateOpts) + // if err != nil { + // return fmt.Errorf("updating status after failure: %w", err) + // } + // return retErr + // } + + upgradedRel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { - return fmt.Errorf("finding helm release: %w", err) + return fmt.Errorf("getting helm release: %w", err) + } + if upgradedRel == nil { + log.Debug("Composition not found after upgrade.") + return fmt.Errorf("composition not found after upgrade") } previousDigest, err := maps.NestedString(mg.Object, "status", "digest") @@ -508,7 +549,7 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("getting previous digest from status: %w", err) } - all, digest, err := processor.DecodeMinRelease(rel) + all, digest, err := processor.DecodeMinRelease(upgradedRel) if err != nil { return fmt.Errorf("decoding release: %w", err) } @@ -652,7 +693,8 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err CompositionDefintionGVR: pkg.CompositionDefinitionInfo.GVR, }) if err != nil { - return fmt.Errorf("generating RBAC using chart-inspector: %w", err) + return fmt.Errorf("generating RBAC for composition %s/%s: %w", + mg.GetNamespace(), mg.GetName(), err) } rbInstaller := rbac.NewRBACInstaller(dyn) err = rbInstaller.UninstallRBAC(generated) diff --git a/internal/composition/composition_test.go b/internal/composition/composition_test.go index eef6756..f067a02 100644 --- a/internal/composition/composition_test.go +++ b/internal/composition/composition_test.go @@ -505,7 +505,6 @@ func TestController(t *testing.T) { } t.Logf("Final Helm Revision Count: %d", finalRevisionCount) - // 5. ASSERTION - This is where the code "Breaks" if finalRevisionCount > initialRevisionCount { t.Errorf("CRITICAL FAILURE: The Observe method is not idempotent! "+ "Helm revisions increased from %d to %d without any Spec changes. "+ diff --git a/internal/tools/dynamic/dynamic_test.go b/internal/tools/dynamic/dynamic_test.go new file mode 100644 index 0000000..3689816 --- /dev/null +++ b/internal/tools/dynamic/dynamic_test.go @@ -0,0 +1,302 @@ +package dynamic + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" +) + +func TestNewRESTMapper(t *testing.T) { + tests := []struct { + name string + config *rest.Config + wantErr bool + }{ + { + name: "nil config should use in-cluster config", + config: nil, + wantErr: true, // Will fail in test environment without kubeconfig + }, + { + name: "valid config should succeed", + config: &rest.Config{ + Host: "https://localhost:8080", + }, + wantErr: false, // Deferred discovery means creation succeeds, errors happen on use + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mapper, err := NewRESTMapper(tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("NewRESTMapper() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && mapper == nil { + t.Error("NewRESTMapper() returned nil mapper without error") + } + }) + } +} + +func TestIsNamespaced(t *testing.T) { + tests := []struct { + name string + mapper meta.RESTMapper + gvk schema.GroupVersionKind + want bool + wantErr bool + }{ + { + name: "nil mapper returns false without error", + mapper: nil, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + want: false, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := IsNamespaced(tt.mapper, tt.gvk) + if (err != nil) != tt.wantErr { + t.Errorf("IsNamespaced() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsNamespaced() = %v, want %v", got, tt.want) + } + }) + } +} + +// MockRESTMapper implements meta.RESTMapper for testing +type MockRESTMapper struct { + scopeName meta.RESTScopeName + err error +} + +// MockRESTScope implements meta.RESTScope for testing +type MockRESTScope struct { + name meta.RESTScopeName +} + +func (m *MockRESTScope) Name() meta.RESTScopeName { + return m.name +} + +func (m *MockRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { + return schema.GroupVersionKind{}, m.err +} + +func (m *MockRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { + return nil, m.err +} + +func (m *MockRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{}, m.err +} + +func (m *MockRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + return nil, m.err +} + +func (m *MockRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + if m.err != nil { + return nil, m.err + } + return &meta.RESTMapping{ + Scope: &MockRESTScope{ + name: m.scopeName, + }, + }, nil +} + +func (m *MockRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { + return nil, m.err +} + +func (m *MockRESTMapper) ResourceSingularizer(resource string) (singular string, err error) { + return "", m.err +} + +func TestIsNamespaced_WithMockMapper(t *testing.T) { + tests := []struct { + name string + scopeName meta.RESTScopeName + mockErr error + gvk schema.GroupVersionKind + want bool + wantErr bool + }{ + { + name: "namespaced resource", + scopeName: meta.RESTScopeNameNamespace, + mockErr: nil, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + want: true, + wantErr: false, + }, + { + name: "cluster-scoped resource", + scopeName: meta.RESTScopeNameRoot, + mockErr: nil, + gvk: schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Namespace", + }, + want: false, + wantErr: false, + }, + { + name: "mapper returns error", + scopeName: meta.RESTScopeNameNamespace, + mockErr: &meta.NoKindMatchError{}, + gvk: schema.GroupVersionKind{ + Group: "unknown", + Version: "v1", + Kind: "Unknown", + }, + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockMapper := &MockRESTMapper{ + scopeName: tt.scopeName, + err: tt.mockErr, + } + + got, err := IsNamespaced(mockMapper, tt.gvk) + if (err != nil) != tt.wantErr { + t.Errorf("IsNamespaced() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsNamespaced() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIsNamespaced_EdgeCases(t *testing.T) { + t.Run("empty GVK", func(t *testing.T) { + mockMapper := &MockRESTMapper{ + scopeName: meta.RESTScopeNameNamespace, + err: nil, + } + + gvk := schema.GroupVersionKind{} + got, err := IsNamespaced(mockMapper, gvk) + if err != nil { + t.Errorf("IsNamespaced() unexpected error = %v", err) + } + if got != true { + t.Errorf("IsNamespaced() = %v, want %v", got, true) + } + }) + + t.Run("core group resources", func(t *testing.T) { + testCases := []struct { + kind string + scopeName meta.RESTScopeName + want bool + }{ + {"Pod", meta.RESTScopeNameNamespace, true}, + {"Service", meta.RESTScopeNameNamespace, true}, + {"Namespace", meta.RESTScopeNameRoot, false}, + {"Node", meta.RESTScopeNameRoot, false}, + {"PersistentVolume", meta.RESTScopeNameRoot, false}, + } + + for _, tc := range testCases { + t.Run(tc.kind, func(t *testing.T) { + mockMapper := &MockRESTMapper{ + scopeName: tc.scopeName, + err: nil, + } + + gvk := schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: tc.kind, + } + + got, err := IsNamespaced(mockMapper, gvk) + if err != nil { + t.Errorf("IsNamespaced() unexpected error = %v", err) + } + if got != tc.want { + t.Errorf("IsNamespaced() for %s = %v, want %v", tc.kind, got, tc.want) + } + }) + } + }) + + t.Run("custom resources", func(t *testing.T) { + mockMapper := &MockRESTMapper{ + scopeName: meta.RESTScopeNameNamespace, + err: nil, + } + + gvk := schema.GroupVersionKind{ + Group: "krateo.io", + Version: "v1alpha1", + Kind: "CompositionDefinition", + } + + got, err := IsNamespaced(mockMapper, gvk) + if err != nil { + t.Errorf("IsNamespaced() unexpected error = %v", err) + } + if got != true { + t.Errorf("IsNamespaced() for custom resource = %v, want %v", got, true) + } + }) +} + +// Benchmark tests +func BenchmarkIsNamespaced(b *testing.B) { + mockMapper := &MockRESTMapper{ + scopeName: meta.RESTScopeNameNamespace, + err: nil, + } + + gvk := schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = IsNamespaced(mockMapper, gvk) + } +} + +func BenchmarkIsNamespaced_NilMapper(b *testing.B) { + gvk := schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = IsNamespaced(nil, gvk) + } +} diff --git a/internal/tools/hasher/hasher.go b/internal/tools/hasher/hasher.go index 9be432f..8b4b38b 100644 --- a/internal/tools/hasher/hasher.go +++ b/internal/tools/hasher/hasher.go @@ -43,15 +43,6 @@ func (h *ObjectHash) SumHash(a ...any) error { return nil } -// func (h *ObjectHash) SumHashStrings(a ...string) error { -// for _, v := range a { -// if _, err := h.Write([]byte(v)); err != nil { -// return err -// } -// } -// return nil -// } - func (h *ObjectHash) Reset() { h.Hash64.Reset() } diff --git a/internal/tools/helmchart/helmchart_test.go b/internal/tools/helmchart/helmchart_test.go deleted file mode 100644 index 473f2db..0000000 --- a/internal/tools/helmchart/helmchart_test.go +++ /dev/null @@ -1,364 +0,0 @@ -//go:build integration -// +build integration - -package helmchart - -import ( - "context" - "fmt" - "testing" - - compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/internal/meta" - - "github.com/krateoplatformops/composition-dynamic-controller/internal/helmclient" - "github.com/krateoplatformops/plumbing/e2e" - "github.com/krateoplatformops/unstructured-runtime/pkg/meta" - "github.com/stretchr/testify/assert" - "helm.sh/helm/v3/pkg/release" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "sigs.k8s.io/e2e-framework/pkg/envconf" - "sigs.k8s.io/e2e-framework/pkg/features" -) - -func ExampleExtractValuesFromSpec() { - res := createDummyResource() - - dat, err := ExtractValuesFromSpec(res) - if err != nil { - panic(err) - } - - fmt.Println(string(dat)) - // Output: - // data: - // counter: 1 - // greeting: Hello World! - // like: false -} - -func TestFindRelease(t *testing.T) { - f := features.New("FindRelease"). - Setup(e2e.Logger("test")). - Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - return ctx - }).Assess("FindRelease", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - - helmClient, err := helmclient.NewClientFromRestConf(&helmclient.RestConfClientOptions{ - RestConfig: cfg.Client().RESTConfig(), - Options: &helmclient.Options{ - Namespace: namespace, - }, - }) - if err != nil { - t.Fatalf("failed to create helm client: %v", err) - } - - releaseName := "my-release" - - // Call the FindRelease function - actualRelease, err := FindRelease(helmClient, releaseName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // Check if the actual release matches the expected release (should be nil for non-existent release) - if actualRelease != nil { - t.Fatalf("expected release %v, got %v", nil, actualRelease) - } - - return ctx - }).Feature() - - testenv.Test(t, f) -} - -func TestFindAllReleases(t *testing.T) { - f := features.New("FindAllReleases"). - Setup(e2e.Logger("test")). - Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - return ctx - }).Assess("FindAllReleases", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - - helmClient, err := helmclient.NewClientFromRestConf(&helmclient.RestConfClientOptions{ - RestConfig: cfg.Client().RESTConfig(), - Options: &helmclient.Options{ - Namespace: namespace, - }, - }) - if err != nil { - t.Fatalf("failed to create helm client: %v", err) - } - - // First, check that we start with no releases - releases, err := FindAllReleases(helmClient) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assert.NotNil(t, releases) - assert.Len(t, releases, 0, "Expected no releases initially") - - dynamicClient, err := dynamic.NewForConfig(cfg.Client().RESTConfig()) - if err != nil { - t.Fatalf("failed to create dynamic client: %v", err) - } - - // Install a successful test release using Install function - successfulReleaseName := "test-release-success" - successfulRes := createDummyResource() - - ls := successfulRes.GetLabels() - if ls == nil { - ls = make(map[string]string) - } - ls[meta.ReleaseNameLabel] = successfulReleaseName - successfulRes.SetLabels(ls) - successfulRes.SetName("success-resource") - successfulRes.SetUID("12345678-1234-1234-1234-123456789001") - - // Create invalid configuration that will cause the PostgreSQL chart to fail - validData := map[string]interface{}{ - "service": map[string]interface{}{ - "type": "NodePort", - "port": int64(31180), - }, - } - unstructured.SetNestedField(successfulRes.Object, validData, "spec") - - successfulOpts := InstallOptions{ - HelmClient: helmClient, - ChartName: "https://charts.krateo.io", - Resource: successfulRes, - Repo: "nginx", - Version: "0.1.0", - CheckResourceOptions: CheckResourceOptions{ - DynamicClient: dynamicClient, - Pluralizer: FakePluralizer{}, - }, - KrateoNamespace: namespace, - } - - // Install the successful release - _, _, err = Install(ctx, successfulOpts) - if err != nil { - t.Fatalf("failed to install successful test chart: %v", err) - } - - // Install a release that will fail using Install function with invalid configuration - failedReleaseName := "test-release-failed" - failedRes := createDummyResource() - - lsFailed := failedRes.GetLabels() - if lsFailed == nil { - lsFailed = make(map[string]string) - } - lsFailed[meta.ReleaseNameLabel] = failedReleaseName - failedRes.SetLabels(lsFailed) - failedRes.SetName("failed-resource") - failedRes.SetUID("12345678-1234-1234-1234-123456789002") - // Create invalid configuration that will cause the PostgreSQL chart to fail - invalidData := map[string]interface{}{ - "service": map[string]interface{}{ - "type": "NodePort", - "port": int64(31180), - }, - } - unstructured.SetNestedField(failedRes.Object, invalidData, "spec") - - failedOpts := InstallOptions{ - HelmClient: helmClient, - ChartName: "https://charts.krateo.io", - Resource: failedRes, - Repo: "nginx", - Version: "0.1.0", - CheckResourceOptions: CheckResourceOptions{ - DynamicClient: dynamicClient, - Pluralizer: FakePluralizer{}, - }, - KrateoNamespace: namespace, - } - - // Try to install the potentially failing release - _, _, err = Install(ctx, failedOpts) - // We don't treat this as fatal since it might fail by design - if err != nil { - t.Logf("Expected potential failure when installing failing release: %v", err) - } - - // Now check that FindAllReleases finds the installed releases - releases, err = FindAllReleases(helmClient) - if err != nil { - t.Fatalf("unexpected error after installing releases: %v", err) - } - - assert.NotNil(t, releases) - // We should have at least 1 release (successful one), possibly 2 if the failed one was also created - assert.GreaterOrEqual(t, len(releases), 1, "Expected to find at least 1 release after installations") - - // Log all found releases and their statuses - t.Logf("Found %d releases:", len(releases)) - for _, release := range releases { - t.Logf("Release '%s' with status: %s", release.Name, release.Info.Status.String()) - } - - // Verify we can find the successful release - var successfulRelease *release.Release - var failedRelease *release.Release - - for _, rel := range releases { - if rel.Name == successfulReleaseName { - successfulRelease = rel - } - if rel.Name == failedReleaseName { - failedRelease = rel - } - } - - // Check successful release - assert.NotNil(t, successfulRelease, "Should find the successful release") - if successfulRelease != nil { - assert.Equal(t, successfulReleaseName, successfulRelease.Name) - assert.Equal(t, namespace, successfulRelease.Namespace) - assert.NotEmpty(t, successfulRelease.Info.Status.String(), "Successful release should have a status") - } - - // Check if failed release was created and found - if failedRelease != nil { - t.Logf("Found failed release '%s' with status: %s", failedRelease.Name, failedRelease.Info.Status.String()) - assert.Equal(t, failedReleaseName, failedRelease.Name) - assert.Equal(t, namespace, failedRelease.Namespace) - assert.NotEmpty(t, failedRelease.Info.Status.String(), "Failed release should have a status") - } else { - t.Logf("Failed release was not created or not found - this might be expected depending on the failure mode") - } - - // Clean up test releases - err = helmClient.UninstallRelease(&helmclient.ChartSpec{ - ReleaseName: successfulReleaseName, - Namespace: namespace, - }) - if err != nil { - t.Logf("Warning: failed to clean up successful test release: %v", err) - } - - if failedRelease != nil { - err = helmClient.UninstallRelease(&helmclient.ChartSpec{ - ReleaseName: failedReleaseName, - Namespace: namespace, - }) - if err != nil { - t.Logf("Warning: failed to clean up failed test release: %v", err) - } - } - - return ctx - }).Feature() - - testenv.Test(t, f) -} - -func TestFindAnyRelease(t *testing.T) { - f := features.New("FindAnyRelease"). - Setup(e2e.Logger("test")). - Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - return ctx - }).Assess("FindAnyRelease", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { - - helmClient, err := helmclient.NewClientFromRestConf(&helmclient.RestConfClientOptions{ - RestConfig: cfg.Client().RESTConfig(), - Options: &helmclient.Options{ - Namespace: namespace, - }, - }) - if err != nil { - t.Fatalf("failed to create helm client: %v", err) - } - - // Test finding non-existent release - nonExistentReleaseName := "non-existent-release" - actualRelease, err := FindAnyRelease(helmClient, nonExistentReleaseName) - assert.Nil(t, actualRelease) - dynamicClient, err := dynamic.NewForConfig(cfg.Client().RESTConfig()) - if err != nil { - t.Fatalf("failed to create dynamic client: %v", err) - } - - // Install a test release - testReleaseName := "test-findany-release" - testRes := createDummyResource() - - ls := testRes.GetLabels() - if ls == nil { - ls = make(map[string]string) - } - ls[meta.ReleaseNameLabel] = testReleaseName - testRes.SetLabels(ls) - testRes.SetName("findany-resource") - testRes.SetUID("12345678-1234-1234-1234-123456789003") - - validData := map[string]interface{}{ - "service": map[string]interface{}{ - "type": "NodePort", - "port": int64(31180), - }, - } - unstructured.SetNestedField(testRes.Object, validData, "spec") - - installOpts := InstallOptions{ - HelmClient: helmClient, - ChartName: "https://charts.krateo.io", - Resource: testRes, - Repo: "nginx", - Version: "0.1.0", - CheckResourceOptions: CheckResourceOptions{ - DynamicClient: dynamicClient, - Pluralizer: FakePluralizer{}, - }, - KrateoNamespace: namespace, - } - - // Install the test release - _, _, err = Install(ctx, installOpts) - if err != nil { - t.Fatalf("failed to install test chart: %v", err) - } - - // Test finding existing release - foundRelease, err := FindAnyRelease(helmClient, testReleaseName) - assert.NoError(t, err) - assert.NotNil(t, foundRelease) - assert.Equal(t, testReleaseName, foundRelease.Name) - assert.Equal(t, namespace, foundRelease.Namespace) - - // Clean up test release - err = helmClient.UninstallRelease(&helmclient.ChartSpec{ - ReleaseName: testReleaseName, - Namespace: namespace, - }) - if err != nil { - t.Logf("Warning: failed to clean up test release: %v", err) - } - - return ctx - }).Feature() - - testenv.Test(t, f) -} - -func createDummyResource() *unstructured.Unstructured { - data := map[string]interface{}{ - "like": false, - "greeting": "Hello World!", - "counter": int64(1), - } - - res := &unstructured.Unstructured{} - compositionMeta.SetReleaseName(res, "demo") - res.SetGroupVersionKind(schema.FromAPIVersionAndKind("dummy-charts.krateo.io/v0-2-0", "DummyChart")) - res.SetName("demo") - res.SetNamespace("demo-system") - unstructured.SetNestedField(res.Object, data, "spec", "data") - - return res -} From 365dde5c0e18035de2abf1cdacb6aa6b40d57787 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Mon, 9 Feb 2026 18:09:05 +0100 Subject: [PATCH 03/11] refactor: streamline helm chart installation and upgrade logic --- internal/composition/composition.go | 110 +++++++++++++++------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 1debbfb..215d139 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -23,7 +23,10 @@ import ( "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/rbac" "github.com/krateoplatformops/plumbing/env" helmconfig "github.com/krateoplatformops/plumbing/helm" + "github.com/krateoplatformops/plumbing/helm/utils" + helmutils "github.com/krateoplatformops/plumbing/helm/utils" "github.com/krateoplatformops/plumbing/helm/v3" + "github.com/krateoplatformops/plumbing/kubeutil/event" "github.com/krateoplatformops/plumbing/maps" "github.com/krateoplatformops/unstructured-runtime/pkg/controller" @@ -244,20 +247,27 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, fmt.Errorf("getting helm client: %w", err) } - values, ok, err := maps.NestedMap(mg.Object, "spec") + values, err := helmutils.ValuesFromSpec(mg) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("getting spec values: %w", err) } - if !ok { - values = map[string]interface{}{} + err = values.InjectGlobalValues(mg, h.pluralizer, krateoNamespace) + if err != nil { + return controller.ExternalObservation{}, fmt.Errorf("injecting global values: %w", err) + } + postrenderLabels, err := utils.LabelPostRenderFromSpec(mg, h.pluralizer, krateoNamespace) + if err != nil { + return controller.ExternalObservation{}, fmt.Errorf("creating label post renderer: %w", err) } upgradedRel, err := hc.Upgrade(ctx, releaseName, pkg.URL, &helmconfig.UpgradeConfig{ ActionConfig: &helmconfig.ActionConfig{ - ChartVersion: pkg.Version, - ChartName: pkg.Repo, - Username: pkg.Auth.Username, - Password: pkg.Auth.Password, - Values: values, + ChartVersion: pkg.Version, + ChartName: pkg.Repo, + Username: pkg.Auth.Username, + Password: pkg.Auth.Password, + InsecureSkipTLSverify: pkg.InsecureSkipTLSverify, + Values: values, + PostRenderer: postrenderLabels, }, MaxHistory: helmMaxHistory, }) @@ -320,10 +330,10 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, err } - // _, err = tools.UpdateStatus(ctx, mg, updateOpts) - // if err != nil { - // return controller.ExternalObservation{}, err - // } + _, err = tools.UpdateStatus(ctx, mg, updateOpts) + if err != nil { + return controller.ExternalObservation{}, err + } log.Debug("Composition Observed - installed", "package", pkg.URL) @@ -405,24 +415,49 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("creating helm client: %w", err) } - values, ok, err := maps.NestedMap(mg.Object, "spec") + values, err := helmutils.ValuesFromSpec(mg) if err != nil { return fmt.Errorf("getting spec values: %w", err) } - if !ok { - values = map[string]interface{}{} + err = values.InjectGlobalValues(mg, h.pluralizer, krateoNamespace) + if err != nil { + return fmt.Errorf("injecting global values: %w", err) + } + postrenderLabels, err := utils.LabelPostRenderFromSpec(mg, h.pluralizer, krateoNamespace) + if err != nil { + return fmt.Errorf("creating label post renderer: %w", err) } - rel, err := hc.Install(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.InstallConfig{ - ActionConfig: &helmconfig.ActionConfig{ - ChartVersion: pkg.Version, - ChartName: pkg.Repo, - Values: values, - }, - }) + actionConfig := &helmconfig.ActionConfig{ + ChartVersion: pkg.Version, + ChartName: pkg.Repo, + Values: values, + Username: pkg.Auth.Username, + Password: pkg.Auth.Password, + InsecureSkipTLSverify: pkg.InsecureSkipTLSverify, + PostRenderer: postrenderLabels, + } + + // Check if the release already exists before attempting to install, this can happen if the create event is triggered after a failed install + rel, err := hc.GetRelease(ctx, compositionMeta.GetReleaseName(mg), &helmconfig.GetConfig{}) if err != nil { - return fmt.Errorf("installing helm chart: %w", err) + return fmt.Errorf("finding helm release: %w", err) + } + if rel != nil { + log.Debug("Release already exists, upgrading instead of installing.") + rel, err = hc.Upgrade(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.UpgradeConfig{ + ActionConfig: actionConfig, + MaxHistory: helmMaxHistory, + }) + } else { + rel, err = hc.Install(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.InstallConfig{ + ActionConfig: actionConfig, + }) + if err != nil { + return fmt.Errorf("installing helm chart: %w", err) + } } + log.Debug("Installing composition package", "package", pkg.URL) all, digest, err := processor.DecodeMinRelease(rel) @@ -506,35 +541,6 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("creating helm client: %w", err) } - // values, ok, err := maps.NestedMap(mg.Object, "spec") - // if err != nil { - // return fmt.Errorf("getting spec values: %w", err) - // } - // if !ok { - // values = map[string]interface{}{} - // } - // upgradedRel, err := hc.Upgrade(ctx, releaseName, pkg.URL, &helmconfig.UpgradeConfig{ - // ActionConfig: &helmconfig.ActionConfig{ - // ChartVersion: pkg.Version, - // ChartName: pkg.Repo, - // Username: pkg.Auth.Username, - // Password: pkg.Auth.Password, - // Values: values, - // }, - // MaxHistory: helmMaxHistory, - // }) - // if err != nil { - // retErr := fmt.Errorf("upgrading helm chart: %w", err) - // condition := condition.Unavailable() - // condition.Message = retErr.Error() - // unstructuredtools.SetConditions(mg, condition) - // _, err = tools.UpdateStatus(ctx, mg, updateOpts) - // if err != nil { - // return fmt.Errorf("updating status after failure: %w", err) - // } - // return retErr - // } - upgradedRel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return fmt.Errorf("getting helm release: %w", err) From bbccb71ad12962fb408f06081c25c126b8197927 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Tue, 10 Feb 2026 11:06:09 +0100 Subject: [PATCH 04/11] feat: enable caching for helm client in Observe, Create, Update, and Delete methods --- internal/composition/composition.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 215d139..8f84385 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -170,6 +170,10 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c if err != nil { return controller.ExternalObservation{}, fmt.Errorf("creating helm client: %w", err) } + hc, err = hc.WithCache() + if err != nil { + return controller.ExternalObservation{}, fmt.Errorf("enabling helm client cache: %w", err) + } rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { @@ -246,6 +250,10 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c if err != nil { return controller.ExternalObservation{}, fmt.Errorf("getting helm client: %w", err) } + hc, err = hc.WithCache() + if err != nil { + return controller.ExternalObservation{}, fmt.Errorf("enabling helm client cache: %w", err) + } values, err := helmutils.ValuesFromSpec(mg) if err != nil { @@ -414,6 +422,10 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err if err != nil { return fmt.Errorf("creating helm client: %w", err) } + hc, err = hc.WithCache() + if err != nil { + return fmt.Errorf("enabling helm client cache: %w", err) + } values, err := helmutils.ValuesFromSpec(mg) if err != nil { @@ -540,6 +552,10 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err if err != nil { return fmt.Errorf("creating helm client: %w", err) } + hc, err = hc.WithCache() + if err != nil { + return fmt.Errorf("enabling helm client cache: %w", err) + } upgradedRel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { @@ -648,6 +664,10 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err if err != nil { return fmt.Errorf("creating helm client: %w", err) } + hc, err = hc.WithCache() + if err != nil { + return fmt.Errorf("enabling helm client cache: %w", err) + } pkg, err := h.packageInfoGetter.WithLogger(log).Get(mg) if err != nil { From 6d897ad063ad5c67c278c10322fa43944088308c Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Tue, 10 Feb 2026 14:53:36 +0100 Subject: [PATCH 05/11] fix: handle nil release in DecodeRelease function --- internal/composition/composition.go | 10 ++++++---- internal/tools/processor/processor.go | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 8f84385..015e495 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -132,6 +132,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c } compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg)) + releaseName = compositionMeta.GetReleaseName(mg) if _, p := compositionMeta.GetGracefullyPausedTime(mg); p && compositionMeta.IsGracefullyPaused(mg) { log.Debug("Composition is gracefully paused, skipping observe.") h.eventRecorder.Event(mg, event.Normal(reasonReconciliationGracefullyPaused, "Observe", "Reconciliation is paused via the gracefully paused annotation.")) @@ -378,6 +379,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err } compositionMeta.SetReleaseName(mg, compositionMeta.CalculateReleaseName(mg)) + releaseName := compositionMeta.GetReleaseName(mg) mg, err = tools.Update(ctx, mg, updateOpts) if err != nil { return fmt.Errorf("updating cr with values: %w", err) @@ -400,7 +402,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err rbgen := rbacgen.NewRBACGen(h.saName, h.saNamespace, &chartInspector) // Get Resources and generate RBAC generated, err := rbgen. - WithBaseName(compositionMeta.GetReleaseName(mg)). + WithBaseName(releaseName). Generate(rbacgen.Parameters{ CompositionName: mg.GetName(), CompositionNamespace: mg.GetNamespace(), @@ -451,18 +453,18 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err } // Check if the release already exists before attempting to install, this can happen if the create event is triggered after a failed install - rel, err := hc.GetRelease(ctx, compositionMeta.GetReleaseName(mg), &helmconfig.GetConfig{}) + rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { return fmt.Errorf("finding helm release: %w", err) } if rel != nil { log.Debug("Release already exists, upgrading instead of installing.") - rel, err = hc.Upgrade(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.UpgradeConfig{ + rel, err = hc.Upgrade(ctx, releaseName, pkg.URL, &helmconfig.UpgradeConfig{ ActionConfig: actionConfig, MaxHistory: helmMaxHistory, }) } else { - rel, err = hc.Install(ctx, compositionMeta.GetReleaseName(mg), pkg.URL, &helmconfig.InstallConfig{ + rel, err = hc.Install(ctx, releaseName, pkg.URL, &helmconfig.InstallConfig{ ActionConfig: actionConfig, }) if err != nil { diff --git a/internal/tools/processor/processor.go b/internal/tools/processor/processor.go index 3931ce2..a125e8d 100644 --- a/internal/tools/processor/processor.go +++ b/internal/tools/processor/processor.go @@ -25,6 +25,9 @@ func DecodeRelease[T any, PT interface { *T MinimalMetaObject }](rel *helm.Release) ([]T, string, error) { + if rel == nil { + return nil, "", nil + } // 1. Fast path: Empty manifest if len(rel.Manifest) == 0 { return nil, "", nil From f74dd14970229692f4d48546aa37e82a9b698d37 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Wed, 11 Feb 2026 12:26:00 +0100 Subject: [PATCH 06/11] docs: update README to reflect changes in HELM_REGISTRY_CONFIG_PATH usage refactor: clean up unused helm registry config variables in composition.go --- README.md | 2 +- internal/composition/composition.go | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 74450e1..5763a31 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ These enviroment varibles can be changed in the Deployment of the composition-dy | URL_PLURALS | NOT USED from version 0.17.1 - URL to krateo pluraliser service | `http://snowplow.krateo-system.svc.cluster.local:8081/api-info/names` | Ignored from version 0.17.1 | | URL_CHART_INSPECTOR | url to chart inspector | `http://chart-inspector.krateo-system.svc.cluster.local:8081/` | | KRATEO_NAMESPACE | namespace where krateo is installed | krateo-system | -| HELM_REGISTRY_CONFIG_PATH | default helm config path | /tmp | +| HELM_REGISTRY_CONFIG_PATH | NOT USED from version '1.0.0' - default helm config path | /tmp | | HELM_MAX_HISTORY | Max Helm History | 3 | | COMPOSITION_CONTROLLER_MAX_ERROR_RETRY_INTERVAL | The maximum interval between retries when an error occurs. This should be less than the half of the poll interval. | 60s | | COMPOSITION_CONTROLLER_MIN_ERROR_RETRY_INTERVAL | The minimum interval between retries when an error occurs. This should be less than max-error-retry-interval. | 1s | diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 015e495..4e04927 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -42,10 +42,8 @@ import ( ) var ( - // helmRegistryConfigPath = env.String(helmRegistryConfigPathEnvVar, helmclient.DefaultRegistryConfigPath) krateoNamespace = env.String(krateoNamespaceEnvVar, krateoNamespaceDefault) - // helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) - helmMaxHistory = env.Int(helmMaxHistoryEnvvar, 3) + helmMaxHistory = env.Int(helmMaxHistoryEnvvar, 3) ) const ( @@ -60,9 +58,8 @@ const ( reasonInstalled = "CompositionInstalled" // Environment variables - helmRegistryConfigPathEnvVar = "HELM_REGISTRY_CONFIG_PATH" - helmMaxHistoryEnvvar = "HELM_MAX_HISTORY" - krateoNamespaceEnvVar = "KRATEO_NAMESPACE" + helmMaxHistoryEnvvar = "HELM_MAX_HISTORY" + krateoNamespaceEnvVar = "KRATEO_NAMESPACE" // Default namespace for Krateo Installation krateoNamespaceDefault = "krateo-system" From 659f4f74df4ccc4281e16dcbb500d0bdd82d5ef8 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Thu, 12 Feb 2026 09:07:33 +0100 Subject: [PATCH 07/11] refactor: remove unused environment variable handling for helm registry config --- internal/composition/composition.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 4e04927..d541242 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -75,12 +75,6 @@ func NewHandler(cfg *rest.Config, chartInspectorUrl string, saName string, saNamespace string) controller.ExternalClient { - // val, ok := os.LookupEnv(helmRegistryConfigPathEnvVar) - // if ok { - // helmRegistryConfigPath = val - // } - - // helmRegistryConfigFile = filepath.Join(helmRegistryConfigPath, registry.CredentialsFileBasename) return &handler{ kubeconfig: cfg, From 72ecc6e544df1e4fc7906d5424dc5d6a25229ce1 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Thu, 12 Feb 2026 11:59:38 +0100 Subject: [PATCH 08/11] fix: ignore not found error during helm chart uninstallation --- internal/composition/composition.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/composition/composition.go b/internal/composition/composition.go index d541242..2391417 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -678,7 +678,9 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return nil } - err = hc.Uninstall(ctx, releaseName, &helmconfig.UninstallConfig{}) + err = hc.Uninstall(ctx, releaseName, &helmconfig.UninstallConfig{ + IgnoreNotFound: true, + }) if err != nil { return fmt.Errorf("uninstalling helm chart: %w", err) } From 2cbfb307069fba2e3329654b43bb71cdd9c746c8 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 13 Feb 2026 17:22:37 +0100 Subject: [PATCH 09/11] refactor: update helm client initialization to use functional options and enable caching --- go.mod | 8 ++--- go.sum | 4 +++ internal/composition/composition.go | 55 ++++++++++++++++------------- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index 507501d..f6dbaed 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/go-logr/logr v1.4.3 github.com/gobuffalo/flect v1.0.3 - github.com/krateoplatformops/plumbing v0.7.2 - github.com/krateoplatformops/unstructured-runtime v0.3.1 + github.com/krateoplatformops/plumbing v1.0.0 + github.com/krateoplatformops/unstructured-runtime v0.3.2 github.com/stretchr/testify v1.11.1 k8s.io/api v0.35.0 k8s.io/apiextensions-apiserver v0.35.0 @@ -140,7 +140,3 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) - -replace github.com/krateoplatformops/plumbing => /Users/matteogastaldello/Documents/plumbing - -replace github.com/krateoplatformops/unstructured-runtime => /Users/matteogastaldello/Documents/unstructured-runtime diff --git a/go.sum b/go.sum index bf27f16..6c6791c 100644 --- a/go.sum +++ b/go.sum @@ -167,6 +167,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/krateoplatformops/plumbing v1.0.0 h1:xDVFDSJOSwyJQs/Q+ebMzy5cBrqg9D0hra1Oga739vU= +github.com/krateoplatformops/plumbing v1.0.0/go.mod h1:sqOPL9L/qhuANK6PrsNeMpntLjg7q/2Xd/kYf259+vU= +github.com/krateoplatformops/unstructured-runtime v0.3.2 h1:da2WZxrQNtWvYgYNL/DfIsSMUmsRUbglkWU2/XqY1nE= +github.com/krateoplatformops/unstructured-runtime v0.3.2/go.mod h1:19uT87wZzRSjrfk3731Xhdt8ww7vnsXhljy4jk0cuWA= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= diff --git a/internal/composition/composition.go b/internal/composition/composition.go index 2391417..af879d9 100644 --- a/internal/composition/composition.go +++ b/internal/composition/composition.go @@ -158,14 +158,14 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c return controller.ExternalObservation{}, fmt.Errorf("updating cr with values: %w", err) } - hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) + hc, err := helm.NewClient(h.kubeconfig, + helm.WithNamespace(mg.GetNamespace()), + helm.WithLogger(h.getHelmLogger(meta.IsVerbose(mg))), + helm.WithCache(), + ) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("creating helm client: %w", err) } - hc, err = hc.WithCache() - if err != nil { - return controller.ExternalObservation{}, fmt.Errorf("enabling helm client cache: %w", err) - } rel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { @@ -238,14 +238,13 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { return tracer.WithRoundTripper(rt) } - hc, err = helm.NewClient(cfg, mg.GetNamespace(), slog.Default().Handler()) + hc, err = helm.NewClient(cfg, + helm.WithNamespace(mg.GetNamespace()), + helm.WithCache(), + ) if err != nil { return controller.ExternalObservation{}, fmt.Errorf("getting helm client: %w", err) } - hc, err = hc.WithCache() - if err != nil { - return controller.ExternalObservation{}, fmt.Errorf("enabling helm client cache: %w", err) - } values, err := helmutils.ValuesFromSpec(mg) if err != nil { @@ -411,14 +410,13 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("installing rbac: %w", err) } - hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) + hc, err := helm.NewClient(h.kubeconfig, + helm.WithNamespace(mg.GetNamespace()), + helm.WithLogger(h.getHelmLogger(meta.IsVerbose(mg))), + ) if err != nil { return fmt.Errorf("creating helm client: %w", err) } - hc, err = hc.WithCache() - if err != nil { - return fmt.Errorf("enabling helm client cache: %w", err) - } values, err := helmutils.ValuesFromSpec(mg) if err != nil { @@ -541,14 +539,13 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err } // Update the helm chart - hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) + hc, err := helm.NewClient(h.kubeconfig, + helm.WithNamespace(mg.GetNamespace()), + helm.WithLogger(h.getHelmLogger(meta.IsVerbose(mg))), + ) if err != nil { return fmt.Errorf("creating helm client: %w", err) } - hc, err = hc.WithCache() - if err != nil { - return fmt.Errorf("enabling helm client cache: %w", err) - } upgradedRel, err := hc.GetRelease(ctx, releaseName, &helmconfig.GetConfig{}) if err != nil { @@ -653,14 +650,13 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return fmt.Errorf("helm chart package info getter must be specified") } - hc, err := helm.NewClient(h.kubeconfig, mg.GetNamespace(), slog.Default().Handler()) + hc, err := helm.NewClient(h.kubeconfig, + helm.WithNamespace(mg.GetNamespace()), + helm.WithLogger(h.getHelmLogger(meta.IsVerbose(mg))), + ) if err != nil { return fmt.Errorf("creating helm client: %w", err) } - hc, err = hc.WithCache() - if err != nil { - return fmt.Errorf("enabling helm client cache: %w", err) - } pkg, err := h.packageInfoGetter.WithLogger(log).Get(mg) if err != nil { @@ -734,3 +730,12 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return nil } + +func (h *handler) getHelmLogger(verbose bool) func(format string, v ...interface{}) { + if verbose { + return func(format string, v ...interface{}) { + slog.Debug(fmt.Sprintf(format, v...)) + } + } + return func(format string, v ...interface{}) {} +} From 50edd40ff0bff4f50fba3a88ca8976a32cbd9847 Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 13 Feb 2026 17:39:24 +0100 Subject: [PATCH 10/11] fix: correct testdataPath for getter_test.go to ensure proper test execution --- internal/tools/archive/getter_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/tools/archive/getter_test.go b/internal/tools/archive/getter_test.go index a8a0ae2..dc63f12 100644 --- a/internal/tools/archive/getter_test.go +++ b/internal/tools/archive/getter_test.go @@ -15,7 +15,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/gobuffalo/flect" - // "github.com/krateoplatformops/composition-dynamic-controller/internal/tools/archive" "github.com/krateoplatformops/plumbing/e2e" xenv "github.com/krateoplatformops/plumbing/env" "github.com/krateoplatformops/unstructured-runtime/pkg/pluralizer" @@ -52,7 +51,7 @@ var ( ) const ( - testdataPath = "../../../../testdata" + testdataPath = "../../../testdata" ) type FakePluralizer struct{} From c3af199fbb7ba9a676e77688c09e0ed56059f6ec Mon Sep 17 00:00:00 2001 From: Matteo Gastaldello Date: Fri, 13 Feb 2026 17:49:11 +0100 Subject: [PATCH 11/11] fix: update empty manifest check in DecodeRelease to trim whitespace --- internal/tools/processor/processor.go | 2 +- internal/tools/processor/processor_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/tools/processor/processor.go b/internal/tools/processor/processor.go index a125e8d..3db07d9 100644 --- a/internal/tools/processor/processor.go +++ b/internal/tools/processor/processor.go @@ -29,7 +29,7 @@ func DecodeRelease[T any, PT interface { return nil, "", nil } // 1. Fast path: Empty manifest - if len(rel.Manifest) == 0 { + if strings.TrimSpace(rel.Manifest) == "" { return nil, "", nil } diff --git a/internal/tools/processor/processor_test.go b/internal/tools/processor/processor_test.go index 7ce2c7a..9ff7b8d 100644 --- a/internal/tools/processor/processor_test.go +++ b/internal/tools/processor/processor_test.go @@ -1,6 +1,7 @@ package processor import ( + "fmt" "strings" "testing" @@ -174,6 +175,9 @@ func TestDigestConsistency_Empty(t *testing.T) { digestFast, err := ComputeReleaseDigest(rel) assert.NoError(t, err) + fmt.Println("Digest for empty manifest (full):", digestFull) + fmt.Println("Digest for empty manifest (fast):", digestFast) + assert.Equal(t, "", digestFull) assert.Equal(t, "", digestFast) }