github.com/verrazzano/verrazzano@v1.7.1/cluster-operator/controllers/vmc/update_status.go (about)

     1  // Copyright (c) 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 vmc
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  
    11  	clustersv1alpha1 "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1"
    12  	"github.com/verrazzano/verrazzano/cluster-operator/internal/capi"
    13  	vzconstants "github.com/verrazzano/verrazzano/pkg/constants"
    14  	"github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/v1beta1"
    15  	"k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    18  	"k8s.io/apimachinery/pkg/runtime/schema"
    19  	"k8s.io/apimachinery/pkg/types"
    20  	capiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1"
    21  	clipkg "sigs.k8s.io/controller-runtime/pkg/client"
    22  )
    23  
    24  const (
    25  	importedProviderDisplayName = "Imported"
    26  	ocneProviderDisplayName     = "Oracle OCNE on OCI"
    27  	okeProviderDisplayName      = "Oracle OKE"
    28  )
    29  
    30  // for unit testing
    31  var getCAPIClientFunc = capi.GetClusterClient
    32  
    33  // updateStatus updates the status of the VMC in the cluster, with all provided conditions, after setting the vmc.Status.State field for the cluster
    34  func (r *VerrazzanoManagedClusterReconciler) updateStatus(ctx context.Context, vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
    35  	// Update the VMC's status.state
    36  	if err := r.updateState(vmc); err != nil {
    37  		return err
    38  	}
    39  
    40  	// Update the VMC's status.imported
    41  	imported := vmc.Status.ClusterRef == nil
    42  	vmc.Status.Imported = &imported
    43  
    44  	// Update the VMC's status.provider
    45  	if err := r.updateProvider(vmc); err != nil {
    46  		return err
    47  	}
    48  
    49  	// Conditionally update the VMC's Kubernetes version
    50  	k8sUpdateNeeded, err := r.shouldUpdateK8sVersion(vmc)
    51  	if err != nil {
    52  		return err
    53  	} else if k8sUpdateNeeded {
    54  		if err := r.updateK8sVersionUsingCAPI(vmc); err != nil {
    55  			return err
    56  		}
    57  	}
    58  
    59  	// Fetch the existing VMC to avoid conflicts in the status update
    60  	existingVMC := &clustersv1alpha1.VerrazzanoManagedCluster{}
    61  	err = r.Get(context.TODO(), types.NamespacedName{Namespace: vmc.Namespace, Name: vmc.Name}, existingVMC)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	// Replace the existing status conditions and state with the conditions generated from this reconcile
    67  	for _, genCondition := range vmc.Status.Conditions {
    68  		r.setStatusCondition(existingVMC, genCondition, genCondition.Type == clustersv1alpha1.ConditionManifestPushed)
    69  	}
    70  	existingVMC.Status.State = vmc.Status.State
    71  	existingVMC.Status.ArgoCDRegistration = vmc.Status.ArgoCDRegistration
    72  	existingVMC.Status.Imported = vmc.Status.Imported
    73  	existingVMC.Status.Provider = vmc.Status.Provider
    74  	if k8sUpdateNeeded {
    75  		existingVMC.Status.Kubernetes.Version = vmc.Status.Kubernetes.Version
    76  	}
    77  
    78  	r.log.Debugf("Updating Status of VMC %s: %v", vmc.Name, vmc.Status.Conditions)
    79  	return r.Status().Update(ctx, existingVMC)
    80  }
    81  
    82  // updateProvider sets the VMC's status.provider field
    83  func (r *VerrazzanoManagedClusterReconciler) updateProvider(vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
    84  	// This VMC represents an imported cluster.
    85  	if vmc.Status.ClusterRef == nil {
    86  		vmc.Status.Provider = importedProviderDisplayName
    87  		return nil
    88  	}
    89  
    90  	// This VMC represents a CAPI cluster. Get the provider and update the VMC.
    91  	clusterNamespacedName := types.NamespacedName{
    92  		Name:      vmc.Status.ClusterRef.Name,
    93  		Namespace: vmc.Status.ClusterRef.Namespace,
    94  	}
    95  	capiCluster := &capiv1beta1.Cluster{}
    96  	if err := r.Client.Get(context.TODO(), clusterNamespacedName, capiCluster); err != nil {
    97  		if errors.IsNotFound(err) {
    98  			return nil
    99  		}
   100  		return err
   101  	}
   102  	provider, err := r.getCAPIProviderDisplayString(capiCluster)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	vmc.Status.Provider = provider
   107  	return nil
   108  }
   109  
   110  // getCAPIProviderDisplayString returns the string to populate the VMC's status.provider field, based on information taken from the
   111  // provided CAPI Cluster.
   112  func (r *VerrazzanoManagedClusterReconciler) getCAPIProviderDisplayString(capiCluster *capiv1beta1.Cluster) (string, error) {
   113  	// If this CAPI Cluster was created using ClusterClass, then parse capiCluster differently.
   114  	if capiCluster.Spec.Topology != nil {
   115  		clusterClass, err := capi.GetClusterClassFromCluster(context.TODO(), r.Client, capiCluster)
   116  		if err != nil {
   117  			if errors.IsNotFound(err) {
   118  				r.log.Progressf("could not find ClusterClass %s/%s: %v", clusterClass.GetNamespace(), clusterClass.GetName(), err)
   119  				return "", nil
   120  			}
   121  			return "", err
   122  		}
   123  		return r.getCAPIProviderDisplayStringClusterClass(clusterClass)
   124  	}
   125  
   126  	// This cluster does not use ClusterClass.
   127  	// Get infrastructure provider
   128  	if capiCluster.Spec.InfrastructureRef == nil {
   129  		return "", fmt.Errorf("clusterAPI cluster %s/%s has an unset spec.infrastructureRef field", capiCluster.Namespace, capiCluster.Name)
   130  	}
   131  	infraProvider := capiCluster.Spec.InfrastructureRef.Kind
   132  	if infraProvider == "" {
   133  		return "", fmt.Errorf("clusterAPI cluster %s/%s has an empty infrastructure provider", capiCluster.Namespace, capiCluster.Name)
   134  	}
   135  
   136  	// Get control plane provider
   137  	if capiCluster.Spec.ControlPlaneRef == nil {
   138  		return "", fmt.Errorf("clusterAPI cluster %s/%s has an unset spec.controlPlaneRef field", capiCluster.Namespace, capiCluster.Name)
   139  	}
   140  	cpProvider := capiCluster.Spec.ControlPlaneRef.Kind
   141  	if cpProvider == "" {
   142  		return "", fmt.Errorf("clusterAPI cluster %s/%s has an empty control plane provider", capiCluster.Namespace, capiCluster.Name)
   143  	}
   144  
   145  	return r.formProviderDisplayString(infraProvider, cpProvider), nil
   146  }
   147  
   148  // getCAPIProviderDisplayStringClusterClass returns the string to populate the VMC's status.provider field, given the ClusterClass
   149  // associated with this managed cluster.
   150  func (r *VerrazzanoManagedClusterReconciler) getCAPIProviderDisplayStringClusterClass(clusterClass *capiv1beta1.ClusterClass) (string, error) {
   151  	// Get infrastructure provider
   152  	if clusterClass.Spec.Infrastructure.Ref == nil {
   153  		return "", fmt.Errorf("cluster class %s/%s has an unset spec.infrastructure.ref field", clusterClass.Namespace, clusterClass.Name)
   154  	}
   155  	infraProvider := clusterClass.Spec.Infrastructure.Ref.Kind
   156  	if infraProvider == "" {
   157  		return "", fmt.Errorf("cluster class %s/%s has an empty infrastructure provider", clusterClass.Namespace, clusterClass.Name)
   158  	}
   159  
   160  	// Get control plane provider
   161  	if clusterClass.Spec.ControlPlane.Ref == nil {
   162  		return "", fmt.Errorf("cluster class %s/%s has an unset spec.controlPlane.ref field", clusterClass.Namespace, clusterClass.Name)
   163  	}
   164  	cpProvider := clusterClass.Spec.ControlPlane.Ref.Kind
   165  	if cpProvider == "" {
   166  		return "", fmt.Errorf("cluster class %s/%s has an empty control plane provider", clusterClass.Namespace, clusterClass.Name)
   167  	}
   168  
   169  	// Remove the "Template" suffix from the provider names
   170  	infraProvider = strings.TrimSuffix(infraProvider, "Template")
   171  	cpProvider = strings.TrimSuffix(cpProvider, "Template")
   172  
   173  	return r.formProviderDisplayString(infraProvider, cpProvider), nil
   174  }
   175  
   176  // formProviderDisplayString forms the display string for the VMC's status.provider field, given the infrastructure and
   177  // control plane provider strings
   178  func (r *VerrazzanoManagedClusterReconciler) formProviderDisplayString(infraProvider, cpProvider string) string {
   179  	// Use specialized strings for OKE and OCNE special cases
   180  	if infraProvider == capi.OCNEInfrastructureProvider && cpProvider == capi.OCNEControlPlaneProvider {
   181  		return ocneProviderDisplayName
   182  	} else if infraProvider == capi.OKEInfrastructureProvider && cpProvider == capi.OKEControlPlaneProvider {
   183  		return okeProviderDisplayName
   184  	}
   185  	// Otherwise, return this generic format for the provider display string
   186  	provider := fmt.Sprintf("%s on %s Infrastructure", cpProvider, infraProvider)
   187  	return provider
   188  }
   189  
   190  // updateState sets the vmc.Status.State for the given VMC.
   191  // The state field functions differently according to whether this VMC references an underlying ClusterAPI cluster.
   192  func (r *VerrazzanoManagedClusterReconciler) updateState(vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
   193  	// If there is no underlying CAPI cluster, set the state field based on the lastAgentConnectTime
   194  	if vmc.Status.ClusterRef == nil {
   195  		r.updateStateFromLastAgentConnectTime(vmc)
   196  		return nil
   197  	}
   198  
   199  	// If there is an underlying CAPI cluster, set the state field according to the phase of the CAPI cluster.
   200  	capiClusterPhase, err := r.getCAPIClusterPhase(vmc.Status.ClusterRef)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	if capiClusterPhase != "" {
   205  		vmc.Status.State = capiClusterPhase
   206  	}
   207  	return nil
   208  }
   209  
   210  // updateStateFromLastAgentConnectTime sets the vmc.Status.State according to the lastAgentConnectTime,
   211  // setting possible values of Active, Inactive, or Pending.
   212  func (r *VerrazzanoManagedClusterReconciler) updateStateFromLastAgentConnectTime(vmc *clustersv1alpha1.VerrazzanoManagedCluster) {
   213  	if vmc.Status.LastAgentConnectTime != nil {
   214  		currentTime := metav1.Now()
   215  		// Using the current plus added time to find the difference with lastAgentConnectTime to validate
   216  		// if it exceeds the max allowed time before changing the state of the vmc resource.
   217  		maxPollingTime := currentTime.Add(vzconstants.VMCAgentPollingTimeInterval * vzconstants.MaxTimesVMCAgentPollingTime)
   218  		timeDiff := maxPollingTime.Sub(vmc.Status.LastAgentConnectTime.Time)
   219  		if int(timeDiff.Minutes()) > vzconstants.MaxTimesVMCAgentPollingTime {
   220  			vmc.Status.State = clustersv1alpha1.StateInactive
   221  		} else if vmc.Status.State == "" {
   222  			vmc.Status.State = clustersv1alpha1.StatePending
   223  		} else {
   224  			vmc.Status.State = clustersv1alpha1.StateActive
   225  		}
   226  	}
   227  }
   228  
   229  // getCAPIClusterPhase returns the phase reported by the CAPI Cluster CR which is referenced by clusterRef.
   230  func (r *VerrazzanoManagedClusterReconciler) getCAPIClusterPhase(clusterRef *clustersv1alpha1.ClusterReference) (clustersv1alpha1.StateType, error) {
   231  	// Get the CAPI Cluster CR
   232  	clusterNamespacedName := types.NamespacedName{
   233  		Name:      clusterRef.Name,
   234  		Namespace: clusterRef.Namespace,
   235  	}
   236  	cluster := &capiv1beta1.Cluster{}
   237  	if err := r.Client.Get(context.TODO(), clusterNamespacedName, cluster); err != nil {
   238  		if errors.IsNotFound(err) {
   239  			return "", nil
   240  		}
   241  		return "", err
   242  	}
   243  
   244  	// Validate that the CAPI Phase is a proper StateType for the VMC
   245  	switch state := clustersv1alpha1.StateType(cluster.Status.Phase); state {
   246  	case clustersv1alpha1.StatePending,
   247  		clustersv1alpha1.StateProvisioning,
   248  		clustersv1alpha1.StateProvisioned,
   249  		clustersv1alpha1.StateDeleting,
   250  		clustersv1alpha1.StateUnknown,
   251  		clustersv1alpha1.StateFailed:
   252  		return state, nil
   253  	default:
   254  		r.log.Progressf("retrieved an invalid ClusterAPI Cluster phase of %s", state)
   255  		return clustersv1alpha1.StateUnknown, nil
   256  	}
   257  }
   258  
   259  // shouldUpdateK8sVersion determines if this VMC reconciler should update the VMC's Kubernetes version.
   260  func (r *VerrazzanoManagedClusterReconciler) shouldUpdateK8sVersion(vmc *clustersv1alpha1.VerrazzanoManagedCluster) (bool, error) {
   261  	// The VMC controller cannot update the Kubernetes version if this is not a CAPI cluster.
   262  	if vmc.Status.ClusterRef == nil {
   263  		return false, nil
   264  	}
   265  
   266  	// If Verrazzano is installed on the workload cluster, then let the verrazzano cluster agent handle updating the K8s version.
   267  	capiClusterName := types.NamespacedName{Name: vmc.Status.ClusterRef.Name, Namespace: vmc.Status.ClusterRef.Namespace}
   268  	capiClient, err := getCAPIClientFunc(context.TODO(), r.Client, capiClusterName, r.Scheme)
   269  	if err != nil {
   270  		return false, fmt.Errorf("failed to get client for ClusterAPI cluster %s: %v", capiClusterName, err)
   271  	}
   272  	vzList := &v1beta1.VerrazzanoList{}
   273  	if err = capiClient.List(context.TODO(), vzList, &clipkg.ListOptions{}); err != nil {
   274  		// If verrazzanos are either not found or if the verrazzano CRD is not defined on this cluster,
   275  		// then return true
   276  		vzGroupVersionResource := schema.GroupVersionResource{
   277  			Group:    v1beta1.SchemeGroupVersion.Group,
   278  			Version:  v1beta1.SchemeGroupVersion.Version,
   279  			Resource: "verrazzanos",
   280  		}
   281  		_, gvkErr := capiClient.RESTMapper().KindFor(vzGroupVersionResource)
   282  		if errors.IsNotFound(err) || gvkErr != nil {
   283  			return true, nil
   284  		}
   285  		return false, fmt.Errorf("error listing verrazzanos in ClusterAPI cluster %s: %v", capiClusterName, err)
   286  	}
   287  	if len(vzList.Items) > 0 {
   288  		return false, nil
   289  	}
   290  	return true, nil
   291  }
   292  
   293  // updateK8sVersionUsingCAPI updates the VMC's status.kubernetes.version field, retrieving the version from ClusterAPI CRs
   294  func (r *VerrazzanoManagedClusterReconciler) updateK8sVersionUsingCAPI(vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
   295  	// Get the CAPI Cluster CR
   296  	clusterNamespacedName := types.NamespacedName{
   297  		Name:      vmc.Status.ClusterRef.Name,
   298  		Namespace: vmc.Status.ClusterRef.Namespace,
   299  	}
   300  	cluster := &capiv1beta1.Cluster{}
   301  	if err := r.Client.Get(context.TODO(), clusterNamespacedName, cluster); err != nil {
   302  		if errors.IsNotFound(err) {
   303  			return nil
   304  		}
   305  		return err
   306  	}
   307  
   308  	// Get control plane ref
   309  	cpKind := cluster.Spec.ControlPlaneRef.Kind
   310  	cpAPIVersion := cluster.Spec.ControlPlaneRef.APIVersion
   311  	cpList := &unstructured.UnstructuredList{}
   312  	cpList.SetAPIVersion(cpAPIVersion)
   313  	cpList.SetKind(cpKind)
   314  	if err := r.List(context.TODO(), cpList, clipkg.InNamespace(clusterNamespacedName.Namespace)); err != nil {
   315  		return fmt.Errorf("error listing control plane objects: %v", err)
   316  	}
   317  	if len(cpList.Items) < 1 {
   318  		return fmt.Errorf("failed to find %s objects", cpKind)
   319  	}
   320  	k8sVersion, found, err := unstructured.NestedString(cpList.Items[0].Object, "status", "version")
   321  	if !found {
   322  		return fmt.Errorf("could not find status.version field in %s object", cpKind)
   323  	} else if err != nil {
   324  		return fmt.Errorf("error accessing status.version field in %s object: %v", cpKind, err)
   325  	}
   326  
   327  	// Set the K8s version in the VMC
   328  	vmc.Status.Kubernetes.Version = k8sVersion
   329  	return nil
   330  }