github.com/verrazzano/verrazzano@v1.7.1/application-operator/mcagent/mcagent_cattle_agent.go (about)

     1  // Copyright (c) 2022, 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  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Jeffail/gabs/v2"
    15  	"github.com/verrazzano/verrazzano/pkg/constants"
    16  	"github.com/verrazzano/verrazzano/pkg/k8s/resource"
    17  	"github.com/verrazzano/verrazzano/pkg/k8sutil"
    18  	"github.com/verrazzano/verrazzano/platform-operator/controllers/verrazzano/component/common"
    19  	"go.uber.org/zap"
    20  	appsv1 "k8s.io/api/apps/v1"
    21  	corev1 "k8s.io/api/core/v1"
    22  	"k8s.io/apimachinery/pkg/api/errors"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"k8s.io/apimachinery/pkg/util/yaml"
    27  	"k8s.io/client-go/dynamic"
    28  	"k8s.io/client-go/kubernetes"
    29  	"k8s.io/client-go/rest"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  )
    32  
    33  const cattleAgent = "cattle-cluster-agent"
    34  const clusterreposName = "rancher-charts"
    35  
    36  var cattleClusterReposGVR = schema.GroupVersionResource{
    37  	Group:    "catalog.cattle.io",
    38  	Version:  "v1",
    39  	Resource: "clusterrepos",
    40  }
    41  
    42  // Override of getDeployment is for unit testing only
    43  var getDeploymentFunc = getDeployment
    44  
    45  func setDeploymentFunc(deployFunc func(config *rest.Config, namespace string, name string) (*appsv1.Deployment, error)) {
    46  	getDeploymentFunc = deployFunc
    47  }
    48  func resetDeploymentFunc() {
    49  	getDeploymentFunc = getDeployment
    50  }
    51  
    52  // syncCattleClusterAgent syncs the Rancher cattle-cluster-agent deployment
    53  // and the cattle-credentials secret from the admin cluster to the managed cluster
    54  // if they have changed in the registration-manifest
    55  func (s *Syncer) syncCattleClusterAgent(currentCattleAgentHash string, kubeconfigPath string) (string, error) {
    56  	manifestSecret := corev1.Secret{}
    57  	err := s.AdminClient.Get(s.Context, client.ObjectKey{
    58  		Namespace: constants.VerrazzanoMultiClusterNamespace,
    59  		Name:      getManifestSecretName(s.ManagedClusterName),
    60  	}, &manifestSecret)
    61  
    62  	if err != nil {
    63  		return currentCattleAgentHash, fmt.Errorf("failed to fetch manifest secret for %s cluster: %v", s.ManagedClusterName, err)
    64  	}
    65  	s.Log.Debugf(fmt.Sprintf("Found manifest secret for %s cluster: %s", s.ManagedClusterName, manifestSecret.Name))
    66  
    67  	manifestData := manifestSecret.Data["yaml"]
    68  	yamlSections := bytes.Split(manifestData, []byte("---\n"))
    69  
    70  	cattleAgentResource, cattleCredentialResource := checkForCattleResources(yamlSections)
    71  	if cattleAgentResource == nil || cattleCredentialResource == nil {
    72  		s.Log.Debugf("The registration manifest doesn't contain the required resources. Will try to update the cattle-cluster-agent in the next iteration")
    73  		return currentCattleAgentHash, nil
    74  	}
    75  
    76  	newCattleAgentHash := createHash(cattleAgentResource)
    77  
    78  	// If the rancher-webhook deployment does not exist, then this may be the first time the
    79  	// environment is being upgraded to Rancher 2.7.8 or higher. If the deployment does not exist
    80  	// then always update the cattle resources.
    81  	config, err := k8sutil.BuildKubeConfig(kubeconfigPath)
    82  	if err != nil {
    83  		s.Log.Errorf("failed to create incluster config: %v", err)
    84  		return currentCattleAgentHash, err
    85  	}
    86  	_, err = getDeploymentFunc(config, common.CattleSystem, "rancher-webhook")
    87  	if err != nil && !errors.IsNotFound(err) {
    88  		return currentCattleAgentHash, err
    89  	}
    90  	forceUpdate := false
    91  	if errors.IsNotFound(err) {
    92  		forceUpdate = true
    93  	}
    94  
    95  	// We have a previous hash to compare to
    96  	if !forceUpdate && len(currentCattleAgentHash) > 0 {
    97  		// If they are the same, do nothing
    98  		if currentCattleAgentHash == newCattleAgentHash {
    99  			return currentCattleAgentHash, nil
   100  		}
   101  	}
   102  
   103  	// No previous hash or the hash has changed
   104  	// Sync the cattle-agent and update the hash for next iterations
   105  	if forceUpdate {
   106  		s.Log.Info("Updating the cattle-cluster-agent because no rancher-webhook deployment found")
   107  	} else {
   108  		s.Log.Info("No previous cattle hash found or cattle hash has changed. Updating the cattle-cluster-agent")
   109  	}
   110  	err = updateCattleResources(cattleAgentResource, cattleCredentialResource, s.Log, config)
   111  	if err != nil {
   112  		return currentCattleAgentHash, fmt.Errorf("failed to update the cattle-cluster-agent on %s cluster: %v", s.ManagedClusterName, err)
   113  	}
   114  	s.Log.Infof("Successfully synched cattle-cluster-agent")
   115  
   116  	return newCattleAgentHash, nil
   117  }
   118  
   119  // checkForCattleResources iterates through the list of resources in the manifest yaml
   120  // and returns the cattle-cluster-agent deployment and cattle-credentials secret if found
   121  func checkForCattleResources(yamlData [][]byte) (*gabs.Container, *gabs.Container) {
   122  	var cattleAgentResource, cattleCredentialResource *gabs.Container
   123  	for _, eachResource := range yamlData {
   124  		json, _ := yaml.ToJSON(eachResource)
   125  		container, _ := gabs.ParseJSON(json)
   126  
   127  		name := strings.Trim(container.Path("metadata.name").String(), "\"")
   128  		namespace := strings.Trim(container.Path("metadata.namespace").String(), "\"")
   129  		kind := strings.Trim(container.Path("kind").String(), "\"")
   130  
   131  		if name == cattleAgent && namespace == constants.RancherSystemNamespace && kind == "Deployment" {
   132  			cattleAgentResource = container
   133  		} else if strings.Contains(name, "cattle-credentials-") && namespace == constants.RancherSystemNamespace && kind == "Secret" {
   134  			cattleCredentialResource = container
   135  		}
   136  	}
   137  
   138  	return cattleAgentResource, cattleCredentialResource
   139  }
   140  
   141  // updateCattleResources patches the cattle-cluster-agent and creates the cattle-credentials secret
   142  func updateCattleResources(cattleAgentResource *gabs.Container, cattleCredentialResource *gabs.Container, log *zap.SugaredLogger, config *rest.Config) error {
   143  	// Scale the cattle-cluster-agent deployment to 0
   144  	prevReplicas, err := scaleDownRancherAgentDeployment(config, log)
   145  	if err != nil {
   146  		log.Errorf("failed to scale %s deployment: %v", cattleAgent, err)
   147  		return err
   148  	}
   149  	if err = deleteClusterRepos(config); err != nil {
   150  		log.Error(err)
   151  		return err
   152  	}
   153  
   154  	cattleAgentResource.Set(prevReplicas, "spec", "replicas")
   155  	patch := cattleAgentResource.Bytes()
   156  	gvr := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
   157  	err = resource.PatchResourceFromBytes(gvr, types.StrategicMergePatchType, constants.RancherSystemNamespace, cattleAgent, patch, config)
   158  	if err != nil {
   159  		log.Errorf("failed to patch cattle-cluster-agent: %v", err)
   160  		return err
   161  	}
   162  
   163  	err = resource.CreateOrUpdateResourceFromBytesUsingConfig(cattleCredentialResource.Bytes(), config)
   164  	if err != nil {
   165  		log.Errorf("failed to create new cattle-credential: %v", err)
   166  		return err
   167  	}
   168  	log.Debugf("Successfully patched cattle-cluster-agent and created a new cattle-credential secret")
   169  
   170  	return nil
   171  }
   172  
   173  // createHash returns a hash of the cattle-cluster-agent deployment
   174  func createHash(cattleAgent *gabs.Container) string {
   175  	data := cattleAgent.Path("spec.template.spec.containers.0").Bytes()
   176  	sha := sha256.New()
   177  	sha.Write(data)
   178  
   179  	return string(sha.Sum(nil))
   180  }
   181  
   182  // getManifestSecretName returns the manifest secret name for a managed cluster on the admin cluster
   183  func getManifestSecretName(clusterName string) string {
   184  	manifestSecretSuffix := "-manifest"
   185  	return generateManagedResourceName(clusterName) + manifestSecretSuffix
   186  }
   187  
   188  // scaleDownRancherAgentDeployment scales the Rancher Agent deployment to 0 replicas
   189  func scaleDownRancherAgentDeployment(config *rest.Config, log *zap.SugaredLogger) (int32, error) {
   190  	var prevReplicas int32 = 1
   191  	zero := int32(0)
   192  
   193  	// Get the cattle-cluster-agent deployment object
   194  	deployment, err := getDeploymentFunc(config, common.CattleSystem, cattleAgent)
   195  	if err != nil {
   196  		if errors.IsNotFound(err) {
   197  			return prevReplicas, nil
   198  		}
   199  		return 0, err
   200  	}
   201  	namespacedName := types.NamespacedName{Name: cattleAgent, Namespace: common.CattleSystem}
   202  	if deployment.Spec.Replicas != nil {
   203  		prevReplicas = *deployment.Spec.Replicas
   204  	}
   205  
   206  	if deployment.Status.AvailableReplicas == zero {
   207  		// deployment is scaled to the desired value, we're done
   208  		return prevReplicas, nil
   209  	}
   210  
   211  	c, err := kubernetes.NewForConfig(config)
   212  	if err != nil {
   213  		return 0, err
   214  	}
   215  
   216  	if deployment.Spec.Replicas == nil || *deployment.Spec.Replicas > 0 {
   217  		log.Infof("Scaling Rancher deployment %s to %d replicas", namespacedName, zero)
   218  		deployment.Spec.Replicas = &zero
   219  		deployment, err = c.AppsV1().Deployments(common.CattleSystem).Update(context.TODO(), deployment, metav1.UpdateOptions{})
   220  		if err != nil {
   221  			err2 := fmt.Errorf("Failed to scale Rancher deployment %v to %d replicas: %v", namespacedName, zero, err)
   222  			log.Error(err2)
   223  			return prevReplicas, err2
   224  		}
   225  	}
   226  
   227  	// Wait for replica count to be reached
   228  	for tries := 0; tries < retryCount; tries++ {
   229  		deployment, err = getDeployment(config, common.CattleSystem, cattleAgent)
   230  		if err != nil {
   231  			return prevReplicas, err
   232  		}
   233  		if deployment.Status.AvailableReplicas == zero {
   234  			break
   235  		}
   236  		time.Sleep(retryDelay)
   237  	}
   238  	return prevReplicas, nil
   239  }
   240  
   241  // deleteClusterRepos - delete the clusterrepos object that contains the cached charts
   242  func deleteClusterRepos(config *rest.Config) error {
   243  	c, err := dynamic.NewForConfig(config)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	err = c.Resource(cattleClusterReposGVR).Delete(context.TODO(), clusterreposName, metav1.DeleteOptions{})
   248  	if err != nil && !errors.IsNotFound(err) {
   249  		return fmt.Errorf("Failed to delete clusterrrepos %s: %v", clusterreposName, err)
   250  	}
   251  	return nil
   252  }
   253  
   254  // getDeployment - return a Deployment object
   255  func getDeployment(config *rest.Config, namespace string, name string) (*appsv1.Deployment, error) {
   256  	c, err := kubernetes.NewForConfig(config)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	// Get the deployment object
   262  	deployment, err := c.AppsV1().Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	return deployment, nil
   267  }