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 }