mirror of
https://github.com/portainer/portainer.git
synced 2026-06-23 04:10:29 +00:00
chore(policies): use generic policy reconcile system so more than helm can be used [c9s-88] (#2613)
This commit is contained in:
+50
-8
@@ -560,8 +560,9 @@ type (
|
||||
}
|
||||
|
||||
PolicyChartSummary struct {
|
||||
ChartName string `json:"ChartName"`
|
||||
Fingerprint string `json:"Fingerprint"`
|
||||
ChartName string `json:"ChartName"`
|
||||
Fingerprint string `json:"Fingerprint"`
|
||||
PolicyID PolicyID `json:"PolicyID,omitempty"` // 0 when server hasn't populated the field
|
||||
}
|
||||
|
||||
PolicyChartStatus struct {
|
||||
@@ -583,17 +584,19 @@ type (
|
||||
}
|
||||
|
||||
PolicyChartBundle struct {
|
||||
PolicyChartSummary `mapstructure:",squash"`
|
||||
EncodedTgz string `json:"EncodedTgz"`
|
||||
Namespace string `json:"Namespace"`
|
||||
ReleaseName string `json:"ReleaseName,omitempty"`
|
||||
PolicyChartSummary `mapstructure:",squash"`
|
||||
EncodedTgz string `json:"EncodedTgz"`
|
||||
Namespace string `json:"Namespace"`
|
||||
ReleaseName string `json:"ReleaseName,omitempty"`
|
||||
// Base64 YAML kubectl-applied by the agent before Helm install when set (e.g. Gatekeeper gatekeeper-system namespace + PSA labels).
|
||||
PreReleaseManifest string `json:"PreReleaseManifest,omitempty"`
|
||||
EncodedValues string `json:"EncodedValues"`
|
||||
PreInstallDeletions []ResourceDeletion `json:"PreInstallDeletions,omitempty"`
|
||||
PreInstallAdoptions []ResourceAdoption `json:"PreInstallAdoptions,omitempty"`
|
||||
// WaitForCRDs lists CRD names that must be registered in API discovery after
|
||||
// this chart installs before the agent proceeds to the next chart.
|
||||
WaitForCRDs []string `json:"WaitForCRDs,omitempty"`
|
||||
// NoWait disables waiting for pods to be ready after install.
|
||||
// Set to true for externally sourced charts whose startup timing cannot be controlled,
|
||||
// to avoid leaving the release stuck in pending-install.
|
||||
NoWait bool `json:"NoWait,omitempty"`
|
||||
}
|
||||
|
||||
@@ -623,6 +626,45 @@ type (
|
||||
|
||||
PolicyID int
|
||||
|
||||
// PolicyDesiredState is the per-policy desired state sent from server to agent
|
||||
// in PollStatusResponse.PolicyStates (per-policy payload format).
|
||||
PolicyDesiredState struct {
|
||||
PolicyID PolicyID `json:"policyID"`
|
||||
Type string `json:"type"` // e.g. "helm-k8s"
|
||||
Fingerprint string `json:"fingerprint"` // install-affecting only; restore manifest excluded
|
||||
Config []byte `json:"config"` // handler-specific config blob (e.g. HelmPolicyConfig JSON)
|
||||
}
|
||||
|
||||
// PolicyStatesAsyncPayload is the value of an async "policyStates" command.
|
||||
// States carries per-policy desired state; ChartBundles carries helm chart tarballs
|
||||
// for policies that need installing (emitted only on mutation, never idle polls).
|
||||
PolicyStatesAsyncPayload struct {
|
||||
States []PolicyDesiredState `json:"states"`
|
||||
ChartBundles []PolicyChartBundle `json:"chartBundles,omitempty"`
|
||||
RestoreBundle RestoreSettingsBundle `json:"restoreBundle,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyActualState is the per-policy actual state reported by the agent
|
||||
// via PUT /endpoints/{id}/edge/policies/statuses.
|
||||
PolicyActualState struct {
|
||||
PolicyID PolicyID `json:"policyID"`
|
||||
Type string `json:"type"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Status string `json:"status"` // applying|applied|failed|removing
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// HelmPolicyConfig is the Config payload for "helm-k8s" PolicyDesiredState entries.
|
||||
// Bundles are not included in the poll response Config — they travel separately
|
||||
// (sync: on-demand GetCharts; async: PolicyStatesCommandPayload.ChartBundles).
|
||||
// RestoreSettings is helm-internal metadata and is intentionally NOT part of the
|
||||
// fingerprint — see Fingerprint contract in reconcile-refactor-plan.md.
|
||||
HelmPolicyConfig struct {
|
||||
Charts []PolicyChartSummary `json:"charts"`
|
||||
Bundles []PolicyChartBundle `json:"bundles,omitempty"`
|
||||
RestoreSettings *RestoreSettings `json:"restoreSettings,omitempty"`
|
||||
}
|
||||
|
||||
// PolicyType represents the type of policy
|
||||
PolicyType string
|
||||
)
|
||||
|
||||
@@ -5,81 +5,83 @@ import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { Icon, IconProps } from '@@/Icon';
|
||||
|
||||
// the classes are typed in full because tailwind doesn't render the interpolated classes
|
||||
const badge = cva('inline-flex w-fit items-center gap-1 font-medium', {
|
||||
variants: {
|
||||
type: {
|
||||
success:
|
||||
'bg-success-2 text-success-9 th-dark:bg-success-10 th-dark:text-success-3 th-highcontrast:bg-success-10 th-highcontrast:text-success-3',
|
||||
warn: 'bg-warning-2 text-warning-9 th-dark:bg-warning-10 th-dark:text-warning-3 th-highcontrast:bg-warning-10 th-highcontrast:text-warning-3',
|
||||
danger:
|
||||
'bg-error-2 text-error-9 th-dark:bg-error-10 th-dark:text-error-3 th-highcontrast:bg-error-10 th-highcontrast:text-error-3',
|
||||
info: 'bg-blue-2 text-blue-9 th-dark:bg-blue-10 th-dark:text-blue-3 th-highcontrast:bg-blue-10 th-highcontrast:text-blue-3',
|
||||
// the secondary classes are a bit darker in light mode and a bit lighter in dark mode
|
||||
successSecondary:
|
||||
'bg-success-3 text-success-9 th-dark:bg-success-9 th-dark:text-success-3 th-highcontrast:bg-success-9 th-highcontrast:text-success-3',
|
||||
warnSecondary:
|
||||
'bg-warning-3 text-warning-9 th-dark:bg-warning-9 th-dark:text-warning-3 th-highcontrast:bg-warning-9 th-highcontrast:text-warning-3',
|
||||
dangerSecondary:
|
||||
'bg-error-3 text-error-9 th-dark:bg-error-9 th-dark:text-error-3 th-highcontrast:bg-error-9 th-highcontrast:text-error-3',
|
||||
infoSecondary:
|
||||
'bg-blue-3 text-blue-9 th-dark:bg-blue-9 th-dark:text-blue-3 th-highcontrast:bg-blue-9 th-highcontrast:text-blue-3',
|
||||
muted:
|
||||
'bg-gray-3 text-gray-9 th-dark:bg-gray-9 th-dark:text-gray-3 th-highcontrast:bg-gray-9 th-highcontrast:text-gray-3',
|
||||
accent:
|
||||
'bg-indigo-2 text-indigo-9 th-dark:bg-indigo-10 th-dark:text-indigo-3 th-highcontrast:bg-indigo-10 th-highcontrast:text-indigo-3',
|
||||
custom: '',
|
||||
const badge = cva(
|
||||
'inline-flex w-fit items-center gap-1 font-medium [&>a]:hover:text-inherit [&>a]:text-inherit',
|
||||
{
|
||||
variants: {
|
||||
type: {
|
||||
success:
|
||||
'bg-success-2 text-success-9 th-dark:bg-success-10 th-dark:text-success-3 th-highcontrast:bg-success-10 th-highcontrast:text-success-3',
|
||||
warn: 'bg-warning-2 text-warning-9 th-dark:bg-warning-10 th-dark:text-warning-3 th-highcontrast:bg-warning-10 th-highcontrast:text-warning-3',
|
||||
danger:
|
||||
'bg-error-2 text-error-9 th-dark:bg-error-10 th-dark:text-error-3 th-highcontrast:bg-error-10 th-highcontrast:text-error-3',
|
||||
info: 'bg-blue-2 text-blue-9 th-dark:bg-blue-10 th-dark:text-blue-3 th-highcontrast:bg-blue-10 th-highcontrast:text-blue-3',
|
||||
// the secondary classes are a bit darker in light mode and a bit lighter in dark mode
|
||||
successSecondary:
|
||||
'bg-success-3 text-success-9 th-dark:bg-success-9 th-dark:text-success-3 th-highcontrast:bg-success-9 th-highcontrast:text-success-3',
|
||||
warnSecondary:
|
||||
'bg-warning-3 text-warning-9 th-dark:bg-warning-9 th-dark:text-warning-3 th-highcontrast:bg-warning-9 th-highcontrast:text-warning-3',
|
||||
dangerSecondary:
|
||||
'bg-error-3 text-error-9 th-dark:bg-error-9 th-dark:text-error-3 th-highcontrast:bg-error-9 th-highcontrast:text-error-3',
|
||||
infoSecondary:
|
||||
'bg-blue-3 text-blue-9 th-dark:bg-blue-9 th-dark:text-blue-3 th-highcontrast:bg-blue-9 th-highcontrast:text-blue-3',
|
||||
muted:
|
||||
'bg-gray-3 text-gray-9 th-dark:bg-gray-9 th-dark:text-gray-3 th-highcontrast:bg-gray-9 th-highcontrast:text-gray-3',
|
||||
accent:
|
||||
'bg-indigo-2 text-indigo-9 th-dark:bg-indigo-10 th-dark:text-indigo-3 th-highcontrast:bg-indigo-10 th-highcontrast:text-indigo-3',
|
||||
custom: '',
|
||||
},
|
||||
shape: {
|
||||
pill: 'rounded-full',
|
||||
rect: 'rounded',
|
||||
},
|
||||
size: {
|
||||
sm: 'px-1.5 py-px text-[10px]',
|
||||
md: '!text-xs px-2 py-0.5',
|
||||
},
|
||||
bordered: {
|
||||
true: '!border border-solid th-highcontrast:border-white',
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
pill: 'rounded-full',
|
||||
rect: 'rounded',
|
||||
compoundVariants: [
|
||||
{
|
||||
type: ['success', 'successSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-success-4 th-dark:border-success-8',
|
||||
},
|
||||
{
|
||||
type: ['warn', 'warnSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-warning-4 th-dark:border-warning-8',
|
||||
},
|
||||
{
|
||||
type: ['danger', 'dangerSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-error-4 th-dark:border-error-8',
|
||||
},
|
||||
{
|
||||
type: ['info', 'infoSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-blue-4 th-dark:border-blue-8',
|
||||
},
|
||||
{
|
||||
type: 'muted',
|
||||
bordered: true,
|
||||
className: 'border-gray-4 th-dark:border-gray-8',
|
||||
},
|
||||
{
|
||||
type: 'accent',
|
||||
bordered: true,
|
||||
className: 'border-indigo-4 th-dark:border-indigo-8',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
type: 'info',
|
||||
shape: 'pill',
|
||||
size: 'md',
|
||||
},
|
||||
size: {
|
||||
sm: 'px-1.5 py-px text-[10px]',
|
||||
md: '!text-xs px-2 py-0.5',
|
||||
},
|
||||
bordered: {
|
||||
true: '!border border-solid th-highcontrast:border-white',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
type: ['success', 'successSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-success-4 th-dark:border-success-8',
|
||||
},
|
||||
{
|
||||
type: ['warn', 'warnSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-warning-4 th-dark:border-warning-8',
|
||||
},
|
||||
{
|
||||
type: ['danger', 'dangerSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-error-4 th-dark:border-error-8',
|
||||
},
|
||||
{
|
||||
type: ['info', 'infoSecondary'],
|
||||
bordered: true,
|
||||
className: 'border-blue-4 th-dark:border-blue-8',
|
||||
},
|
||||
{
|
||||
type: 'muted',
|
||||
bordered: true,
|
||||
className: 'border-gray-4 th-dark:border-gray-8',
|
||||
},
|
||||
{
|
||||
type: 'accent',
|
||||
bordered: true,
|
||||
className: 'border-indigo-4 th-dark:border-indigo-8',
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
type: 'info',
|
||||
shape: 'pill',
|
||||
size: 'md',
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export type BadgeType = NonNullable<
|
||||
Exclude<VariantProps<typeof badge>['type'], 'custom'>
|
||||
|
||||
@@ -20,6 +20,9 @@ interface Props extends AutomationTestingProps {
|
||||
label: string;
|
||||
isSubMenu?: boolean;
|
||||
ignorePaths?: string[];
|
||||
/** When a create or detail path id differs from the list path, includePaths can be used to specify which paths should also mark the item as active.
|
||||
*
|
||||
* E.g. including the portainer.wizard.endpoints (create environment) path to activate the portainer.endpoints sidebar item. */
|
||||
includePaths?: string[];
|
||||
count?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package libpolicy
|
||||
|
||||
import (
|
||||
"hash/fnv"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// ConfigFingerprint is the generic primitive: FNV-1a over serialized policy
|
||||
// config bytes. It is a change detector, not a security boundary; collisions
|
||||
// are acceptable because a later poll or policy edit will retry reconciliation.
|
||||
// Callers must ensure the bytes are deterministic (e.g. collections sorted
|
||||
// before marshaling).
|
||||
//
|
||||
// New policy types should call this after marshaling their own config struct.
|
||||
func ConfigFingerprint(config []byte) string {
|
||||
h := fnv.New32a()
|
||||
h.Write(config)
|
||||
return strconv.FormatUint(uint64(h.Sum32()), 16)
|
||||
}
|
||||
|
||||
// HelmPolicyFingerprint computes a fingerprint for a Helm policy from its chart
|
||||
// summaries. Charts are sorted by name before hashing so the result is
|
||||
// order-independent. The restore manifest is excluded because it does not
|
||||
// affect what gets installed.
|
||||
//
|
||||
// Both server-ee and the agent import this function to guarantee they compute
|
||||
// identical fingerprints for the same chart summaries.
|
||||
func HelmPolicyFingerprint(chartSummaries []portainer.PolicyChartSummary) string {
|
||||
sorted := make([]portainer.PolicyChartSummary, len(chartSummaries))
|
||||
copy(sorted, chartSummaries)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i].ChartName < sorted[j].ChartName })
|
||||
b, err := json.Marshal(portainer.HelmPolicyConfig{Charts: sorted})
|
||||
if err != nil {
|
||||
// json.Marshal of a known struct with no custom marshalers cannot error.
|
||||
return ""
|
||||
}
|
||||
return ConfigFingerprint(b)
|
||||
}
|
||||
Reference in New Issue
Block a user