refactor(helm): helm binary to sdk refactor [r8s-229] (#463)

Co-authored-by: stevensbkang <skan070@gmail.com>
release/2.28.0
Ali 2025-03-13 12:20:16 +13:00 committed by GitHub
parent 0d25f3f430
commit b5961d79f8
56 changed files with 2222 additions and 819 deletions

View File

@ -49,6 +49,7 @@ import (
"github.com/portainer/portainer/pkg/build"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/gofrs/uuid"
@ -169,8 +170,8 @@ func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheMan
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
return libhelm.NewHelmPackageManager()
}
func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
@ -437,7 +438,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
helmPackageManager, err := initHelmPackageManager()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}

View File

@ -10,7 +10,7 @@ import (
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://kubernetes.github.io/ingress-nginx","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)

View File

@ -605,7 +605,7 @@
"GlobalDeploymentOptions": {
"hideStacksFunctionality": false
},
"HelmRepositoryURL": "",
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
"InternalAuthSettings": {
"RequiredPasswordLength": 12
},

View File

@ -1,6 +1,7 @@
package helm
import (
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
@ -8,8 +9,8 @@ import (
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/portainer/portainer/pkg/libhelm/options"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux"
@ -23,11 +24,11 @@ type Handler struct {
jwtService portainer.JWTService
kubeClusterAccessService kubernetes.KubeClusterAccessService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
helmPackageManager libhelmtypes.HelmPackageManager
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, jwtService portainer.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeClusterAccessService kubernetes.KubeClusterAccessService) *Handler {
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, jwtService portainer.JWTService, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelmtypes.HelmPackageManager, kubeClusterAccessService kubernetes.KubeClusterAccessService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
@ -57,7 +58,7 @@ func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStor
}
// NewTemplateHandler creates a template handler to manage environment(endpoint) group operations.
func NewTemplateHandler(bouncer security.BouncerService, helmPackageManager libhelm.HelmPackageManager) *Handler {
func NewTemplateHandler(bouncer security.BouncerService, helmPackageManager libhelmtypes.HelmPackageManager) *Handler {
h := &Handler{
Router: mux.NewRouter(),
helmPackageManager: helmPackageManager,
@ -78,7 +79,7 @@ func NewTemplateHandler(bouncer security.BouncerService, helmPackageManager libh
// getHelmClusterAccess obtains the core k8s cluster access details from request.
// The cluster access includes the cluster server url, the user's bearer token and the tls certificate.
// The cluster access is passed in as kube config CLI params to helm binary.
// The cluster access is passed in as kube config CLI params to helm.
func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.KubernetesClusterAccess, *httperror.HandlerError) {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
@ -107,6 +108,9 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
kubeConfigInternal := handler.kubeClusterAccessService.GetClusterDetails(hostURL, endpoint.ID, true)
return &options.KubernetesClusterAccess{
ClusterName: fmt.Sprintf("%s-%s", "portainer-cluster", endpoint.Name),
ContextName: fmt.Sprintf("%s-%s", "portainer-ctx", endpoint.Name),
UserName: fmt.Sprintf("%s-%s", "portainer-sa-user", tokenData.Username),
ClusterServerURL: kubeConfigInternal.ClusterServerURL,
CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile,
AuthToken: bearerToken,

View File

@ -13,8 +13,8 @@ import (
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm/binary/test"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
@ -34,7 +34,7 @@ func Test_helmDelete(t *testing.T) {
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
helmPackageManager := test.NewMockHelmPackageManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)

View File

@ -99,15 +99,11 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
}
installOpts := options.InstallOptions{
Name: p.Name,
Chart: p.Chart,
Namespace: p.Namespace,
Repo: p.Repo,
KubernetesClusterAccess: &options.KubernetesClusterAccess{
ClusterServerURL: clusterAccess.ClusterServerURL,
CertificateAuthorityFile: clusterAccess.CertificateAuthorityFile,
AuthToken: clusterAccess.AuthToken,
},
Name: p.Name,
Chart: p.Chart,
Namespace: p.Namespace,
Repo: p.Repo,
KubernetesClusterAccess: clusterAccess,
}
if p.Values != "" {

View File

@ -15,9 +15,9 @@ import (
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm/binary/test"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
@ -38,14 +38,14 @@ func Test_helmInstall(t *testing.T) {
is.NoError(err, "Error initiating jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
helmPackageManager := test.NewMockHelmPackageManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)
is.NotNil(h, "Handler should not fail")
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://kubernetes.github.io/ingress-nginx"}
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default", Repo: "https://charts.bitnami.com/bitnami"}
optdata, err := json.Marshal(options)
is.NoError(err)

View File

@ -14,9 +14,9 @@ import (
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/pkg/libhelm/binary/test"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/segmentio/encoding/json"
"github.com/stretchr/testify/assert"
@ -37,7 +37,7 @@ func Test_helmList(t *testing.T) {
is.NoError(err, "Error initialising jwt service")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
helmPackageManager := test.NewMockHelmPackageManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService("", "", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, jwtService, kubernetesDeployer, helmPackageManager, kubeClusterAccessService)

View File

@ -8,19 +8,19 @@ import (
"testing"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/pkg/libhelm/binary/test"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func Test_helmRepoSearch(t *testing.T) {
is := assert.New(t)
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
helmPackageManager := test.NewMockHelmPackageManager()
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
assert.NotNil(t, h, "Handler should not fail")
repos := []string{"https://kubernetes.github.io/ingress-nginx", "https://portainer.github.io/k8s"}
repos := []string{"https://charts.bitnami.com/bitnami", "https://portainer.github.io/k8s"}
for _, repo := range repos {
t.Run(repo, func(t *testing.T) {

View File

@ -9,14 +9,14 @@ import (
"testing"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/pkg/libhelm/binary/test"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func Test_helmShow(t *testing.T) {
is := assert.New(t)
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
helmPackageManager := test.NewMockHelmPackageManager()
h := NewTemplateHandler(helper.NewTestRequestBouncer(), helmPackageManager)
is.NotNil(h, "Handler should not fail")
@ -31,7 +31,7 @@ func Test_helmShow(t *testing.T) {
t.Run(cmd, func(t *testing.T) {
is.NotNil(h, "Handler should not fail")
repoUrlEncoded := url.QueryEscape("https://kubernetes.github.io/ingress-nginx")
repoUrlEncoded := url.QueryEscape("https://charts.bitnami.com/bitnami")
chart := "nginx"
req := httptest.NewRequest("GET", fmt.Sprintf("/templates/helm/%s?repo=%s&chart=%s", cmd, repoUrlEncoded, chart), nil)
rr := httptest.NewRecorder()

View File

@ -46,7 +46,7 @@ type settingsUpdatePayload struct {
// Whether telemetry is enabled
EnableTelemetry *bool `example:"false"`
// Helm repository URL
HelmRepositoryURL *string `example:"https://kubernetes.github.io/ingress-nginx"`
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
// Kubectl Shell Image
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default

View File

@ -67,7 +67,7 @@ import (
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/libhelm"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
"github.com/rs/zerolog/log"
)
@ -103,7 +103,7 @@ type Server struct {
DockerClientFactory *dockerclient.ClientFactory
KubernetesClientFactory *cli.ClientFactory
KubernetesDeployer portainer.KubernetesDeployer
HelmPackageManager libhelm.HelmPackageManager
HelmPackageManager libhelmtypes.HelmPackageManager
Scheduler *scheduler.Scheduler
ShutdownCtx context.Context
ShutdownTrigger context.CancelFunc

View File

@ -588,7 +588,7 @@ type (
// User identifier
UserID UserID `json:"UserId" example:"1"`
// Helm repository URL
URL string `json:"URL" example:"https://kubernetes.github.io/ingress-nginx"`
URL string `json:"URL" example:"https://charts.bitnami.com/bitnami"`
}
// QuayRegistryData represents data required for Quay registry to work
@ -984,8 +984,8 @@ type (
KubeconfigExpiry string `json:"KubeconfigExpiry" example:"24h"`
// Whether telemetry is enabled
EnableTelemetry bool `json:"EnableTelemetry" example:"false"`
// Helm repository URL, defaults to ""
HelmRepositoryURL string `json:"HelmRepositoryURL"`
// Helm repository URL, defaults to "https://charts.bitnami.com/bitnami"
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
@ -1673,8 +1673,8 @@ const (
DefaultEdgeAgentCheckinIntervalInSeconds = 5
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/v3/templates.json"
// DefaultHelmrepositoryURL set to empty string until oci support is added
DefaultHelmRepositoryURL = ""
// DefaultHelmrepositoryURL represents the URL to the official templates supported by Bitnami
DefaultHelmRepositoryURL = "https://charts.bitnami.com/bitnami"
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
DefaultUserSessionTimeout = "8h"
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared

View File

@ -62,19 +62,19 @@ describe('HelmApplicationView', () => {
expect(await screen.findByText('Helm details')).toBeInTheDocument();
// Check for the release details
expect(screen.getByText('Release')).toBeInTheDocument();
expect(await screen.findByText('Release')).toBeInTheDocument();
// Check for the table content
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Chart')).toBeInTheDocument();
expect(screen.getByText('App version')).toBeInTheDocument();
expect(await screen.findByText('Name')).toBeInTheDocument();
expect(await screen.findByText('Chart')).toBeInTheDocument();
expect(await screen.findByText('App version')).toBeInTheDocument();
// Check for the actual values
expect(screen.getByTestId('k8sAppDetail-appName')).toHaveTextContent(
expect(await screen.findByTestId('k8sAppDetail-appName')).toHaveTextContent(
'test-release'
);
expect(screen.getByText('test-chart-1.0.0')).toBeInTheDocument();
expect(screen.getByText('1.0.0')).toBeInTheDocument();
expect(await screen.findByText('test-chart-1.0.0')).toBeInTheDocument();
expect(await screen.findByText('1.0.0')).toBeInTheDocument();
});
it('should display error message when API request fails', async () => {

View File

@ -1,37 +1,13 @@
import { useCurrentStateAndParams } from '@uirouter/react';
import { PageHeader } from '@/react/components/PageHeader';
import { Widget, WidgetBody, WidgetTitle } from '@/react/components/Widget';
import helm from '@/assets/ico/vendor/helm.svg?c';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { ViewLoading } from '@@/ViewLoading';
import { Alert } from '@@/Alert';
import { useHelmRelease } from './queries/useHelmRelease';
import { HelmDetailsWidget } from './HelmDetailsWidget';
export function HelmApplicationView() {
const { params } = useCurrentStateAndParams();
const environmentId = useEnvironmentId();
const name = params.name as string;
const namespace = params.namespace as string;
const {
data: release,
isLoading,
error,
} = useHelmRelease(environmentId, name, namespace);
if (isLoading) {
return <ViewLoading />;
}
if (error || !release) {
return (
<Alert color="error" title="Failed to load Helm application details" />
);
}
const { name, namespace } = params;
return (
<>
@ -46,32 +22,7 @@ export function HelmApplicationView() {
<div className="row">
<div className="col-sm-12">
<Widget>
<WidgetTitle icon={helm} title="Release" />
<WidgetBody>
<table className="table">
<tbody>
<tr>
<td className="!border-none w-40">Name</td>
<td
className="!border-none min-w-[140px]"
data-cy="k8sAppDetail-appName"
>
{release.name}
</td>
</tr>
<tr>
<td className="!border-t">Chart</td>
<td className="!border-t">{release.chart}</td>
</tr>
<tr>
<td>App version</td>
<td>{release.app_version}</td>
</tr>
</tbody>
</table>
</WidgetBody>
</Widget>
<HelmDetailsWidget name={name} namespace={namespace} />
</div>
</div>
</>

View File

@ -0,0 +1,67 @@
import {
Loading,
Widget,
WidgetBody,
WidgetTitle,
} from '@/react/components/Widget';
import helm from '@/assets/ico/vendor/helm.svg?c';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { Alert } from '@@/Alert';
import { useHelmRelease } from './queries/useHelmRelease';
interface HelmDetailsWidgetProps {
name: string;
namespace: string;
}
export function HelmDetailsWidget({ name, namespace }: HelmDetailsWidgetProps) {
const environmentId = useEnvironmentId();
const {
data: release,
isInitialLoading,
isError,
} = useHelmRelease(environmentId, name, namespace);
return (
<Widget>
<WidgetTitle icon={helm} title="Release" />
<WidgetBody>
{isInitialLoading && <Loading />}
{isError && (
<Alert
color="error"
title="Failed to load Helm application details"
/>
)}
{!isInitialLoading && !isError && release && (
<table className="table">
<tbody>
<tr>
<td className="!border-none w-40">Name</td>
<td
className="!border-none min-w-[140px]"
data-cy="k8sAppDetail-appName"
>
{release.name}
</td>
</tr>
<tr>
<td className="!border-t">Chart</td>
<td className="!border-t">{release.chart}</td>
</tr>
<tr>
<td>App version</td>
<td>{release.app_version}</td>
</tr>
</tbody>
</table>
)}
</WidgetBody>
</Widget>
);
}

View File

@ -23,7 +23,6 @@ GIT_COMMIT_HASH=${GIT_COMMIT_HASH:-$(git rev-parse --short HEAD)}
# populate dependencies versions
DOCKER_VERSION=$(jq -r '.docker' < "${BINARY_VERSION_FILE}")
HELM_VERSION=$(jq -r '.helm' < "${BINARY_VERSION_FILE}")
KUBECTL_VERSION=$(jq -r '.kubectl' < "${BINARY_VERSION_FILE}")
COMPOSE_VERSION=$(go list -m -f '{{.Version}}' github.com/docker/compose/v2)
@ -52,7 +51,6 @@ ldflags="-s -X 'github.com/portainer/liblicense.LicenseServerBaseURL=https://api
-X 'github.com/portainer/portainer/pkg/build.GoVersion=${GO_VERSION}' \
-X 'github.com/portainer/portainer/pkg/build.DepComposeVersion=${COMPOSE_VERSION}' \
-X 'github.com/portainer/portainer/pkg/build.DepDockerVersion=${DOCKER_VERSION}' \
-X 'github.com/portainer/portainer/pkg/build.DepHelmVersion=${HELM_VERSION}' \
-X 'github.com/portainer/portainer/pkg/build.DepKubectlVersion=${KUBECTL_VERSION}'"
echo "$ldflags"

View File

@ -17,12 +17,10 @@ echo "Checking and downloading binaries for docker ${dockerVersion}, helm ${helm
# Determine the binary file names based on the platform
dockerBinary="dist/docker"
helmBinary="dist/helm"
kubectlBinary="dist/kubectl"
if [ "$PLATFORM" == "windows" ]; then
dockerBinary="dist/docker.exe"
helmBinary="dist/helm.exe"
kubectlBinary="dist/kubectl.exe"
fi
@ -34,14 +32,6 @@ else
echo "Docker binary already exists, skipping download."
fi
# Check and download helm binary
if [ ! -f "$helmBinary" ]; then
echo "Downloading helm binary..."
/usr/bin/env bash ./build/download_helm_binary.sh "$PLATFORM" "$ARCH" "$helmVersion"
else
echo "Helm binary already exists, skipping download."
fi
# Check and download kubectl binary
if [ ! -f "$kubectlBinary" ]; then
echo "Downloading kubectl binary..."

View File

@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 3 ]]; then
echo "Illegal number of parameters" >&2
exit 1
fi
PLATFORM=$1
ARCH=$2
HELM_VERSION=$3
HELM_DIST="helm-$HELM_VERSION-$PLATFORM-$ARCH"
if [[ ${PLATFORM} == "windows" ]]; then
wget --tries=3 --waitretry=30 --quiet -O tmp.zip "https://get.helm.sh/${HELM_DIST}.zip" && unzip -o -j tmp.zip "${PLATFORM}-${ARCH}/helm.exe" -d dist && rm -f tmp.zip
else
wget -qO- "https://get.helm.sh/${HELM_DIST}.tar.gz" | tar -x -z --strip-components 1 "${PLATFORM}-${ARCH}/helm"
mv "helm" "dist/helm"
chmod +x "dist/helm"
fi

View File

@ -11,7 +11,6 @@ LABEL org.opencontainers.image.title="Portainer" \
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
COPY dist/docker /
COPY dist/helm /
COPY dist/kubectl /
COPY dist/mustache-templates /mustache-templates/
COPY dist/portainer /

View File

@ -11,7 +11,6 @@ LABEL org.opencontainers.image.title="Portainer" \
com.docker.extension.additional-urls="[{\"title\":\"Website\",\"url\":\"https://www.portainer.io?utm_campaign=DockerCon&utm_source=DockerDesktop\"},{\"title\":\"Documentation\",\"url\":\"https://docs.portainer.io\"},{\"title\":\"Support\",\"url\":\"https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA\"}]"
COPY dist/docker /
COPY dist/helm /
COPY dist/kubectl /
COPY dist/mustache-templates /mustache-templates/
COPY dist/portainer /

View File

@ -10,7 +10,6 @@ USER ContainerAdministrator
COPY dist/mingit/ mingit/
COPY dist/docker.exe /
COPY dist/helm.exe /
COPY dist/kubectl.exe /
COPY dist/mustache-templates /mustache-templates/
COPY dist/portainer.exe /

114
go.mod
View File

@ -55,10 +55,11 @@ require (
golang.org/x/sync v0.10.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.29.2
k8s.io/apimachinery v0.29.2
k8s.io/client-go v0.29.2
k8s.io/metrics v0.27.4
helm.sh/helm/v3 v3.17.1
k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1
k8s.io/metrics v0.32.1
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)
@ -70,8 +71,12 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
@ -89,9 +94,11 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/console v1.0.4 // indirect
@ -106,8 +113,8 @@ require (
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.1.10 // indirect
github.com/containers/storage v1.53.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/buildx v0.18.0 // indirect
github.com/docker/cli-docs-tool v0.8.0 // indirect
@ -120,39 +127,49 @@ require (
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsevents v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.10 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/in-toto/in-toto-golang v0.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/ansi v1.0.3 // indirect
@ -162,22 +179,28 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.2.2 // 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/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.17.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
github.com/moby/sys/mountinfo v0.7.2 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@ -188,28 +211,34 @@ require (
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runtime-spec v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rubenv/sql-migrate v1.7.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
@ -223,44 +252,55 @@ require (
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect
go.opentelemetry.io/otel v1.25.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 // indirect
go.opentelemetry.io/otel/metric v1.25.0 // indirect
go.opentelemetry.io/otel/sdk v1.25.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.25.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect
golang.org/x/time v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/apiserver v0.32.1 // indirect
k8s.io/cli-runtime v0.32.1 // indirect
k8s.io/component-base v0.32.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/kubectl v0.32.1 // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
oras.land/oras-go v1.2.5 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.18.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
tags.cncf.io/container-device-interface v0.8.0 // indirect
)

297
go.sum
View File

@ -1,9 +1,7 @@
cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
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/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA=
@ -15,12 +13,22 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg6
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@ -90,7 +98,11 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc=
@ -106,13 +118,13 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.4.5 h1:p4ih4Jb6VgGPLPxh3fSFVKAjFHtZd+7HVLCSFzcFx9Y=
@ -161,14 +173,17 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs=
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/buildx v0.18.0 h1:rSauXHeJt90NvtXrLK5J992Eb0UPJZs2vV3u1zTf1nE=
@ -209,13 +224,19 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c=
github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -223,12 +244,16 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
@ -237,24 +262,26 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E=
github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw=
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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA=
github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -264,13 +291,15 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
@ -293,6 +322,10 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY=
github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ=
@ -305,24 +338,29 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
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/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -339,8 +377,8 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
@ -357,8 +395,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
@ -402,9 +440,17 @@ 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/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
@ -423,19 +469,27 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/buildkit v0.17.2 h1:/jgk/MuXbA7jeXMkknOpHYB+Ct4aNvQHkBB7SxD3D4U=
github.com/moby/buildkit v0.17.2/go.mod h1:vr5vltV8wt4F2jThbNOChfbAklJ0DOW11w36v210hOg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@ -444,8 +498,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
@ -469,6 +523,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@ -480,12 +536,12 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
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/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@ -505,6 +561,10 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -513,24 +573,27 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
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.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -544,11 +607,14 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
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/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ=
github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA=
@ -563,6 +629,8 @@ github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQ
github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
@ -575,8 +643,9 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY=
github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk=
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
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 v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
@ -592,6 +661,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -632,6 +703,8 @@ github.com/viney-shih/go-lock v1.1.1 h1:SwzDPPAiHpcwGCr5k8xD15d2gQSo8d4roRYd7TDV
github.com/viney-shih/go-lock v1.1.1/go.mod h1:Yijm78Ljteb3kRiJrbLAxVntkUukGu5uzSxq/xV7OO8=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss=
github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -641,9 +714,17 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw=
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
@ -652,34 +733,34 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0=
go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08=
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
@ -769,15 +850,15 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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=
@ -805,6 +886,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
@ -826,26 +909,44 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A=
k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0=
k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg=
k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/metrics v0.27.4 h1:2s04bods7rA507iouGbxD55YrKNlFjLYzm30noOl9Sk=
k8s.io/metrics v0.27.4/go.mod h1:kRvfhFC7wCQEFvu6H92uiV7v05z3Ty/vtluYT5D2Xpk=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
helm.sh/helm/v3 v3.17.1 h1:gzVoAD+qVuoJU6KDMSAeo0xRJ6N1znRxz3wyuXRmJDk=
helm.sh/helm/v3 v3.17.1/go.mod h1:nvreuhuR+j78NkQcLC3TYoprCKStLyw5P4T7E5itv2w=
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw=
k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto=
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak=
k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw=
k8s.io/cli-runtime v0.32.1 h1:19nwZPlYGJPUDbhAxDIS2/oydCikvKMHsxroKNGA2mM=
k8s.io/cli-runtime v0.32.1/go.mod h1:NJPbeadVFnV2E7B7vF+FvU09mpwYlZCu8PqjzfuOnkY=
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk=
k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w=
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-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/kubectl v0.32.1 h1:/btLtXLQUU1rWx8AEvX9jrb9LaI6yeezt3sFALhB8M8=
k8s.io/kubectl v0.32.1/go.mod h1:sezNuyWi1STk4ZNPVRIFfgjqMI6XMf+oCVLjZen/pFQ=
k8s.io/metrics v0.32.1 h1:Ou4nrEtZS2vFf7OJCf9z3+2kr0A00kQzfoSwxg0gXps=
k8s.io/metrics v0.32.1/go.mod h1:cLnai9XKYby1tNMX+xe8p9VLzTqrxYPcmqfCBoWObcM=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U=
sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E=
sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 h1:SqYE5+A2qvRhErbsXFfUEUmpWEKxxRSMgGLkvRAFOV4=
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78/go.mod h1:B7Wf0Ya4DHF9Yw+qfZuJijQYkWicqDa+79Ytmmq3Kjg=
tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc=

View File

@ -42,9 +42,6 @@ var (
// DepDockerVersion is the version of the Docker binary shipped with the application.
DepDockerVersion string
// DepHelmVersion is the version of the Helm binary shipped with the application.
DepHelmVersion string
// DepKubectlVersion is the version of the Kubectl binary shipped with the application.
DepKubectlVersion string
)
@ -92,7 +89,6 @@ func GetBuildInfo() BuildInfo {
func GetDependenciesInfo() DependenciesInfo {
return DependenciesInfo{
DockerVersion: DepDockerVersion,
HelmVersion: DepHelmVersion,
KubectlVersion: DepKubectlVersion,
ComposeVersion: DepComposeVersion,
}

View File

@ -1,29 +0,0 @@
package binary
import (
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
)
// Get runs `helm get` with specified get options.
// The get options translate to CLI arguments which are passed in to the helm binary when executing install.
func (hbpm *helmBinaryPackageManager) Get(getOpts options.GetOptions) ([]byte, error) {
if getOpts.Name == "" || getOpts.ReleaseResource == "" {
return nil, errors.New("release name and release resource are required")
}
args := []string{
string(getOpts.ReleaseResource),
getOpts.Name,
}
if getOpts.Namespace != "" {
args = append(args, "--namespace", getOpts.Namespace)
}
result, err := hbpm.runWithKubeConfig("get", args, getOpts.KubernetesClusterAccess, getOpts.Env)
if err != nil {
return nil, errors.Wrap(err, "failed to run helm get on specified args")
}
return result, nil
}

View File

@ -1,62 +0,0 @@
package binary
import (
"bytes"
"os"
"os/exec"
"path"
"runtime"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
)
// helmBinaryPackageManager is a wrapper for the helm binary which implements HelmPackageManager
type helmBinaryPackageManager struct {
binaryPath string
}
// NewHelmBinaryPackageManager initializes a new HelmPackageManager service.
func NewHelmBinaryPackageManager(binaryPath string) *helmBinaryPackageManager {
return &helmBinaryPackageManager{binaryPath: binaryPath}
}
// runWithKubeConfig will execute run against the provided Kubernetes cluster with kubeconfig as cli arguments.
func (hbpm *helmBinaryPackageManager) runWithKubeConfig(command string, args []string, kca *options.KubernetesClusterAccess, env []string) ([]byte, error) {
cmdArgs := make([]string, 0)
if kca != nil {
cmdArgs = append(cmdArgs, "--kube-apiserver", kca.ClusterServerURL)
cmdArgs = append(cmdArgs, "--kube-token", kca.AuthToken)
cmdArgs = append(cmdArgs, "--kube-ca-file", kca.CertificateAuthorityFile)
}
cmdArgs = append(cmdArgs, args...)
return hbpm.run(command, cmdArgs, env)
}
// run will execute helm command against the provided Kubernetes cluster.
// The endpointId and authToken are dynamic params (based on the user) that allow helm to execute commands
// in the context of the current user against specified k8s cluster.
func (hbpm *helmBinaryPackageManager) run(command string, args []string, env []string) ([]byte, error) {
cmdArgs := make([]string, 0)
cmdArgs = append(cmdArgs, command)
cmdArgs = append(cmdArgs, args...)
helmPath := path.Join(hbpm.binaryPath, "helm")
if runtime.GOOS == "windows" {
helmPath = path.Join(hbpm.binaryPath, "helm.exe")
}
var stderr bytes.Buffer
cmd := exec.Command(helmPath, cmdArgs...)
cmd.Stderr = &stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, env...)
output, err := cmd.Output()
if err != nil {
return nil, errors.Wrap(err, stderr.String())
}
return output, nil
}

View File

@ -1,48 +0,0 @@
package binary
import (
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
)
// Install runs `helm install` with specified install options.
// The install options translate to CLI arguments which are passed in to the helm binary when executing install.
func (hbpm *helmBinaryPackageManager) Install(installOpts options.InstallOptions) (*release.Release, error) {
if installOpts.Name == "" {
installOpts.Name = "--generate-name"
}
args := []string{
installOpts.Name,
installOpts.Chart,
"--repo", installOpts.Repo,
"--output", "json",
}
if installOpts.Namespace != "" {
args = append(args, "--namespace", installOpts.Namespace)
}
if installOpts.ValuesFile != "" {
args = append(args, "--values", installOpts.ValuesFile)
}
if installOpts.Wait {
args = append(args, "--wait")
}
if installOpts.PostRenderer != "" {
args = append(args, "--post-renderer", installOpts.PostRenderer)
}
result, err := hbpm.runWithKubeConfig("install", args, installOpts.KubernetesClusterAccess, installOpts.Env)
if err != nil {
return nil, errors.Wrap(err, "failed to run helm install on specified args")
}
response := &release.Release{}
err = json.Unmarshal(result, &response)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal helm install response to Release struct")
}
return response, nil
}

View File

@ -1,118 +0,0 @@
package binary
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/stretchr/testify/assert"
)
func createValuesFile(values string) (string, error) {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return "", err
}
_, err = file.WriteString(values)
if err != nil {
file.Close()
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
return file.Name(), nil
}
// getHelmBinaryPath is helper function to get local helm binary path (if helm is in path)
func getHelmBinaryPath() (string, error) {
path, err := exec.LookPath("helm")
if err != nil {
return "", err
}
dir, err := filepath.Abs(filepath.Dir(path))
if err != nil {
return "", err
}
return dir, nil
}
func Test_Install(t *testing.T) {
ensureIntegrationTest(t)
is := assert.New(t)
path, err := getHelmBinaryPath()
is.NoError(err, "helm binary must exist in path to run tests")
hbpm := NewHelmBinaryPackageManager(path)
t.Run("successfully installs nginx chart with name test-nginx", func(t *testing.T) {
// helm install test-nginx --repo https://kubernetes.github.io/ingress-nginx nginx
installOpts := options.InstallOptions{
Name: "test-nginx",
Chart: "nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hbpm.Install(installOpts)
defer hbpm.run("uninstall", []string{"test-nginx"}, nil)
is.NoError(err, "should successfully install release", release)
})
t.Run("successfully installs nginx chart with generated name", func(t *testing.T) {
// helm install --generate-name --repo https://kubernetes.github.io/ingress-nginx nginx
installOpts := options.InstallOptions{
Chart: "nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hbpm.Install(installOpts)
defer hbpm.run("uninstall", []string{release.Name}, nil)
is.NoError(err, "should successfully install release", release)
})
t.Run("successfully installs nginx with values", func(t *testing.T) {
// helm install test-nginx-2 --repo https://kubernetes.github.io/ingress-nginx nginx --values /tmp/helm-values3161785816
values, err := createValuesFile("service:\n port: 8081")
is.NoError(err, "should create a values file")
defer os.Remove(values)
installOpts := options.InstallOptions{
Name: "test-nginx-2",
Chart: "nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
release, err := hbpm.Install(installOpts)
defer hbpm.run("uninstall", []string{"test-nginx-2"}, nil)
is.NoError(err, "should successfully install release", release)
})
t.Run("successfully installs portainer chart with name portainer-test", func(t *testing.T) {
// helm install portainer-test portainer --repo https://portainer.github.io/k8s/
installOpts := options.InstallOptions{
Name: "portainer-test",
Chart: "portainer",
Repo: "https://portainer.github.io/k8s/",
}
release, err := hbpm.Install(installOpts)
defer hbpm.run("uninstall", []string{installOpts.Name}, nil)
is.NoError(err, "should successfully install release", release)
})
}
func ensureIntegrationTest(t *testing.T) {
if _, ok := os.LookupEnv("INTEGRATION_TEST"); !ok {
t.Skip("skip an integration test")
}
}

View File

@ -1,38 +0,0 @@
package binary
import (
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
)
// List runs `helm list --output json --filter <filter> --selector <selector> --namespace <namespace>` with specified list options.
// The list options translate to CLI args the helm binary
func (hbpm *helmBinaryPackageManager) List(listOpts options.ListOptions) ([]release.ReleaseElement, error) {
args := []string{"--output", "json"}
if listOpts.Filter != "" {
args = append(args, "--filter", listOpts.Filter)
}
if listOpts.Selector != "" {
args = append(args, "--selector", listOpts.Selector)
}
if listOpts.Namespace != "" {
args = append(args, "--namespace", listOpts.Namespace)
}
result, err := hbpm.runWithKubeConfig("list", args, listOpts.KubernetesClusterAccess, listOpts.Env)
if err != nil {
return []release.ReleaseElement{}, errors.Wrap(err, "failed to run helm list on specified args")
}
response := []release.ReleaseElement{}
err = json.Unmarshal(result, &response)
if err != nil {
return []release.ReleaseElement{}, errors.Wrap(err, "failed to unmarshal helm list response to releastElement list")
}
return response, nil
}

View File

@ -1,118 +0,0 @@
package binary
// Package common implements common functionality for the helm.
// The functionality does not rely on the implementation of `HelmPackageManager`
import (
"net/http"
"net/url"
"path"
"time"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
"gopkg.in/yaml.v3"
)
var errRequiredSearchOptions = errors.New("repo is required")
var errInvalidRepoURL = errors.New("the request failed since either the Helm repository was not found or the index.yaml is not valid")
type File struct {
APIVersion string `yaml:"apiVersion" json:"apiVersion"`
Entries map[string][]Entry `yaml:"entries" json:"entries"`
Generated string `yaml:"generated" json:"generated"`
}
type Annotations struct {
Category string `yaml:"category" json:"category"`
}
type Entry struct {
Annotations *Annotations `yaml:"annotations" json:"annotations,omitempty"`
Created string `yaml:"created" json:"created"`
Deprecated bool `yaml:"deprecated" json:"deprecated"`
Description string `yaml:"description" json:"description"`
Digest string `yaml:"digest" json:"digest"`
Home string `yaml:"home" json:"home"`
Name string `yaml:"name" json:"name"`
Sources []string `yaml:"sources" json:"sources"`
Urls []string `yaml:"urls" json:"urls"`
Version string `yaml:"version" json:"version"`
Icon string `yaml:"icon" json:"icon,omitempty"`
}
// SearchRepo downloads the `index.yaml` file for specified repo, parses it and returns JSON to caller.
// The functionality is similar to that of what `helm search repo [chart] --repo <repo>` CLI runs;
// this approach is used instead since the `helm search repo` requires a repo to be added to the global helm cache
func (hbpm *helmBinaryPackageManager) SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error) {
if searchRepoOpts.Repo == "" {
return nil, errRequiredSearchOptions
}
client := searchRepoOpts.Client
if client == nil {
// The current index.yaml is ~9MB on bitnami.
// At a slow @2mbit download = 40s. @100bit = ~1s.
// I'm seeing 3 - 4s over wifi.
// Give ample time but timeout for now. Can be improved in the future
client = &http.Client{
Timeout: 300 * time.Second,
Transport: http.DefaultTransport,
}
}
// Allow redirect behavior to be overridden if specified.
if client.CheckRedirect == nil {
client.CheckRedirect = defaultCheckRedirect
}
url, err := url.ParseRequestURI(searchRepoOpts.Repo)
if err != nil {
return nil, errors.Wrap(err, "invalid helm chart URL: "+searchRepoOpts.Repo)
}
url.Path = path.Join(url.Path, "index.yaml")
resp, err := client.Get(url.String())
if err != nil {
return nil, errInvalidRepoURL
}
defer resp.Body.Close()
var file File
err = yaml.NewDecoder(resp.Body).Decode(&file)
if err != nil {
return nil, errInvalidRepoURL
}
// Validate index.yaml
if file.APIVersion == "" || file.Entries == nil {
return nil, errInvalidRepoURL
}
result, err := json.Marshal(file)
if err != nil {
return nil, errInvalidRepoURL
}
return result, nil
}
// defaultCheckRedirect is a default CheckRedirect for helm
// We don't allow redirects to URLs not ending in index.yaml
// After that we follow the go http client behavior which is to stop
// after a maximum of 10 redirects
func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
// The request url must end in index.yaml
if path.Base(req.URL.Path) != "index.yaml" {
return errors.New("the request URL must end in index.yaml")
}
// default behavior below
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}

View File

@ -1,50 +0,0 @@
package binary
import (
"testing"
"github.com/portainer/portainer/pkg/libhelm/libhelmtest"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/stretchr/testify/assert"
)
func Test_SearchRepo(t *testing.T) {
libhelmtest.EnsureIntegrationTest(t)
is := assert.New(t)
hpm := NewHelmBinaryPackageManager("")
type testCase struct {
name string
url string
invalid bool
}
tests := []testCase{
{"not a helm repo", "https://portainer.io", true},
{"ingress helm repo", "https://kubernetes.github.io/ingress-nginx", false},
{"portainer helm repo", "https://portainer.github.io/k8s/", false},
{"gitlap helm repo with trailing slash", "https://charts.gitlab.io/", false},
{"elastic helm repo with trailing slash", "https://helm.elastic.co/", false},
{"fabric8.io helm repo with trailing slash", "https://fabric8.io/helm/", false},
{"lensesio helm repo without trailing slash", "https://lensesio.github.io/kafka-helm-charts", false},
}
for _, test := range tests {
func(tc testCase) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
response, err := hpm.SearchRepo(options.SearchRepoOptions{Repo: tc.url})
if tc.invalid {
is.Errorf(err, "error expected: %s", tc.url)
} else {
is.NoError(err, "no error expected: %s", tc.url)
}
if err == nil {
is.NotEmpty(response, "response expected")
}
})
}(test)
}
}

View File

@ -1,29 +0,0 @@
package binary
import (
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
)
var errRequiredShowOptions = errors.New("chart, repo and output format are required")
// Show runs `helm show <command> <chart> --repo <repo>` with specified show options.
// The show options translate to CLI arguments which are passed in to the helm binary when executing install.
func (hbpm *helmBinaryPackageManager) Show(showOpts options.ShowOptions) ([]byte, error) {
if showOpts.Chart == "" || showOpts.Repo == "" || showOpts.OutputFormat == "" {
return nil, errRequiredShowOptions
}
args := []string{
string(showOpts.OutputFormat),
showOpts.Chart,
"--repo", showOpts.Repo,
}
result, err := hbpm.run("show", args, showOpts.Env)
if err != nil {
return nil, errors.New("the request failed since either the Helm repository was not found or the chart does not exist")
}
return result, nil
}

View File

@ -1,29 +0,0 @@
package binary
import (
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
)
var errRequiredUninstallOptions = errors.New("release name is required")
// Uninstall runs `helm uninstall <name> --namespace <namespace>` with specified uninstall options.
// The uninstall options translate to CLI arguments which are passed in to the helm binary when executing uninstall.
func (hbpm *helmBinaryPackageManager) Uninstall(uninstallOpts options.UninstallOptions) error {
if uninstallOpts.Name == "" {
return errRequiredUninstallOptions
}
args := []string{uninstallOpts.Name}
if uninstallOpts.Namespace != "" {
args = append(args, "--namespace", uninstallOpts.Namespace)
}
_, err := hbpm.runWithKubeConfig("uninstall", args, uninstallOpts.KubernetesClusterAccess, uninstallOpts.Env)
if err != nil {
return errors.Wrap(err, "failed to run helm uninstall on specified args")
}
return nil
}

View File

@ -1,22 +1,11 @@
package libhelm
import (
"errors"
"github.com/portainer/portainer/pkg/libhelm/binary"
"github.com/portainer/portainer/pkg/libhelm/sdk"
"github.com/portainer/portainer/pkg/libhelm/types"
)
// HelmConfig is a struct that holds the configuration for the Helm package manager
type HelmConfig struct {
BinaryPath string `example:"/portainer/dist"`
}
var errBinaryPathNotSpecified = errors.New("binary path not specified")
// NewHelmPackageManager returns a new instance of HelmPackageManager based on HelmConfig
func NewHelmPackageManager(config HelmConfig) (HelmPackageManager, error) {
if config.BinaryPath != "" {
return binary.NewHelmBinaryPackageManager(config.BinaryPath), nil
}
return nil, errBinaryPathNotSpecified
func NewHelmPackageManager() (types.HelmPackageManager, error) {
return sdk.NewHelmSDKPackageManager(), nil
}

View File

@ -2,6 +2,9 @@ package options
// KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data
type KubernetesClusterAccess struct {
ClusterName string `example:"portainer-cluster-endpoint-1"`
ContextName string `example:"portainer-ctx-endpoint-1"`
UserName string `example:"portainer-user-endpoint-1"`
ClusterServerURL string `example:"https://mycompany.k8s.com"`
CertificateAuthorityFile string `example:"/data/tls/localhost.crt"`
AuthToken string `example:"ey..."`

165
pkg/libhelm/sdk/client.go Normal file
View File

@ -0,0 +1,165 @@
package sdk
import (
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"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"
"k8s.io/client-go/tools/clientcmd/api"
)
// newRESTClientGetter creates a custom RESTClientGetter using the provided client config
type clientConfigGetter struct {
clientConfig clientcmd.ClientConfig
namespace string
}
// initActionConfig initializes the action configuration with kubernetes config
func (hspm *HelmSDKPackageManager) initActionConfig(actionConfig *action.Configuration, namespace string, k8sAccess *options.KubernetesClusterAccess) error {
// If namespace is not provided, use the default namespace
if namespace == "" {
namespace = "default"
}
if k8sAccess == nil {
// Use default kubeconfig
settings := cli.New()
clientGetter := settings.RESTClientGetter()
return actionConfig.Init(clientGetter, namespace, "secret", hspm.logf)
}
// Create client config
configAPI := generateConfigAPI(namespace, k8sAccess)
clientConfig := clientcmd.NewDefaultClientConfig(*configAPI, &clientcmd.ConfigOverrides{})
// Create a custom RESTClientGetter that uses our in-memory config
clientGetter, err := newRESTClientGetter(clientConfig, namespace)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("cluster_name", k8sAccess.ClusterName).
Str("cluster_url", k8sAccess.ClusterServerURL).
Str("user_name", k8sAccess.UserName).
Err(err).
Msg("failed to create client getter")
return err
}
return actionConfig.Init(clientGetter, namespace, "secret", hspm.logf)
}
// generateConfigAPI generates a new kubeconfig configuration
func generateConfigAPI(namespace string, k8sAccess *options.KubernetesClusterAccess) *api.Config {
// Create in-memory kubeconfig configuration
configAPI := api.NewConfig()
// Create cluster
cluster := api.NewCluster()
cluster.Server = k8sAccess.ClusterServerURL
if k8sAccess.CertificateAuthorityFile != "" {
// If we have a CA file, use it
cluster.CertificateAuthority = k8sAccess.CertificateAuthorityFile
} else {
// Otherwise skip TLS verification
cluster.InsecureSkipTLSVerify = true
}
// Create auth info with token
authInfo := api.NewAuthInfo()
authInfo.Token = k8sAccess.AuthToken
// Create context
context := api.NewContext()
context.Cluster = k8sAccess.ClusterName
context.AuthInfo = k8sAccess.UserName
context.Namespace = namespace
// Add to config
configAPI.Clusters[k8sAccess.ClusterName] = cluster
configAPI.AuthInfos[k8sAccess.UserName] = authInfo
configAPI.Contexts[k8sAccess.ContextName] = context
configAPI.CurrentContext = k8sAccess.ContextName
return configAPI
}
func newRESTClientGetter(clientConfig clientcmd.ClientConfig, namespace string) (*clientConfigGetter, error) {
if clientConfig == nil {
log.Error().
Str("context", "HelmClient").
Msg("client config is nil")
return nil, errors.New("client config provided during the helm client initialization was nil. Check the kubernetes cluster access configuration")
}
return &clientConfigGetter{
clientConfig: clientConfig,
namespace: namespace,
}, nil
}
func (c *clientConfigGetter) ToRESTConfig() (*rest.Config, error) {
if c.clientConfig == nil {
log.Error().
Str("context", "HelmClient").
Msg("client config is nil")
return nil, errors.New("client config provided during the helm client initialization was nil. Check the kubernetes cluster access configuration")
}
return c.clientConfig.ClientConfig()
}
func (c *clientConfigGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
// Create the discovery client
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
// Wrap the discovery client with a cached discovery client
return memory.NewMemCacheClient(discoveryClient), nil
}
func (c *clientConfigGetter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := c.ToDiscoveryClient()
if err != nil {
return nil, err
}
// Create a REST mapper from the discovery client
return restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient), nil
}
func (c *clientConfigGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return c.clientConfig
}
// parseValues parses YAML values data into a map
func (hspm *HelmSDKPackageManager) parseValues(data []byte) (map[string]any, error) {
// Use Helm's built-in chartutil.ReadValues which properly handles the conversion
// from map[interface{}]interface{} to map[string]interface{}
return chartutil.ReadValues(data)
}
// logf is a log helper function for Helm
func (hspm *HelmSDKPackageManager) logf(format string, v ...any) {
// Use zerolog for structured logging
log.Debug().
Str("context", "HelmClient").
Msgf(format, v...)
}

View File

@ -0,0 +1,156 @@
package sdk
import (
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/action"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func Test_InitActionConfig(t *testing.T) {
is := assert.New(t)
hspm := NewHelmSDKPackageManager()
t.Run("with nil k8sAccess should use default kubeconfig", func(t *testing.T) {
actionConfig := new(action.Configuration)
err := hspm.(*HelmSDKPackageManager).initActionConfig(actionConfig, "default", nil)
// The function should not fail by design, even when not running in a k8s environment
is.NoError(err, "should not return error when not in k8s environment")
})
t.Run("with k8sAccess should create in-memory config", func(t *testing.T) {
actionConfig := new(action.Configuration)
k8sAccess := &options.KubernetesClusterAccess{
ClusterServerURL: "https://kubernetes.default.svc",
AuthToken: "test-token",
}
// The function should not fail by design
err := hspm.(*HelmSDKPackageManager).initActionConfig(actionConfig, "default", k8sAccess)
is.NoError(err, "should not return error when using in-memory config")
})
t.Run("with k8sAccess and CA file should create config with CA", func(t *testing.T) {
actionConfig := new(action.Configuration)
k8sAccess := &options.KubernetesClusterAccess{
ClusterServerURL: "https://kubernetes.default.svc",
AuthToken: "test-token",
CertificateAuthorityFile: "/path/to/ca.crt",
}
// The function should not fail by design
err := hspm.(*HelmSDKPackageManager).initActionConfig(actionConfig, "default", k8sAccess)
is.NoError(err, "should not return error when using in-memory config with CA")
})
}
func Test_ClientConfigGetter(t *testing.T) {
is := assert.New(t)
// Create a mock client config
configAPI := api.NewConfig()
// Create cluster
cluster := api.NewCluster()
cluster.Server = "https://kubernetes.default.svc"
cluster.InsecureSkipTLSVerify = true
// Create auth info
authInfo := api.NewAuthInfo()
authInfo.Token = "test-token"
// Create context
context := api.NewContext()
context.Cluster = "test-cluster"
context.AuthInfo = "test-user"
context.Namespace = "default"
// Add to config
configAPI.Clusters["test-cluster"] = cluster
configAPI.AuthInfos["test-user"] = authInfo
configAPI.Contexts["test-context"] = context
configAPI.CurrentContext = "test-context"
clientConfig := clientcmd.NewDefaultClientConfig(*configAPI, &clientcmd.ConfigOverrides{})
// Create client config getter
clientGetter, err := newRESTClientGetter(clientConfig, "default")
is.NoError(err, "should not return error when creating client getter")
// Test ToRESTConfig
restConfig, err := clientGetter.ToRESTConfig()
is.NoError(err, "should not return error when creating REST config")
is.NotNil(restConfig, "should return non-nil REST config")
is.Equal("https://kubernetes.default.svc", restConfig.Host, "host should be https://kubernetes.default.svc")
is.Equal("test-token", restConfig.BearerToken, "bearer token should be test-token")
// Test ToDiscoveryClient
discoveryClient, err := clientGetter.ToDiscoveryClient()
is.NoError(err, "should not return error when creating discovery client")
is.NotNil(discoveryClient, "should return non-nil discovery client")
// Test ToRESTMapper
restMapper, err := clientGetter.ToRESTMapper()
is.NoError(err, "should not return error when creating REST mapper")
is.NotNil(restMapper, "should return non-nil REST mapper")
// Test ToRawKubeConfigLoader
config := clientGetter.ToRawKubeConfigLoader()
is.NotNil(config, "should return non-nil config loader")
}
func Test_ParseValues(t *testing.T) {
is := assert.New(t)
hspm := NewHelmSDKPackageManager()
t.Run("should parse valid YAML values", func(t *testing.T) {
yamlData := []byte(`
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
`)
values, err := hspm.(*HelmSDKPackageManager).parseValues(yamlData)
is.NoError(err, "should parse valid YAML without error")
is.NotNil(values, "should return non-nil values")
// Verify structure
service, ok := values["service"].(map[string]interface{})
is.True(ok, "service should be a map")
is.Equal("ClusterIP", service["type"], "service type should be ClusterIP")
is.Equal(float64(80), service["port"], "service port should be 80")
resources, ok := values["resources"].(map[string]interface{})
is.True(ok, "resources should be a map")
limits, ok := resources["limits"].(map[string]interface{})
is.True(ok, "limits should be a map")
is.Equal("100m", limits["cpu"], "cpu limit should be 100m")
is.Equal("128Mi", limits["memory"], "memory limit should be 128Mi")
})
t.Run("should handle invalid YAML", func(t *testing.T) {
yamlData := []byte(`
service:
type: ClusterIP
port: 80
invalid yaml
`)
_, err := hspm.(*HelmSDKPackageManager).parseValues(yamlData)
is.Error(err, "should return error for invalid YAML")
})
t.Run("should handle empty YAML", func(t *testing.T) {
yamlData := []byte(``)
values, err := hspm.(*HelmSDKPackageManager).parseValues(yamlData)
is.NoError(err, "should not return error for empty YAML")
is.NotNil(values, "should return non-nil values for empty YAML")
is.Len(values, 0, "should return empty map for empty YAML")
})
}

210
pkg/libhelm/sdk/install.go Normal file
View File

@ -0,0 +1,210 @@
package sdk
import (
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/postrender"
)
// Install implements the HelmPackageManager interface by using the Helm SDK to install a chart.
func (hspm *HelmSDKPackageManager) Install(installOpts options.InstallOptions) (*release.Release, error) {
log.Debug().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("name", installOpts.Name).
Str("namespace", installOpts.Namespace).
Str("repo", installOpts.Repo).
Bool("wait", installOpts.Wait).
Msg("Installing Helm chart")
if installOpts.Name == "" {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("name", installOpts.Name).
Str("namespace", installOpts.Namespace).
Str("repo", installOpts.Repo).
Bool("wait", installOpts.Wait).
Msg("Name is required for helm release installation")
return nil, errors.New("name is required for helm release installation")
}
// Initialize action configuration with kubernetes config
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, installOpts.Namespace, installOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("namespace", installOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration for helm release installation")
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release installation")
}
installClient, err := initInstallClient(actionConfig, installOpts)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm install client for helm release installation")
return nil, errors.Wrap(err, "failed to initialize helm install client for helm release installation")
}
values, err := hspm.GetHelmValuesFromFile(installOpts.ValuesFile)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to get Helm values from file for helm release installation")
return nil, errors.Wrap(err, "failed to get Helm values from file for helm release installation")
}
chart, err := hspm.loadAndValidateChart(installClient, installOpts)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to load and validate chart for helm release installation")
return nil, errors.Wrap(err, "failed to load and validate chart for helm release installation")
}
// Run the installation
log.Info().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("name", installOpts.Name).
Str("namespace", installOpts.Namespace).
Msg("Running chart installation for helm release")
helmRelease, err := installClient.Run(chart, values)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Str("name", installOpts.Name).
Str("namespace", installOpts.Namespace).
Err(err).
Msg("Failed to install helm chart for helm release installation")
return nil, errors.Wrap(err, "helm was not able to install the chart for helm release installation")
}
return &release.Release{
Name: helmRelease.Name,
Namespace: helmRelease.Namespace,
Chart: release.Chart{
Metadata: &release.Metadata{
Name: helmRelease.Chart.Metadata.Name,
Version: helmRelease.Chart.Metadata.Version,
AppVersion: helmRelease.Chart.Metadata.AppVersion,
},
},
Labels: helmRelease.Labels,
Version: helmRelease.Version,
Manifest: helmRelease.Manifest,
}, nil
}
// loadAndValidateChart locates and loads the chart, and validates it.
// it also checks for chart dependencies and updates them if necessary.
// it returns the chart information.
func (hspm *HelmSDKPackageManager) loadAndValidateChart(installClient *action.Install, installOpts options.InstallOptions) (*chart.Chart, error) {
// Locate and load the chart
chartPath, err := installClient.ChartPathOptions.LocateChart(installOpts.Chart, hspm.settings)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Err(err).
Msg("Failed to locate chart for helm release installation")
return nil, errors.Wrapf(err, "failed to find the helm chart at the path: %s/%s", installOpts.Repo, installOpts.Chart)
}
chartReq, err := loader.Load(chartPath)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to load chart for helm release installation")
return nil, errors.Wrap(err, "failed to load chart for helm release installation")
}
// Check chart dependencies to make sure all are present in /charts
if chartDependencies := chartReq.Metadata.Dependencies; chartDependencies != nil {
if err := action.CheckDependencies(chartReq, chartDependencies); err != nil {
err = errors.Wrap(err, "failed to check chart dependencies for helm release installation")
if !installClient.DependencyUpdate {
return nil, err
}
log.Debug().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Msg("Updating chart dependencies for helm release installation")
providers := getter.All(hspm.settings)
manager := &downloader.Manager{
Out: os.Stdout,
ChartPath: chartPath,
Keyring: installClient.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: providers,
RepositoryConfig: hspm.settings.RepositoryConfig,
RepositoryCache: hspm.settings.RepositoryCache,
Debug: hspm.settings.Debug,
}
if err := manager.Update(); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", installOpts.Chart).
Err(err).
Msg("Failed to update chart dependencies for helm release installation")
return nil, errors.Wrap(err, "failed to update chart dependencies for helm release installation")
}
// Reload the chart with the updated Chart.lock file.
if chartReq, err = loader.Load(chartPath); err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Err(err).
Msg("Failed to reload chart after dependency update for helm release installation")
return nil, errors.Wrap(err, "failed to reload chart after dependency update for helm release installation")
}
}
}
return chartReq, nil
}
// initInstallClient initializes the install client with the given options
// and return the install client.
func initInstallClient(actionConfig *action.Configuration, installOpts options.InstallOptions) (*action.Install, error) {
installClient := action.NewInstall(actionConfig)
installClient.CreateNamespace = true
installClient.DependencyUpdate = true
installClient.ReleaseName = installOpts.Name
installClient.Namespace = installOpts.Namespace
installClient.ChartPathOptions.RepoURL = installOpts.Repo
installClient.Wait = installOpts.Wait
if installOpts.PostRenderer != "" {
postRenderer, err := postrender.NewExec(installOpts.PostRenderer)
if err != nil {
return nil, errors.Wrap(err, "failed to create post renderer")
}
installClient.PostRenderer = postRenderer
}
return installClient, nil
}

View File

@ -0,0 +1,190 @@
package sdk
import (
"os"
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func createValuesFile(values string) (string, error) {
file, err := os.CreateTemp("", "helm-values")
if err != nil {
return "", err
}
_, err = file.WriteString(values)
if err != nil {
file.Close()
return "", err
}
err = file.Close()
if err != nil {
return "", err
}
return file.Name(), nil
}
func Test_Install(t *testing.T) {
test.EnsureIntegrationTest(t)
is := assert.New(t)
// Create a new SDK package manager
hspm := NewHelmSDKPackageManager()
t.Run("successfully installs nginx chart with name test-nginx", func(t *testing.T) {
// SDK equivalent of: helm install test-nginx --repo https://kubernetes.github.io/ingress-nginx nginx
installOpts := options.InstallOptions{
Name: "test-nginx",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hspm.Install(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx",
})
}
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
is.Equal("test-nginx", release.Name, "release name should match")
is.Equal(1, release.Version, "release version should be 1")
is.NotEmpty(release.Manifest, "release manifest should not be empty")
})
t.Run("successfully installs nginx with values", func(t *testing.T) {
// SDK equivalent of: helm install test-nginx-2 --repo https://kubernetes.github.io/ingress-nginx nginx --values /tmp/helm-values3161785816
values, err := createValuesFile("service:\n port: 8081")
is.NoError(err, "should create a values file")
defer os.Remove(values)
installOpts := options.InstallOptions{
Name: "test-nginx-2",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
release, err := hspm.Install(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-2",
})
}
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
is.Equal("test-nginx-2", release.Name, "release name should match")
is.Equal(1, release.Version, "release version should be 1")
is.NotEmpty(release.Manifest, "release manifest should not be empty")
})
t.Run("successfully installs portainer chart with name portainer-test", func(t *testing.T) {
// SDK equivalent of: helm install portainer-test portainer --repo https://portainer.github.io/k8s/
installOpts := options.InstallOptions{
Name: "portainer-test",
Chart: "portainer",
Repo: "https://portainer.github.io/k8s/",
}
release, err := hspm.Install(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: installOpts.Name,
})
}
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
is.Equal("portainer-test", release.Name, "release name should match")
is.Equal(1, release.Version, "release version should be 1")
is.NotEmpty(release.Manifest, "release manifest should not be empty")
})
t.Run("install with values as string", func(t *testing.T) {
// First create a values file since InstallOptions doesn't support values as string directly
values, err := createValuesFile("service:\n port: 8082")
is.NoError(err, "should create a values file")
defer os.Remove(values)
// Install with values file
installOpts := options.InstallOptions{
Name: "test-nginx-3",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
ValuesFile: values,
}
release, err := hspm.Install(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-3",
})
}
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
is.Equal("test-nginx-3", release.Name, "release name should match")
})
t.Run("install with namespace", func(t *testing.T) {
// Install with namespace
installOpts := options.InstallOptions{
Name: "test-nginx-4",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
Namespace: "default",
}
release, err := hspm.Install(installOpts)
if release != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "test-nginx-4",
Namespace: "default",
})
}
is.NoError(err, "should successfully install release")
is.NotNil(release, "should return non-nil release")
is.Equal("test-nginx-4", release.Name, "release name should match")
is.Equal("default", release.Namespace, "release namespace should match")
})
t.Run("returns an error when name is not provided", func(t *testing.T) {
installOpts := options.InstallOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
_, err := hspm.Install(installOpts)
is.Error(err, "should return an error when name is not provided")
is.Equal(err.Error(), "name is required")
})
t.Run("install with invalid chart", func(t *testing.T) {
// Install with invalid chart
installOpts := options.InstallOptions{
Name: "test-invalid",
Chart: "non-existent-chart",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
_, err := hspm.Install(installOpts)
is.Error(err, "should return error when chart doesn't exist")
is.Equal(err.Error(), "failed to find the helm chart at the path: https://kubernetes.github.io/ingress-nginx/non-existent-chart")
})
t.Run("install with invalid repo", func(t *testing.T) {
// Install with invalid repo
installOpts := options.InstallOptions{
Name: "test-invalid-repo",
Chart: "nginx",
Repo: "https://non-existent-repo.example.com",
}
_, err := hspm.Install(installOpts)
is.Error(err, "should return error when repo doesn't exist")
})
}

108
pkg/libhelm/sdk/list.go Normal file
View File

@ -0,0 +1,108 @@
package sdk
import (
"fmt"
"strconv"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
sdkrelease "helm.sh/helm/v3/pkg/release"
)
// List implements the HelmPackageManager interface by using the Helm SDK to list releases.
// It returns a slice of ReleaseElement.
func (hspm *HelmSDKPackageManager) List(listOpts options.ListOptions) ([]release.ReleaseElement, error) {
log.Debug().
Str("context", "HelmClient").
Str("namespace", listOpts.Namespace).
Str("filter", listOpts.Filter).
Str("selector", listOpts.Selector).
Msg("Listing Helm releases")
// Initialize action configuration with kubernetes config
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, listOpts.Namespace, listOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("namespace", listOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration")
return nil, errors.Wrap(err, "failed to initialize helm configuration")
}
listClient, err := initListClient(actionConfig, listOpts)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm list client")
}
// Run the list operation
releases, err := listClient.Run()
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to list helm releases")
return []release.ReleaseElement{}, errors.Wrap(err, "failed to list helm releases")
}
// Convert from SDK release type to our release element type and return
return convertToReleaseElements(releases), nil
}
// convertToReleaseElements converts from the SDK release type to our release element type
func convertToReleaseElements(releases []*sdkrelease.Release) []release.ReleaseElement {
elements := make([]release.ReleaseElement, len(releases))
for i, rel := range releases {
chartName := fmt.Sprintf("%s-%s", rel.Chart.Metadata.Name, rel.Chart.Metadata.Version)
elements[i] = release.ReleaseElement{
Name: rel.Name,
Namespace: rel.Namespace,
Revision: strconv.Itoa(rel.Version),
Updated: rel.Info.LastDeployed.String(),
Status: string(rel.Info.Status),
Chart: chartName,
AppVersion: rel.Chart.Metadata.AppVersion,
}
}
return elements
}
// initListClient initializes the list client with the given options
// and return the list client.
func initListClient(actionConfig *action.Configuration, listOpts options.ListOptions) (*action.List, error) {
listClient := action.NewList(actionConfig)
// Configure list options
if listOpts.Filter != "" {
listClient.Filter = listOpts.Filter
}
if listOpts.Selector != "" {
listClient.Selector = listOpts.Selector
}
// If no namespace is specified in options, list across all namespaces
if listOpts.Namespace == "" {
listClient.AllNamespaces = true
}
// No limit by default
listClient.Limit = 0
// Show all releases, even if in a pending or failed state
listClient.All = true
// Set state mask to ensure proper filtering by status
listClient.SetStateMask()
return listClient, nil
}

View File

@ -0,0 +1,72 @@
package sdk
import (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/time"
)
func Test_ConvertToReleaseElements(t *testing.T) {
is := assert.New(t)
// Create mock releases
releases := []*release.Release{
{
Name: "release1",
Namespace: "default",
Version: 1,
Info: &release.Info{
Status: release.StatusDeployed,
LastDeployed: time.Now(),
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "chart1",
Version: "1.0.0",
AppVersion: "1.0.0",
},
},
},
{
Name: "release2",
Namespace: "kube-system",
Version: 2,
Info: &release.Info{
Status: release.StatusFailed,
LastDeployed: time.Now(),
},
Chart: &chart.Chart{
Metadata: &chart.Metadata{
Name: "chart2",
Version: "2.0.0",
AppVersion: "2.0.0",
},
},
},
}
// Convert to release elements
elements := convertToReleaseElements(releases)
// Verify conversion
is.Len(elements, 2, "should return 2 release elements")
// Verify first release
is.Equal("release1", elements[0].Name, "first release name should be release1")
is.Equal("default", elements[0].Namespace, "first release namespace should be default")
is.Equal("1", elements[0].Revision, "first release revision should be 1")
is.Equal(string(release.StatusDeployed), elements[0].Status, "first release status should be deployed")
is.Equal("chart1-1.0.0", elements[0].Chart, "first release chart should be chart1-1.0.0")
is.Equal("1.0.0", elements[0].AppVersion, "first release app version should be 1.0.0")
// Verify second release
is.Equal("release2", elements[1].Name, "second release name should be release2")
is.Equal("kube-system", elements[1].Namespace, "second release namespace should be kube-system")
is.Equal("2", elements[1].Revision, "second release revision should be 2")
is.Equal(string(release.StatusFailed), elements[1].Status, "second release status should be failed")
is.Equal("chart2-2.0.0", elements[1].Chart, "second release chart should be chart2-2.0.0")
is.Equal("2.0.0", elements[1].AppVersion, "second release app version should be 2.0.0")
}

View File

@ -0,0 +1,23 @@
package sdk
import (
"time"
"github.com/portainer/portainer/pkg/libhelm/types"
"helm.sh/helm/v3/pkg/cli"
)
// HelmSDKPackageManager is a wrapper for the helm SDK which implements HelmPackageManager
type HelmSDKPackageManager struct {
settings *cli.EnvSettings
timeout time.Duration
}
// NewHelmSDKPackageManager initializes a new HelmPackageManager service using the Helm SDK
func NewHelmSDKPackageManager() types.HelmPackageManager {
settings := cli.New()
return &HelmSDKPackageManager{
settings: settings,
timeout: 300 * time.Second, // 5 minutes default timeout
}
}

View File

@ -0,0 +1,29 @@
package sdk
import (
"testing"
"time"
"github.com/portainer/portainer/pkg/libhelm/types"
"github.com/stretchr/testify/assert"
)
func Test_NewHelmSDKPackageManager(t *testing.T) {
is := assert.New(t)
// Test that NewHelmSDKPackageManager returns a non-nil HelmPackageManager
manager := NewHelmSDKPackageManager()
is.NotNil(manager, "should return non-nil HelmPackageManager")
// Test that the returned manager is of the correct type
_, ok := manager.(*HelmSDKPackageManager)
is.True(ok, "should return a *HelmSDKPackageManager")
// Test that the manager has the expected fields
sdkManager := manager.(*HelmSDKPackageManager)
is.NotNil(sdkManager.settings, "should have non-nil settings")
is.Equal(300*time.Second, sdkManager.timeout, "should have 5 minute timeout")
// Test that the manager implements the HelmPackageManager interface
var _ types.HelmPackageManager = manager
}

View File

@ -0,0 +1,351 @@
package sdk
import (
"net/url"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
)
var (
errRequiredSearchOptions = errors.New("repo is required")
errInvalidRepoURL = errors.New("the request failed since either the Helm repository was not found or the index.yaml is not valid")
)
type RepoIndex struct {
APIVersion string `json:"apiVersion"`
Entries map[string][]ChartInfo `json:"entries"`
Generated string `json:"generated"`
}
// SearchRepo downloads the `index.yaml` file for specified repo, parses it and returns JSON to caller.
func (hspm *HelmSDKPackageManager) SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error) {
// Validate input options
if err := validateSearchRepoOptions(searchRepoOpts); err != nil {
log.Error().
Str("context", "HelmClient").
Str("repo", searchRepoOpts.Repo).
Err(err).
Msg("Missing required search repo options")
return nil, err
}
log.Debug().
Str("context", "HelmClient").
Str("repo", searchRepoOpts.Repo).
Msg("Searching repository")
// Parse and validate the repository URL
repoURL, err := parseRepoURL(searchRepoOpts.Repo)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("repo", searchRepoOpts.Repo).
Err(err).
Msg("Invalid repository URL")
return nil, err
}
// Set up Helm CLI environment
repoSettings := cli.New()
// Ensure all required Helm directories exist
if err := ensureHelmDirectoriesExist(repoSettings); err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to ensure Helm directories exist")
return nil, errors.Wrap(err, "failed to ensure Helm directories exist")
}
// Download the index file and update repository configuration
indexPath, err := downloadRepoIndex(repoURL.String(), repoSettings, searchRepoOpts.Repo)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("repo_url", repoURL.String()).
Err(err).
Msg("Failed to download repository index")
return nil, err
}
// Load and parse the index file
log.Debug().
Str("context", "HelmClient").
Str("index_path", indexPath).
Msg("Loading index file")
indexFile, err := loadIndexFile(indexPath)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("index_path", indexPath).
Err(err).
Msg("Failed to load index file")
return nil, err
}
// Convert the index file to our response format
result, err := convertIndexToResponse(indexFile)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to convert index to response format")
return nil, errors.Wrap(err, "failed to convert index to response format")
}
log.Debug().
Str("context", "HelmClient").
Str("repo", searchRepoOpts.Repo).
Int("entries_count", len(indexFile.Entries)).
Msg("Successfully searched repository")
return json.Marshal(result)
}
// validateSearchRepoOptions validates the required search repository options.
func validateSearchRepoOptions(opts options.SearchRepoOptions) error {
if opts.Repo == "" {
return errRequiredSearchOptions
}
return nil
}
// parseRepoURL parses and validates the repository URL.
func parseRepoURL(repoURL string) (*url.URL, error) {
parsedURL, err := url.ParseRequestURI(repoURL)
if err != nil {
return nil, errors.Wrap(err, "invalid helm chart URL: "+repoURL)
}
return parsedURL, nil
}
// downloadRepoIndex downloads the index.yaml file from the repository and updates
// the repository configuration.
func downloadRepoIndex(repoURLString string, repoSettings *cli.EnvSettings, repoName string) (string, error) {
log.Debug().
Str("context", "helm_sdk_repo_index").
Str("repo_url", repoURLString).
Str("repo_name", repoName).
Msg("Creating chart repository object")
// Create chart repository object
rep, err := repo.NewChartRepository(
&repo.Entry{
URL: repoURLString,
},
getter.All(repoSettings),
)
if err != nil {
log.Error().
Str("context", "helm_sdk_repo_index").
Str("repo_url", repoURLString).
Err(err).
Msg("Failed to create chart repository object")
return "", errInvalidRepoURL
}
// Load repository configuration file
f, err := repo.LoadFile(repoSettings.RepositoryConfig)
if err != nil {
log.Error().
Str("context", "helm_sdk_repo_index").
Str("repo_config", repoSettings.RepositoryConfig).
Err(err).
Msg("Failed to load repo config")
return "", errors.Wrap(err, "failed to load repo config")
}
// Download the index file
log.Debug().
Str("context", "helm_sdk_repo_index").
Str("repo_url", repoURLString).
Msg("Downloading index file")
indexPath, err := rep.DownloadIndexFile()
if err != nil {
log.Error().
Str("context", "helm_sdk_repo_index").
Str("repo_url", repoURLString).
Err(err).
Msg("Failed to download index file")
return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURLString)
}
// Update repository configuration
c := repo.Entry{
Name: repoName,
URL: repoURLString,
}
f.Update(&c)
// Write updated configuration
repoFile := repoSettings.RepositoryConfig
if err := f.WriteFile(repoFile, 0644); err != nil {
log.Error().
Str("context", "helm_sdk_repo_index").
Str("repo_file", repoSettings.RepositoryConfig).
Err(err).
Msg("Failed to write repository configuration")
return "", errors.Wrap(err, "failed to write repository configuration")
}
log.Debug().
Str("context", "helm_sdk_repo_index").
Str("index_path", indexPath).
Msg("Successfully downloaded index file")
return indexPath, nil
}
// loadIndexFile loads the index file from the given path.
func loadIndexFile(indexPath string) (*repo.IndexFile, error) {
indexFile, err := repo.LoadIndexFile(indexPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to load downloaded index file: %s", indexPath)
}
return indexFile, nil
}
// convertIndexToResponse converts the Helm index file to our response format.
func convertIndexToResponse(indexFile *repo.IndexFile) (RepoIndex, error) {
result := RepoIndex{
APIVersion: indexFile.APIVersion,
Entries: make(map[string][]ChartInfo),
Generated: indexFile.Generated.String(),
}
// Convert Helm SDK types to our response types
for name, charts := range indexFile.Entries {
result.Entries[name] = convertChartsToChartInfo(charts)
}
return result, nil
}
// convertChartsToChartInfo converts Helm chart entries to ChartInfo objects.
func convertChartsToChartInfo(charts []*repo.ChartVersion) []ChartInfo {
chartInfos := make([]ChartInfo, len(charts))
for i, chart := range charts {
chartInfos[i] = ChartInfo{
Name: chart.Name,
Version: chart.Version,
AppVersion: chart.AppVersion,
Description: chart.Description,
Deprecated: chart.Deprecated,
Created: chart.Created.String(),
Digest: chart.Digest,
Home: chart.Home,
Sources: chart.Sources,
URLs: chart.URLs,
Icon: chart.Icon,
Annotations: chart.Annotations,
}
}
return chartInfos
}
// ChartInfo represents a Helm chart in the repository index
type ChartInfo struct {
Name string `json:"name"`
Version string `json:"version"`
AppVersion string `json:"appVersion"`
Description string `json:"description"`
Deprecated bool `json:"deprecated"`
Created string `json:"created"`
Digest string `json:"digest"`
Home string `json:"home"`
Sources []string `json:"sources"`
URLs []string `json:"urls"`
Icon string `json:"icon,omitempty"`
Annotations any `json:"annotations,omitempty"`
}
// ensureHelmDirectoriesExist checks and creates required Helm directories if they don't exist
func ensureHelmDirectoriesExist(settings *cli.EnvSettings) error {
log.Debug().
Str("context", "helm_sdk_dirs").
Msg("Ensuring Helm directories exist")
// List of directories to ensure exist
directories := []string{
filepath.Dir(settings.RepositoryConfig), // Repository config directory
settings.RepositoryCache, // Repository cache directory
filepath.Dir(settings.RegistryConfig), // Registry config directory
settings.PluginsDirectory, // Plugins directory
}
// Create each directory if it doesn't exist
for _, dir := range directories {
if dir == "" {
continue // Skip empty paths
}
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0700); err != nil {
log.Error().
Str("context", "helm_sdk_dirs").
Str("directory", dir).
Err(err).
Msg("Failed to create directory")
return errors.Wrapf(err, "failed to create directory: %s", dir)
}
}
}
// Ensure registry config file exists
if settings.RegistryConfig != "" {
if _, err := os.Stat(settings.RegistryConfig); os.IsNotExist(err) {
// Create the directory if it doesn't exist
dir := filepath.Dir(settings.RegistryConfig)
if err := os.MkdirAll(dir, 0700); err != nil {
log.Error().
Str("context", "helm_sdk_dirs").
Str("directory", dir).
Err(err).
Msg("Failed to create directory")
return errors.Wrapf(err, "failed to create directory: %s", dir)
}
// Create an empty registry config file
if _, err := os.Create(settings.RegistryConfig); err != nil {
log.Error().
Str("context", "helm_sdk_dirs").
Str("file", settings.RegistryConfig).
Err(err).
Msg("Failed to create registry config file")
return errors.Wrapf(err, "failed to create registry config file: %s", settings.RegistryConfig)
}
}
}
// Ensure repository config file exists
if settings.RepositoryConfig != "" {
if _, err := os.Stat(settings.RepositoryConfig); os.IsNotExist(err) {
// Create an empty repository config file with default yaml structure
f := repo.NewFile()
if err := f.WriteFile(settings.RepositoryConfig, 0644); err != nil {
log.Error().
Str("context", "helm_sdk_dirs").
Str("file", settings.RepositoryConfig).
Err(err).
Msg("Failed to create repository config file")
return errors.Wrapf(err, "failed to create repository config file: %s", settings.RepositoryConfig)
}
}
}
log.Debug().
Str("context", "helm_sdk_dirs").
Msg("Successfully ensured all Helm directories exist")
return nil
}

View File

@ -0,0 +1,84 @@
package sdk
import (
"encoding/json"
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/stretchr/testify/assert"
)
type testCase struct {
name string
url string
invalid bool
}
var tests = []testCase{
{"not a helm repo", "https://portainer.io", true},
{"ingress helm repo", "https://kubernetes.github.io/ingress-nginx", false},
{"portainer helm repo", "https://portainer.github.io/k8s/", false},
{"elastic helm repo with trailing slash", "https://helm.elastic.co/", false},
{"lensesio helm repo without trailing slash", "https://lensesio.github.io/kafka-helm-charts", false},
}
func Test_SearchRepo(t *testing.T) {
is := assert.New(t)
// Create a new SDK package manager
hspm := NewHelmSDKPackageManager()
for _, test := range tests {
func(tc testCase) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
response, err := hspm.SearchRepo(options.SearchRepoOptions{Repo: tc.url})
if tc.invalid {
is.Errorf(err, "error expected: %s", tc.url)
} else {
is.NoError(err, "no error expected: %s", tc.url)
}
if err == nil {
is.NotEmpty(response, "response expected")
}
})
}(test)
}
t.Run("search repo with keyword", func(t *testing.T) {
// Search for charts with keyword
searchOpts := options.SearchRepoOptions{
Repo: "https://kubernetes.github.io/ingress-nginx",
}
responseBytes, err := hspm.SearchRepo(searchOpts)
// The function should not fail by design, even when not running in a k8s environment
is.NoError(err, "should not return error when not in k8s environment")
is.NotNil(responseBytes, "should return non-nil response")
is.NotEmpty(responseBytes, "should return non-empty response")
// Parse the ext response
var repoIndex RepoIndex
err = json.Unmarshal(responseBytes, &repoIndex)
is.NoError(err, "should parse JSON response without error")
is.NotEmpty(repoIndex, "should have at least one chart")
// Verify charts structure apiVersion, entries, generated
is.Equal("v1", repoIndex.APIVersion, "apiVersion should be v1")
is.NotEmpty(repoIndex.Entries, "entries should not be empty")
is.NotEmpty(repoIndex.Generated, "generated should not be empty")
// there should be at least one chart
is.Greater(len(repoIndex.Entries), 0, "should have at least one chart")
})
t.Run("search repo with empty repo URL", func(t *testing.T) {
// Search with empty repo URL
searchOpts := options.SearchRepoOptions{
Repo: "",
}
_, err := hspm.SearchRepo(searchOpts)
is.Error(err, "should return error when repo URL is empty")
})
}

131
pkg/libhelm/sdk/show.go Normal file
View File

@ -0,0 +1,131 @@
package sdk
import (
"fmt"
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
)
var errRequiredShowOptions = errors.New("chart, repo and output format are required")
// Show implements the HelmPackageManager interface by using the Helm SDK to show chart information.
// It supports showing chart values, readme, and chart details based on the provided ShowOptions.
func (hspm *HelmSDKPackageManager) Show(showOpts options.ShowOptions) ([]byte, error) {
if showOpts.Chart == "" || showOpts.Repo == "" || showOpts.OutputFormat == "" {
log.Error().
Str("context", "HelmClient").
Str("chart", showOpts.Chart).
Str("repo", showOpts.Repo).
Str("output_format", string(showOpts.OutputFormat)).
Msg("Missing required show options")
return nil, errRequiredShowOptions
}
log.Debug().
Str("context", "HelmClient").
Str("chart", showOpts.Chart).
Str("repo", showOpts.Repo).
Str("output_format", string(showOpts.OutputFormat)).
Msg("Showing chart information")
// Initialize action configuration
actionConfig := new(action.Configuration)
if err := actionConfig.Init(nil, "", "", func(format string, v ...interface{}) {}); err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm configuration")
return nil, fmt.Errorf("failed to initialize helm configuration: %w", err)
}
// Create temporary directory for chart download
tempDir, err := os.MkdirTemp("", "helm-show-*")
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to create temp directory")
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Create showClient action
showClient, err := initShowClient(actionConfig, showOpts)
if err != nil {
log.Error().
Str("context", "HelmClient").
Err(err).
Msg("Failed to initialize helm show client")
return nil, fmt.Errorf("failed to initialize helm show client: %w", err)
}
// Locate and load the chart
log.Debug().
Str("context", "HelmClient").
Str("chart", showOpts.Chart).
Str("repo", showOpts.Repo).
Msg("Locating chart")
chartPath, err := showClient.ChartPathOptions.LocateChart(showOpts.Chart, hspm.settings)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart", showOpts.Chart).
Str("repo", showOpts.Repo).
Err(err).
Msg("Failed to locate chart")
return nil, fmt.Errorf("failed to locate chart: %w", err)
}
// Get the output based on the requested format
output, err := showClient.Run(chartPath)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("chart_path", chartPath).
Str("output_format", string(showOpts.OutputFormat)).
Err(err).
Msg("Failed to show chart info")
return nil, fmt.Errorf("failed to show chart info: %w", err)
}
log.Debug().
Str("context", "HelmClient").
Str("chart", showOpts.Chart).
Int("output_size", len(output)).
Msg("Successfully retrieved chart information")
return []byte(output), nil
}
// initShowClient initializes the show client with the given options
// and return the show client.
func initShowClient(actionConfig *action.Configuration, showOpts options.ShowOptions) (*action.Show, error) {
showClient := action.NewShowWithConfig(action.ShowAll, actionConfig)
showClient.ChartPathOptions.RepoURL = showOpts.Repo
showClient.ChartPathOptions.Version = "" // Latest version
// Set output type based on ShowOptions
switch showOpts.OutputFormat {
case options.ShowAll:
showClient.OutputFormat = action.ShowAll
case options.ShowChart:
showClient.OutputFormat = action.ShowChart
case options.ShowValues:
showClient.OutputFormat = action.ShowValues
case options.ShowReadme:
showClient.OutputFormat = action.ShowReadme
default:
log.Error().
Str("context", "HelmClient").
Str("output_format", string(showOpts.OutputFormat)).
Msg("Unsupported output format")
return nil, fmt.Errorf("unsupported output format: %s", showOpts.OutputFormat)
}
return showClient, nil
}

View File

@ -0,0 +1,102 @@
package sdk
import (
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func Test_Show(t *testing.T) {
test.EnsureIntegrationTest(t)
is := assert.New(t)
// Create a new SDK package manager
hspm := NewHelmSDKPackageManager()
// install the ingress-nginx chart to test the show command
installOpts := options.InstallOptions{
Name: "ingress-nginx",
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
release, err := hspm.Install(installOpts)
if release != nil || err != nil {
defer hspm.Uninstall(options.UninstallOptions{
Name: "ingress-nginx",
})
}
t.Run("show requires chart, repo and output format", func(t *testing.T) {
showOpts := options.ShowOptions{
Chart: "",
Repo: "",
OutputFormat: "",
}
_, err := hspm.Show(showOpts)
is.Error(err, "should return error when required options are missing")
is.Contains(err.Error(), "chart, repo and output format are required", "error message should indicate required options")
})
t.Run("show chart values", func(t *testing.T) {
showOpts := options.ShowOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
OutputFormat: options.ShowValues,
}
values, err := hspm.Show(showOpts)
is.NoError(err, "should not return error when not in k8s environment")
is.NotEmpty(values, "should return non-empty values")
})
t.Run("show chart readme", func(t *testing.T) {
showOpts := options.ShowOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
OutputFormat: options.ShowReadme,
}
readme, err := hspm.Show(showOpts)
is.NoError(err, "should not return error when not in k8s environment")
is.NotEmpty(readme, "should return non-empty readme")
})
t.Run("show chart definition", func(t *testing.T) {
showOpts := options.ShowOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
OutputFormat: options.ShowChart,
}
chart, err := hspm.Show(showOpts)
is.NoError(err, "should not return error when not in k8s environment")
is.NotNil(chart, "should return non-nil chart definition")
})
t.Run("show all chart info", func(t *testing.T) {
showOpts := options.ShowOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
OutputFormat: options.ShowAll,
}
info, err := hspm.Show(showOpts)
is.NoError(err, "should not return error when not in k8s environment")
is.NotEmpty(info, "should return non-empty chart info")
})
t.Run("show with invalid output format", func(t *testing.T) {
// Show with invalid output format
showOpts := options.ShowOptions{
Chart: "ingress-nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
OutputFormat: "invalid",
}
_, err := hspm.Show(showOpts)
is.Error(err, "should return error with invalid output format")
is.Contains(err.Error(), "unsupported output format", "error message should indicate invalid output format")
})
}

View File

@ -0,0 +1,70 @@
package sdk
import (
"github.com/pkg/errors"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/rs/zerolog/log"
"helm.sh/helm/v3/pkg/action"
)
// Uninstall implements the HelmPackageManager interface by using the Helm SDK to uninstall a release.
func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOptions) error {
if uninstallOpts.Name == "" {
log.Error().
Str("context", "HelmClient").
Msg("Release name is required")
return errors.New("release name is required")
}
log.Debug().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Msg("Uninstalling Helm release")
// Initialize action configuration with kubernetes config
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Err(err).
Msg("Failed to initialize helm configuration")
return errors.Wrap(err, "failed to initialize helm configuration")
}
// Create uninstallClient action
uninstallClient := action.NewUninstall(actionConfig)
// 'foreground' means the parent object remains in a "terminating" state until all of its children are deleted. This ensures that all dependent resources are completely removed before finalizing the deletion of the parent resource.
uninstallClient.DeletionPropagation = "foreground" // "background" or "orphan"
// Run the uninstallation
log.Info().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Msg("Running uninstallation")
result, err := uninstallClient.Run(uninstallOpts.Name)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Err(err).
Msg("Failed to uninstall helm release")
return errors.Wrap(err, "failed to uninstall helm release")
}
if result != nil {
log.Debug().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("release_info", result.Release.Info.Description).
Msg("Uninstall result details")
}
return nil
}

View File

@ -0,0 +1,65 @@
package sdk
import (
"testing"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func Test_Uninstall(t *testing.T) {
test.EnsureIntegrationTest(t)
is := assert.New(t)
// Create a new SDK package manager
hspm := NewHelmSDKPackageManager()
t.Run("uninstall requires a release name", func(t *testing.T) {
// Try to uninstall without a release name
uninstallOpts := options.UninstallOptions{
Name: "",
}
err := hspm.Uninstall(uninstallOpts)
is.Error(err, "should return error when release name is empty")
is.Contains(err.Error(), "release name is required", "error message should indicate release name is required")
})
t.Run("uninstall a non-existent release", func(t *testing.T) {
// Try to uninstall a release that doesn't exist
uninstallOpts := options.UninstallOptions{
Name: "non-existent-release",
}
err := hspm.Uninstall(uninstallOpts)
// The function should not fail by design, even when not running in a k8s environment
// However, it should return an error for a non-existent release
is.Error(err, "should return error when release doesn't exist")
is.Contains(err.Error(), "not found", "error message should indicate release not found")
})
// This test is commented out as it requires a real release to be installed first
t.Run("successfully uninstall an existing release", func(t *testing.T) {
// First install a release
installOpts := options.InstallOptions{
Name: "test-uninstall",
Chart: "nginx",
Repo: "https://kubernetes.github.io/ingress-nginx",
}
// Install the release
_, err := hspm.Install(installOpts)
if err != nil {
t.Logf("Error installing release: %v", err)
t.Skip("Skipping uninstall test because install failed")
return
}
// Now uninstall it
uninstallOpts := options.UninstallOptions{
Name: "test-uninstall",
}
err = hspm.Uninstall(uninstallOpts)
is.NoError(err, "should successfully uninstall release")
})
}

42
pkg/libhelm/sdk/values.go Normal file
View File

@ -0,0 +1,42 @@
package sdk
import (
"os"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
// GetHelmValuesFromFile reads the values file and parses it into a map[string]any
// and returns the map.
func (hspm *HelmSDKPackageManager) GetHelmValuesFromFile(valuesFile string) (map[string]any, error) {
var vals map[string]any
if valuesFile != "" {
log.Debug().
Str("context", "HelmClient").
Str("values_file", valuesFile).
Msg("Reading values file")
valuesData, err := os.ReadFile(valuesFile)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("values_file", valuesFile).
Err(err).
Msg("Failed to read values file")
return nil, errors.Wrap(err, "failed to read values file")
}
vals, err = hspm.parseValues(valuesData)
if err != nil {
log.Error().
Str("context", "HelmClient").
Str("values_file", valuesFile).
Err(err).
Msg("Failed to parse values file")
return nil, errors.Wrap(err, "failed to parse values file")
}
}
return vals, nil
}

View File

@ -1,4 +1,4 @@
package libhelmtest
package test
import (
"os"

View File

@ -3,9 +3,9 @@ package test
import (
"strings"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"github.com/portainer/portainer/pkg/libhelm/types"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
@ -31,8 +31,8 @@ const (
// Do not use this package for concurrent tests.
type helmMockPackageManager struct{}
// NewMockHelmBinaryPackageManager initializes a new HelmPackageManager service (a mock instance)
func NewMockHelmBinaryPackageManager(binaryPath string) libhelm.HelmPackageManager {
// NewMockHelmPackageManager initializes a new HelmPackageManager service (a mock instance)
func NewMockHelmPackageManager() types.HelmPackageManager {
return &helmMockPackageManager{}
}

View File

@ -1,16 +1,26 @@
package libhelm
package types
import (
"github.com/portainer/portainer/pkg/libhelm/options"
"github.com/portainer/portainer/pkg/libhelm/release"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/repo"
)
// HelmPackageManager represents a service that interfaces with Helm
type HelmPackageManager interface {
Show(showOpts options.ShowOptions) ([]byte, error)
SearchRepo(searchRepoOpts options.SearchRepoOptions) ([]byte, error)
Get(getOpts options.GetOptions) ([]byte, error)
List(listOpts options.ListOptions) ([]release.ReleaseElement, error)
Install(installOpts options.InstallOptions) (*release.Release, error)
Uninstall(uninstallOpts options.UninstallOptions) error
}
type Repository interface {
Charts() (repo.ChartVersions, error)
}
type HelmRepo struct {
Settings *cli.EnvSettings
Orig *repo.Entry
}

View File

@ -15,6 +15,10 @@ func ValidateHelmRepositoryURL(repoUrl string, client *http.Client) error {
return errors.New("URL is required")
}
if strings.HasPrefix(repoUrl, "oci://") {
return errors.New("OCI repositories are not supported yet")
}
url, err := url.ParseRequestURI(repoUrl)
if err != nil {
return fmt.Errorf("invalid helm repository URL '%s': %w", repoUrl, err)

View File

@ -3,12 +3,12 @@ package libhelm
import (
"testing"
"github.com/portainer/portainer/pkg/libhelm/libhelmtest"
"github.com/portainer/portainer/pkg/libhelm/test"
"github.com/stretchr/testify/assert"
)
func Test_ValidateHelmRepositoryURL(t *testing.T) {
libhelmtest.EnsureIntegrationTest(t)
test.EnsureIntegrationTest(t)
is := assert.New(t)
type testCase struct {
@ -26,7 +26,7 @@ func Test_ValidateHelmRepositoryURL(t *testing.T) {
{"not helm repo", "http://google.com", true},
{"not valid repo with trailing slash", "http://google.com/", true},
{"not valid repo with trailing slashes", "http://google.com////", true},
{"ingress helm repo", "https://kubernetes.github.io/ingress-nginx/", false},
{"bitnami helm repo", "https://charts.bitnami.com/bitnami/", false},
{"gitlap helm repo", "https://charts.gitlab.io/", false},
{"portainer helm repo", "https://portainer.github.io/k8s/", false},
{"elastic helm repo", "https://helm.elastic.co/", false},