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 }