mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 20:46:42 +00:00
* 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: preserve ca-bundles injected from external providers Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: security measures and prometheusrules Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
263 lines
6.1 KiB
Go
263 lines
6.1 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"
|
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
|
|
)
|
|
|
|
// Reference
|
|
// +kubebuilder:object:generate=true
|
|
type ResourceReference struct {
|
|
runtime.VersionKind `json:",inline"`
|
|
|
|
// 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 string `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(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 = FastTemplate(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,
|
|
validateNamespace NamespaceValidator,
|
|
) ([]*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
|
|
}
|
|
|
|
ns := ref.Namespace
|
|
if ns == "" && namespace != "" {
|
|
ns = namespace
|
|
}
|
|
|
|
if validateNamespace != nil && ns != "" {
|
|
if err := validateNamespace(ns); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return ref.loadResources(ctx, kubeClient, ns, additionSelectors)
|
|
}
|
|
|
|
func (t ResourceReference) IsNamespacedGVK(
|
|
restMapper k8smeta.RESTMapper,
|
|
) (bool, error) {
|
|
return IsNamespacedGVK(restMapper, t.APIVersion, t.Kind)
|
|
}
|
|
|
|
func IsNamespacedGVK(
|
|
restMapper k8smeta.RESTMapper,
|
|
apiVersion string,
|
|
kind string,
|
|
) (bool, error) {
|
|
gv, err := schema.ParseGroupVersion(apiVersion)
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid apiVersion %q: %w", apiVersion, err)
|
|
}
|
|
|
|
gvk := gv.WithKind(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)
|
|
}
|
|
|
|
return mapping.Scope.Name() == k8smeta.RESTScopeNameNamespace, nil
|
|
}
|
|
|
|
func (t ResourceReference) loadResources(
|
|
ctx context.Context,
|
|
kubeClient client.Client,
|
|
namespace string,
|
|
additionSelectors []labels.Selector,
|
|
) ([]*unstructured.Unstructured, error) {
|
|
log := log.FromContext(ctx)
|
|
|
|
log.V(5).Info("gathering resources",
|
|
"apiVersion", t.APIVersion,
|
|
"kind", t.Kind,
|
|
"name", t.Name,
|
|
"namespace", 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: namespace,
|
|
}
|
|
|
|
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 namespace != "" {
|
|
opts = append(opts, client.InNamespace(namespace))
|
|
}
|
|
|
|
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...)
|
|
|
|
selectorStrings := make([]string, 0, len(all))
|
|
|
|
for _, s := range all {
|
|
if s != nil {
|
|
selectorStrings = append(selectorStrings, s.String())
|
|
}
|
|
}
|
|
|
|
log.V(5).Info("applying combined label selector",
|
|
"namespace", namespace,
|
|
"selectors", selectorStrings,
|
|
"combinedSelector", combined.String(),
|
|
)
|
|
|
|
opts = append(opts, client.MatchingLabelsSelector{Selector: combined})
|
|
} else {
|
|
log.V(5).Info("listing without label selector",
|
|
"namespace", namespace,
|
|
)
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
log.V(5).Info("gathered resources",
|
|
"namespace", namespace,
|
|
"count", len(results),
|
|
)
|
|
|
|
return results, nil
|
|
}
|