mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 09:04:48 +00:00
feat(addons): install support [R8S-1138] (#3189)
This commit is contained in:
@@ -18966,15 +18966,17 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: '**Access policy**: authenticated'
|
||||
description: |-
|
||||
**Access policy**: authenticated
|
||||
`repo` may be omitted when `chart` is a self-contained "oci://host/path" reference.
|
||||
operationId: HelmShow
|
||||
parameters:
|
||||
- description: Helm repository URL
|
||||
- description: Helm repository URL (required unless chart is a self-contained
|
||||
oci:// reference)
|
||||
in: query
|
||||
name: repo
|
||||
required: true
|
||||
type: string
|
||||
- description: Chart name
|
||||
- description: Chart name, or a self-contained oci:// chart reference
|
||||
in: query
|
||||
name: chart
|
||||
required: true
|
||||
|
||||
@@ -38,7 +38,7 @@ func (handler *Handler) helmList(w http.ResponseWriter, r *http.Request) *httper
|
||||
KubernetesClusterAccess: clusterAccess,
|
||||
}
|
||||
|
||||
// optional namespace. The library defaults to "default"
|
||||
// optional namespace; when omitted the library lists across all namespaces
|
||||
namespace, _ := request.RetrieveQueryParameter(r, "namespace", true)
|
||||
if namespace != "" {
|
||||
listOpts.Namespace = namespace
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/pkg/libhelm/options"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
@@ -18,9 +19,10 @@ import (
|
||||
// @summary Show Helm Chart Information
|
||||
// @description
|
||||
// @description **Access policy**: authenticated
|
||||
// @description `repo` may be omitted when `chart` is a self-contained "oci://host/path" reference.
|
||||
// @tags helm
|
||||
// @param repo query string true "Helm repository URL"
|
||||
// @param chart query string true "Chart name"
|
||||
// @param repo query string false "Helm repository URL (required unless chart is a self-contained oci:// reference)"
|
||||
// @param chart query string true "Chart name, or a self-contained oci:// chart reference"
|
||||
// @param version query string false "Chart version"
|
||||
// @param command path string true "chart/values/readme"
|
||||
// @security ApiKeyAuth
|
||||
@@ -33,24 +35,35 @@ import (
|
||||
// @failure 500 "Server error"
|
||||
// @router /templates/helm/{command} [get]
|
||||
func (handler *Handler) helmShow(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing `repo` query parameter"))
|
||||
}
|
||||
_, err := url.ParseRequestURI(repo)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo)))
|
||||
}
|
||||
|
||||
if err := ssrf.CheckURL(r.Context(), repo); err != nil {
|
||||
return httperror.BadRequest("Repository URL blocked by SSRF policy", err)
|
||||
}
|
||||
|
||||
chart := r.URL.Query().Get("chart")
|
||||
if chart == "" {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing `chart` query parameter"))
|
||||
}
|
||||
|
||||
// A self-contained "oci://host/path" chart reference carries its own source
|
||||
// (e.g. Portainer addon charts), so no repo is required to locate it.
|
||||
selfContainedChart := strings.HasPrefix(chart, "oci://")
|
||||
|
||||
repo := r.URL.Query().Get("repo")
|
||||
if repo == "" && !selfContainedChart {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing `repo` query parameter"))
|
||||
}
|
||||
|
||||
if repo != "" {
|
||||
if _, err := url.ParseRequestURI(repo); err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.Wrap(err, fmt.Sprintf("provided URL %q is not valid", repo)))
|
||||
}
|
||||
if err := ssrf.CheckURL(r.Context(), repo); err != nil {
|
||||
return httperror.BadRequest("Repository URL blocked by SSRF policy", err)
|
||||
}
|
||||
}
|
||||
|
||||
if selfContainedChart {
|
||||
if err := ssrf.CheckURL(r.Context(), chart); err != nil {
|
||||
return httperror.BadRequest("Chart reference blocked by SSRF policy", err)
|
||||
}
|
||||
}
|
||||
|
||||
version, err := request.RetrieveQueryParameter(r, "version", true)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.Wrap(err, fmt.Sprintf("provided version %q is not valid", version)))
|
||||
|
||||
@@ -22,13 +22,23 @@ type clientConfigGetter struct {
|
||||
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
|
||||
// namespaceOrDefault returns "default" for an empty namespace. Actions that
|
||||
// operate on a single release pass their namespace through this before calling
|
||||
// initActionConfig, so an omitted namespace resolves to "default" rather than
|
||||
// initializing the release storage cluster-wide.
|
||||
func namespaceOrDefault(namespace string) string {
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
return "default"
|
||||
}
|
||||
return namespace
|
||||
}
|
||||
|
||||
// initActionConfig initializes the action configuration scoped to the given
|
||||
// namespace, which may be empty: an empty namespace initializes the release
|
||||
// storage cluster-wide, which is how `helm list --all-namespaces` sees
|
||||
// releases in every namespace (action.List.AllNamespaces alone does not widen
|
||||
// the storage scope).
|
||||
func (hspm *HelmSDKPackageManager) initActionConfig(actionConfig *action.Configuration, namespace string, k8sAccess *options.KubernetesClusterAccess) error {
|
||||
// Setup logging for Helm SDK using zerolog
|
||||
logger := log.With().Str("context", "HelmClient").Logger()
|
||||
logOptions := slogzerolog.Option{
|
||||
|
||||
@@ -20,7 +20,7 @@ func (hspm *HelmSDKPackageManager) Get(getOptions options.GetOptions) (*release.
|
||||
Msg("Get Helm release")
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, getOptions.Namespace, getOptions.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(getOptions.Namespace), getOptions.KubernetesClusterAccess)
|
||||
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
||||
@@ -20,7 +20,7 @@ func (hspm *HelmSDKPackageManager) GetHistory(historyOptions options.HistoryOpti
|
||||
Msg("Get Helm history")
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, historyOptions.Namespace, historyOptions.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(historyOptions.Namespace), historyOptions.KubernetesClusterAccess)
|
||||
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
||||
@@ -37,7 +37,7 @@ func (hspm *HelmSDKPackageManager) install(installOpts options.InstallOptions) (
|
||||
|
||||
// Initialize action configuration with kubernetes config
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, installOpts.Namespace, installOpts.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(installOpts.Namespace), installOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
// error is already logged in initActionConfig
|
||||
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release installation")
|
||||
|
||||
@@ -23,7 +23,10 @@ func (hspm *HelmSDKPackageManager) List(listOpts options.ListOptions) ([]release
|
||||
Str("selector", listOpts.Selector).
|
||||
Msg("Listing Helm releases")
|
||||
|
||||
// Initialize action configuration with kubernetes config
|
||||
// Initialize action configuration with kubernetes config. The namespace is
|
||||
// passed through as-is: an empty namespace keeps the release storage
|
||||
// cluster-wide so the list covers every namespace, mirroring how the Helm
|
||||
// CLI implements `helm list --all-namespaces`.
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, listOpts.Namespace, listOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func (hspm *HelmSDKPackageManager) doesReleaseExist(releaseName, namespace string, clusterAccess *options.KubernetesClusterAccess) (bool, error) {
|
||||
// Initialize action configuration
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, namespace, clusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(namespace), clusterAccess)
|
||||
if err != nil {
|
||||
// error is already logged in initActionConfig
|
||||
return false, fmt.Errorf("failed to initialize helm configuration: %w", err)
|
||||
|
||||
@@ -30,7 +30,7 @@ func (hspm *HelmSDKPackageManager) Rollback(rollbackOpts options.RollbackOptions
|
||||
|
||||
// Initialize action configuration with kubernetes config
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, rollbackOpts.Namespace, rollbackOpts.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(rollbackOpts.Namespace), rollbackOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release rollback")
|
||||
}
|
||||
|
||||
+10
-1
@@ -2,6 +2,7 @@ package sdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/portainer/pkg/libhelm/cache"
|
||||
@@ -12,10 +13,18 @@ import (
|
||||
|
||||
var errRequiredShowOptions = errors.New("chart, output format and either repo or registry are required")
|
||||
|
||||
// isSelfContainedOCIChartRef reports whether chart is a complete "oci://host/path"
|
||||
// reference that needs no separate repo or registry to be located (e.g. Portainer
|
||||
// addon charts, which are resolved from a bare oci:// ref with no Repo/Registry).
|
||||
func isSelfContainedOCIChartRef(chart string) bool {
|
||||
return strings.HasPrefix(chart, "oci://")
|
||||
}
|
||||
|
||||
// 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 == "" && IsHTTPRepository(showOpts.Registry)) || showOpts.OutputFormat == "" {
|
||||
missingSource := showOpts.Repo == "" && IsHTTPRepository(showOpts.Registry) && !isSelfContainedOCIChartRef(showOpts.Chart)
|
||||
if showOpts.Chart == "" || missingSource || showOpts.OutputFormat == "" {
|
||||
log.Error().
|
||||
Str("context", "HelmClient").
|
||||
Str("chart", showOpts.Chart).
|
||||
|
||||
@@ -9,6 +9,12 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIsSelfContainedOCIChartRef(t *testing.T) {
|
||||
assert.True(t, isSelfContainedOCIChartRef("oci://ghcr.io/portainer/charts/portainer-run"))
|
||||
assert.False(t, isSelfContainedOCIChartRef("ingress-nginx"))
|
||||
assert.False(t, isSelfContainedOCIChartRef(""))
|
||||
}
|
||||
|
||||
func Test_Show(t *testing.T) {
|
||||
t.Parallel()
|
||||
test.EnsureIntegrationTest(t)
|
||||
@@ -54,6 +60,19 @@ func Test_Show(t *testing.T) {
|
||||
is.NotEmpty(values, "should return non-empty values")
|
||||
})
|
||||
|
||||
t.Run("show chart values for a self-contained OCI ref with no repo or registry", func(t *testing.T) {
|
||||
// Mirrors how Portainer addon charts are resolved: a bare "oci://host/path"
|
||||
// reference with no separate Repo/Registry, same as Install/Upgrade already accept.
|
||||
showOpts := options.ShowOptions{
|
||||
Chart: "oci://ghcr.io/portainer/charts/portal-template",
|
||||
OutputFormat: options.ShowValues,
|
||||
}
|
||||
values, err := hspm.Show(showOpts)
|
||||
|
||||
require.NoError(t, err, "a self-contained oci:// chart ref must not require a separate repo or registry")
|
||||
is.NotEmpty(values, "should return non-empty values")
|
||||
})
|
||||
|
||||
t.Run("show chart readme", func(t *testing.T) {
|
||||
showOpts := options.ShowOptions{
|
||||
Chart: "ingress-nginx",
|
||||
|
||||
@@ -28,7 +28,7 @@ func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOpti
|
||||
|
||||
// Initialize action configuration with kubernetes config
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(uninstallOpts.Namespace), uninstallOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
// error is already logged in initActionConfig
|
||||
return errors.Wrap(err, "failed to initialize helm configuration")
|
||||
@@ -103,7 +103,7 @@ func (hspm *HelmSDKPackageManager) ForceRemoveRelease(uninstallOpts options.Unin
|
||||
Msg("Force-removing release history (skipping resource deletion)")
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(uninstallOpts.Namespace), uninstallOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to initialize helm configuration for force-remove")
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (hspm *HelmSDKPackageManager) Upgrade(upgradeOpts options.InstallOptions) (
|
||||
|
||||
// Initialize action configuration with kubernetes config
|
||||
actionConfig := new(action.Configuration)
|
||||
err = hspm.initActionConfig(actionConfig, upgradeOpts.Namespace, upgradeOpts.KubernetesClusterAccess)
|
||||
err = hspm.initActionConfig(actionConfig, namespaceOrDefault(upgradeOpts.Namespace), upgradeOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
// error is already logged in initActionConfig
|
||||
return nil, errors.Wrap(err, "failed to initialize helm configuration for helm release upgrade")
|
||||
|
||||
@@ -84,7 +84,7 @@ func (hspm *HelmSDKPackageManager) getValues(getOpts options.GetOptions) (releas
|
||||
Msg("Getting values")
|
||||
|
||||
actionConfig := new(action.Configuration)
|
||||
err := hspm.initActionConfig(actionConfig, getOpts.Namespace, getOpts.KubernetesClusterAccess)
|
||||
err := hspm.initActionConfig(actionConfig, namespaceOrDefault(getOpts.Namespace), getOpts.KubernetesClusterAccess)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("context", "HelmClient").
|
||||
|
||||
Reference in New Issue
Block a user