Skip to content
instance.go 8.98 KiB
Newer Older
Timothee Gosselin's avatar
Timothee Gosselin committed
/*

Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.gnu.org/licenses/agpl-3.0.html

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package reconciler

import (
	"context"
	"sort"

	"github.com/hashicorp/go-version"
Timothee Gosselin's avatar
Timothee Gosselin committed
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Timothee Gosselin's avatar
Timothee Gosselin committed
	"k8s.libre.sh/controller-utils/application"
	interfaces "k8s.libre.sh/controller-utils/interfaces"
	"k8s.libre.sh/controller-utils/status"
	ctrl "sigs.k8s.io/controller-runtime"
Timothee Gosselin's avatar
Timothee Gosselin committed
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type InstanceReconciler struct {
	*ReconcilerBase
	Instance  application.Instance
	SyncOrder map[int]string
}

func NewInstanceReconciler(r *ReconcilerBase, i application.Instance, order map[int]string) *InstanceReconciler {
	return &InstanceReconciler{
		ReconcilerBase: r,
		Instance:       i,
		SyncOrder:      order,
	}
}

func (r *InstanceReconciler) ManageError(owner status.ObjectWithStatus) (reconcile.Result, error) {

	return reconcile.Result{}, nil
}

Timothee Gosselin's avatar
Timothee Gosselin committed
func RemoverOwnerRef(c client.Client, obj interfaces.Object, owner metav1.Object) error {

	objectKey, err := client.ObjectKeyFromObject(obj)
	if err != nil {
		return err
	}

	if err := c.Get(context.Background(), objectKey, obj); err != nil {
		return err
	}

	ownerRefs := []metav1.OwnerReference{}

	for _, ref := range obj.GetOwnerReferences() {
		if ref.UID != owner.GetUID() {
			ownerRefs = append(ownerRefs, ref)
		}
	}

	obj.SetOwnerReferences(ownerRefs)

	if err := c.Update(context.Background(), obj); err != nil {
		return err
	}

	return nil
}

func (r *InstanceReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) {

Timothee Gosselin's avatar
Timothee Gosselin committed
	appStatus := r.Instance.GetApplicationStatus()
	oldStatus := appStatus.DeepCopy()

Timothee Gosselin's avatar
Timothee Gosselin committed
	///////////////////////////////////
	/////////// Finalizers ////////////
	//////////////////////////////////

	// name of our custom finalizer
	myFinalizerName := "apps.bound.finalizers.k8s.libre.sh"
	// examine DeletionTimestamp to determine if object is under deletion

	bar := r.Instance.GetOwner()
	if bar.GetDeletionTimestamp().IsZero() {
		// The object is not being deleted, so if it does not have our finalizer,
		// then lets add the finalizer and update the object. This is equivalent
		// registering our finalizer.
		if !controllerutil.ContainsFinalizer(r.Instance.GetOwner(), myFinalizerName) {
			controllerutil.AddFinalizer(r.Instance.GetOwner(), myFinalizerName)
		}

		if err := r.GetClient().Update(context.Background(), r.Instance.GetOwner()); err != nil {
			return ctrl.Result{}, err
		}
	} else {
		// The object is being deleted
		if controllerutil.ContainsFinalizer(r.Instance.GetOwner(), myFinalizerName) {
			foo := r.Instance.GetOwner().GetApplicationStatus()

			if err := CleanUpOwnerRef(foo.Watched.Objects, r.Instance.GetOwner(), r.GetClient()); err != nil {
				return ctrl.Result{}, err
			}
		}

		// remove our finalizer from the list and update it.
		controllerutil.RemoveFinalizer(r.Instance.GetOwner(), myFinalizerName)
		if err := r.GetClient().Update(context.Background(), r.Instance.GetOwner()); err != nil {
			return ctrl.Result{}, err
		}
		// Stop reconciliation as the item is being deleted
		return ctrl.Result{}, nil
	}

	err := application.Init(r.Instance, r.ReconcilerBase)
	if err != nil {
		return reconcile.Result{}, err
	}

Timothee Gosselin's avatar
Timothee Gosselin committed
	/////////////////////////////////////
	/////////////////////////////////////

Timothee Gosselin's avatar
Timothee Gosselin committed
	// Sync Settings
Timothee Gosselin's avatar
Timothee Gosselin committed
	for name, s := range r.Instance.GetSettings() {

		res, err := SyncComponent(context.TODO(), r.ReconcilerBase, s, r.Instance.GetOwner())
		cptStatus := status.ComponentStatus{
			Resources: res,
		}
Timothee Gosselin's avatar
Timothee Gosselin committed

Timothee Gosselin's avatar
Timothee Gosselin committed
		if len(appStatus.Settings) == 0 {
			appStatus.Settings = make(map[string]status.ComponentStatus)
		}
Timothee Gosselin's avatar
Timothee Gosselin committed
		if err != nil {
Timothee Gosselin's avatar
Timothee Gosselin committed
			cptStatus.ComponentsReady = "NotReady"
			appStatus.Settings[name] = cptStatus

Timothee Gosselin's avatar
Timothee Gosselin committed
			return ctrl.Result{}, err
		}
Timothee Gosselin's avatar
Timothee Gosselin committed

Timothee Gosselin's avatar
Timothee Gosselin committed
		cptStatus.ComponentsReady = "Ready"

		appStatus.Settings[name] = cptStatus
	cpts := make(map[string]application.ComponentMutate)

	jobs := r.Instance.GetJobs()

	// TODO TO FIX
	current, err := version.NewVersion(r.Instance.GetApplicationStatus().Version)
	// get nextcloud desired version in spec
	desired, err := version.NewVersion(r.Instance.GetVersion())

	if jobs != nil && len(jobs) > 0 {
		for name, j := range jobs {
			cpts[name] = j
		if len(r.Instance.GetApplicationStatus().Version) == 0 {
			// Sync INSTALL
			res, err := SyncComponent(context.TODO(), r.ReconcilerBase, jobs["install"], r.Instance.GetOwner())
			cptStatus := status.ComponentStatus{
				Resources: res,
			}
			if len(appStatus.Components) == 0 {
				appStatus.Components = make(map[string]status.ComponentStatus)
			}
			if err != nil {
				cptStatus.ComponentsReady = "NotReady"
				appStatus.Components["jobs"] = cptStatus

				return ctrl.Result{}, err
			}
			cptStatus.ComponentsReady = "Ready"
			appStatus.Components["jobs"] = cptStatus
			// ADD manage success

			//		return ctrl.Result{}, nil
			delete(jobs, "upgrade")
		} else {
			switch current.Compare(desired) {
			case 1:
				// current version is greater than desired, no downgrade
				// TODO Format error
				return ctrl.Result{}, err
			case -1:
				// current version lower than desired, upgrade job

				// SYNC UPGRADE
				delete(jobs, "install")
			case 0:
				// SYNC CPTS
				// current version and desired are the same, normal start
				delete(jobs, "install")
				delete(jobs, "upgrade")
			}
Timothee Gosselin's avatar
Timothee Gosselin committed
	// Sync Components
	for name, c := range r.Instance.GetComponents() {
		cpts[name] = c
	}

	sts, err := SyncComponents(context.TODO(), r.ReconcilerBase, cpts, r.Instance.GetOwner(), r.SyncOrder)

	appStatus.Components = sts.Components
	appStatus.SetConditions(sts.Conditions...)
Timothee Gosselin's avatar
Timothee Gosselin committed

	appStatus.Version = desired.String()
	owner := r.Instance.GetOwner()
	owner.SetApplicationStatus(*appStatus)
	// TODO Improve
	if err != nil {
		if err := r.ReconcilerBase.GetClient().Status().Update(context.TODO(), owner); err != nil {
			return ctrl.Result{}, err
		}
		return ctrl.Result{}, err
	}

	if err := r.ReconcilerBase.GetClient().Status().Update(context.TODO(), owner); err != nil {
Timothee Gosselin's avatar
Timothee Gosselin committed
		return ctrl.Result{}, err
	}
	// Clean up resources and update status
	err = CleanUpResources(oldStatus.GetResources(), appStatus.GetResources(), r.ReconcilerBase.GetClient(), r.Instance.GetNamespace())
	if err != nil {
Timothee Gosselin's avatar
Timothee Gosselin committed
		return ctrl.Result{}, err
Timothee Gosselin's avatar
Timothee Gosselin committed
	diff := Difference(&oldStatus.Watched, &appStatus.Watched)

	if len(diff.Objects) > 0 {
		if err := CleanUpOwnerRef(diff.Objects, r.Instance.GetOwner(), r.GetClient()); err != nil {
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

Timothee Gosselin's avatar
Timothee Gosselin committed
func CleanUpOwnerRef(osts []status.ObjectStatus, owner metav1.Object, c client.Client) error {
	for _, ost := range osts {

		ro := RuntimeObjFromObjStatus(ost)
		ro.SetNamespace(owner.GetNamespace())

		if err := RemoverOwnerRef(c, ro, owner); err != nil {
			return err
		}
	}

	return nil
}

func RuntimeObjFromObjStatus(ost status.ObjectStatus) interfaces.Object {

	switch ost.Kind {
	case "ConfigMap":
		obj := &corev1.ConfigMap{}
		obj.SetName(ost.Name)
		return obj
	case "Secret":
		obj := &corev1.Secret{}
		obj.SetName(ost.Name)
		return obj
	}

	return nil
}

func Difference(original, desired *status.Resources) *status.Resources {

	diff := &status.Resources{}
	diffByKey := make(map[string]status.ObjectStatus, len(original.Objects))
	desiredByKey := make(map[string]status.ObjectStatus, len(desired.Objects))

	for _, obj := range original.Objects {
		diffByKey[obj.Name] = obj
	}

	for _, obj := range desired.Objects {
		desiredByKey[obj.Name] = obj
	}

	for item := range desiredByKey {
		delete(diffByKey, item)
	}

	for _, obj := range diffByKey {
		diff.Objects = append(diff.Objects, obj)
	}

	return diff
}

func SyncComponents(ctx context.Context, r interfaces.Reconcile, cpts map[string]application.ComponentMutate, owner interfaces.Object, order map[int]string) (appStatus status.ApplicationStatus, err error) {

	keys := []int{}
	appStatus.Components = make(map[string]status.ComponentStatus)

	for k := range order {
		keys = append(keys, k)
	}

	sort.Ints(keys)

	for _, k := range keys {
		if cpts[order[k]] != nil {
			resources, err := SyncComponent(ctx, r, cpts[order[k]], owner)
			cptStatus := status.ComponentStatus{
				Resources: resources,
			}
			if err != nil {
				// manage error
				cptStatus.ComponentsReady = "NotReady"
				appStatus.Components[order[k]] = cptStatus
				appStatus.SetConditions(status.ReconcileError(err))
				return appStatus, err
			}
			cptStatus.ComponentsReady = "Ready"
			appStatus.Components[order[k]] = cptStatus
	appStatus.SetConditions(status.ReconcileSuccess())
	return appStatus, nil
}