Files
capsule/pkg/template/reference.go
T

257 lines
6.0 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/runtime/gvk"
"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)
// Reference
// +kubebuilder:object:generate=true
type ResourceReference struct {
gvk.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) {
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,
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
}