github.com/verrazzano/verrazzano@v1.7.1/cluster-operator/controllers/vmc/sync_thanos.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  	"encoding/json"
     9  	"fmt"
    10  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    11  
    12  	clustersv1alpha1 "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1"
    13  	"github.com/verrazzano/verrazzano/pkg/log/vzlog"
    14  	"github.com/verrazzano/verrazzano/pkg/vzcr"
    15  	"github.com/verrazzano/verrazzano/platform-operator/constants"
    16  	"github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/thanos"
    17  	"google.golang.org/protobuf/types/known/wrapperspb"
    18  	istionet "istio.io/api/networking/v1beta1"
    19  	istioclinet "istio.io/client-go/pkg/apis/networking/v1beta1"
    20  	appsv1 "k8s.io/api/apps/v1"
    21  	v1 "k8s.io/api/core/v1"
    22  	k8sapiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/types"
    25  	controllerruntime "sigs.k8s.io/controller-runtime"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  	"sigs.k8s.io/yaml"
    28  )
    29  
    30  const (
    31  	serviceEntryCRDName    = "serviceentries.networking.istio.io"
    32  	destinationRuleCRDName = "destinationrules.networking.istio.io"
    33  	thanosQueryDeployName  = "thanos-query"
    34  	verrazzanoManagedLabel = "verrazzano_cluster"
    35  	thanosGrpcIngressPort  = 443
    36  
    37  	istioVolumeAnnotation      = "sidecar.istio.io/userVolume"
    38  	istioVolumeMountAnnotation = "sidecar.istio.io/userVolumeMount"
    39  	istioVolumeName            = "managed-certs"
    40  	istioCertPath              = "/etc/certs"
    41  )
    42  
    43  // thanosServiceDiscovery represents one element in the Thanos service discovery YAML. The YAML
    44  // format contains a list of thanosServiceDiscovery elements
    45  // The format of this object is outlined here https://github.com/thanos-io/thanos/blob/main/docs/service-discovery.md#file-service-discovery
    46  type thanosServiceDiscovery struct {
    47  	Targets []string          `json:"targets"`
    48  	Labels  map[string]string `json:"labels"`
    49  }
    50  
    51  const ThanosManagedClusterEndpointsConfigMap = "verrazzano-thanos-endpoints"
    52  const serviceDiscoveryKey = "servicediscovery.yml"
    53  
    54  // syncThanosQuery will perform the necessary sync to make sure Thanos Query on admin cluster can
    55  // talk to Thanos Query on managed cluster (this involves updating the endpoints ConfigMap and
    56  // the Istio config needed for TLS communication to managed cluster)
    57  // TODO - we will also need to add the cluster's CA cert for Thanos Query to use
    58  func (r *VerrazzanoManagedClusterReconciler) syncThanosQuery(ctx context.Context,
    59  	vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
    60  
    61  	if err := r.syncThanosQueryEndpoint(ctx, vmc); err != nil {
    62  		return err
    63  	}
    64  	if err := r.createOrUpdateCACertVolume(vmc, r.addCACertToDeployment); err != nil {
    65  		return err
    66  	}
    67  	if err := r.createOrUpdateServiceEntry(vmc.Name, vmc.Status.ThanosQueryStore, thanosGrpcIngressPort); err != nil {
    68  		return err
    69  	}
    70  	return r.createOrUpdateDestinationRule(vmc, vmc.Status.ThanosQueryStore, thanosGrpcIngressPort)
    71  }
    72  
    73  // syncThanosQueryEndpoint will update the config map used by Thanos Query with the managed cluster
    74  // Thanos store API endpoint.
    75  func (r *VerrazzanoManagedClusterReconciler) syncThanosQueryEndpoint(ctx context.Context,
    76  	vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
    77  	return r.addThanosHostIfNotPresent(ctx, vmc.Status.ThanosQueryStore, vmc.Name)
    78  }
    79  
    80  func (r *VerrazzanoManagedClusterReconciler) syncThanosQueryEndpointDelete(ctx context.Context, vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
    81  	if err := r.removeThanosHostFromConfigMap(ctx, vmc.Name, r.log); err != nil {
    82  		return err
    83  	}
    84  	if err := r.deleteDestinationRule(vmc.Name); err != nil {
    85  		return err
    86  	}
    87  	if err := r.deleteServiceEntry(vmc.Name); err != nil {
    88  		return err
    89  	}
    90  	return nil
    91  }
    92  
    93  func (r *VerrazzanoManagedClusterReconciler) removeThanosHostFromConfigMap(ctx context.Context, vmcName string, log vzlog.VerrazzanoLogger) error {
    94  	// Check if Thanos is enabled before getting the endpoints ConfigMap
    95  	// to avoid repeating error message when Thanos is disabled
    96  	thanosEnabled, err := r.isThanosEnabled()
    97  	if err != nil || !thanosEnabled {
    98  		return err
    99  	}
   100  	configMap, err := r.getThanosEndpointsConfigMap(ctx)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	serviceDiscoveryList, err := parseThanosEndpointsConfigMap(configMap, log)
   105  	if err != nil {
   106  		// nothing to do - we can't remove entries from an invalid config map - next time an add happens,
   107  		// we will try to automatically resolve the issue
   108  		return nil
   109  	}
   110  
   111  	for i, serviceDiscovery := range serviceDiscoveryList {
   112  		if findLabelName(serviceDiscovery, vmcName) {
   113  			serviceDiscoveryList = append(serviceDiscoveryList[:i], serviceDiscoveryList[i+1:]...)
   114  			return r.createOrUpdateThanosEndpointConfigMap(ctx, serviceDiscoveryList, vmcName, configMap)
   115  		}
   116  	}
   117  	return nil
   118  }
   119  
   120  func (r *VerrazzanoManagedClusterReconciler) createOrUpdateThanosEndpointConfigMap(ctx context.Context, serviceDiscoveryList []*thanosServiceDiscovery, vmcName string, configMap *v1.ConfigMap) error {
   121  	newServiceDiscoveryYaml, err := yaml.Marshal(serviceDiscoveryList)
   122  	if err != nil {
   123  		return r.log.ErrorfNewErr("Failed to serialize Thanos endpoints config map content for VMC %s: %v", vmcName, err)
   124  	}
   125  	result, err := controllerruntime.CreateOrUpdate(ctx, r.Client, configMap, func() error {
   126  		configMap.Data[serviceDiscoveryKey] = string(newServiceDiscoveryYaml)
   127  		return nil
   128  	})
   129  	if err != nil {
   130  		return r.log.ErrorfNewErr("Failed to update Thanos endpoints config map after removing endpoint for VMC %s: %v", vmcName, err)
   131  	}
   132  	if result != controllerutil.OperationResultNone {
   133  		r.log.Infof("The Thanos endpoint Configmap %s has been modified for VMC %s", configMap.Name, vmcName)
   134  	}
   135  	return nil
   136  }
   137  
   138  func (r *VerrazzanoManagedClusterReconciler) addThanosHostIfNotPresent(ctx context.Context, host, vmcName string) error {
   139  	configMap, err := r.getThanosEndpointsConfigMap(ctx)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	serviceDiscoveryList, err := parseThanosEndpointsConfigMap(configMap, r.log)
   144  	if err != nil {
   145  		// We will wipe out and repopulate the config map if it could not be parsed
   146  		r.log.Info("Clearing and repopulating Thanos endpoints ConfigMap due to parse error")
   147  		serviceDiscoveryList = []*thanosServiceDiscovery{}
   148  	}
   149  	hostEndpoint := toGrpcTarget(host)
   150  
   151  	for i, serviceDiscovery := range serviceDiscoveryList {
   152  		if findLabelAndHost(serviceDiscovery, hostEndpoint, vmcName) {
   153  			// already exists, nothing to be added
   154  			r.log.Debugf("Managed cluster endpoint %s is already present in the Thanos endpoints config map", hostEndpoint)
   155  			return nil
   156  		}
   157  		if findLabelName(serviceDiscovery, vmcName) {
   158  			// label exists, but host has changed
   159  			r.log.Debugf("Modifying managed cluster endpoint %s to Thanos endpoints for VMC %s", hostEndpoint, vmcName)
   160  			serviceDiscoveryList[i] = &thanosServiceDiscovery{
   161  				Targets: []string{hostEndpoint},
   162  				Labels:  serviceDiscovery.Labels,
   163  			}
   164  			return r.createOrUpdateThanosEndpointConfigMap(ctx, serviceDiscoveryList, vmcName, configMap)
   165  		}
   166  	}
   167  	// not found, add this host endpoint and update the config map
   168  	r.log.Debugf("Adding managed cluster endpoint %s to Thanos endpoints config map", hostEndpoint)
   169  	serviceDiscoveryList = append(serviceDiscoveryList, &thanosServiceDiscovery{
   170  		Targets: []string{hostEndpoint},
   171  		Labels: map[string]string{
   172  			verrazzanoManagedLabel: vmcName,
   173  		},
   174  	})
   175  	return r.createOrUpdateThanosEndpointConfigMap(ctx, serviceDiscoveryList, vmcName, configMap)
   176  }
   177  
   178  func findLabelAndHost(serviceDiscovery *thanosServiceDiscovery, host, name string) bool {
   179  	if val, ok := serviceDiscovery.Labels[verrazzanoManagedLabel]; !ok || val != name {
   180  		return false
   181  	}
   182  	for _, target := range serviceDiscovery.Targets {
   183  		if target == host {
   184  			return true
   185  		}
   186  	}
   187  	return false
   188  }
   189  
   190  // findLabelName parses the service discovery labels and matches it with a given name
   191  func findLabelName(serviceDiscovery *thanosServiceDiscovery, name string) bool {
   192  	val, ok := serviceDiscovery.Labels[verrazzanoManagedLabel]
   193  	return ok && val == name
   194  }
   195  
   196  func parseThanosEndpointsConfigMap(configMap *v1.ConfigMap, log vzlog.VerrazzanoLogger) ([]*thanosServiceDiscovery, error) {
   197  	// ConfigMap format for Thanos endpoints is
   198  	// servicediscovery.yml: |
   199  	//  - targets:
   200  	//    - example.com:443
   201  	//    labels:
   202  	//      verrazzano_cluster: managed
   203  	// The format is outlined here: https://github.com/thanos-io/thanos/blob/main/docs/service-discovery.md#file-service-discovery
   204  	serviceDiscoveryYaml, exists := configMap.Data[serviceDiscoveryKey]
   205  	serviceDiscoveryArray := []*thanosServiceDiscovery{}
   206  	var err error
   207  	if exists {
   208  		err = yaml.Unmarshal([]byte(serviceDiscoveryYaml), &serviceDiscoveryArray)
   209  		// TODO if parse fails wipe it out and let it be repopulated
   210  		if err != nil {
   211  			return nil, log.ErrorfNewErr("Failed to parse Thanos endpoints config map %s/%s, error: %v", configMap.Namespace, configMap.Name, err)
   212  		}
   213  	}
   214  	return serviceDiscoveryArray, nil
   215  }
   216  
   217  func (r *VerrazzanoManagedClusterReconciler) getThanosEndpointsConfigMap(ctx context.Context) (*v1.ConfigMap, error) {
   218  	configMapNsn := types.NamespacedName{
   219  		Namespace: thanos.ComponentNamespace,
   220  		Name:      ThanosManagedClusterEndpointsConfigMap,
   221  	}
   222  	configMap := v1.ConfigMap{}
   223  	if err := r.Get(ctx, configMapNsn, &configMap); err != nil {
   224  		r.log.Errorf("failed to fetch the Thanos endpoints ConfigMap %s/%s, %v", configMapNsn.Namespace, configMapNsn.Name, err)
   225  		return nil, err
   226  	}
   227  	return &configMap, nil
   228  }
   229  
   230  func (r *VerrazzanoManagedClusterReconciler) isThanosEnabled() (bool, error) {
   231  	vz, err := r.getVerrazzanoResource()
   232  	if err != nil {
   233  		r.log.Errorf("Failed to retrieve Verrazzano CR: %v", err)
   234  		return false, err
   235  	}
   236  	return vzcr.IsThanosEnabled(vz), nil
   237  }
   238  
   239  func toGrpcTarget(hostname string) string {
   240  	return fmt.Sprintf("%s:%d", hostname, thanosGrpcIngressPort)
   241  }
   242  
   243  // createOrUpdateCACertVolume updates a volume on the Istio Proxy sidecar on the Thanos Deployment to supply CA certs from managed clusters
   244  // This CA cert will be applied to the Destination rule to allow TLS communication to managed cluster query endpoints
   245  func (r *VerrazzanoManagedClusterReconciler) createOrUpdateCACertVolume(vmc *clustersv1alpha1.VerrazzanoManagedCluster, mutation func(*appsv1.Deployment, *clustersv1alpha1.VerrazzanoManagedCluster) error) error {
   246  	queryDeploy := appsv1.Deployment{
   247  		ObjectMeta: metav1.ObjectMeta{
   248  			Namespace: constants.VerrazzanoMonitoringNamespace,
   249  			Name:      thanosQueryDeployName,
   250  		},
   251  	}
   252  	_, err := controllerruntime.CreateOrUpdate(context.TODO(), r.Client, &queryDeploy, func() error {
   253  		return mutation(&queryDeploy, vmc)
   254  	})
   255  	return err
   256  }
   257  
   258  func (r *VerrazzanoManagedClusterReconciler) addCACertToDeployment(queryDeploy *appsv1.Deployment, vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
   259  	if queryDeploy.Spec.Template.ObjectMeta.Annotations == nil {
   260  		queryDeploy.Spec.Template.ObjectMeta.Annotations = map[string]string{}
   261  	}
   262  	istioVolume, err := r.unmarshalIstioVolumesAnnotation(*queryDeploy)
   263  	if err != nil {
   264  		// Clear the Volume annotation because it has been corrupted
   265  		delete(queryDeploy.Spec.Template.ObjectMeta.Annotations, istioVolumeAnnotation)
   266  		return nil
   267  	}
   268  
   269  	if err := r.createCAVolume(queryDeploy, istioVolume, vmc); err != nil {
   270  		return err
   271  	}
   272  	return r.createCAVolumeMount(queryDeploy)
   273  }
   274  
   275  // createOrUpdateServiceEntry ensures that an Istio ServiceEntry exists for a managed cluster Thanos endpoint. The ServiceEntry is
   276  // used along with a DestinationRule to initiate TLS to the managed cluster ingress. Skip processing if the ServiceEntry CRD
   277  // does not exist in the cluster.
   278  func (r *VerrazzanoManagedClusterReconciler) createOrUpdateServiceEntry(name, host string, port uint32) error {
   279  	isInstalled, err := r.isCRDInstalled(serviceEntryCRDName)
   280  	if err != nil {
   281  		return r.log.ErrorfNewErr("Unable to determine if CRD %s is installed: %v", serviceEntryCRDName, err)
   282  	}
   283  	if !isInstalled {
   284  		r.log.Debugf("CRD %s does not exist in cluster, skipping creating/updating ServiceEntry", serviceEntryCRDName)
   285  		return nil
   286  	}
   287  
   288  	// NOTE: We cannot use controller-runtime CreateOrUpdate here because DeepEqual does not work with protobuf-generated types
   289  	se := &istioclinet.ServiceEntry{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: constants.VerrazzanoMonitoringNamespace}}
   290  
   291  	// get the ServiceEntry, if it exists we update it, if it does not exist we create it
   292  	err = r.Client.Get(context.TODO(), client.ObjectKey{Namespace: constants.VerrazzanoMonitoringNamespace, Name: name}, se)
   293  	if client.IgnoreNotFound(err) != nil {
   294  		return r.log.ErrorfNewErr("Unable to get ServiceEntry %s/%s: %v", constants.VerrazzanoMonitoringNamespace, name, err)
   295  	}
   296  	if err == nil {
   297  		// we should do some basic fields checks and only update if there are changes, but for now this will have to do
   298  		populateServiceEntry(se, host, port)
   299  		if err = r.Client.Update(context.TODO(), se); err != nil {
   300  			return r.log.ErrorfNewErr("Unable to update ServiceEntry %s/%s: %v", constants.VerrazzanoMonitoringNamespace, name, err)
   301  		}
   302  		return nil
   303  	}
   304  
   305  	populateServiceEntry(se, host, port)
   306  	if err = r.Client.Create(context.TODO(), se); err != nil {
   307  		return r.log.ErrorfNewErr("Unable to create ServiceEntry %s/%s: %v", constants.VerrazzanoMonitoringNamespace, name, err)
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func populateServiceEntry(se *istioclinet.ServiceEntry, host string, port uint32) {
   314  	se.Spec.Hosts = []string{host}
   315  	se.Spec.Ports = []*istionet.ServicePort{
   316  		{
   317  			Name:       "grpc",
   318  			Number:     port,
   319  			TargetPort: port,
   320  			Protocol:   "GRPC",
   321  		},
   322  	}
   323  	se.Spec.Resolution = istionet.ServiceEntry_DNS
   324  }
   325  
   326  // createOrUpdateDestinationRule ensures that an Istio DestinationRule exists for a managed cluster Thanos endpoint. The DestinationRule is
   327  // used along with a ServiceEntry to initiate TLS to the managed cluster ingress. Skip processing if the DestinationRule CRD
   328  // does not exist in the cluster.
   329  func (r *VerrazzanoManagedClusterReconciler) createOrUpdateDestinationRule(vmc *clustersv1alpha1.VerrazzanoManagedCluster, host string, port uint32) error {
   330  	isInstalled, err := r.isCRDInstalled(destinationRuleCRDName)
   331  	if err != nil {
   332  		r.log.Errorf("Unable to determine if CRD %s is installed: %v", destinationRuleCRDName, err)
   333  		return err
   334  	}
   335  	if !isInstalled {
   336  		r.log.Debugf("CRD %s does not exist in cluster, skipping creating/updating DestinationRule", destinationRuleCRDName)
   337  		return nil
   338  	}
   339  
   340  	// NOTE: We cannot use controller-runtime CreateOrUpdate here because DeepEqual does not work with protobuf-generated types
   341  	dr := &istioclinet.DestinationRule{ObjectMeta: metav1.ObjectMeta{Name: vmc.Name, Namespace: constants.VerrazzanoMonitoringNamespace}}
   342  
   343  	// get the DestinationRule, if it exists we update it, if it does not exist we create it
   344  	err = r.Client.Get(context.TODO(), client.ObjectKey{Namespace: constants.VerrazzanoMonitoringNamespace, Name: vmc.Name}, dr)
   345  	if client.IgnoreNotFound(err) != nil {
   346  		return r.log.ErrorfNewErr("Unable to get DestinationRule %s/%s: %v", constants.VerrazzanoMonitoringNamespace, vmc.Name, err)
   347  	}
   348  	if err == nil {
   349  		// we should do some basic fields checks and only update if there are changes, but for now this will have to do
   350  		populateDestinationRule(dr, host, port, vmc)
   351  		if err = r.Client.Update(context.TODO(), dr); err != nil {
   352  			return r.log.ErrorfNewErr("Unable to update DestinationRule %s/%s: %v", constants.VerrazzanoMonitoringNamespace, vmc.Name, err)
   353  		}
   354  		return nil
   355  	}
   356  
   357  	populateDestinationRule(dr, host, port, vmc)
   358  	if err = r.Client.Create(context.TODO(), dr); err != nil {
   359  		return r.log.ErrorfNewErr("Unable to create DestinationRule %s/%s: %v", constants.VerrazzanoMonitoringNamespace, vmc.Name, err)
   360  	}
   361  
   362  	return nil
   363  }
   364  
   365  func populateDestinationRule(dr *istioclinet.DestinationRule, host string, port uint32, vmc *clustersv1alpha1.VerrazzanoManagedCluster) {
   366  	dr.Spec.Host = host
   367  	dr.Spec.TrafficPolicy = &istionet.TrafficPolicy{
   368  		PortLevelSettings: []*istionet.TrafficPolicy_PortTrafficPolicy{
   369  			{
   370  				Port: &istionet.PortSelector{
   371  					Number: port,
   372  				},
   373  				Tls: &istionet.ClientTLSSettings{
   374  					Mode:               istionet.ClientTLSSettings_SIMPLE,
   375  					InsecureSkipVerify: wrapperspb.Bool(false),
   376  					CaCertificates:     fmt.Sprintf("%s/%s", istioCertPath, getCAKey(vmc)),
   377  					Sni:                host,
   378  				},
   379  			},
   380  		},
   381  	}
   382  }
   383  
   384  // isCRDInstalled returns true if the named CRD exists in the cluster, otherwise false.
   385  func (r *VerrazzanoManagedClusterReconciler) isCRDInstalled(crdName string) (bool, error) {
   386  	crd := &k8sapiext.CustomResourceDefinition{}
   387  	err := r.Client.Get(context.TODO(), client.ObjectKey{Name: crdName}, crd)
   388  	if client.IgnoreNotFound(err) != nil {
   389  		return false, err
   390  	}
   391  	return err == nil, nil
   392  }
   393  
   394  // deleteServiceEntry deletes an Istio ServiceEntry. No error is returned if the ServiceEntry is not found.
   395  func (r *VerrazzanoManagedClusterReconciler) deleteServiceEntry(name string) error {
   396  	isInstalled, err := r.isCRDInstalled(serviceEntryCRDName)
   397  	if err != nil {
   398  		r.log.Errorf("Unable to determine if CRD %s is installed: %v", serviceEntryCRDName, err)
   399  		return err
   400  	}
   401  	if !isInstalled {
   402  		r.log.Debugf("CRD %s does not exist in cluster, skipping creating/updating DestinationRule", serviceEntryCRDName)
   403  		return nil
   404  	}
   405  
   406  	se := &istioclinet.ServiceEntry{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: constants.VerrazzanoMonitoringNamespace}}
   407  	err = r.Client.Delete(context.TODO(), se)
   408  	return client.IgnoreNotFound(err)
   409  }
   410  
   411  // deleteDestinationRule deletes an Istio DestinationRule. No error is returned if the DestinationRule is not found.
   412  func (r *VerrazzanoManagedClusterReconciler) deleteDestinationRule(name string) error {
   413  	isInstalled, err := r.isCRDInstalled(destinationRuleCRDName)
   414  	if err != nil {
   415  		r.log.Errorf("Unable to determine if CRD %s is installed: %v", destinationRuleCRDName, err)
   416  		return err
   417  	}
   418  	if !isInstalled {
   419  		r.log.Debugf("CRD %s does not exist in cluster, skipping creating/updating DestinationRule", destinationRuleCRDName)
   420  		return nil
   421  	}
   422  
   423  	dr := &istioclinet.DestinationRule{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: constants.VerrazzanoMonitoringNamespace}}
   424  	err = r.Client.Delete(context.TODO(), dr)
   425  	return client.IgnoreNotFound(err)
   426  }
   427  
   428  // createCAVolumeMount Adds the CA Volume mount as an Istio annotation to the deployment if it does not exist
   429  func (r *VerrazzanoManagedClusterReconciler) createCAVolumeMount(deployment *appsv1.Deployment) error {
   430  	if _, ok := deployment.Spec.Template.Annotations[istioVolumeMountAnnotation]; !ok {
   431  		volumeMount := []v1.VolumeMount{
   432  			{
   433  				Name:      istioVolumeName,
   434  				MountPath: istioCertPath,
   435  			},
   436  		}
   437  		volumeMountJSON, err := json.Marshal(volumeMount)
   438  		if err != nil {
   439  			return r.log.ErrorfNewErr("Failed to marshal VolumeMount object for Volume %s Istio Annotation: %v", istioVolumeName, err)
   440  		}
   441  		deployment.Spec.Template.Annotations[istioVolumeMountAnnotation] = string(volumeMountJSON)
   442  	}
   443  	return nil
   444  }
   445  
   446  // unmarshalIstioVolumesAnnotations returns the Volumes pod annotations from a deployment object
   447  func (r *VerrazzanoManagedClusterReconciler) unmarshalIstioVolumesAnnotation(deployment appsv1.Deployment) (v1.Volume, error) {
   448  	istioVolume := []v1.Volume{{Name: istioVolumeName}}
   449  	podAnnotations := deployment.Spec.Template.ObjectMeta.Annotations
   450  	if volumeJSON, volumeOk := podAnnotations[istioVolumeAnnotation]; volumeOk {
   451  		err := json.Unmarshal([]byte(volumeJSON), &istioVolume)
   452  		if err != nil {
   453  			return istioVolume[0], r.log.ErrorfNewErr("Failed to unmarshal the Istio volume annotation, clearing the volume annotations due to corruption: %v", err)
   454  		}
   455  	}
   456  	return istioVolume[0], nil
   457  }
   458  
   459  // createCAVolume adds the CA mount given a Volume object from the Istio annotations and creates the annotation on the deployment
   460  func (r *VerrazzanoManagedClusterReconciler) createCAVolume(deployment *appsv1.Deployment, volume v1.Volume, vmc *clustersv1alpha1.VerrazzanoManagedCluster) error {
   461  	if volume.Secret == nil {
   462  		volume.Secret = &v1.SecretVolumeSource{
   463  			SecretName: constants.PromManagedClusterCACertsSecretName,
   464  		}
   465  	}
   466  	volumeJSON, err := json.Marshal([]v1.Volume{volume})
   467  	if err != nil {
   468  		return r.log.ErrorfNewErr("Failed to marshal Volume %s Istio Annotation: %v", istioVolumeName, err)
   469  	}
   470  	deployment.Spec.Template.Annotations[istioVolumeAnnotation] = string(volumeJSON)
   471  	return nil
   472  }