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 }