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  }