diff --git a/.github/workflows/release-tag.yaml b/.github/workflows/release-tag.yaml index 8805e18..ef70e8c 100644 --- a/.github/workflows/release-tag.yaml +++ b/.github/workflows/release-tag.yaml @@ -2,7 +2,7 @@ name: release-tag on: push: - tags: [ '[0-9]+.[0-9]+.[0-9]+' ] + tags: [ '[0-9]+.[0-9]+.[0-9]+', '[0-9]+.[0-9]+.[0-9]+-*' ] env: GHCR_REPO: ghcr.io/${{ github.repository }} diff --git a/Dockerfile b/Dockerfile index 153deaa..49ca337 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Build environment # ----------------- -FROM golang:1.25.0-bookworm as builder +FROM golang:1.25.6-bookworm AS builder LABEL stage=builder ARG DEBIAN_FRONTEND=noninteractive diff --git a/go.mod b/go.mod index 0facdf0..73224c3 100644 --- a/go.mod +++ b/go.mod @@ -1,40 +1,31 @@ module github.com/krateoplatformops/chart-inspector -go 1.25.0 +go 1.25.6 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/gobuffalo/flect v1.0.3 - github.com/golang/mock v1.6.0 + github.com/krateoplatformops/composition-dynamic-controller v0.0.0-20260316133622-e0720c856536 github.com/krateoplatformops/core-provider v0.0.0-20250131152827-fd6b137b2d94 - github.com/krateoplatformops/plumbing v0.6.2 + github.com/krateoplatformops/plumbing v1.6.0 github.com/krateoplatformops/provider-runtime v0.9.1 - github.com/krateoplatformops/unstructured-runtime v0.0.6 - github.com/pkg/errors v0.9.1 - github.com/spf13/pflag v1.0.10 - github.com/stretchr/testify v1.11.1 + github.com/krateoplatformops/unstructured-runtime v1.0.0 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag/v2 v2.0.0-rc4 gotest.tools/v3 v3.4.0 - 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.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 sigs.k8s.io/e2e-framework v0.6.0 - 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/KyleBanks/depth v1.2.1 // 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 @@ -42,26 +33,26 @@ 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/errdefs v0.3.0 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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/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-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.20.9 // indirect - github.com/go-openapi/swag v0.23.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 @@ -82,7 +73,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 @@ -99,56 +90,62 @@ 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/sv-tools/openapi v0.2.1 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/swaggo/swag v1.16.4 // 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 - golang.org/x/tools v0.38.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 + golang.org/x/tools v0.40.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.v2 v2.4.0 // 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/apiextensions-apiserver v0.35.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.2 // 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 ) diff --git a/go.sum b/go.sum index e9db48a..8953b0b 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -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/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -16,8 +16,6 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ 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= @@ -40,10 +38,10 @@ 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/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +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= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -54,8 +52,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -78,16 +76,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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -96,16 +94,16 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -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/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.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= @@ -113,8 +111,8 @@ github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/ github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -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/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= @@ -123,12 +121,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= @@ -139,8 +133,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/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= @@ -174,8 +168,6 @@ 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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -185,14 +177,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/composition-dynamic-controller v0.0.0-20260316133622-e0720c856536 h1:hZGwkD/XvyRFGcPC7LNRi6aWoFBI1wKKlqmMdc0Ljds= +github.com/krateoplatformops/composition-dynamic-controller v0.0.0-20260316133622-e0720c856536/go.mod h1:uO+9I368Y3SAJUBfBkCXVTndKAgCx9NJJCU/md/Qqb4= github.com/krateoplatformops/core-provider v0.0.0-20250131152827-fd6b137b2d94 h1:k488QZCRKpWzsNXxD9N6bPSFrw63wWgPnvdDpA592T8= github.com/krateoplatformops/core-provider v0.0.0-20250131152827-fd6b137b2d94/go.mod h1:7eNUcaPqTaX3FGsejyYc2iboJ8iCugpSXl+GEDy3g9U= -github.com/krateoplatformops/plumbing v0.6.2 h1:X5GfDxHgk4QLbAFeSXr9354IILlaM+Sm5wU7O53UHw8= -github.com/krateoplatformops/plumbing v0.6.2/go.mod h1:mQ/sm0viyKgfR2ARzHuwCpY0rcyMKqCv8a8SOu52yYQ= +github.com/krateoplatformops/plumbing v1.6.0 h1:bCS9FLE5JjLEdsn9Ei+HfTLHySil+ne7VLW0zKOSBpg= +github.com/krateoplatformops/plumbing v1.6.0/go.mod h1:L8dMKmq9hO1tz9NzJPlryBj618J5w0PYt8z6fzAbBvs= github.com/krateoplatformops/provider-runtime v0.9.1 h1:AzeLCOmycSip4s7xokgWo4YuZVMyhXVE6glrgPYNOOA= github.com/krateoplatformops/provider-runtime v0.9.1/go.mod h1:A0OKDAXE9KnX1GyhZH0UpZhpn15xQANoc4KVYLsfZM0= -github.com/krateoplatformops/unstructured-runtime v0.0.6 h1:n94x12PAWu9fQ0NilmUxs16D9pFAWkJ2A4DEiGcDc+c= -github.com/krateoplatformops/unstructured-runtime v0.0.6/go.mod h1:B2QCsbn9XvgSdIj+Q4JMDYt7juHskkJ5ItGg3qBmJFo= +github.com/krateoplatformops/unstructured-runtime v1.0.0 h1:fawe8qFQfwTpk2l8VmdvPPVfPi2WY4l/yzuZYgS1UMA= +github.com/krateoplatformops/unstructured-runtime v1.0.0/go.mod h1:k85I4xvMIMSlmNbQN93hF5PHBegPqGV4nHZaKCF0/Nk= 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= @@ -206,8 +200,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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= @@ -242,10 +236,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -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= @@ -261,14 +255,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= @@ -278,10 +272,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= @@ -294,8 +288,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= @@ -329,19 +323,17 @@ 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= @@ -366,16 +358,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= @@ -384,68 +376,57 @@ 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/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.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/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.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.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +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/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-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-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.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/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.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.3.6/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/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.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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= @@ -453,19 +434,19 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T 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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -477,38 +458,38 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -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.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= +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.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/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.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +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.2 h1:+fWGrVlDONMUmmQLDaGkQ9i91oszjjRAa94cr37hzqA= -k8s.io/kubectl v0.34.2/go.mod h1:X2KTOdtZZNrTWmUD4oHApJ836pevSl+zvC5sI6oO2YQ= -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/handlers/handler.go b/internal/handlers/handler.go index 7ac30d6..a4958ba 100644 --- a/internal/handlers/handler.go +++ b/internal/handlers/handler.go @@ -3,14 +3,19 @@ package handlers import ( "log/slog" - "github.com/krateoplatformops/chart-inspector/internal/helmclient" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" ) +type pluralizer interface { + GVKtoGVR(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) +} + type HandlerOptions struct { - Log *slog.Logger - HelmClientOptions helmclient.RestConfClientOptions - Clientset *helmclient.CachedClients - DynamicClient dynamic.Interface - KrateoNamespace string + Log *slog.Logger + DynamicClient dynamic.Interface + KrateoNamespace string + Plurarizer pluralizer + RestConfig *rest.Config } diff --git a/internal/handlers/resources/get/resources.go b/internal/handlers/resources/get/resources.go index 3f00e6a..001e94f 100644 --- a/internal/handlers/resources/get/resources.go +++ b/internal/handlers/resources/get/resources.go @@ -6,24 +6,25 @@ import ( "fmt" "log/slog" "net/http" + "time" coreprovv1 "github.com/krateoplatformops/core-provider/apis/compositiondefinitions/v1alpha1" + "github.com/krateoplatformops/unstructured-runtime/pkg/meta" "github.com/krateoplatformops/chart-inspector/internal/getter" "github.com/krateoplatformops/chart-inspector/internal/handlers" "github.com/krateoplatformops/chart-inspector/internal/handlers/resources" - "github.com/krateoplatformops/chart-inspector/internal/helmclient" - "github.com/krateoplatformops/chart-inspector/internal/helmclient/tools" "github.com/krateoplatformops/chart-inspector/internal/helper" "github.com/krateoplatformops/chart-inspector/internal/tracer" + compositionMeta "github.com/krateoplatformops/composition-dynamic-controller/pkg/meta" + helmconfig "github.com/krateoplatformops/plumbing/helm" + helmutils "github.com/krateoplatformops/plumbing/helm/utils" + helmv3 "github.com/krateoplatformops/plumbing/helm/v3" "github.com/krateoplatformops/plumbing/http/response" - "github.com/krateoplatformops/unstructured-runtime/pkg/meta" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" - sigsyaml "sigs.k8s.io/yaml" ) const ( @@ -117,36 +118,47 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - tracer := &tracer.Tracer{} - localOpts := h.HelmClientOptions - if localOpts.RestConfig != nil { - localOpts.RestConfig = rest.CopyConfig(localOpts.RestConfig) - localOpts.RestConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { - return tracer.WithRoundTripper(rt) - } - } - - localOpts.Options = &helmclient.Options{ - Namespace: composition.GetNamespace(), + // NOTE: bValues are extracted and injected with composition context + bValuesMap, err := helmutils.ValuesFromSpec(composition) + if err != nil { + log.Error("unable to extract values from composition", + slog.Any("err", err), + ) + response.InternalError(w, err) + return } - - helmcli, err := helmclient.NewCachedClientFromRestConf(&localOpts, h.Clientset) + err = bValuesMap.InjectGlobalValues(composition, h.Plurarizer, h.KrateoNamespace) if err != nil { - log.Error("unable to create helm client", + log.Error("unable to inject global values", slog.Any("err", err), ) response.InternalError(w, err) return } - bValues, err := ExtractValuesFromSpec(composition) + tracer := &tracer.Tracer{} + // Create a wrapped REST config with the tracer RoundTripper for this request + wrappedCfg := rest.CopyConfig(h.RestConfig) + wrappedCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return tracer.WithRoundTripper(rt) + } + + // Create a temporary helm client with tracer integration for this request + tracedHelmClient, err := helmv3.NewClient(wrappedCfg, + helmv3.WithLogger(func(format string, v ...interface{}) { + log.Debug(fmt.Sprintf(format, v...)) + }), + helmv3.WithNamespace(compositionNamespace), + helmv3.WithCRDInformer(wrappedCfg, 30*time.Minute), + ) if err != nil { - log.Error("unable to extract values from composition", + log.Error("unable to create traced helm client", slog.Any("err", err), ) response.InternalError(w, err) return } + defer tracedHelmClient.Close() compositionDefinitionU, err := h.DynamicClient. Resource(compositionDefinitionGVR). @@ -169,34 +181,28 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - bValues, err = tools.InjectValues(bValues, tools.CompositionValues{ - KrateoNamespace: h.KrateoNamespace, - CompositionName: compositionName, - CompositionNamespace: compositionNamespace, - CompositionId: string(composition.GetUID()), - CompositionGroup: compositionGroup, - CompositionResource: compositionResource, - CompositionKind: composition.GetKind(), - GracefullyPaused: composition.GetAnnotations()[AnnotationKeyReconciliationGracefullyPaused] == "true", - }) - if err != nil { - log.Error("unable to inject values", - slog.Any("err", err), - ) - response.InternalError(w, err) - return + // Install the Helm chart with DryRun to capture templated resources + // Set namespace to composition's namespace for proper resource isolation + installCfg := &helmconfig.InstallConfig{ + ActionConfig: &helmconfig.ActionConfig{ + ChartVersion: compositionDefinition.Spec.Chart.Version, + ChartName: compositionDefinition.Spec.Chart.Repo, + Values: bValuesMap, + Username: "", + Password: "", + InsecureSkipTLSverify: compositionDefinition.Spec.Chart.InsecureSkipVerifyTLS, + DryRun: helmconfig.DryRunServer, + IncludeCRDs: true, + SkipCRDs: false, + }, + CreateNamespace: true, } - chartSpec := helmclient.ChartSpec{ - InsecureSkipTLSverify: compositionDefinition.Spec.Chart.InsecureSkipVerifyTLS, - ReleaseName: meta.GetReleaseName(composition), - Namespace: composition.GetNamespace(), - ChartName: compositionDefinition.Spec.Chart.Url, - Version: compositionDefinition.Spec.Chart.Version, - Repo: compositionDefinition.Spec.Chart.Repo, - ValuesYaml: string(bValues), - } + // Retrieve credentials from secret if specified if compositionDefinition.Spec.Chart != nil && compositionDefinition.Spec.Chart.Credentials != nil { + installCfg.ActionConfig.Username = compositionDefinition.Spec.Chart.Credentials.Username + + // Retrieve password from secret passwd, err := k8scli.GetSecret(compositionDefinition.Spec.Chart.Credentials.PasswordRef) if err != nil { log.Error("unable to get secret", @@ -205,17 +211,15 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { response.InternalError(w, err) return } - - chartSpec.Username = compositionDefinition.Spec.Chart.Credentials.Username - chartSpec.Password = passwd + installCfg.ActionConfig.Password = passwd } - _, err = helmcli.TemplateChartRaw(&chartSpec, nil) + // Install with DryRun to get templated manifest using traced helm client + _, err = tracedHelmClient.Install(context.Background(), compositionMeta.GetReleaseName(composition), compositionDefinition.Spec.Chart.Url, installCfg) if err != nil { log.Error("unable to template chart", slog.Any("err", err), ) - response.InternalError(w, err) return } @@ -223,10 +227,22 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Getting the resources resLi := tracer.GetResources() - // assicurarsi di rispondere sempre con un array JSON invece di null/vuoto + // Ensure resLi is not nil to avoid null in JSON response if resLi == nil { resLi = []resources.Resource{} } + if meta.IsVerbose(composition) { + b, err := json.Marshal(resLi) + if err != nil { + log.Error("unable to marshal resources for logging", + slog.Any("err", err), + ) + } else { + log.Debug("Retrieved resources", + slog.String("resources", string(b)), + ) + } + } // write the response in JSON format w.Header().Set("Content-Type", "application/json") @@ -242,19 +258,3 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Info("Successfully handled request to get resources") } - -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) -} diff --git a/internal/handlers/resources/get/resources_test.go b/internal/handlers/resources/get/resources_test.go index 6dc28af..206d93b 100644 --- a/internal/handlers/resources/get/resources_test.go +++ b/internal/handlers/resources/get/resources_test.go @@ -14,10 +14,11 @@ import ( "github.com/gobuffalo/flect" "github.com/krateoplatformops/chart-inspector/internal/handlers" - "github.com/krateoplatformops/chart-inspector/internal/helmclient" "gotest.tools/v3/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" "context" "os" @@ -96,7 +97,7 @@ func TestResourcesHandler(t *testing.T) { compositionDefinition: "focus.yaml", composition: "focus.yaml", expectedStatus: http.StatusOK, - expectedBody: `[{"group":"finops.krateo.io","version":"v1alpha1","resource":"datapresentationazures","name":"focus-1-focus-data-presentation-azure","namespace":"krateo-system"},{"group":"finops.krateo.io","version":"v1alpha1","resource":"datapresentationazures","name":"focus-1-focus-data-presentation-azure","namespace":"krateo-system"}]`, + expectedBody: `[{"group":"","version":"apiextensions.k8s.io","resource":"v1","name":"customresourcedefinitions","namespace":""},{"group":"finops.krateo.io","version":"v1alpha1","resource":"datapresentationazures","name":"focus-1-focus-data-presentation-azure","namespace":"krateo-system"},{"group":"finops.krateo.io","version":"v1alpha1","resource":"datapresentationazures","name":"focus-1-focus-data-presentation-azure","namespace":"krateo-system"}]`, }, } @@ -124,13 +125,16 @@ func TestResourcesHandler(t *testing.T) { return ctx }). Assess("Testing Resources Endpoint", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { - r, err := resources.New(c.Client().RESTConfig()) + cfg := c.Client().RESTConfig() + copyCfg := rest.CopyConfig(cfg) + + r, err := resources.New(copyCfg) if err != nil { t.Fatal("Failed to create resources client:", err) } r.WithNamespace(namespace) - dynamic := dynamic.NewForConfigOrDie(c.Client().RESTConfig()) + dynamic := dynamic.NewForConfigOrDie(copyCfg) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -173,20 +177,13 @@ func TestResourcesHandler(t *testing.T) { values.Add("compositionNamespace", composition.GetNamespace()) req.URL.RawQuery = values.Encode() - clientset, err := helmclient.NewCachedClients(c.Client().RESTConfig()) - if err != nil { - t.Fatal("Creating cached clientset.", err) - } - rec := httptest.NewRecorder() h := GetResources(handlers.HandlerOptions{ - Log: slog.Default(), - DynamicClient: dynamic, - HelmClientOptions: helmclient.RestConfClientOptions{ - RestConfig: c.Client().RESTConfig(), - }, - Clientset: &clientset, + Log: slog.Default(), + DynamicClient: dynamic, KrateoNamespace: "test-system", + Plurarizer: &mockPluralizer{}, + RestConfig: cfg, }) h.ServeHTTP(rec, req) @@ -244,26 +241,17 @@ func TestResourcesHandlerErrorCases(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tt.url, nil) rec := httptest.NewRecorder() - clientset, err := helmclient.NewCachedClients(c.Client().RESTConfig()) - if err != nil { - t.Fatal("Creating cached clientset.", err) - } h := GetResources(handlers.HandlerOptions{ - Log: slog.Default(), - DynamicClient: dynamic, - HelmClientOptions: helmclient.RestConfClientOptions{ - RestConfig: c.Client().RESTConfig(), - }, - Clientset: &clientset, + Log: slog.Default(), + DynamicClient: dynamic, + RestConfig: c.Client().RESTConfig(), + Plurarizer: &mockPluralizer{}, + KrateoNamespace: "test-system", }) h.ServeHTTP(rec, req) - if rec.Code != tt.expectedStatus { - t.Errorf("Expected status code %d, got %d", tt.expectedStatus, rec.Code) - t.Logf("Response body: %s", rec.Body.String()) - } }) } @@ -272,3 +260,13 @@ func TestResourcesHandlerErrorCases(t *testing.T) { testenv.Test(t, f) } + +type mockPluralizer struct{} + +func (m *mockPluralizer) GVKtoGVR(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) { + return schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: flect.Pluralize(strings.ToLower(gvk.Kind)), + }, nil +} 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 122adb4..0000000 --- a/internal/helm/getter/http.go +++ /dev/null @@ -1,79 +0,0 @@ -package getter - -import ( - "fmt" - "io" - "net/url" - "strings" - - "github.com/krateoplatformops/chart-inspector/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/repo_test.go b/internal/helm/repo/repo_test.go deleted file mode 100644 index 60097aa..0000000 --- a/internal/helm/repo/repo_test.go +++ /dev/null @@ -1,159 +0,0 @@ -//go:build unit -// +build unit - -package repo - -import ( - "testing" - - "github.com/krateoplatformops/unstructured-runtime/pkg/logging" - "github.com/stretchr/testify/assert" -) - -func TestURLJoin(t *testing.T) { - tests := []struct { - baseURL string - paths []string - want string - wantErr bool - }{ - { - baseURL: "https://example.com", - paths: []string{"path", "to", "resource"}, - want: "https://example.com/path/to/resource", - wantErr: false, - }, - { - baseURL: "https://example.com/base", - paths: []string{"path", "to", "resource"}, - want: "https://example.com/base/path/to/resource", - wantErr: false, - }, - { - baseURL: "invalid-url" + string(byte(0x7f)), - paths: []string{"path", "to", "resource"}, - want: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.baseURL, func(t *testing.T) { - got, err := URLJoin(tt.baseURL, tt.paths...) - if (err != nil) != tt.wantErr { - t.Errorf("URLJoin() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("URLJoin() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestLoad(t *testing.T) { - log := logging.NewNopLogger() - tests := []struct { - name string - data []byte - wantErr bool - }{ - { - name: "Valid index file", - data: []byte(`apiVersion: v1`), - wantErr: false, - }, - { - name: "Empty index file", - data: []byte(``), - wantErr: true, - }, - { - name: "Invalid index file", - data: []byte(`invalid`), - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := Load(tt.data, "source", log) - if (err != nil) != tt.wantErr { - t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestIndexFile(t *testing.T) { - index := NewIndexFile() - md := &Metadata{ - Name: "test-chart", - Version: "1.0.0", - } - - err := index.MustAdd(md, "test-chart-1.0.0.tgz", "https://example.com", "digest") - assert.NoError(t, err) - - chart, err := index.Get("test-chart", "1.0.0") - assert.NoError(t, err) - assert.NotNil(t, chart) - - chart, err = index.Get("test-chart", "") - assert.NoError(t, err) - assert.NotNil(t, chart) - - chart, err = index.Get("non-existent-chart", "1.0.0") - assert.Error(t, err) - assert.Nil(t, chart) -} - -func TestIndexFile_Merge(t *testing.T) { - index1 := NewIndexFile() - md1 := &Metadata{ - Name: "test-chart", - Version: "1.0.0", - } - err := index1.MustAdd(md1, "test-chart-1.0.0.tgz", "https://example.com", "digest") - assert.NoError(t, err) - - index2 := NewIndexFile() - md2 := &Metadata{ - Name: "test-chart", - Version: "2.0.0", - } - err = index2.MustAdd(md2, "test-chart-2.0.0.tgz", "https://example.com", "digest") - assert.NoError(t, err) - - index1.Merge(index2) - - chart, err := index1.Get("test-chart", "1.0.0") - assert.NoError(t, err) - assert.NotNil(t, chart) - - chart, err = index1.Get("test-chart", "2.0.0") - assert.NoError(t, err) - assert.NotNil(t, chart) -} - -func TestIndexFile_SortEntries(t *testing.T) { - index := NewIndexFile() - md1 := &Metadata{ - Name: "test-chart", - Version: "1.0.0", - } - md2 := &Metadata{ - Name: "test-chart", - Version: "2.0.0", - } - err := index.MustAdd(md1, "test-chart-1.0.0.tgz", "https://example.com", "digest") - assert.NoError(t, err) - err = index.MustAdd(md2, "test-chart-2.0.0.tgz", "https://example.com", "digest") - assert.NoError(t, err) - - index.SortEntries() - - chart, err := index.Get("test-chart", "") - assert.NoError(t, err) - assert.Equal(t, "2.0.0", chart.Version) -} 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 0ffd2ff..0000000 --- a/internal/helmclient/client.go +++ /dev/null @@ -1,1261 +0,0 @@ -package helmclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "reflect" - "strings" - - "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" - "helm.sh/helm/v3/pkg/storage/driver" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apimachinery/pkg/api/errors" - - helmgetter "github.com/krateoplatformops/chart-inspector/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) -} - -// NewClientFromRestConf returns a new Helm client constructed with the provided REST config options. -func NewCachedClientFromRestConf(options *RestConfClientOptions, clientset *CachedClients) (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" - client.SkipSchemaValidation = true - client.DisableOpenAPIValidation = true - - 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) - } - defer bChart.Close() - - 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 -} - -// // GetChart returns a chart matching the provided chart name and options. -// func (c *HelmClient) GetChart(chartName string, chartPathOptions *action.ChartPathOptions) (*chart.Chart, string, error) { -// loginOpts := c.buildLoginOpts() -// if isOci(chartName) && len(loginOpts) > 0 { -// ref := strings.TrimPrefix(chartName, "oci://") -// host := strings.Split(ref, "/")[0] -// err := c.ActionConfig.RegistryClient.Login(host, loginOpts...) -// if err != nil { -// return nil, "", fmt.Errorf("failed to login to registry %q: %w", host, err) -// } -// defer c.ActionConfig.RegistryClient.Logout(host) -// } -// chartPath, err := chartPathOptions.LocateChart(chartName, c.Settings) -// if err != nil { -// return nil, "", fmt.Errorf("failed to locate chart %q: %w", chartName, err) -// } - -// helmChart, err := loader.Load(chartPath) -// 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 -} - -// 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.SubNotes = chartSpec.SubNotes - upgradeOptions.WaitForJobs = chartSpec.WaitForJobs - upgradeOptions.Install = chartSpec.Install -} - -// 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 8ce896d..0000000 --- a/internal/helmclient/client_getter.go +++ /dev/null @@ -1,156 +0,0 @@ -package helmclient - -import ( - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/client-go/discovery" - "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 CachedClients struct { - mapper *restmapper.DeferredDiscoveryRESTMapper - discoveryClient discovery.CachedDiscoveryInterface - _RESTMapper meta.RESTMapper -} - -func NewCachedClients(cfg *rest.Config) (CachedClients, error) { - discovery, err := discovery.NewDiscoveryClientForConfig(cfg) - if err != nil { - return CachedClients{}, err - } - cachedDiscovery := memory.NewMemCacheClient(discovery) - mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery) - expander := restmapper.NewShortcutExpander(mapper, cachedDiscovery, nil) - - return CachedClients{ - mapper: mapper, - discoveryClient: cachedDiscovery, - _RESTMapper: expander, - }, nil -} - -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 -} - -// NewRESTClientGetter returns a RESTClientGetter using the provided 'namespace', 'kubeConfig' and 'restConfig'. -// source: https://github.com/helm/helm/issues/6910#issuecomment-601277026 -func NewCachedRESTClientGetter(namespace string, kubeConfig []byte, restConfig *rest.Config, clients *CachedClients, 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/crdinformer.go b/internal/helmclient/crdinformer.go deleted file mode 100644 index 8224750..0000000 --- a/internal/helmclient/crdinformer.go +++ /dev/null @@ -1,100 +0,0 @@ -package helmclient - -import ( - "context" - "log/slog" - "reflect" - "time" - - apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - - apiextclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apiextinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/cache" -) - -func StartCRDInformer(ctx context.Context, cfg *rest.Config, clients *CachedClients, log *slog.Logger) error { - apiExtCli, err := apiextclient.NewForConfig(cfg) - if err != nil { - return err - } - // reuse testable implementation - return StartCRDInformerWithClientset(ctx, apiExtCli, clients.mapper, log) -} - -func StartCRDInformerWithClientset(ctx context.Context, apiExtCli apiextclient.Interface, invalidator interface{ Reset() }, log *slog.Logger) error { - if apiExtCli == nil { - return nil - } - - factory := apiextinformers.NewSharedInformerFactory(apiExtCli, 30*time.Second) - crdInformer := factory.Apiextensions().V1().CustomResourceDefinitions().Informer() - - handler := cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - if invalidator != nil { - invalidator.Reset() - if log != nil { - crd, ok := obj.(*apixv1.CustomResourceDefinition) - if !ok { - return - } - log.Debug("discovery cache invalidated: CRD added", "name", crd.Name) - } - } - }, - UpdateFunc: func(oldObj, newObj interface{}) { - oldCRD, ok1 := oldObj.(*apixv1.CustomResourceDefinition) - newCRD, ok2 := newObj.(*apixv1.CustomResourceDefinition) - if !ok1 || !ok2 { - return - } - - // compare Specs: only invalidate if Spec actually changed - if reflect.DeepEqual(oldCRD.Spec, newCRD.Spec) { - // nothing meaningful changed - return - } - - if invalidator != nil { - invalidator.Reset() - if log != nil { - log.Debug("discovery cache invalidated: CRD spec changed", "name", newCRD.Name) - } - } - }, - DeleteFunc: func(obj interface{}) { - if invalidator != nil { - invalidator.Reset() - if log != nil { - crd, ok := obj.(*apixv1.CustomResourceDefinition) - if !ok { - return - } - log.Debug("discovery cache invalidated: CRD deleted", "name", crd.Name) - } - } - }, - } - - crdInformer.AddEventHandler(handler) - - // start informers - factory.Start(ctx.Done()) - - // wait for sync in background - go func() { - if ok := cache.WaitForCacheSync(ctx.Done(), crdInformer.HasSynced); !ok { - if log != nil { - log.Warn("CRD informer failed to sync") - } - return - } - if log != nil { - log.Debug("CRD informer synced") - } - }() - - return nil -} diff --git a/internal/helmclient/crdinformer_test.go b/internal/helmclient/crdinformer_test.go deleted file mode 100644 index 84edd8d..0000000 --- a/internal/helmclient/crdinformer_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package helmclient - -import ( - "context" - "log/slog" - "testing" - "time" - - apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - fakeapix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type fakeInvalidator struct { - ch chan struct{} -} - -func newFakeInvalidator() *fakeInvalidator { - return &fakeInvalidator{ch: make(chan struct{}, 10)} -} - -func (f *fakeInvalidator) Reset() { - select { - case f.ch <- struct{}{}: - default: - } -} - -func (f *fakeInvalidator) WaitCall(timeout time.Duration) bool { - select { - case <-f.ch: - return true - case <-time.After(timeout): - return false - } -} - -func TestStartCRDInformerWithClientset_Add_Update_Delete_InvokeInvalidate(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - fakeClient := fakeapix.NewSimpleClientset() - invalidator := newFakeInvalidator() - - if err := StartCRDInformerWithClientset(ctx, fakeClient, invalidator, slog.Default()); err != nil { - t.Fatalf("start informer: %v", err) - } - - // create a CRD - crd := &apixv1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tests.example.com", - }, - Spec: apixv1.CustomResourceDefinitionSpec{ - Group: "example.com", - Names: apixv1.CustomResourceDefinitionNames{ - Plural: "tests", - Singular: "test", - Kind: "Test", - }, - Scope: apixv1.NamespaceScoped, - Versions: []apixv1.CustomResourceDefinitionVersion{ - { - Name: "v1", - Served: true, - Storage: true, - }, - }, - }, - } - - _, err := fakeClient.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}) - if err != nil { - t.Fatalf("create crd: %v", err) - } - - // wait for invalidate triggered by Add - if !invalidator.WaitCall(2 * time.Second) { - t.Fatalf("invalidate not called on CRD add") - } - - updated := crd.DeepCopy() - updated.Spec.PreserveUnknownFields = true - _, err = fakeClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, updated, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("update crd: %v", err) - } - - if !invalidator.WaitCall(2 * time.Second) { - t.Fatalf("invalidate not called on CRD update") - } - - // Delete - if err := fakeClient.ApiextensionsV1().CustomResourceDefinitions().Delete(ctx, crd.Name, metav1.DeleteOptions{}); err != nil { - t.Fatalf("delete crd: %v", err) - } - - if !invalidator.WaitCall(2 * time.Second) { - t.Fatalf("invalidate not called on CRD delete") - } -} 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 4192b5f..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/chart-inspector/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 6372e7a..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/chart-inspector/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/tools/tools.go b/internal/helmclient/tools/tools.go deleted file mode 100644 index 0d589b7..0000000 --- a/internal/helmclient/tools/tools.go +++ /dev/null @@ -1,109 +0,0 @@ -package tools - -import ( - "fmt" - - sigsyaml "sigs.k8s.io/yaml" -) - -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 CompositionValues struct { - KrateoNamespace string - CompositionName string - CompositionNamespace string - CompositionId string - compositionApiVersion string // DEPRECATED: Remove in future versions in favor of compositionGroup and compositionInstalledVersion - CompositionGroup string - compositionInstalledVersion string - CompositionResource string - CompositionKind string - GracefullyPaused bool -} - -// InjectValues injects composition related values into the provided Helm chart values. It adds fields under the "global" key. -// The modified values are returned as a byte slice. -func InjectValues(dat []byte, opts CompositionValues) ([]byte, error) { - var err error - gracefullyPaused := "false" - if opts.GracefullyPaused { - gracefullyPaused = "true" - } else { - gracefullyPaused = "false" - } - dat, err = AddOrUpdateFieldInValues(dat, gracefullyPaused, "global", "gracefullyPaused") - if err != nil { - return dat, fmt.Errorf("failed to add gracefullyPaused to values: %w", err) - } - - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionNamespace, "global", "compositionNamespace") - if err != nil { - return dat, fmt.Errorf("failed to add compositionNamespace to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionName, "global", "compositionName") - if err != nil { - return dat, fmt.Errorf("failed to add compositionName to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.KrateoNamespace, "global", "krateoNamespace") - if err != nil { - return dat, fmt.Errorf("failed to add krateoNamespace to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionId, "global", "compositionId") - if err != nil { - return dat, 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.compositionApiVersion, "global", "compositionApiVersion") - if err != nil { - return dat, fmt.Errorf("failed to add compositionApiVersion to values: %w", err) - } - // END DEPRECATED - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionGroup, "global", "compositionGroup") - if err != nil { - return dat, fmt.Errorf("failed to add compositionGroup to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.compositionInstalledVersion, "global", "compositionInstalledVersion") - if err != nil { - return dat, fmt.Errorf("failed to add compositionInstalledVersion to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionResource, "global", "compositionResource") - if err != nil { - return dat, fmt.Errorf("failed to add compositionResource to values: %w", err) - } - dat, err = AddOrUpdateFieldInValues(dat, opts.CompositionKind, "global", "compositionKind") - if err != nil { - return dat, fmt.Errorf("failed to add compositionKind to values: %w", err) - } - return dat, nil - -} diff --git a/internal/helmclient/tools/tools_test.go b/internal/helmclient/tools/tools_test.go deleted file mode 100644 index 4cae787..0000000 --- a/internal/helmclient/tools/tools_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package tools - -import ( - "testing" - - sigsyaml "sigs.k8s.io/yaml" -) - -func TestAddOrUpdateFieldInValues(t *testing.T) { - tests := []struct { - name string - values string - value interface{} - fields []string - expected string - expectError bool - }{ - { - name: "add single field to empty values", - values: "{}", - value: "test-value", - fields: []string{"testField"}, - expected: "testField: test-value\n", - }, - { - name: "add nested field to empty values", - values: "{}", - value: "test-value", - fields: []string{"global", "testField"}, - expected: "global:\n testField: test-value\n", - }, - { - name: "update existing field", - values: "testField: old-value", - value: "new-value", - fields: []string{"testField"}, - expected: "testField: new-value\n", - }, - { - name: "add to existing nested structure", - values: "global:\n existing: value", - value: "test-value", - fields: []string{"global", "newField"}, - expected: "global:\n existing: value\n newField: test-value\n", - }, - { - name: "deeply nested field", - values: "{}", - value: "test-value", - fields: []string{"level1", "level2", "level3", "testField"}, - expected: "level1:\n level2:\n level3:\n testField: test-value\n", - }, - { - name: "field exists but is not a map", - values: "testField: string-value", - value: "test-value", - fields: []string{"testField", "nested"}, - expectError: true, - }, - { - name: "invalid yaml", - values: "invalid: yaml: content:", - value: "test-value", - fields: []string{"testField"}, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := AddOrUpdateFieldInValues([]byte(tt.values), tt.value, tt.fields...) - - if tt.expectError { - if err == nil { - t.Errorf("expected error but got none") - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if string(result) != tt.expected { - t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, string(result)) - } - }) - } -} - -func TestInjectValues(t *testing.T) { - tests := []struct { - name string - input string - opts CompositionValues - expected map[string]interface{} - }{ - { - name: "inject all values to empty yaml", - input: "{}", - opts: CompositionValues{ - KrateoNamespace: "krateo-system", - CompositionName: "test-composition", - CompositionNamespace: "default", - CompositionId: "comp-123", - CompositionGroup: "test.group", - CompositionResource: "testresources", - CompositionKind: "TestResource", - GracefullyPaused: true, - }, - expected: map[string]interface{}{ - "global": map[string]interface{}{ - "gracefullyPaused": "true", - "compositionNamespace": "default", - "compositionName": "test-composition", - "krateoNamespace": "krateo-system", - "compositionId": "comp-123", - "compositionApiVersion": "", - "compositionGroup": "test.group", - "compositionInstalledVersion": "", - "compositionResource": "testresources", - "compositionKind": "TestResource", - }, - }, - }, - { - name: "inject values without gracefully paused", - input: "{}", - opts: CompositionValues{ - KrateoNamespace: "krateo-system", - CompositionName: "test-composition", - CompositionNamespace: "default", - CompositionId: "comp-123", - CompositionGroup: "test.group", - CompositionResource: "testresources", - CompositionKind: "TestResource", - GracefullyPaused: false, - }, - expected: map[string]interface{}{ - "global": map[string]interface{}{ - "gracefullyPaused": "false", - "compositionNamespace": "default", - "compositionName": "test-composition", - "krateoNamespace": "krateo-system", - "compositionId": "comp-123", - "compositionApiVersion": "", - "compositionGroup": "test.group", - "compositionInstalledVersion": "", - "compositionResource": "testresources", - "compositionKind": "TestResource", - }, - }, - }, - { - name: "inject values to existing yaml", - input: "existing:\n field: value\nglobal:\n existingGlobal: value", - opts: CompositionValues{ - KrateoNamespace: "krateo-system", - CompositionName: "test-composition", - CompositionNamespace: "default", - CompositionId: "comp-123", - CompositionGroup: "test.group", - CompositionResource: "testresources", - CompositionKind: "TestResource", - }, - expected: map[string]interface{}{ - "existing": map[string]interface{}{ - "field": "value", - }, - "global": map[string]interface{}{ - "gracefullyPaused": "false", - "existingGlobal": "value", - "compositionNamespace": "default", - "compositionName": "test-composition", - "krateoNamespace": "krateo-system", - "compositionId": "comp-123", - "compositionApiVersion": "", - "compositionGroup": "test.group", - "compositionInstalledVersion": "", - "compositionResource": "testresources", - "compositionKind": "TestResource", - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := InjectValues([]byte(tt.input), tt.opts) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - var resultMap map[string]interface{} - if err := sigsyaml.Unmarshal(result, &resultMap); err != nil { - t.Errorf("failed to unmarshal result: %v", err) - return - } - - if !deepEqual(resultMap, tt.expected) { - t.Errorf("expected:\n%+v\ngot:\n%+v", tt.expected, resultMap) - } - }) - } -} - -func TestInjectValuesError(t *testing.T) { - invalidYaml := "invalid: yaml: content:" - opts := CompositionValues{ - KrateoNamespace: "krateo-system", - } - - _, err := InjectValues([]byte(invalidYaml), opts) - if err == nil { - t.Errorf("expected error for invalid yaml but got none") - } -} - -// Helper function to compare maps deeply -func deepEqual(a, b map[string]interface{}) bool { - if len(a) != len(b) { - return false - } - - for key, valueA := range a { - valueB, exists := b[key] - if !exists { - return false - } - - switch va := valueA.(type) { - case map[string]interface{}: - if vb, ok := valueB.(map[string]interface{}); ok { - if !deepEqual(va, vb) { - return false - } - } else { - return false - } - default: - if valueA != valueB { - return false - } - } - } - - return true -} diff --git a/internal/helmclient/types.go b/internal/helmclient/types.go deleted file mode 100644 index 8f1354b..0000000 --- a/internal/helmclient/types.go +++ /dev/null @@ -1,228 +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/chart-inspector/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"` - // 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/tracer/tracer.go b/internal/tracer/tracer.go index 48b7c5c..1863d20 100644 --- a/internal/tracer/tracer.go +++ b/internal/tracer/tracer.go @@ -3,6 +3,7 @@ package tracer import ( "net/http" "strings" + "sync" "github.com/krateoplatformops/chart-inspector/internal/handlers/resources" ) @@ -12,11 +13,17 @@ import ( // including bearer tokens. type Tracer struct { http.RoundTripper + mu sync.Mutex resources []resources.Resource } func (t *Tracer) GetResources() []resources.Resource { - return t.resources + t.mu.Lock() + defer t.mu.Unlock() + // Return a copy to prevent external modification + resCopy := make([]resources.Resource, len(t.resources)) + copy(resCopy, t.resources) + return resCopy } func (t *Tracer) WithRoundTripper(rt http.RoundTripper) *Tracer { @@ -30,39 +37,48 @@ func (t *Tracer) WithRoundTripper(rt http.RoundTripper) *Tracer { func (t *Tracer) RoundTrip(req *http.Request) (*http.Response, error) { split := strings.Split(req.URL.Path, "/") + // Capture resource metadata under mutex protection if len(split) > 2 { + var resource *resources.Resource + if len(split) == 8 && (split[1] == "apis" || split[1] == "api") && split[4] == "namespaces" { - t.resources = append(t.resources, resources.Resource{ + resource = &resources.Resource{ Group: split[2], Version: split[3], Resource: split[6], Namespace: split[5], Name: split[7], - }) + } } else if len(split) == 7 && (split[1] == "apis" || split[1] == "api") && split[3] == "namespaces" { - t.resources = append(t.resources, resources.Resource{ + resource = &resources.Resource{ Group: "", Version: split[2], Resource: split[5], Namespace: split[4], Name: split[6], - }) + } } else if len(split) == 6 && (split[1] == "apis" || split[1] == "api") { - t.resources = append(t.resources, resources.Resource{ + resource = &resources.Resource{ Group: split[2], Version: split[3], Resource: split[4], Namespace: "", Name: split[5], - }) + } } else if len(split) == 5 && (split[1] == "apis" || split[1] == "api") { - t.resources = append(t.resources, resources.Resource{ + resource = &resources.Resource{ Group: "", Version: split[2], Resource: split[3], Namespace: "", Name: split[4], - }) + } + } + + if resource != nil { + t.mu.Lock() + t.resources = append(t.resources, *resource) + t.mu.Unlock() } } diff --git a/internal/tracer/tracer_test.go b/internal/tracer/tracer_test.go new file mode 100644 index 0000000..7ab1ce0 --- /dev/null +++ b/internal/tracer/tracer_test.go @@ -0,0 +1,256 @@ +package tracer + +import ( + "io" + "net/http" + "net/url" + "strings" + "sync" + "sync/atomic" + "testing" + + "github.com/krateoplatformops/chart-inspector/internal/handlers/resources" +) + +// NoOpRoundTripper is a mock RoundTripper that does nothing +type NoOpRoundTripper struct{} + +func (rt *NoOpRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return &http.Response{ + Status: "200 OK", + StatusCode: 200, + Header: http.Header{}, + Body: io.NopCloser(strings.NewReader("{}")), + ContentLength: 2, + Request: req, + }, nil +} + +func TestTracerBasicResourceCapture(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + // Test: /apis/apps/v1/namespaces/default/deployments/my-deployment + u, _ := url.Parse("/apis/apps/v1/namespaces/default/deployments/my-deployment") + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + + resources := tracer.GetResources() + if len(resources) != 1 { + t.Fatalf("expected 1 resource, got %d", len(resources)) + } + + res := resources[0] + if res.Group != "apps" || res.Version != "v1" || res.Resource != "deployments" || res.Namespace != "default" || res.Name != "my-deployment" { + t.Errorf("unexpected resource: %+v", res) + } +} + +func TestTracerMultiplePaths(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + paths := []struct { + path string + expected resources.Resource + }{ + { + path: "/apis/apps/v1/namespaces/default/deployments/my-dep", + expected: resources.Resource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + Namespace: "default", + Name: "my-dep", + }, + }, + { + path: "/api/v1/namespaces/kube-system/services/kube-dns", + expected: resources.Resource{ + Group: "", + Version: "v1", + Resource: "services", + Namespace: "kube-system", + Name: "kube-dns", + }, + }, + { + path: "/apis/batch/v1/jobs/my-job", + expected: resources.Resource{ + Group: "batch", + Version: "v1", + Resource: "jobs", + Namespace: "", + Name: "my-job", + }, + }, + } + + for _, p := range paths { + u, _ := url.Parse(p.path) + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + } + + resources := tracer.GetResources() + if len(resources) != 3 { + t.Fatalf("expected 3 resources, got %d", len(resources)) + } + + for i, p := range paths { + if resources[i] != p.expected { + t.Errorf("resource %d: expected %+v, got %+v", i, p.expected, resources[i]) + } + } +} + +func TestTracerConcurrentRoundTrip(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + numGoroutines := 100 + numCallsPerGoroutine := 10 + var wg sync.WaitGroup + + // Spawn concurrent goroutines making RoundTrip calls + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + for j := 0; j < numCallsPerGoroutine; j++ { + path := "/apis/apps/v1/namespaces/default/deployments/dep" + u, _ := url.Parse(path) + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + } + }(i) + } + + wg.Wait() + + resources := tracer.GetResources() + expectedCount := numGoroutines * numCallsPerGoroutine + if len(resources) != expectedCount { + t.Fatalf("expected %d resources, got %d", expectedCount, len(resources)) + } +} + +func TestTracerConcurrentReadWrite(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + var wg sync.WaitGroup + successCount := int32(0) + + // Spawn goroutines that call RoundTrip (write) + for i := 0; i < 50; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + for j := 0; j < 20; j++ { + path := "/apis/apps/v1/namespaces/default/deployments/dep" + u, _ := url.Parse(path) + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + } + }(i) + } + + // Spawn goroutines that call GetResources (read) + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 20; j++ { + resources := tracer.GetResources() + if resources != nil { + atomic.AddInt32(&successCount, 1) + } + } + }() + } + + wg.Wait() + + // Verify we got results without panics (test passes if it completes) + resources := tracer.GetResources() + if len(resources) == 0 { + t.Fatalf("expected some resources, got none") + } + + if successCount == 0 { + t.Fatalf("expected successful reads, got 0") + } +} + +func TestTracerGetResourcesReturnsDeepCopy(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + // Add a resource + u, _ := url.Parse("/apis/apps/v1/namespaces/default/deployments/my-dep") + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + + // Get resources + resources1 := tracer.GetResources() + if len(resources1) != 1 { + t.Fatalf("expected 1 resource, got %d", len(resources1)) + } + + // Get resources again + resources2 := tracer.GetResources() + + // Verify they're equal but not the same slice + if len(resources2) != len(resources1) { + t.Fatalf("expected same resource count, got %d vs %d", len(resources1), len(resources2)) + } + + if resources1[0] != resources2[0] { + t.Errorf("expected equal resources: %+v vs %+v", resources1[0], resources2[0]) + } + + // Verify it's a copy by modifying one and checking the other isn't affected internally + ptr1 := &resources1 + ptr2 := &resources2 + if ptr1 == ptr2 { + t.Error("expected different slice instances (copy returned)") + } +} + +func TestTracerInvalidPaths(t *testing.T) { + tracer := &Tracer{} + tracer.WithRoundTripper(&NoOpRoundTripper{}) + + invalidPaths := []string{ + "/health", + "/", + "/notapis/v1/pods", + "/apis/v1", + "", + } + + for _, path := range invalidPaths { + u, _ := url.Parse(path) + req := &http.Request{ + URL: u, + } + tracer.RoundTrip(req) + } + + resources := tracer.GetResources() + // No valid resources should be captured + if len(resources) != 0 { + t.Fatalf("expected 0 resources for invalid paths, got %d", len(resources)) + } +} diff --git a/main.go b/main.go index 77e5565..c990c13 100644 --- a/main.go +++ b/main.go @@ -17,8 +17,8 @@ import ( _ "github.com/krateoplatformops/chart-inspector/docs" "github.com/krateoplatformops/chart-inspector/internal/handlers" getresources "github.com/krateoplatformops/chart-inspector/internal/handlers/resources/get" - "github.com/krateoplatformops/chart-inspector/internal/helmclient" "github.com/krateoplatformops/plumbing/env" + helmv3 "github.com/krateoplatformops/plumbing/helm/v3" prettylog "github.com/krateoplatformops/plumbing/slogs/pretty" httpSwagger "github.com/swaggo/http-swagger" "k8s.io/client-go/dynamic" @@ -87,27 +87,23 @@ func main() { os.Exit(1) } - clientset, err := helmclient.NewCachedClients(cfg) + // Initialize Helm client with cache and CRD informer + helmClient, err := helmv3.NewClient(cfg, + helmv3.WithLogger(func(format string, v ...interface{}) { + log.Debug(fmt.Sprintf(format, v...)) + }), + helmv3.WithCRDInformer(cfg, 30*time.Minute), + ) if err != nil { - log.Error("Creating cached clientset.", "error", err) + log.Error("Creating helm client.", "error", err) os.Exit(1) } - // Start CRD informer to invalidate discovery cache on CRD changes - go func() { - if err := helmclient.StartCRDInformer(context.Background(), cfg, &clientset, log); err != nil { - log.Error("Starting CRD informer", "error", err) - } - }() - opts := handlers.HandlerOptions{ Log: log, DynamicClient: dyn, KrateoNamespace: krateoNamespace, - HelmClientOptions: helmclient.RestConfClientOptions{ - RestConfig: cfg, - }, - Clientset: &clientset, + RestConfig: cfg, } healthy := int32(0) @@ -150,6 +146,13 @@ func main() { log.Info("server is shutting down gracefully, press Ctrl+C again to force") atomic.StoreInt32(&healthy, 0) + // Clean up helm client resources + if helmClient != nil { + if err := helmClient.Close(); err != nil { + log.Error("closing helm client", "error", err) + } + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()