fix(policy): pod security constraints - develop [R8S-808] (#1758)

Co-authored-by: Phil Calder <4473109+predlac@users.noreply.github.com>
Co-authored-by: Viktor Pettersson <viktor.pettersson@portainer.io>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: nickl-portainer <nicholas.loomans@portainer.io>
This commit is contained in:
Steven Kang
2026-02-10 08:46:02 +09:00
committed by GitHub
parent d611087513
commit a1208974ac
6 changed files with 89 additions and 4 deletions

View File

@@ -598,7 +598,7 @@ type (
// RestoreSettings contains instructions for restoring environment-level settings
RestoreSettings struct {
Manifest string `json:"manifest"` // Base64-encoded Kubernetes YAML manifest
Manifest string `json:"manifest,omitempty"` // Base64-encoded Kubernetes YAML manifest
}
// RestoreSettingsBundle maps restore type to restoration instructions

View File

@@ -1,10 +1,18 @@
package options
import "time"
// UninstallOptions are portainer supported options for `helm uninstall`
type UninstallOptions struct {
Name string
Namespace string
KubernetesClusterAccess *KubernetesClusterAccess
// Wait blocks until all resources are deleted before returning (helm uninstall --wait).
// Use when a restore will be applied immediately after uninstall so resources are gone first.
Wait bool
// Timeout is how long to wait for resources to be deleted when Wait is true (default 15m).
Timeout time.Duration
Env []string
}

View File

@@ -1,10 +1,13 @@
package sdk
import (
"time"
"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/storage/driver"
)
// Uninstall implements the HelmPackageManager interface by using the Helm SDK to uninstall a release.
@@ -34,6 +37,12 @@ func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOpti
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"
uninstallClient.Wait = uninstallOpts.Wait
if uninstallOpts.Timeout == 0 {
uninstallClient.Timeout = 15 * time.Minute
} else {
uninstallClient.Timeout = uninstallOpts.Timeout
}
// Run the uninstallation
log.Info().
@@ -63,3 +72,53 @@ func (hspm *HelmSDKPackageManager) Uninstall(uninstallOpts options.UninstallOpti
return nil
}
// ForceRemoveRelease removes all release history (Helm secrets) without attempting
// to delete Kubernetes resources. This is a last-resort recovery mechanism for when
// a standard Uninstall fails because CRDs are missing and Helm can't build kubernetes
// objects for deletion, leaving the release stuck with no way to recover.
func (hspm *HelmSDKPackageManager) ForceRemoveRelease(uninstallOpts options.UninstallOptions) error {
if uninstallOpts.Name == "" {
return errors.New("release name is required")
}
log.Warn().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Str("namespace", uninstallOpts.Namespace).
Msg("Force-removing release history (skipping resource deletion)")
actionConfig := new(action.Configuration)
err := hspm.initActionConfig(actionConfig, uninstallOpts.Namespace, uninstallOpts.KubernetesClusterAccess)
if err != nil {
return errors.Wrap(err, "failed to initialize helm configuration for force-remove")
}
// Get all release versions from Helm's storage (Kubernetes secrets)
versions, err := actionConfig.Releases.History(uninstallOpts.Name)
if err != nil {
if errors.Is(err, driver.ErrReleaseNotFound) {
log.Debug().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Msg("Release not found in storage, nothing to force-remove")
return nil
}
return errors.Wrap(err, "failed to get release history for force-remove")
}
// Delete each release version from storage
for _, v := range versions {
if _, err := actionConfig.Releases.Delete(v.Name, v.Version); err != nil {
return errors.Wrapf(err, "failed to delete release version %d for force-remove", v.Version)
}
}
log.Info().
Str("context", "HelmClient").
Str("release", uninstallOpts.Name).
Int("versions_removed", len(versions)).
Msg("Successfully force-removed all release history")
return nil
}

View File

@@ -110,6 +110,11 @@ func (hpm helmMockPackageManager) Uninstall(uninstallOpts options.UninstallOptio
return nil
}
// ForceRemoveRelease removes release history without deleting resources (not thread safe)
func (hpm helmMockPackageManager) ForceRemoveRelease(uninstallOpts options.UninstallOptions) error {
return hpm.Uninstall(uninstallOpts)
}
// List a helm chart (not thread safe)
func (hpm helmMockPackageManager) List(listOpts options.ListOptions) ([]release.ReleaseElement, error) {
return mockCharts, nil

View File

@@ -14,6 +14,10 @@ type HelmPackageManager interface {
List(listOpts options.ListOptions) ([]release.ReleaseElement, error)
Upgrade(upgradeOpts options.InstallOptions) (*release.Release, error)
Uninstall(uninstallOpts options.UninstallOptions) error
// ForceRemoveRelease removes all release history (Helm secrets) without attempting
// to delete Kubernetes resources. Use as a last resort when Uninstall fails because
// CRDs are missing and Helm can't build kubernetes objects for deletion.
ForceRemoveRelease(uninstallOpts options.UninstallOptions) error
Get(getOpts options.GetOptions) (*release.Release, error)
GetHistory(historyOpts options.HistoryOptions) ([]*release.Release, error)
Rollback(rollbackOpts options.RollbackOptions) (*release.Release, error)

View File

@@ -7,6 +7,7 @@ import (
"os"
"strings"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -160,8 +161,9 @@ func (c *Client) applyResource(ctx context.Context, dynamicClient dynamic.Interf
return "", fmt.Errorf("failed to marshal object to JSON: %w", err)
}
// Apply using Server-Side Apply
// This is more efficient and handles field ownership better than traditional apply
// Apply using Server-Side Apply (Patch). If the resource does not exist (404),
// fall back to Create so restoration can create Deployments and other resources
// that were removed (e.g. by Helm uninstall).
patchOptions := metav1.PatchOptions{
FieldManager: "portainer",
Force: boolPtr(true),
@@ -175,7 +177,14 @@ func (c *Client) applyResource(ctx context.Context, dynamicClient dynamic.Interf
patchOptions,
)
if err != nil {
return "", fmt.Errorf("failed to apply %s %s/%s: %w", gvk.Kind, namespace, name, err)
if apierrors.IsNotFound(err) {
_, createErr := resourceClient.Create(ctx, obj, metav1.CreateOptions{})
if createErr != nil {
return "", fmt.Errorf("failed to create %s %s/%s: %w", gvk.Kind, namespace, name, createErr)
}
} else {
return "", fmt.Errorf("failed to apply %s %s/%s: %w", gvk.Kind, namespace, name, err)
}
}
// Format output message