Files
capsule/pkg/template/reference.go
Oliver Bähler a6b830b1af feat: add ruleset api(#1844)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix(config): remove usergroups default

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: conflicts

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* feat(api): add rulestatus api

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
2026-01-27 14:28:48 +01:00

223 lines
5.6 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package template
import (
"context"
"fmt"
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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/projectcapsule/capsule/pkg/api/meta"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
// Reference
// +kubebuilder:object:generate=true
type ResourceReference struct {
// Kind of the referent.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"`
// API version of the referent.
APIVersion string `json:"apiVersion" protobuf:"bytes,5,opt,name=apiVersion"`
// Name of the values referent. This is useful
// when you traying to get a specific resource
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +optional
Name string `json:"name,omitempty"`
// Namespace of the values referent.
// +optional
Namespace meta.RFC1123SubdomainName `json:"namespace,omitempty"`
// Selector which allows to get any amount of these resources based on labels
// +optional
Selector *metav1.LabelSelector `json:"selector,omitempty"`
// Only relevant if name is set. If an item is not optional, there will be an error thrown when it does not exist
// +kubebuilder:default:=true
Optional bool `json:"optional,omitempty"`
}
func (t ResourceReference) RequiresTemplating() bool {
if RequiresFastTemplate(t.Name) {
return true
}
if RequiresFastTemplate(string(t.Namespace)) {
return true
}
if SelectorRequiresTemplating(t.Selector) {
return true
}
return false
}
func (t ResourceReference) LoadTemplated(templateContext map[string]string) (ResourceReference, error) {
if !t.RequiresTemplating() || templateContext == nil {
return t, nil
}
out := t
// Name + Namespace
if out.Name != "" {
out.Name = FastTemplate(out.Name, templateContext)
}
if out.Namespace != "" {
out.Namespace = meta.RFC1123SubdomainName(
FastTemplate(string(out.Namespace), templateContext),
)
}
// Selector
if out.Selector != nil {
selCopy, err := FastTemplateLabelSelector(out.Selector, templateContext)
if err != nil {
return ResourceReference{}, err
}
out.Selector = selCopy
}
return out, nil
}
func (t ResourceReference) LoadResources(
ctx context.Context,
kubeClient client.Client,
restMapper k8smeta.RESTMapper,
namespace string,
additionSelectors []labels.Selector,
templateContext map[string]string,
allowClusterScoped bool,
) ([]*unstructured.Unstructured, error) {
isNamespaced, err := t.IsNamespacedGVK(restMapper)
if err != nil {
return nil, err
}
if !allowClusterScoped && !isNamespaced {
return nil, fmt.Errorf("cluster-scoped kind %s/%s is not allowed", t.APIVersion, t.Kind)
}
ref, err := t.LoadTemplated(templateContext)
if err != nil {
return nil, err
}
return ref.loadResources(ctx, kubeClient, restMapper, namespace, additionSelectors)
}
func (t ResourceReference) IsNamespacedGVK(
restMapper k8smeta.RESTMapper,
) (bool, error) {
gv, err := schema.ParseGroupVersion(t.APIVersion)
if err != nil {
return false, fmt.Errorf("invalid apiVersion %q: %w", t.APIVersion, err)
}
gvk := gv.WithKind(t.Kind)
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return false, fmt.Errorf("failed to resolve GVK %s: %w", gvk.String(), err)
}
isNamespaced := mapping.Scope.Name() == k8smeta.RESTScopeNameNamespace
return isNamespaced, nil
}
func (t ResourceReference) loadResources(
ctx context.Context,
kubeClient client.Client,
restMapper k8smeta.RESTMapper,
namespace string,
additionSelectors []labels.Selector,
) ([]*unstructured.Unstructured, error) {
ns := t.Namespace
if namespace != "" {
ns = meta.RFC1123SubdomainName(namespace)
}
// GET path (single object)
if t.Name != "" {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion(t.APIVersion)
obj.SetKind(t.Kind)
key := client.ObjectKey{
Name: t.Name,
Namespace: string(ns),
}
if err := kubeClient.Get(ctx, key, obj); err != nil {
if apierrors.IsNotFound(err) && t.Optional {
return nil, nil
}
return nil, fmt.Errorf("failed to get %s/%s: %w", t.Kind, t.Name, err)
}
return []*unstructured.Unstructured{obj}, nil
}
list := &unstructured.UnstructuredList{}
list.SetAPIVersion(t.APIVersion)
list.SetKind(t.Kind + "List")
var opts []client.ListOption
if ns != "" {
opts = append(opts, client.InNamespace(string(ns)))
}
// Convert t.Selector (metav1) to labels.Selector if present
var tenantSel labels.Selector
if t.Selector != nil {
s, err := metav1.LabelSelectorAsSelector(t.Selector)
if err != nil {
return nil, fmt.Errorf("invalid label selector: %w", err)
}
tenantSel = s
}
all := make([]labels.Selector, 0, len(additionSelectors)+1)
for _, s := range additionSelectors {
if s != nil {
all = append(all, s)
}
}
if tenantSel != nil {
all = append(all, tenantSel)
}
if len(all) > 0 {
combined := selectors.CombineSelectors(all...)
opts = append(opts, client.MatchingLabelsSelector{Selector: combined})
}
if err := kubeClient.List(ctx, list, opts...); err != nil {
return nil, fmt.Errorf("failed to list %s: %w", t.Kind, err)
}
results := make([]*unstructured.Unstructured, 0, len(list.Items))
for i := range list.Items {
results = append(results, list.Items[i].DeepCopy())
}
return results, nil
}