mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
227 lines
6.8 KiB
Go
227 lines
6.8 KiB
Go
package resourcereferences
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/imdario/mergo"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
|
|
|
"github.com/formancehq/go-libs/collectionutils"
|
|
"github.com/formancehq/operator/api/formance.com/v1beta1"
|
|
"github.com/formancehq/operator/internal/core"
|
|
"github.com/pkg/errors"
|
|
v1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/selection"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
)
|
|
|
|
//+kubebuilder:rbac:groups=formance.com,resources=resourcereferences,verbs=get;list;watch;create;update;patch;delete
|
|
//+kubebuilder:rbac:groups=formance.com,resources=resourcereferences/status,verbs=get;update;patch
|
|
//+kubebuilder:rbac:groups=formance.com,resources=resourcereferences/finalizers,verbs=update
|
|
// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
|
|
|
|
func init() {
|
|
core.Init(
|
|
core.WithStackDependencyReconciler[*v1beta1.ResourceReference](Reconcile,
|
|
core.WithWatch[*v1beta1.ResourceReference, *v1.Secret](watchResource[*v1.Secret]),
|
|
core.WithWatch[*v1beta1.ResourceReference, *v1.ServiceAccount](watchResource[*v1.ServiceAccount]),
|
|
),
|
|
)
|
|
}
|
|
|
|
func watchResource[T client.Object](ctx core.Context, object T) []reconcile.Request {
|
|
ret := make([]reconcile.Request, 0)
|
|
|
|
// Watch resources created by the ResourceReference
|
|
var resourceReference string
|
|
for _, reference := range object.GetOwnerReferences() {
|
|
gvk, err := apiutil.GVKForObject(&v1beta1.ResourceReference{}, ctx.GetScheme())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
apiVersion, kind := gvk.ToAPIVersionAndKind()
|
|
if reference.Kind == kind && reference.APIVersion == apiVersion {
|
|
resourceReference = reference.Name
|
|
break
|
|
}
|
|
}
|
|
|
|
if resourceReference != "" {
|
|
return append(ret, reconcile.Request{
|
|
NamespacedName: types.NamespacedName{
|
|
Name: resourceReference,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Watch resources which should be replicated by the ResourceReferences
|
|
if object.GetLabels()[v1beta1.StackLabel] != "any" {
|
|
for _, stack := range strings.Split(object.GetLabels()[v1beta1.StackLabel], ",") {
|
|
ret = append(ret, core.BuildReconcileRequests(
|
|
ctx,
|
|
ctx.GetClient(),
|
|
ctx.GetScheme(),
|
|
&v1beta1.ResourceReference{},
|
|
client.MatchingFields{
|
|
"stack": stack,
|
|
},
|
|
)...)
|
|
}
|
|
} else {
|
|
ret = append(ret, core.BuildReconcileRequests(
|
|
ctx,
|
|
ctx.GetClient(),
|
|
ctx.GetScheme(),
|
|
&v1beta1.ResourceReference{},
|
|
)...)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
const (
|
|
AnyValue = "any"
|
|
|
|
RewrittenResourceName = "formance.com/referenced-by-name"
|
|
)
|
|
|
|
func Reconcile(ctx core.Context, stack *v1beta1.Stack, req *v1beta1.ResourceReference) error {
|
|
resource, err := findMatchingResource(ctx, stack.Name, *req.Spec.GroupVersionKind, req.Spec.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gvk := schema.GroupVersionKind{
|
|
Group: req.Spec.GroupVersionKind.Group,
|
|
Version: req.Spec.GroupVersionKind.Version,
|
|
Kind: req.Spec.GroupVersionKind.Kind,
|
|
}
|
|
|
|
if req.Status.SyncedResource != "" && req.Spec.Name != req.Status.SyncedResource {
|
|
oldResource := &unstructured.Unstructured{}
|
|
oldResource.SetGroupVersionKind(gvk)
|
|
err := ctx.GetClient().Get(ctx, types.NamespacedName{
|
|
Namespace: stack.Name,
|
|
Name: req.Status.SyncedResource,
|
|
}, oldResource)
|
|
if client.IgnoreNotFound(err) != nil {
|
|
return err
|
|
}
|
|
|
|
if err == nil { // Can be not found, if the resource has been manually deleted
|
|
patch := client.MergeFrom(oldResource.DeepCopy())
|
|
if err := controllerutil.RemoveOwnerReference(req, oldResource, ctx.GetScheme()); err == nil {
|
|
if err := ctx.GetClient().Patch(ctx, oldResource, patch); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
if len(oldResource.GetOwnerReferences()) == 0 {
|
|
if err := ctx.GetClient().Delete(ctx, oldResource); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
annotations := make(map[string]any)
|
|
originalMetadata := resource.UnstructuredContent()["metadata"]
|
|
if originalMetadata != nil {
|
|
metadata := originalMetadata.(map[string]any)
|
|
originalAnnotations := metadata["annotations"]
|
|
if originalAnnotations != nil {
|
|
annotations = originalAnnotations.(map[string]any)
|
|
}
|
|
}
|
|
|
|
unstructured.RemoveNestedField(resource.UnstructuredContent(), "metadata")
|
|
unstructured.RemoveNestedField(resource.UnstructuredContent(), "status")
|
|
|
|
newResource := &unstructured.Unstructured{}
|
|
newResource.SetGroupVersionKind(gvk)
|
|
newResource.SetNamespace(stack.Name)
|
|
newResource.SetName(req.Spec.Name)
|
|
|
|
_, err = controllerutil.CreateOrUpdate(ctx, ctx.GetClient(), newResource, func() error {
|
|
content := newResource.UnstructuredContent()
|
|
if err := mergo.MergeWithOverwrite(&content, resource.UnstructuredContent()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := unstructured.SetNestedMap(content, annotations, "metadata", "annotations"); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
hasOwnerReference, err := core.HasOwnerReference(ctx, req, newResource)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !hasOwnerReference {
|
|
if err := controllerutil.SetOwnerReference(req, newResource, ctx.GetScheme()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Status.Hash = core.HashFromResources(newResource)
|
|
req.Status.SyncedResource = req.Spec.Name
|
|
|
|
return nil
|
|
}
|
|
|
|
func findMatchingResource(ctx core.Context, stack string, gvk metav1.GroupVersionKind, name string) (*unstructured.Unstructured, error) {
|
|
requirement, err := labels.NewRequirement(v1beta1.StackLabel, selection.In, []string{stack, AnyValue})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
list := &unstructured.UnstructuredList{}
|
|
list.SetGroupVersionKind(schema.GroupVersionKind{
|
|
Group: gvk.Group,
|
|
Version: gvk.Version,
|
|
Kind: gvk.Kind,
|
|
})
|
|
if err := ctx.GetClient().List(ctx, list, &client.ListOptions{
|
|
LabelSelector: labels.NewSelector().Add(*requirement),
|
|
}); err != nil {
|
|
return nil, errors.Wrap(err, "listing resources")
|
|
}
|
|
|
|
foundResources := make([]*unstructured.Unstructured, 0)
|
|
for _, item := range list.Items {
|
|
resourceName, ok := item.GetAnnotations()[RewrittenResourceName]
|
|
if !ok {
|
|
resourceName = item.GetName()
|
|
}
|
|
|
|
if resourceName != name {
|
|
continue
|
|
}
|
|
foundResources = append(foundResources, item.DeepCopy())
|
|
}
|
|
|
|
if len(foundResources) > 1 {
|
|
return nil, fmt.Errorf("found more than one matching item for '%s': %s", name, collectionutils.Map(foundResources, func(from *unstructured.Unstructured) string {
|
|
return from.GetName()
|
|
}))
|
|
}
|
|
if len(foundResources) == 0 {
|
|
return nil, fmt.Errorf("item not found: %s", name)
|
|
}
|
|
|
|
return foundResources[0], nil
|
|
}
|