|
|
@@ -29,7 +29,9 @@ import (
|
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
|
|
+ "sigs.k8s.io/controller-runtime/pkg/handler"
|
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
|
|
"github.com/LocoStack/loco-operator/api/v1alpha1"
|
|
|
"github.com/LocoStack/loco-operator/internal/reconciler"
|
|
|
@@ -45,6 +47,7 @@ type ComponentReconciler struct {
|
|
|
// +kubebuilder:rbac:groups=locostack.com,resources=components,verbs=get;list;watch;create;update;patch;delete
|
|
|
// +kubebuilder:rbac:groups=locostack.com,resources=components/status,verbs=get;update;patch
|
|
|
// +kubebuilder:rbac:groups=locostack.com,resources=components/finalizers,verbs=update
|
|
|
+// +kubebuilder:rbac:groups=locostack.com,resources=*,verbs=get;list;watch
|
|
|
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch
|
|
|
// +kubebuilder:rbac:groups="",resources=persistentvolumes,verbs=get;list;watch;create;update;patch
|
|
|
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch
|
|
|
@@ -99,31 +102,35 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
|
|
|
})
|
|
|
log.Error(err, "Failed to reconcile Component", "namespace", req.NamespacedName.Namespace, "name", req.NamespacedName.Name)
|
|
|
_ = r.Status().Patch(ctx, comp, patch)
|
|
|
- return ctrl.Result{}, nil
|
|
|
+ return ctrl.Result{}, err
|
|
|
}
|
|
|
|
|
|
comp.Status.Endpoint = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", componentReconciler.ResourceName(""), comp.Namespace, tmpl.Spec.Runtime.Port)
|
|
|
comp.Status.ObservedGeneration = comp.Generation
|
|
|
- if len(dependencies) > 0 {
|
|
|
- comp.Status.Dependencies = make(map[string]map[string]int64)
|
|
|
- for _, dep := range dependencies {
|
|
|
- gvk, err := apiutil.GVKForObject(dep, r.Scheme)
|
|
|
- if err != nil {
|
|
|
- log.Error(err, "Failed to get GVK for dependency", "dependency", dep)
|
|
|
- continue
|
|
|
- }
|
|
|
- if gvk.Group != v1alpha1.GroupVersion.Group {
|
|
|
- continue
|
|
|
- }
|
|
|
- kind := gvk.Kind
|
|
|
- name := dep.GetName()
|
|
|
- generation := dep.GetGeneration()
|
|
|
+ // rebuilt from scratch on every reconcile, including when there are no dependencies
|
|
|
+ // left: dependencies that are gone have to drop out of the status too, since this is
|
|
|
+ // what tells them whether the Component is still serving their current generation
|
|
|
+ comp.Status.Dependencies = nil
|
|
|
+ for _, dep := range dependencies {
|
|
|
+ gvk, err := apiutil.GVKForObject(dep, r.Scheme)
|
|
|
+ if err != nil {
|
|
|
+ log.Error(err, "Failed to get GVK for dependency", "dependency", dep)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if gvk.Group != v1alpha1.GroupVersion.Group {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ kind := gvk.Kind
|
|
|
+ name := dep.GetName()
|
|
|
+ generation := dep.GetGeneration()
|
|
|
|
|
|
- if _, exists := comp.Status.Dependencies[kind]; !exists {
|
|
|
- comp.Status.Dependencies[kind] = make(map[string]int64)
|
|
|
- }
|
|
|
- comp.Status.Dependencies[kind][name] = generation
|
|
|
+ if comp.Status.Dependencies == nil {
|
|
|
+ comp.Status.Dependencies = make(map[string]map[string]int64)
|
|
|
+ }
|
|
|
+ if _, exists := comp.Status.Dependencies[kind]; !exists {
|
|
|
+ comp.Status.Dependencies[kind] = make(map[string]int64)
|
|
|
}
|
|
|
+ comp.Status.Dependencies[kind][name] = generation
|
|
|
}
|
|
|
apimeta.SetStatusCondition(&comp.Status.Conditions, metav1.Condition{
|
|
|
Type: "Reconciled",
|
|
|
@@ -184,13 +191,69 @@ func (r *ComponentReconciler) getComponentReconciler(comp *v1alpha1.Component, s
|
|
|
|
|
|
// SetupWithManager sets up the controller with the Manager.
|
|
|
func (r *ComponentReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|
|
- return ctrl.NewControllerManagedBy(mgr).
|
|
|
+ c := mgr.GetClient()
|
|
|
+ builder := ctrl.NewControllerManagedBy(mgr).
|
|
|
For(&v1alpha1.Component{}).
|
|
|
Owns(&corev1.ConfigMap{}).
|
|
|
Owns(&corev1.Secret{}).
|
|
|
Owns(&corev1.PersistentVolumeClaim{}).
|
|
|
Owns(&corev1.Service{}).
|
|
|
Owns(&appsv1.Deployment{}).
|
|
|
+ // a Component renders its config from the objects it depends on, so every one of
|
|
|
+ // them has to be able to trigger a re-render - and with it a rollout
|
|
|
+ Watches(&v1alpha1.Component{}, handler.EnqueueRequestsFromMapFunc(componentDependencyMapFunc(c))).
|
|
|
+ Watches(&corev1.ConfigMap{}, SkillConfigMapEventHandler(c)).
|
|
|
+ Watches(&corev1.Secret{}, AuthSecretEventHandler(c))
|
|
|
+ for _, dep := range componentDependencyKinds {
|
|
|
+ builder = builder.Watches(dep.object, handler.EnqueueRequestsFromMapFunc(dependencyMapFunc(c, dep.kind)))
|
|
|
+ }
|
|
|
+ return builder.
|
|
|
Named("component").
|
|
|
Complete(r)
|
|
|
}
|
|
|
+
|
|
|
+var componentDependencyKinds = []struct {
|
|
|
+ kind string
|
|
|
+ object client.Object
|
|
|
+}{
|
|
|
+ {"ExternalModel", &v1alpha1.ExternalModel{}},
|
|
|
+ {"ManagedModel", &v1alpha1.ManagedModel{}},
|
|
|
+ {"ExternalTool", &v1alpha1.ExternalTool{}},
|
|
|
+ {"ManagedTool", &v1alpha1.ManagedTool{}},
|
|
|
+ {"Skill", &v1alpha1.Skill{}},
|
|
|
+ {"ExternalKnowledgeBase", &v1alpha1.ExternalKnowledgeBase{}},
|
|
|
+ {"ManagedKnowledgeBase", &v1alpha1.ManagedKnowledgeBase{}},
|
|
|
+ {"Agent", &v1alpha1.Agent{}},
|
|
|
+}
|
|
|
+
|
|
|
+func componentDependencyMapFunc(c client.Client) handler.MapFunc {
|
|
|
+ return func(ctx context.Context, obj client.Object) []reconcile.Request {
|
|
|
+ comp, ok := obj.(*v1alpha1.Component)
|
|
|
+ if !ok {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ stackName := ""
|
|
|
+ if comp.Spec.StackRef != nil {
|
|
|
+ stackName = comp.Spec.StackRef.Name
|
|
|
+ }
|
|
|
+ stackWide := stackName != "" && stackComponentCategories[comp.Spec.Category]
|
|
|
+ return dependentComponentRequests(ctx, c, comp.GetNamespace(), func(other *v1alpha1.Component) bool {
|
|
|
+ if other.GetName() == comp.GetName() {
|
|
|
+ // the Component's own events already reach the controller through For()
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if componentDependsOn(other, "Component", comp.GetName(), comp.GetNamespace()) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return stackWide && other.Spec.StackRef != nil && other.Spec.StackRef.Name == stackName
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func dependencyMapFunc(c client.Client, kind string) handler.MapFunc {
|
|
|
+ return func(ctx context.Context, obj client.Object) []reconcile.Request {
|
|
|
+ return dependentComponentRequests(ctx, c, obj.GetNamespace(), func(comp *v1alpha1.Component) bool {
|
|
|
+ return componentDependsOn(comp, kind, obj.GetName(), obj.GetNamespace())
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|