mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
* feat: implement namespace metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add ingress enforcment Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
383 lines
10 KiB
Go
383 lines
10 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package rulestatus
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/dynamic"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/pkg/api/rules"
|
|
apiruntime "github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
)
|
|
|
|
const managedMetadataListPageSize int64 = 500
|
|
|
|
type managedMetadataTarget struct {
|
|
gvr schema.GroupVersionResource
|
|
gvk schema.GroupVersionKind
|
|
}
|
|
|
|
func (r Manager) reconcileManagedMetadata(ctx context.Context, instance *capsulev1beta2.RuleStatus, previous, current []*rules.NamespaceRuleBodyNamespace) error {
|
|
if r.RESTConfig == nil {
|
|
return fmt.Errorf("REST config is required for managed metadata reconciliation")
|
|
}
|
|
|
|
dynamicClient, err := dynamic.NewForConfig(r.RESTConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manager := ruleStatusFieldManager(instance)
|
|
|
|
namespaceGVK := schema.GroupVersionKind{Version: "v1", Kind: "Namespace"}
|
|
previousLabels, previousAnnotations := managedMetadataForGVK(namespaceGVK, previous)
|
|
|
|
labels, annotations := managedMetadataForGVK(namespaceGVK, current)
|
|
|
|
if hasMetadata(previousLabels, previousAnnotations) || hasMetadata(labels, annotations) {
|
|
if err := reconcileObjectManagedMetadata(ctx, dynamicClient, schema.GroupVersionResource{Version: "v1", Resource: "namespaces"}, namespaceGVK, "", instance.GetNamespace(), previousLabels, previousAnnotations, labels, annotations, manager); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
targets, err := managedMetadataTargets(r.RESTMapper, previous, current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, target := range targets {
|
|
previousLabels, previousAnnotations := managedMetadataForGVK(target.gvk, previous)
|
|
labels, annotations := managedMetadataForGVK(target.gvk, current)
|
|
|
|
if err := reconcileManagedMetadataTarget(
|
|
ctx,
|
|
dynamicClient,
|
|
target,
|
|
instance.GetNamespace(),
|
|
previousLabels,
|
|
previousAnnotations,
|
|
labels,
|
|
annotations,
|
|
manager,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func reconcileManagedMetadataTarget(
|
|
ctx context.Context,
|
|
dynamicClient dynamic.Interface,
|
|
target managedMetadataTarget,
|
|
namespace string,
|
|
previousLabels, previousAnnotations, labels, annotations map[string]string,
|
|
manager string,
|
|
) error {
|
|
continueToken := ""
|
|
|
|
for {
|
|
items, err := dynamicClient.Resource(target.gvr).Namespace(namespace).List(ctx, metav1.ListOptions{
|
|
Limit: managedMetadataListPageSize,
|
|
Continue: continueToken,
|
|
})
|
|
if err != nil {
|
|
if isManagedMetadataObjectGone(err) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("list %s in namespace %q: %w", target.gvr.String(), namespace, err)
|
|
}
|
|
|
|
for i := range items.Items {
|
|
if err := reconcileObjectManagedMetadata(
|
|
ctx,
|
|
dynamicClient,
|
|
target.gvr,
|
|
target.gvk,
|
|
namespace,
|
|
items.Items[i].GetName(),
|
|
previousLabels,
|
|
previousAnnotations,
|
|
labels,
|
|
annotations,
|
|
manager,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
continueToken = items.GetContinue()
|
|
if continueToken == "" {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func managedMetadataTargets(
|
|
mapper k8smeta.RESTMapper,
|
|
ruleSets ...[]*rules.NamespaceRuleBodyNamespace,
|
|
) ([]managedMetadataTarget, error) {
|
|
targets := make(map[schema.GroupVersionResource]managedMetadataTarget)
|
|
|
|
for _, bodies := range ruleSets {
|
|
for _, body := range bodies {
|
|
if body == nil || body.Enforce == nil {
|
|
continue
|
|
}
|
|
|
|
for _, rule := range body.Enforce.Metadata {
|
|
if !metadataRuleHasManagedValues(rule) {
|
|
continue
|
|
}
|
|
|
|
if rule.HasWildcard() {
|
|
return nil, fmt.Errorf("managed metadata requires concrete apiGroups and kinds")
|
|
}
|
|
|
|
for _, kind := range rule.Kinds {
|
|
kind = strings.TrimSpace(kind)
|
|
for _, apiGroup := range rule.StatusAPIGroups() {
|
|
mapping, err := managedMetadataRESTMapping(mapper, apiGroup, kind)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve managed metadata target %q/%q: %w", apiGroup, kind, err)
|
|
}
|
|
|
|
if mapping.GroupVersionKind.Group == "" &&
|
|
mapping.GroupVersionKind.Version == apiruntime.CoreAPIVersion &&
|
|
mapping.GroupVersionKind.Kind == "Namespace" {
|
|
continue
|
|
}
|
|
|
|
if mapping.Scope.Name() != k8smeta.RESTScopeNameNamespace {
|
|
return nil, fmt.Errorf("managed metadata target %s is not namespaced", mapping.GroupVersionKind.String())
|
|
}
|
|
|
|
targets[mapping.Resource] = managedMetadataTarget{
|
|
gvr: mapping.Resource,
|
|
gvk: mapping.GroupVersionKind,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out := make([]managedMetadataTarget, 0, len(targets))
|
|
for _, target := range targets {
|
|
out = append(out, target)
|
|
}
|
|
|
|
sort.Slice(out, func(i, j int) bool {
|
|
return out[i].gvr.String() < out[j].gvr.String()
|
|
})
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func managedMetadataRESTMapping(
|
|
mapper k8smeta.RESTMapper,
|
|
apiGroup string,
|
|
kind string,
|
|
) (*k8smeta.RESTMapping, error) {
|
|
if mapper == nil {
|
|
return nil, fmt.Errorf("REST mapper is required for managed metadata reconciliation")
|
|
}
|
|
|
|
apiGroup = strings.TrimSpace(apiGroup)
|
|
if apiGroup == "" || apiGroup == apiruntime.CoreAPIVersion {
|
|
return mapper.RESTMapping(schema.GroupKind{Kind: kind}, apiruntime.CoreAPIVersion)
|
|
}
|
|
|
|
if gv, err := schema.ParseGroupVersion(apiGroup); err == nil && strings.Contains(apiGroup, "/") {
|
|
return mapper.RESTMapping(schema.GroupKind{Group: gv.Group, Kind: kind}, gv.Version)
|
|
}
|
|
|
|
return mapper.RESTMapping(schema.GroupKind{Group: apiGroup, Kind: kind})
|
|
}
|
|
|
|
func reconcileObjectManagedMetadata(ctx context.Context, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, namespace, name string, previousLabels, previousAnnotations, labels, annotations map[string]string, manager string) error {
|
|
removedLabels := removedMetadataKeys(previousLabels, labels)
|
|
|
|
removedAnnotations := removedMetadataKeys(previousAnnotations, annotations)
|
|
|
|
if hasRemovedMetadata(removedLabels, removedAnnotations) {
|
|
if err := removeManagedMetadata(ctx, dynamicClient, gvr, namespace, name, removedLabels, removedAnnotations); err != nil {
|
|
if isManagedMetadataObjectGone(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
return applyManagedMetadata(ctx, dynamicClient, gvr, gvk, namespace, name, labels, annotations, manager)
|
|
}
|
|
|
|
func removedMetadataKeys(previous, current map[string]string) map[string]any {
|
|
removed := map[string]any{}
|
|
|
|
for key := range previous {
|
|
if _, ok := current[key]; !ok {
|
|
removed[key] = nil
|
|
}
|
|
}
|
|
|
|
return removed
|
|
}
|
|
|
|
func hasRemovedMetadata(labels, annotations map[string]any) bool {
|
|
return len(labels) > 0 || len(annotations) > 0
|
|
}
|
|
|
|
func removeManagedMetadata(ctx context.Context, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, namespace, name string, labels, annotations map[string]any) error {
|
|
metadata := map[string]any{}
|
|
if len(labels) > 0 {
|
|
metadata["labels"] = labels
|
|
}
|
|
|
|
if len(annotations) > 0 {
|
|
metadata["annotations"] = annotations
|
|
}
|
|
|
|
raw, err := json.Marshal(map[string]any{"metadata": metadata})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var resource dynamic.ResourceInterface = dynamicClient.Resource(gvr)
|
|
if namespace != "" {
|
|
resource = dynamicClient.Resource(gvr).Namespace(namespace)
|
|
}
|
|
|
|
_, err = resource.Patch(ctx, name, types.MergePatchType, raw, metav1.PatchOptions{})
|
|
|
|
return err
|
|
}
|
|
|
|
func hasMetadata(labels, annotations map[string]string) bool {
|
|
return len(labels) > 0 || len(annotations) > 0
|
|
}
|
|
|
|
func metadataRuleHasManagedValues(rule rules.MetadataRule) bool {
|
|
for _, policy := range rule.Labels {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for _, policy := range rule.Annotations {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func managedMetadataForGVK(gvk schema.GroupVersionKind, bodies []*rules.NamespaceRuleBodyNamespace) (map[string]string, map[string]string) {
|
|
labels, annotations := map[string]string{}, map[string]string{}
|
|
|
|
for _, body := range bodies {
|
|
if body == nil || body.Enforce == nil {
|
|
continue
|
|
}
|
|
|
|
for _, rule := range body.Enforce.Metadata {
|
|
if !rule.MatchesGroupVersionKind(gvk) {
|
|
continue
|
|
}
|
|
|
|
for key, policy := range rule.Labels {
|
|
if policy.Managed != nil {
|
|
labels[key] = *policy.Managed
|
|
}
|
|
}
|
|
|
|
for key, policy := range rule.Annotations {
|
|
if policy.Managed != nil {
|
|
annotations[key] = *policy.Managed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return labels, annotations
|
|
}
|
|
|
|
func hasManagedMetadata(bodies []*rules.NamespaceRuleBodyNamespace) bool {
|
|
for _, body := range bodies {
|
|
if body == nil || body.Enforce == nil {
|
|
continue
|
|
}
|
|
|
|
for _, rule := range body.Enforce.Metadata {
|
|
for _, policy := range rule.Labels {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for _, policy := range rule.Annotations {
|
|
if policy.Managed != nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func applyManagedMetadata(ctx context.Context, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, gvk schema.GroupVersionKind, namespace, name string, labels, annotations map[string]string, manager string) error {
|
|
metadata := map[string]any{"name": name, "labels": labels, "annotations": annotations}
|
|
|
|
var resource dynamic.ResourceInterface = dynamicClient.Resource(gvr)
|
|
|
|
if namespace != "" {
|
|
metadata["namespace"] = namespace
|
|
resource = dynamicClient.Resource(gvr).Namespace(namespace)
|
|
}
|
|
|
|
payload := map[string]any{"apiVersion": gvk.GroupVersion().String(), "kind": gvk.Kind, "metadata": metadata}
|
|
|
|
raw, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
force := true
|
|
|
|
_, err = resource.Patch(ctx, name, types.ApplyPatchType, raw, metav1.PatchOptions{FieldManager: manager, Force: &force})
|
|
if isManagedMetadataObjectGone(err) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func isManagedMetadataObjectGone(err error) bool {
|
|
return apierrors.IsNotFound(err)
|
|
}
|
|
|
|
func ruleStatusFieldManager(instance *capsulev1beta2.RuleStatus) string {
|
|
sum := sha256.Sum256([]byte(instance.GetNamespace() + "/" + instance.GetName() + "/" + string(instance.GetUID())))
|
|
|
|
return "projectcapsule.dev/rulestatus-" + hex.EncodeToString(sum[:8])
|
|
}
|