/*
Copyright 2020 The cert-manager Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

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 cainjector

import (
	"context"
	"fmt"
	"strings"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/cert-manager/cert-manager/internal/cainjector/feature"
	certmanager "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	logf "github.com/cert-manager/cert-manager/pkg/logs"
	utilfeature "github.com/cert-manager/cert-manager/pkg/util/feature"
)

// This file contains logic to create reconcilers. By default a
// reconciler is created for each of the injectables- CustomResourceDefinition,
// Validating/MutatingWebhookConfiguration, APIService and gets triggered for
// events on those resources as well as on Secrets and Certificates.

// reconciler syncs CA data from source to injectable.
type reconciler struct {
	// newInjectableTarget knows how to create a new injectable targt for
	// the injectable being reconciled.
	newInjectableTarget NewInjectableTarget
	// sources is a list of available 'data sources' that can be used to extract
	// caBundles from various source.
	// This is defined as a variable to allow an instance of the secret-based
	// cainjector to run even when Certificate resources cannot we watched due to
	// the conversion webhook not being available.
	sources []caDataSource

	log logr.Logger
	client.Client

	// if set, the reconciler is namespace scoped
	namespace string

	// fieldManager is the manager name used for the Apply operations.
	fieldManager string

	resourceName string // just used for logging
}

// Reconcile attempts to ensure that a particular injectable has all the CAs injected that
// it has requested.
func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	// fetch the target object
	target := r.newInjectableTarget()

	log := r.log.WithValues("kind", r.resourceName, "name", req.Name)
	log.V(logf.DebugLevel).Info("Parsing injectable")

	if err := r.Client.Get(ctx, req.NamespacedName, target.AsObject()); err != nil {
		if dropNotFound(err) == nil {
			// don't requeue on deletions, which yield a non-found object
			log.V(logf.DebugLevel).Info("ignoring", "reason", "not found", "err", err)
			return ctrl.Result{}, nil
		}
		log.Error(err, "unable to fetch target object to inject into")
		return ctrl.Result{}, err
	}

	metaObj, err := meta.Accessor(target.AsObject())
	if err != nil {
		log.Error(err, "unable to get metadata for object")
		return ctrl.Result{}, err
	}

	// ignore resources that are being deleted
	if !metaObj.GetDeletionTimestamp().IsZero() {
		log.V(logf.DebugLevel).Info("ignoring", "reason", "object has a non-zero deletion timestamp")
		return ctrl.Result{}, nil
	}

	// ensure that it wants injection
	dataSource, err := r.caDataSourceFor(log, metaObj)
	if err != nil {
		log.V(logf.DebugLevel).Info("failed to determine ca data source for injectable")
		return ctrl.Result{}, nil
	}

	caData, err := dataSource.ReadCA(ctx, log, metaObj, r.namespace)
	if apierrors.IsForbidden(err) {
		log.V(logf.InfoLevel).Info("cainjector was forbidden to retrieve the ca data source")
		return ctrl.Result{}, nil
	}
	if err != nil {
		log.Error(err, "failed to read CA from data source")
		return ctrl.Result{}, err
	}

	if caData == nil {
		log.V(logf.InfoLevel).Info("could not find any ca data in data source for target")
		return ctrl.Result{}, nil
	}

	// actually do the injection
	target.SetCA(caData)

	// actually update with injected CA data
	if utilfeature.DefaultFeatureGate.Enabled(feature.ServerSideApply) {
		obj, patch := target.AsApplyObject()
		if patch != nil {
			err = r.Client.Patch(ctx, obj, patch, &client.PatchOptions{
				Force: ptr.To(true), FieldManager: r.fieldManager,
			})
		}
	} else {
		err = r.Client.Update(ctx, target.AsObject())
	}

	if err != nil {
		log.Error(err, "unable to update target object with new CA data")
		return ctrl.Result{}, err
	}

	log.V(logf.InfoLevel).Info("Updated object")

	return ctrl.Result{}, nil
}

func (r *reconciler) caDataSourceFor(log logr.Logger, metaObj metav1.Object) (caDataSource, error) {
	for _, s := range r.sources {
		if s.Configured(log, metaObj) {
			return s, nil
		}
	}
	return nil, fmt.Errorf("could not determine ca data source for resource")
}

// dropNotFound ignores the given error if it's a not-found error,
// but otherwise just returns the argument.
// TODO: we don't use this pattern anywhere else in this project so probably doesn't make sense here either
func dropNotFound(err error) error {
	if apierrors.IsNotFound(err) {
		return nil
	}
	return err
}

// owningCertForSecret gets the name of the owning certificate for a
// given secret, returning nil if no such object exists.
func owningCertForSecret(secret *corev1.Secret) *types.NamespacedName {
	val, ok := secret.Annotations[certmanager.CertificateNameKey]
	if !ok {
		return nil
	}
	return &types.NamespacedName{
		Name:      val,
		Namespace: secret.Namespace,
	}
}

// splitNamespacedName turns the string form of a namespaced name
// (<namespace>/<name>) back into a types.NamespacedName.
func splitNamespacedName(nameStr string) types.NamespacedName {
	splitPoint := strings.IndexRune(nameStr, types.Separator)
	if splitPoint == -1 {
		return types.NamespacedName{Name: nameStr}
	}
	return types.NamespacedName{Namespace: nameStr[:splitPoint], Name: nameStr[splitPoint+1:]}
}
