Files
open-cluster-management/pkg/work/webhook/common/validator.go
xuezhao d83c822129 Add duplicate manifest detection in ManifestWork webhook validation (#1310)
This commit adds validation to detect and reject duplicate manifests
in ManifestWork resources. A manifest is considered duplicate when
it has the same apiVersion, kind, namespace, and name as another
manifest in the same ManifestWork.

This prevents issues where duplicate manifests with different specs
can cause state inconsistency, as the Work Agent applies manifests
sequentially and later entries would overwrite earlier ones.

The validation returns a clear error message indicating the duplicate
manifest's index and the index of its first occurrence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: xuezhaojun <zxue@redhat.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 06:09:25 +00:00

105 lines
2.8 KiB
Go

package common
import (
"encoding/json"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
workv1 "open-cluster-management.io/api/work/v1"
)
type Validator struct {
limit int
}
var ManifestValidator = &Validator{limit: 500 * 1024} // the default manifest limit is 500k.
func (m *Validator) WithLimit(limit int) {
m.limit = limit
}
func (m *Validator) ValidateManifests(manifests []workv1.Manifest) error {
if len(manifests) == 0 {
return apierrors.NewBadRequest("Workload manifests should not be empty")
}
totalSize := 0
for _, manifest := range manifests {
totalSize += manifest.Size()
}
if totalSize > m.limit {
return fmt.Errorf("the size of manifests is %v bytes which exceeds the %v limit", totalSize, m.limit)
}
// Track seen manifests to detect duplicates
seen := sets.New[string]()
for i, manifest := range manifests {
err := validateManifest(manifest.Raw)
if err != nil {
return err
}
// Check for duplicate manifests
info, err := extractManifestInfo(manifest.Raw)
if err != nil {
return fmt.Errorf("failed to extract metadata from manifest at index %d: %w", i, err)
}
if seen.Has(info.key) {
return fmt.Errorf("duplicate manifest for resource %s/%s with resource type %s", info.namespace, info.name, info.gvk)
}
seen.Insert(info.key)
}
return nil
}
func validateManifest(manifest []byte) error {
// If the manifest cannot be decoded, return err
unstructuredObj := &unstructured.Unstructured{}
err := unstructuredObj.UnmarshalJSON(manifest)
if err != nil {
return err
}
// The object must have name specified, generateName is not allowed in manifestwork
if unstructuredObj.GetName() == "" {
return fmt.Errorf("name must be set in manifest")
}
if unstructuredObj.GetGenerateName() != "" {
return fmt.Errorf("generateName must not be set in manifest")
}
return nil
}
// manifestInfo contains the metadata needed for duplicate detection and error messages.
type manifestInfo struct {
key string // unique key for duplicate detection: apiVersion/kind/namespace/name
name string
namespace string
gvk string // apiVersion.kind format for error messages
}
// extractManifestInfo extracts metadata from a manifest for duplicate detection.
func extractManifestInfo(manifest []byte) (*manifestInfo, error) {
meta := &metav1.PartialObjectMetadata{}
if err := json.Unmarshal(manifest, meta); err != nil {
return nil, err
}
return &manifestInfo{
key: fmt.Sprintf("%s/%s/%s/%s", meta.APIVersion, meta.Kind, meta.Namespace, meta.Name),
name: meta.Name,
namespace: meta.Namespace,
gvk: fmt.Sprintf("%s.%s", meta.APIVersion, meta.Kind),
}, nil
}