...

Source file src/edge-infra.dev/pkg/edge/iam/ctl/clientctl/client_controller.go

Documentation: edge-infra.dev/pkg/edge/iam/ctl/clientctl

     1  /*
     2  Copyright 2022.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package clientctl
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"time"
    25  
    26  	apiv1 "k8s.io/api/core/v1"
    27  
    28  	apierrs "k8s.io/apimachinery/pkg/api/errors"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/types"
    31  
    32  	ctrl "sigs.k8s.io/controller-runtime"
    33  
    34  	"sigs.k8s.io/controller-runtime/pkg/builder"
    35  	kclient "sigs.k8s.io/controller-runtime/pkg/client"
    36  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    37  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    38  
    39  	logger "sigs.k8s.io/controller-runtime/pkg/log"
    40  
    41  	"github.com/go-kivik/kivik/v4"
    42  	"github.com/go-kivik/kivik/v4/couchdb"
    43  	"github.com/go-logr/logr"
    44  
    45  	api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    46  	iamclient "edge-infra.dev/pkg/edge/iam/client"
    47  	"edge-infra.dev/pkg/edge/iam/config"
    48  	"edge-infra.dev/pkg/edge/iam/storage/database"
    49  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    50  	"edge-infra.dev/pkg/lib/logging"
    51  )
    52  
    53  const (
    54  	EdgeIDFinalizer             = "finalizers.id.edge.ncr.com"
    55  	ClientRegistrationSucceeded = "ClientRegistrationSucceeded"
    56  	MissingSecretData           = "MissingSecretData"
    57  	ClientRegistrationFailed    = "ClientRegistrationFailed"
    58  	ClientSecretExistFailure    = "ClientSecretExistFailure"
    59  	ClientSecretCreationFailure = "ClientSecretCreationFailure"
    60  	SaveClientCredentailsFailed = "SaveClientCredentailsFailed"
    61  	CouchDBNotReady             = "CouchDBNotReady"
    62  )
    63  
    64  // ClientReconciler reconciles a Client object
    65  type ClientReconciler struct {
    66  	kclient.Client
    67  	Scheme *runtime.Scheme
    68  	Name   string
    69  	// kubebuilder default metrics
    70  	Metrics metrics.Metrics
    71  }
    72  
    73  var clientStorer iamclient.Storage
    74  
    75  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=clients,verbs=get;list;watch;create;update;patch;delete
    76  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=clients/status,verbs=get;update;patch
    77  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=clients/finalizers,verbs=update
    78  
    79  // Reconcile is part of the main kubernetes reconciliation loop which aims to
    80  // move the current state of the cluster closer to the desired state.
    81  // TODO(user): Modify the Reconcile function to compare the state specified by
    82  // the Client object against the actual cluster state, and then
    83  // perform operations to make the cluster state reflect the state specified by
    84  // the user.
    85  //
    86  // For more details, check Reconcile and its Result here:
    87  // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile
    88  func (r *ClientReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
    89  	var (
    90  		reconcileStart = time.Now()
    91  		log            = logger.FromContext(ctx)
    92  		client         = api.Client{}
    93  	)
    94  
    95  	log.V(2).Info("reconcile client", "req", req)
    96  
    97  	// try to get the client resource
    98  	if err := r.Get(ctx, req.NamespacedName, &client); err != nil {
    99  		// failed to get client resource
   100  		return ctrl.Result{}, kclient.IgnoreNotFound(err)
   101  	}
   102  
   103  	defer func() {
   104  		r.Metrics.RecordReadiness(ctx, &client)
   105  		r.Metrics.RecordDuration(ctx, &client, reconcileStart)
   106  	}()
   107  
   108  	if !controllerutil.ContainsFinalizer(&client, EdgeIDFinalizer) {
   109  		controllerutil.AddFinalizer(&client, EdgeIDFinalizer)
   110  		// return with requeue, so we can reconcile again with finalizer.
   111  		err := r.Update(ctx, &client)
   112  		if err != nil {
   113  			log.Error(err, "error updating client resource after adding finalizer")
   114  		}
   115  		return ctrl.Result{Requeue: true}, nil
   116  	}
   117  	if !client.ObjectMeta.DeletionTimestamp.IsZero() {
   118  		err = r.finalize(ctx, req, &client)
   119  		if err != nil {
   120  			log.Error(err, "error executing finalizer on client resource")
   121  			return ctrl.Result{Requeue: true}, nil
   122  		}
   123  		return ctrl.Result{}, nil
   124  	}
   125  	var reconciled api.Client
   126  	up, reconcileErr := isCouchDBUp()
   127  	if up {
   128  		// client resource exists...
   129  		// reconcile the client, creating a copy so that we avoid mutating our controller's cache
   130  		reconciled, reconcileErr = r.reconcile(ctx, req, *client.DeepCopy())
   131  	} else {
   132  		reconciled = api.MarkNotReady(reconciled, CouchDBNotReady, reconcileErr.Error())
   133  		log.Info("database not ready yet, will try again soon", "error", reconcileErr)
   134  	}
   135  
   136  	// reflect the reconciled status on the API server
   137  	if err := r.updateClientStatus(ctx, req, reconciled.Status); err != nil {
   138  		log.Info("failed to update client status. will retry soon", "status", reconciled.Status)
   139  		return ctrl.Result{Requeue: true}, nil
   140  	}
   141  
   142  	// if reconciliation errors occur, return them and requeue so we re-try
   143  	// if there are reconciliation errors that are nonrecoverable, we can handle
   144  	// them here by setting Requeue: false conditionally
   145  	if !up {
   146  		return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
   147  	}
   148  	if reconcileErr != nil {
   149  		log.Error(reconcileErr, "error reconciling client resource")
   150  		return ctrl.Result{Requeue: true}, nil
   151  	}
   152  	return ctrl.Result{}, nil
   153  }
   154  
   155  func (r *ClientReconciler) finalize(ctx context.Context, req ctrl.Request, client *api.Client) error {
   156  	log := logr.FromContextOrDiscard(ctx)
   157  	log.Info("running finalizer")
   158  	log.Info("unregistering client from DB")
   159  	// this client resource does not exist, make sure to remove it from db too
   160  	if unregisterErr := r.unregisterClient(ctx, req); unregisterErr != nil {
   161  		return unregisterErr
   162  	}
   163  	controllerutil.RemoveFinalizer(client, EdgeIDFinalizer)
   164  	err := r.Update(ctx, client)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	log.Info("executed finalizer")
   169  	return nil
   170  }
   171  
   172  // SetupWithManager sets up the controller with the Manager.
   173  func (r *ClientReconciler) SetupWithManager(mgr ctrl.Manager) error {
   174  	return ctrl.NewControllerManagedBy(mgr).
   175  		For(&api.Client{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
   176  		Complete(r)
   177  }
   178  
   179  func (r *ClientReconciler) reconcile(ctx context.Context, req ctrl.Request, client api.Client) (api.Client, error) {
   180  	var secret *apiv1.Secret
   181  	var err error
   182  
   183  	log := logger.FromContext(ctx)
   184  
   185  	client, secret, err = r.reconcileClientSecret(ctx, req, client)
   186  	if err != nil {
   187  		return client, err
   188  	}
   189  
   190  	// secret exists, let's validate it
   191  	client, err = r.validateClientSecret(client, secret)
   192  	if err != nil {
   193  		log.Error(err, fmt.Sprintf("secret %s/%s is invalid", secret.Name, secret.Namespace))
   194  		return client, err
   195  	}
   196  
   197  	// save the secret to storage
   198  	err = r.saveClientCredentials(ctx, secret)
   199  	if err != nil {
   200  		return api.MarkNotReady(client, SaveClientCredentailsFailed, err.Error()), err
   201  	}
   202  
   203  	// we have a valid client secret, let's now make sure the client is registered...
   204  	client, err = r.registerClient(ctx, req, client, secret)
   205  	if err != nil {
   206  		return client, err
   207  	}
   208  
   209  	// reconciled without errors..
   210  	return api.MarkReady(client, ClientRegistrationSucceeded, "successfully registered the client"), nil
   211  }
   212  
   213  func clientSecretExists(ctx context.Context, req ctrl.Request, c kclient.Client, secretName string) (*apiv1.Secret, error) {
   214  	clientSecret := &apiv1.Secret{}
   215  	err := c.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: secretName}, clientSecret)
   216  	switch {
   217  	case err == nil:
   218  		return clientSecret, nil
   219  	case apierrs.IsNotFound(err):
   220  		return nil, nil
   221  	default:
   222  		return nil, fmt.Errorf("failed to check if client secret exists: %w", err)
   223  	}
   224  }
   225  
   226  func (r *ClientReconciler) validateClientSecret(client api.Client, secret *apiv1.Secret) (api.Client, error) {
   227  	_, found := secret.Data["client_id"]
   228  	if !found {
   229  		e := errors.New(`"client_id property missing"`)
   230  		return api.MarkNotReady(client, MissingSecretData, e.Error()), e
   231  	}
   232  
   233  	_, found = secret.Data["client_secret"]
   234  	if !found {
   235  		e := errors.New(`"client_secret property missing"`)
   236  		return api.MarkNotReady(client, MissingSecretData, e.Error()), e
   237  	}
   238  
   239  	return client, nil
   240  }
   241  
   242  // isCouchDBUp returns true if we can ping couchDB
   243  func isCouchDBUp() (bool, error) {
   244  	log := logging.NewLogger().WithName("isCouchDBUp")
   245  	couchURI := config.CouchDBAddress()
   246  	client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
   247  	couchDBClient, err := kivik.New("couch", couchURI, couchdb.BasicAuth(config.CouchDBUser(), config.CouchDBPassword()), couchdb.OptionHTTPClient(client))
   248  	if err != nil {
   249  		return false, err
   250  	}
   251  	defer func(couchDBClient *kivik.Client) {
   252  		err := couchDBClient.Close()
   253  		if err != nil {
   254  			log.Error(err, "Failed to close CouchDB client")
   255  		}
   256  	}(couchDBClient)
   257  
   258  	online, err := couchDBClient.Ping(context.Background())
   259  	if err != nil {
   260  		log.Error(err, "couchClient.Ping error")
   261  	}
   262  	return online, err
   263  }
   264  
   265  func getClientStorer(log logr.Logger) (iamclient.Storage, error) {
   266  	if clientStorer == nil {
   267  		var err error
   268  		clientStorer, err = database.NewOperatorStore(log)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  	}
   273  	return clientStorer, nil
   274  }
   275  
   276  // updateClientStatus fetches an up-to-date copy of the object we want to update the
   277  // status for and patches its status.
   278  // this is done to minimize cache mutation errors and API server mismatch errors
   279  // that can occur if the patch does not align with the API server's current state
   280  func (r *ClientReconciler) updateClientStatus(ctx context.Context, req ctrl.Request, status api.ClientStatus) error {
   281  	var c api.Client
   282  	if err := r.Get(ctx, req.NamespacedName, &c); err != nil {
   283  		return err
   284  	}
   285  
   286  	patch := kclient.MergeFrom(c.DeepCopy())
   287  	c.Status = status
   288  
   289  	return r.Status().Patch(ctx, &c, patch)
   290  }
   291  

View as plain text