github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/clusters/cluster_utils.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package clusters
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"time"
    10  
    11  	vzctrl "github.com/verrazzano/verrazzano/pkg/controller"
    12  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    13  
    14  	clustersv1alpha1 "github.com/verrazzano/verrazzano/application-operator/apis/clusters/v1alpha1"
    15  	"github.com/verrazzano/verrazzano/application-operator/constants"
    16  	"go.uber.org/zap"
    17  	corev1 "k8s.io/api/core/v1"
    18  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    19  	"k8s.io/apimachinery/pkg/runtime"
    20  	"k8s.io/apimachinery/pkg/types"
    21  	"k8s.io/apimachinery/pkg/util/rand"
    22  	controllerruntime "sigs.k8s.io/controller-runtime"
    23  	"sigs.k8s.io/controller-runtime/pkg/client"
    24  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    25  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    26  )
    27  
    28  // MCLocalRegistrationSecretFullName is the full NamespacedName of the cluster local registration secret
    29  var MCLocalRegistrationSecretFullName = types.NamespacedName{
    30  	Namespace: constants.VerrazzanoSystemNamespace,
    31  	Name:      constants.MCLocalRegistrationSecret}
    32  
    33  // MCRegistrationSecretFullName is the full NamespacedName of the cluster registration secret
    34  var MCRegistrationSecretFullName = types.NamespacedName{
    35  	Namespace: constants.VerrazzanoSystemNamespace,
    36  	Name:      constants.MCRegistrationSecret}
    37  
    38  // ElasticsearchDetails represents all the details needed
    39  // to determine how to connect to an Elasticsearch instance
    40  type ElasticsearchDetails struct {
    41  	URL        string
    42  	SecretName string
    43  }
    44  
    45  // MultiClusterResource interface abstracts methods common to all MultiClusterXXX resource types
    46  // It is defined outside the api resources package since deep-copy code generation cannot handle
    47  // interface types
    48  type MultiClusterResource interface {
    49  	runtime.Object
    50  	GetName() string
    51  	GetNamespace() string
    52  	GetPlacement() clustersv1alpha1.Placement
    53  	GetStatus() clustersv1alpha1.MultiClusterResourceStatus
    54  }
    55  
    56  // StatusUpdateMessage represents a message sent to the Multi Cluster agent by the controllers
    57  // when a MultiCluster Resource's status is updated, with the updates
    58  type StatusUpdateMessage struct {
    59  	NewCondition     clustersv1alpha1.Condition
    60  	NewClusterStatus clustersv1alpha1.ClusterLevelStatus
    61  	Resource         MultiClusterResource
    62  }
    63  
    64  // StatusNeedsUpdate determines based on the current state and conditions of a MultiCluster
    65  // resource, as well as the new state and condition to be set, whether the status update
    66  // needs to be done
    67  func StatusNeedsUpdate(curStatus clustersv1alpha1.MultiClusterResourceStatus,
    68  	newCondition clustersv1alpha1.Condition,
    69  	newClusterStatus clustersv1alpha1.ClusterLevelStatus) bool {
    70  
    71  	foundClusterLevelStatus := false
    72  	for _, existingClusterStatus := range curStatus.Clusters {
    73  		if existingClusterStatus.Name == newClusterStatus.Name &&
    74  			existingClusterStatus.State == newClusterStatus.State {
    75  			foundClusterLevelStatus = true
    76  		}
    77  	}
    78  
    79  	if !foundClusterLevelStatus {
    80  		return true
    81  	}
    82  
    83  	foundCondition := false
    84  	for _, existingCond := range curStatus.Conditions {
    85  		if existingCond.Status == newCondition.Status &&
    86  			existingCond.Message == newCondition.Message &&
    87  			existingCond.Type == newCondition.Type {
    88  			foundCondition = true
    89  			break
    90  		}
    91  	}
    92  
    93  	return !foundCondition
    94  }
    95  
    96  // GetConditionFromResult - Based on the result of a create/update operation on the
    97  // embedded resource, returns the Condition and State that must be set on a MultiCluster
    98  // resource's Status
    99  func GetConditionFromResult(err error, opResult controllerutil.OperationResult, msgPrefix string) clustersv1alpha1.Condition {
   100  	var condition clustersv1alpha1.Condition
   101  	if err != nil {
   102  		condition = clustersv1alpha1.Condition{
   103  			Type:               clustersv1alpha1.DeployFailed,
   104  			Status:             corev1.ConditionTrue,
   105  			Message:            err.Error(),
   106  			LastTransitionTime: time.Now().Format(time.RFC3339),
   107  		}
   108  	} else {
   109  		msg := fmt.Sprintf("%v %v", msgPrefix, opResult)
   110  		condition = clustersv1alpha1.Condition{
   111  			Type:               clustersv1alpha1.DeployComplete,
   112  			Status:             corev1.ConditionTrue,
   113  			Message:            msg,
   114  			LastTransitionTime: time.Now().Format(time.RFC3339),
   115  		}
   116  	}
   117  	return condition
   118  }
   119  
   120  // CreateClusterLevelStatus creates and returns a ClusterLevelStatus object based on the condition
   121  // of an operation on a cluster
   122  func CreateClusterLevelStatus(condition clustersv1alpha1.Condition, clusterName string) clustersv1alpha1.ClusterLevelStatus {
   123  	var state clustersv1alpha1.StateType
   124  	if condition.Type == clustersv1alpha1.DeployComplete {
   125  		state = clustersv1alpha1.Succeeded
   126  	} else if condition.Type == clustersv1alpha1.DeployFailed {
   127  		state = clustersv1alpha1.Failed
   128  	} else {
   129  		state = clustersv1alpha1.Pending
   130  	}
   131  	return clustersv1alpha1.ClusterLevelStatus{
   132  		Name: clusterName, State: state, Message: condition.Message, LastUpdateTime: condition.LastTransitionTime}
   133  }
   134  
   135  // ComputeEffectiveState computes the overall state of the multi cluster resource from the statuses
   136  // at the level of the individual clusters it is placed in
   137  func ComputeEffectiveState(status clustersv1alpha1.MultiClusterResourceStatus, placement clustersv1alpha1.Placement) clustersv1alpha1.StateType {
   138  	clustersSucceeded := 0
   139  	clustersFound := 0
   140  	clustersPending := 0
   141  	clustersFailed := 0
   142  
   143  	for _, cluster := range placement.Clusters {
   144  		for _, clusterStatus := range status.Clusters {
   145  			if clusterStatus.Name == cluster.Name {
   146  				clustersFound++
   147  				if clusterStatus.State == clustersv1alpha1.Pending {
   148  					clustersPending++
   149  				} else if clusterStatus.State == clustersv1alpha1.Succeeded {
   150  					clustersSucceeded++
   151  				} else if clusterStatus.State == clustersv1alpha1.Failed {
   152  					clustersFailed++
   153  				}
   154  			}
   155  		}
   156  	}
   157  	// If any cluster has a failed status, mark the overall state as failed
   158  	if clustersFailed > 0 {
   159  		return clustersv1alpha1.Failed
   160  	}
   161  
   162  	// If any cluster has a pending status, mark the overall state as pending
   163  	if clustersPending > 0 {
   164  		return clustersv1alpha1.Pending
   165  	}
   166  
   167  	// if all clusters succeeded, mark the overall state as succeeded
   168  	// The check for ">=" is because placement on the admin cluster is implied.
   169  	if clustersSucceeded >= len(placement.Clusters) {
   170  		return clustersv1alpha1.Succeeded
   171  	}
   172  
   173  	// otherwise, overall state is pending
   174  	return clustersv1alpha1.Pending
   175  }
   176  
   177  // SetClusterLevelStatus - given a multi cluster resource status object, and a new cluster status
   178  // to be updated, either add or update the cluster status as appropriate
   179  func SetClusterLevelStatus(status *clustersv1alpha1.MultiClusterResourceStatus, newClusterStatus clustersv1alpha1.ClusterLevelStatus) {
   180  	foundClusterIdx := -1
   181  	for i, clusterStatus := range status.Clusters {
   182  		if clusterStatus.Name == newClusterStatus.Name {
   183  			foundClusterIdx = i
   184  		}
   185  	}
   186  	if foundClusterIdx == -1 {
   187  		status.Clusters = append(status.Clusters, newClusterStatus)
   188  	} else {
   189  		status.Clusters[foundClusterIdx] = newClusterStatus
   190  		status.Clusters[foundClusterIdx].LastUpdateTime = time.Now().Format(time.RFC3339)
   191  	}
   192  }
   193  
   194  // NewScheme creates a new scheme that includes this package's object to use for testing
   195  func NewScheme() *runtime.Scheme {
   196  	scheme := runtime.NewScheme()
   197  	_ = clustersv1alpha1.AddToScheme(scheme)
   198  	return scheme
   199  }
   200  
   201  // IsPlacedInThisCluster determines whether the given Placement represents placement in the current
   202  // cluster. Current cluster's identity is determined from the verrazzano-cluster secret
   203  func IsPlacedInThisCluster(ctx context.Context, rdr client.Reader, placement clustersv1alpha1.Placement) bool {
   204  	var clusterSecret corev1.Secret
   205  
   206  	err := fetchClusterSecret(ctx, rdr, &clusterSecret)
   207  	if err != nil {
   208  		return false
   209  	}
   210  	thisCluster := string(clusterSecret.Data[constants.ClusterNameData])
   211  	for _, placementCluster := range placement.Clusters {
   212  		if thisCluster == placementCluster.Name {
   213  			return true
   214  		}
   215  	}
   216  
   217  	return false
   218  }
   219  
   220  // IgnoreNotFoundWithLog returns nil if err is a "Not Found" error, and if not, logs an error
   221  // message that the resource could not be fetched and returns the original error
   222  func IgnoreNotFoundWithLog(err error, log *zap.SugaredLogger) (reconcile.Result, error) {
   223  	if apierrors.IsNotFound(err) {
   224  		log.Debug("Resource has been deleted")
   225  		return reconcile.Result{}, nil
   226  	}
   227  	if err != nil {
   228  		log.Errorf("Failed to fetch resource: %v", err)
   229  	}
   230  	return NewRequeueWithDelay(), nil
   231  }
   232  
   233  // GetClusterName returns the cluster name for a this cluster, empty string if the cluster
   234  // name cannot be determined due to an error.
   235  func GetClusterName(ctx context.Context, rdr client.Reader) string {
   236  	clusterSecret := corev1.Secret{}
   237  	err := fetchClusterSecret(ctx, rdr, &clusterSecret)
   238  	if err != nil {
   239  		return ""
   240  	}
   241  	return string(clusterSecret.Data[constants.ClusterNameData])
   242  }
   243  
   244  // Try to get the registration secret that was created via the registration YAML apply.  If it doesn't
   245  // exist then use the local registration secret that was created at install time.
   246  func fetchClusterSecret(ctx context.Context, rdr client.Reader, clusterSecret *corev1.Secret) error {
   247  	err := rdr.Get(ctx, MCRegistrationSecretFullName, clusterSecret)
   248  	if err == nil {
   249  		return nil
   250  	}
   251  	if !apierrors.IsNotFound(err) {
   252  		return err
   253  	}
   254  	return rdr.Get(ctx, MCLocalRegistrationSecretFullName, clusterSecret)
   255  }
   256  
   257  // UpdateStatus determines whether a status update is needed for the specified mcStatus, given the new
   258  // Condition to be added, and if so, computes the state and calls the callback function to perform
   259  // the status update
   260  func UpdateStatus(resource MultiClusterResource, mcStatus *clustersv1alpha1.MultiClusterResourceStatus, placement clustersv1alpha1.Placement, newCondition clustersv1alpha1.Condition, clusterName string, agentChannel chan StatusUpdateMessage, updateFunc func() error) (controllerruntime.Result, error) {
   261  
   262  	clusterLevelStatus := CreateClusterLevelStatus(newCondition, clusterName)
   263  
   264  	if StatusNeedsUpdate(*mcStatus, newCondition, clusterLevelStatus) {
   265  		addOrUpdateCondition(mcStatus, newCondition)
   266  		SetClusterLevelStatus(mcStatus, clusterLevelStatus)
   267  		mcStatus.State = ComputeEffectiveState(*mcStatus, placement)
   268  		err := updateFunc()
   269  		if err != nil {
   270  			return reconcile.Result{}, err
   271  		}
   272  		if agentChannel != nil {
   273  			// put the status update itself on the agent channel.
   274  			// note that the send will block if the channel buffer is full, which means the
   275  			// reconcile operation will not complete till unblocked
   276  			msg := StatusUpdateMessage{
   277  				NewCondition:     newCondition,
   278  				NewClusterStatus: clusterLevelStatus,
   279  				Resource:         resource,
   280  			}
   281  			agentChannel <- msg
   282  		}
   283  	}
   284  	return reconcile.Result{}, nil
   285  }
   286  
   287  // addOrUpdateCondition adds or updates the newCondition in the status' list of existing conditions
   288  func addOrUpdateCondition(status *clustersv1alpha1.MultiClusterResourceStatus, condition clustersv1alpha1.Condition) {
   289  	var matchingCondition *clustersv1alpha1.Condition
   290  	for i, existingCondition := range status.Conditions {
   291  		if condition.Type == existingCondition.Type &&
   292  			condition.Status == existingCondition.Status &&
   293  			condition.Message == existingCondition.Message {
   294  			// the exact same condition already exists, don't update
   295  			return
   296  		}
   297  		if condition.Type == existingCondition.Type {
   298  			// use the index here since "existingCondition" is a copy and won't point to the object in the slice
   299  			matchingCondition = &status.Conditions[i]
   300  			break
   301  		}
   302  	}
   303  	if matchingCondition == nil {
   304  		status.Conditions = append(status.Conditions, condition)
   305  	} else {
   306  		matchingCondition.Message = condition.Message
   307  		matchingCondition.Status = condition.Status
   308  		matchingCondition.LastTransitionTime = condition.LastTransitionTime
   309  	}
   310  }
   311  
   312  // SetEffectiveStateIfChanged - if the effective state of the resource has changed, set it on the
   313  // in-memory multicluster resource's status. Returns the previous state, whether changed or not
   314  func SetEffectiveStateIfChanged(placement clustersv1alpha1.Placement,
   315  	statusPtr *clustersv1alpha1.MultiClusterResourceStatus) clustersv1alpha1.StateType {
   316  
   317  	effectiveState := ComputeEffectiveState(*statusPtr, placement)
   318  	if effectiveState != statusPtr.State {
   319  		oldState := statusPtr.State
   320  		statusPtr.State = effectiveState
   321  		return oldState
   322  	}
   323  	return statusPtr.State
   324  }
   325  
   326  // DeleteAssociatedResource will retrieve and delete the resource specified by the name. It is used to delete
   327  // the underlying resource corresponding to a MultiClusterxxx wrapper resource (e.g. the OAM app config corresponding
   328  // to a MultiClusterApplicationConfiguration)
   329  func DeleteAssociatedResource(ctx context.Context, c client.Client, mcResource client.Object, finalizerName string, resourceToDelete client.Object, name types.NamespacedName) error {
   330  	// Get and delete the associated with the name specified by resourceToDelete
   331  	err := c.Get(ctx, name, resourceToDelete)
   332  	if err != nil {
   333  		if !apierrors.IsNotFound(err) {
   334  			return err
   335  		}
   336  	} else {
   337  		err = c.Delete(ctx, resourceToDelete)
   338  		if err != nil {
   339  			return err
   340  		}
   341  	}
   342  
   343  	// Deletion succeeded, now we can remove the finalizer
   344  
   345  	// assert the MC object is a controller util Object that can be processed by controllerutil.RemoveFinalizer
   346  	controllerutil.RemoveFinalizer(mcResource, finalizerName)
   347  	err = c.Update(ctx, mcResource)
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  // AddFinalizer adds a finalizer and updates the resource if that finalizer is not already attached to the resource
   356  func AddFinalizer(ctx context.Context, r client.Client, obj client.Object, finalizerName string) (controllerruntime.Result, error) {
   357  	if !controllerutil.ContainsFinalizer(obj, finalizerName) {
   358  		controllerutil.AddFinalizer(obj, finalizerName)
   359  		if err := r.Update(ctx, obj); err != nil {
   360  			return controllerruntime.Result{}, err
   361  		}
   362  	}
   363  
   364  	return controllerruntime.Result{}, nil
   365  }
   366  
   367  // GetRandomRequeueDelay returns a random delay between 2 and 8 secondsto be used for RequeueAfter
   368  func GetRandomRequeueDelay() time.Duration {
   369  	return GetRandomRequeueDelayInRange(2, 8)
   370  }
   371  
   372  // GetRandomRequeueDelayInRange returns a random delay in the given range in seconds, to be used for RequeueAfter
   373  func GetRandomRequeueDelayInRange(lowSeconds, highSeconds int) time.Duration {
   374  	// get a jittered delay to use for requeueing reconcile
   375  	var seconds = rand.IntnRange(lowSeconds, highSeconds)
   376  	return time.Duration(seconds) * time.Second
   377  }
   378  
   379  // NewRequeueWithDelay retruns a result set to requeue in 2 to 3 seconds
   380  func NewRequeueWithDelay() reconcile.Result {
   381  	return vzctrl.NewRequeueWithDelay(2, 3, time.Second)
   382  }
   383  
   384  // NewRequeueWithRandomDelay retruns a result set to requeue after a random delay
   385  func NewRequeueWithRandomDelay(lowSeconds, highSeconds int) reconcile.Result {
   386  	return controllerruntime.Result{Requeue: true, RequeueAfter: GetRandomRequeueDelayInRange(lowSeconds, highSeconds)}
   387  }
   388  
   389  // ShouldRequeue returns true if requeue is needed
   390  func ShouldRequeue(r reconcile.Result) bool {
   391  	return r.Requeue || r.RequeueAfter > 0
   392  }
   393  
   394  // GetResourceLogger will return the controller logger associated with the resource
   395  func GetResourceLogger(controller string, namespacedName types.NamespacedName, obj client.Object) (vzlog.VerrazzanoLogger, error) {
   396  	// Get the resource logger needed to log message using 'progress' and 'once' methods
   397  	log, err := vzlog.EnsureResourceLogger(&vzlog.ResourceConfig{
   398  		Name:           namespacedName.Name,
   399  		Namespace:      namespacedName.Namespace,
   400  		ID:             string(obj.GetUID()),
   401  		Generation:     obj.GetGeneration(),
   402  		ControllerName: controller,
   403  	})
   404  	if err != nil {
   405  		zap.S().Errorf("Failed to create controller logger for %v: %v", namespacedName, err)
   406  	}
   407  
   408  	return log, err
   409  }