github.com/verrazzano/verrazzano@v1.7.0/application-operator/mcagent/mcagent_syncer.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 mcagent 5 6 import ( 7 "context" 8 "fmt" 9 "reflect" 10 "strings" 11 "time" 12 13 clustersv1alpha1 "github.com/verrazzano/verrazzano/application-operator/apis/clusters/v1alpha1" 14 "github.com/verrazzano/verrazzano/application-operator/constants" 15 "github.com/verrazzano/verrazzano/application-operator/controllers/clusters" 16 "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1" 17 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 18 "github.com/verrazzano/verrazzano/pkg/k8sutil" 19 "github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/v1beta1" 20 "go.uber.org/zap" 21 corev1 "k8s.io/api/core/v1" 22 v13 "k8s.io/api/networking/v1" 23 v12 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 24 "k8s.io/apimachinery/pkg/api/errors" 25 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/labels" 27 "k8s.io/apimachinery/pkg/types" 28 "k8s.io/client-go/discovery" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 ) 31 32 // Syncer contains context for synchronize operations 33 type Syncer struct { 34 AdminClient client.Client 35 LocalClient client.Client 36 LocalDiscoveryClient discovery.DiscoveryInterface 37 Log *zap.SugaredLogger 38 ManagedClusterName string 39 Context context.Context 40 41 // List of namespaces to watch for multi-cluster objects. 42 ProjectNamespaces []string 43 StatusUpdateChannel chan clusters.StatusUpdateMessage 44 } 45 46 type adminStatusUpdateFuncType = func(name types.NamespacedName, newCond clustersv1alpha1.Condition, newClusterStatus clustersv1alpha1.ClusterLevelStatus) error 47 48 const retryCount = 3 49 const managedClusterLabel = "verrazzano.io/managed-cluster" 50 const mcAppConfigsLabel = "verrazzano.io/mc-app-configs" 51 52 var ( 53 retryDelay = 3 * time.Second 54 ) 55 56 // Needed for unit testing 57 var getDiscoveryClientFunc = defaultGetDiscoveryClientFunc 58 59 func defaultGetDiscoveryClientFunc() (discovery.DiscoveryInterface, error) { 60 config, err := k8sutil.GetConfigFromController() 61 if err != nil { 62 return nil, fmt.Errorf("failed to get Kubeconfig for this workload cluster: %v", err) 63 } 64 return discovery.NewDiscoveryClientForConfig(config) 65 } 66 67 func setDiscoveryClientFunc(f func() (discovery.DiscoveryInterface, error)) { 68 getDiscoveryClientFunc = f 69 } 70 71 // Check if the placement is for this cluster 72 func (s *Syncer) isThisCluster(placement clustersv1alpha1.Placement) bool { 73 // Loop through the cluster list looking for the cluster name 74 for _, cluster := range placement.Clusters { 75 if cluster.Name == s.ManagedClusterName { 76 return true 77 } 78 } 79 return false 80 } 81 82 // processStatusUpdates monitors the StatusUpdateChannel for any 83 // received messages and processes a batch of them 84 func (s *Syncer) processStatusUpdates() { 85 for i := 0; i < constants.StatusUpdateBatchSize; i++ { 86 // Use a select with default so as to not block on the channel if there are no updates 87 select { 88 case msg := <-s.StatusUpdateChannel: 89 err := s.performAdminStatusUpdate(msg) 90 if err != nil { 91 s.Log.Errorf("Failed to update status on admin cluster for %s/%s from cluster %s after %d retries: %v", 92 msg.Resource.GetNamespace(), msg.Resource.GetName(), 93 msg.NewClusterStatus.Name, retryCount, err) 94 } 95 default: 96 break 97 } 98 } 99 } 100 101 // getVerrazzanoManagedNamespaces - return the list of namespaces that have the Verrazzano managed label set to true 102 func (s *Syncer) getManagedNamespaces() ([]string, error) { 103 nsListSelector, err := labels.Parse(fmt.Sprintf("%s=%s", vzconst.VerrazzanoManagedLabelKey, constants.LabelVerrazzanoManagedDefault)) 104 if err != nil { 105 return nil, fmt.Errorf("failed to create list selector on local cluster: %v", err) 106 } 107 listOptionsGC := &client.ListOptions{LabelSelector: nsListSelector} 108 109 // Get the list of namespaces that were created or managed by VerrazzanoProjects 110 vpNamespaceList := corev1.NamespaceList{} 111 err = s.LocalClient.List(s.Context, &vpNamespaceList, listOptionsGC) 112 if err != nil { 113 return nil, fmt.Errorf("failed to get list of Verrazzano managed namespaces: %v", err) 114 } 115 116 // Convert the result to a list of strings 117 var nsList []string 118 for _, namespace := range vpNamespaceList.Items { 119 nsList = append(nsList, namespace.Name) 120 } 121 122 return nsList, nil 123 } 124 125 func (s *Syncer) performAdminStatusUpdate(msg clusters.StatusUpdateMessage) error { 126 fullResourceName := types.NamespacedName{Name: msg.Resource.GetName(), Namespace: msg.Resource.GetNamespace()} 127 typeName := reflect.TypeOf(msg.Resource).String() 128 var statusUpdateFunc adminStatusUpdateFuncType 129 if strings.Contains(typeName, reflect.TypeOf(clustersv1alpha1.MultiClusterApplicationConfiguration{}).String()) { 130 statusUpdateFunc = s.updateMultiClusterAppConfigStatus 131 } else if strings.Contains(typeName, reflect.TypeOf(clustersv1alpha1.MultiClusterComponent{}).String()) { 132 statusUpdateFunc = s.updateMultiClusterComponentStatus 133 } else if strings.Contains(typeName, reflect.TypeOf(clustersv1alpha1.MultiClusterConfigMap{}).String()) { 134 statusUpdateFunc = s.updateMultiClusterConfigMapStatus 135 } else if strings.Contains(typeName, reflect.TypeOf(clustersv1alpha1.MultiClusterSecret{}).String()) { 136 statusUpdateFunc = s.updateMultiClusterSecretStatus 137 } else if strings.Contains(typeName, reflect.TypeOf(clustersv1alpha1.VerrazzanoProject{}).String()) { 138 statusUpdateFunc = s.updateVerrazzanoProjectStatus 139 } else { 140 return fmt.Errorf("received status update message for unknown resource type %s", typeName) 141 } 142 return s.adminStatusUpdateWithRetry(statusUpdateFunc, fullResourceName, msg.NewCondition, msg.NewClusterStatus) 143 } 144 145 func (s *Syncer) adminStatusUpdateWithRetry(statusUpdateFunc adminStatusUpdateFuncType, 146 name types.NamespacedName, 147 condition clustersv1alpha1.Condition, 148 clusterStatus clustersv1alpha1.ClusterLevelStatus) error { 149 var err error 150 for tries := 0; tries < retryCount; tries++ { 151 err = statusUpdateFunc(name, condition, clusterStatus) 152 if err == nil { 153 break 154 } 155 if !errors.IsConflict(err) { 156 break 157 } 158 159 time.Sleep(retryDelay) 160 } 161 return err 162 } 163 164 func (s *Syncer) updateVMCStatus() error { 165 vmcName := client.ObjectKey{Name: s.ManagedClusterName, Namespace: constants.VerrazzanoMultiClusterNamespace} 166 vmc := v1alpha1.VerrazzanoManagedCluster{} 167 err := s.AdminClient.Get(s.Context, vmcName, &vmc) 168 if err != nil { 169 return err 170 } 171 172 curTime := v1.Now() 173 vmc.Status.LastAgentConnectTime = &curTime 174 apiURL, err := s.getAPIServerURL() 175 if err != nil { 176 return fmt.Errorf("Failed to get api server url for vmc %s with error %v", vmcName, err) 177 } 178 179 vmc.Status.APIUrl = apiURL 180 prometheusHost, err := s.getPrometheusHost() 181 if err != nil { 182 return fmt.Errorf("Failed to get api prometheus host to update VMC %s: %v", vmcName, err) 183 } 184 if prometheusHost != "" { 185 vmc.Status.PrometheusHost = prometheusHost 186 } 187 188 // Get the Thanos API ingress URL from the local managed cluster, and populate 189 // it in the VMC status on the admin cluster, so that admin cluster's Thanos query wire up 190 // to the managed cluster 191 thanosAPIHost, err := s.getThanosQueryStoreAPIHost() 192 if err != nil { 193 return fmt.Errorf("Failed to get Thanos query URL to update VMC %s: %v", vmcName, err) 194 } 195 196 // If Thanos is disabled, we want to empty the host so Prometheus federation returns 197 vmc.Status.ThanosQueryStore = thanosAPIHost 198 199 // Update the Kubernetes version on the VMC 200 k8sVersion, err := s.getWorkloadK8sVersion() 201 if err != nil { 202 return fmt.Errorf("Failed to get Kubernetes information to update VMC %s: %v", vmcName, err) 203 } 204 vmc.Status.Kubernetes.Version = k8sVersion 205 206 // Update the Verrazzano version on the VMC 207 vzVersion, err := s.getWorkloadVZVersion() 208 if err != nil { 209 return fmt.Errorf("Failed to get Verrazzano information to update VMC %s: %v", vmcName, err) 210 } 211 vmc.Status.Verrazzano.Version = vzVersion 212 213 // update status of VMC 214 return s.AdminClient.Status().Update(s.Context, &vmc) 215 } 216 217 // getWorkloadK8sVersion retrieves the current Kubernetes version on this managed cluster 218 func (s *Syncer) getWorkloadK8sVersion() (string, error) { 219 k8sVersion, err := s.LocalDiscoveryClient.ServerVersion() 220 if err != nil { 221 return "", fmt.Errorf("failed to get Kubernetes version on this workload cluster: %v", err) 222 } 223 return k8sVersion.String(), nil 224 } 225 226 // getWorkloadVZVersion retrieves the current VZ version on this managed cluster, 227 // as reported by the VZ CR's status 228 func (s *Syncer) getWorkloadVZVersion() (string, error) { 229 vzList := &v1beta1.VerrazzanoList{} 230 if err := s.LocalClient.List(s.Context, vzList, &client.ListOptions{}); err != nil { 231 return "", fmt.Errorf("error listing Verrazzanos: %v", err) 232 } 233 if len(vzList.Items) > 1 { 234 return "", fmt.Errorf("cannot have more than 1 Verrazzano installation, found %d", len(vzList.Items)) 235 } 236 if len(vzList.Items) == 0 { 237 // If there is no Verrazzano installed on this workload cluster, leave version empty but do not error 238 return "", nil 239 } 240 // Verrazzano is on this cluster, so get the version 241 vzState := string(vzList.Items[0].Status.Version) 242 return vzState, nil 243 } 244 245 // SyncMultiClusterResources - sync multi-cluster objects 246 func (s *Syncer) SyncMultiClusterResources() { 247 // if the MultiClusterApplicationConfiguration CRD does not exist, the other MC resources are 248 // unlikely to exist, and we don't need to sync the resources 249 mcAppConfCRD := v12.CustomResourceDefinition{} 250 if err := s.LocalClient.Get(s.Context, 251 types.NamespacedName{Name: mcAppConfCRDName}, &mcAppConfCRD); err != nil { 252 if errors.IsNotFound(err) { 253 s.Log.Debugf("CRD %s not found - skip syncing multicluster resources", mcAppConfCRDName) 254 return 255 } 256 s.Log.Errorf("Failed retrieving CRD %s: %v", mcAppConfCRDName, err) 257 } 258 err := s.syncVerrazzanoProjects() 259 if err != nil { 260 s.Log.Errorf("Failed syncing VerrazzanoProject objects: %v", err) 261 } 262 263 // Synchronize objects one namespace at a time 264 for _, namespace := range s.ProjectNamespaces { 265 err = s.syncSecretObjects(namespace) 266 if err != nil { 267 s.Log.Errorf("Failed to sync Secret objects: %v", err) 268 } 269 err = s.syncMCSecretObjects(namespace) 270 if err != nil { 271 s.Log.Errorf("Failed to sync MultiClusterSecret objects: %v", err) 272 } 273 err = s.syncMCConfigMapObjects(namespace) 274 if err != nil { 275 s.Log.Errorf("Failed to sync MultiClusterConfigMap objects: %v", err) 276 } 277 err = s.syncMCComponentObjects(namespace) 278 if err != nil { 279 s.Log.Errorf("Failed to sync MultiClusterComponent objects: %v", err) 280 } 281 err = s.syncMCApplicationConfigurationObjects(namespace) 282 if err != nil { 283 s.Log.Errorf("Failed to sync MultiClusterApplicationConfiguration objects: %v", err) 284 } 285 286 s.processStatusUpdates() 287 288 } 289 } 290 291 // getAPIServerURL returns the API Server URL for Verrazzano instance. 292 func (s *Syncer) getAPIServerURL() (string, error) { 293 ingress := &v13.Ingress{} 294 err := s.LocalClient.Get(context.TODO(), types.NamespacedName{Name: constants.VzConsoleIngress, Namespace: constants.VerrazzanoSystemNamespace}, ingress) 295 if err != nil { 296 if errors.IsNotFound(err) { 297 return "", nil 298 } 299 return "", fmt.Errorf("Unable to fetch ingress %s/%s, %v", constants.VerrazzanoSystemNamespace, constants.VzConsoleIngress, err) 300 } 301 return fmt.Sprintf("https://%s", ingress.Spec.Rules[0].Host), nil 302 } 303 304 // getPrometheusHost returns the prometheus host for Verrazzano instance. 305 func (s *Syncer) getPrometheusHost() (string, error) { 306 ingress := &v13.Ingress{} 307 err := s.LocalClient.Get(context.TODO(), types.NamespacedName{Name: constants.VzPrometheusIngress, Namespace: constants.VerrazzanoSystemNamespace}, ingress) 308 if err != nil { 309 if errors.IsNotFound(err) { 310 return "", nil 311 } 312 return "", fmt.Errorf("unable to fetch ingress %s/%s, %v", constants.VerrazzanoSystemNamespace, constants.VzPrometheusIngress, err) 313 } 314 return ingress.Spec.Rules[0].Host, nil 315 } 316 317 // getThanosQueryStoreAPIHost returns the Thanos Query Store API Endpoint URL for Verrazzano instance. 318 func (s *Syncer) getThanosQueryStoreAPIHost() (string, error) { 319 ingress := &v13.Ingress{} 320 err := s.LocalClient.Get(context.TODO(), types.NamespacedName{Name: vzconst.ThanosQueryStoreIngress, Namespace: constants.VerrazzanoSystemNamespace}, ingress) 321 if err != nil { 322 if errors.IsNotFound(err) { 323 return "", nil 324 } 325 return "", fmt.Errorf("unable to fetch ingress %s/%s, %v", constants.VerrazzanoSystemNamespace, constants.VzPrometheusIngress, err) 326 } 327 return ingress.Spec.Rules[0].Host, nil 328 }