github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/cluster/jackal.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package cluster contains Jackal-specific cluster management functions. 5 package cluster 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "strings" 13 "time" 14 15 "github.com/Racer159/jackal/src/config" 16 "github.com/Racer159/jackal/src/pkg/message" 17 "github.com/Racer159/jackal/src/types" 18 autoscalingV2 "k8s.io/api/autoscaling/v2" 19 corev1 "k8s.io/api/core/v1" 20 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 ) 22 23 // GetDeployedJackalPackages gets metadata information about packages that have been deployed to the cluster. 24 // We determine what packages have been deployed to the cluster by looking for specific secrets in the Jackal namespace. 25 // Returns a list of DeployedPackage structs and a list of errors. 26 func (c *Cluster) GetDeployedJackalPackages() ([]types.DeployedPackage, []error) { 27 var deployedPackages = []types.DeployedPackage{} 28 var errorList []error 29 // Get the secrets that describe the deployed packages 30 secrets, err := c.GetSecretsWithLabel(JackalNamespaceName, JackalPackageInfoLabel) 31 if err != nil { 32 return deployedPackages, append(errorList, err) 33 } 34 35 // Process the k8s secret into our internal structs 36 for _, secret := range secrets.Items { 37 if strings.HasPrefix(secret.Name, config.JackalPackagePrefix) { 38 var deployedPackage types.DeployedPackage 39 err := json.Unmarshal(secret.Data["data"], &deployedPackage) 40 // add the error to the error list 41 if err != nil { 42 errorList = append(errorList, fmt.Errorf("unable to unmarshal the secret %s/%s", secret.Namespace, secret.Name)) 43 } else { 44 deployedPackages = append(deployedPackages, deployedPackage) 45 } 46 } 47 } 48 49 // TODO: If we move this function out of `internal` we should return a more standard singular error. 50 return deployedPackages, errorList 51 } 52 53 // GetDeployedPackage gets the metadata information about the package name provided (if it exists in the cluster). 54 // We determine what packages have been deployed to the cluster by looking for specific secrets in the Jackal namespace. 55 func (c *Cluster) GetDeployedPackage(packageName string) (deployedPackage *types.DeployedPackage, err error) { 56 // Get the secret that describes the deployed package 57 secret, err := c.GetSecret(JackalNamespaceName, config.JackalPackagePrefix+packageName) 58 if err != nil { 59 return deployedPackage, err 60 } 61 62 return deployedPackage, json.Unmarshal(secret.Data["data"], &deployedPackage) 63 } 64 65 // StripJackalLabelsAndSecretsFromNamespaces removes metadata and secrets from existing namespaces no longer manged by Jackal. 66 func (c *Cluster) StripJackalLabelsAndSecretsFromNamespaces() { 67 spinner := message.NewProgressSpinner("Removing jackal metadata & secrets from existing namespaces not managed by Jackal") 68 defer spinner.Stop() 69 70 deleteOptions := metav1.DeleteOptions{} 71 listOptions := metav1.ListOptions{ 72 LabelSelector: config.JackalManagedByLabel + "=jackal", 73 } 74 75 if namespaces, err := c.GetNamespaces(); err != nil { 76 spinner.Errorf(err, "Unable to get k8s namespaces") 77 } else { 78 for _, namespace := range namespaces.Items { 79 if _, ok := namespace.Labels[agentLabel]; ok { 80 spinner.Updatef("Removing Jackal Agent label for namespace %s", namespace.Name) 81 delete(namespace.Labels, agentLabel) 82 namespaceCopy := namespace 83 if _, err = c.UpdateNamespace(&namespaceCopy); err != nil { 84 // This is not a hard failure, but we should log it 85 spinner.Errorf(err, "Unable to update the namespace labels for %s", namespace.Name) 86 } 87 } 88 89 spinner.Updatef("Removing Jackal secrets for namespace %s", namespace.Name) 90 err := c.Clientset.CoreV1(). 91 Secrets(namespace.Name). 92 DeleteCollection(context.TODO(), deleteOptions, listOptions) 93 if err != nil { 94 spinner.Errorf(err, "Unable to delete secrets from namespace %s", namespace.Name) 95 } 96 } 97 } 98 99 spinner.Success() 100 } 101 102 // PackageSecretNeedsWait checks if a package component has a running webhook that needs to be waited on. 103 func (c *Cluster) PackageSecretNeedsWait(deployedPackage *types.DeployedPackage, component types.JackalComponent, skipWebhooks bool) (needsWait bool, waitSeconds int, hookName string) { 104 105 // Skip checking webhook status when '--skip-webhooks' flag is provided and for YOLO packages 106 if skipWebhooks || deployedPackage == nil || deployedPackage.Data.Metadata.YOLO { 107 return false, 0, "" 108 } 109 110 // Look for the specified component 111 hookMap, componentExists := deployedPackage.ComponentWebhooks[component.Name] 112 if !componentExists { 113 return false, 0, "" // Component not found, no need to wait 114 } 115 116 // Check if there are any "Running" webhooks for the component that we need to wait for 117 for hookName, webhook := range hookMap { 118 if webhook.Status == types.WebhookStatusRunning { 119 return true, webhook.WaitDurationSeconds, hookName 120 } 121 } 122 123 // If we get here, the component doesn't need to wait for a webhook to run 124 return false, 0, "" 125 } 126 127 // RecordPackageDeploymentAndWait records the deployment of a package to the cluster and waits for any webhooks to complete. 128 func (c *Cluster) RecordPackageDeploymentAndWait(pkg types.JackalPackage, components []types.DeployedComponent, connectStrings types.ConnectStrings, generation int, component types.JackalComponent, skipWebhooks bool) (deployedPackage *types.DeployedPackage, err error) { 129 130 deployedPackage, err = c.RecordPackageDeployment(pkg, components, connectStrings, generation) 131 if err != nil { 132 return nil, err 133 } 134 135 packageNeedsWait, waitSeconds, hookName := c.PackageSecretNeedsWait(deployedPackage, component, skipWebhooks) 136 // If no webhooks need to complete, we can return immediately. 137 if !packageNeedsWait { 138 return nil, nil 139 } 140 141 // Timebox the amount of time we wait for a webhook to complete before erroring 142 waitDuration := types.DefaultWebhookWaitDuration 143 if waitSeconds > 0 { 144 waitDuration = time.Duration(waitSeconds) * time.Second 145 } 146 timeout := time.After(waitDuration) 147 148 // We need to wait for this package to finish having webhooks run, create a spinner and keep checking until it's ready 149 spinner := message.NewProgressSpinner("Waiting for webhook '%s' to complete for component '%s'", hookName, component.Name) 150 defer spinner.Stop() 151 for packageNeedsWait { 152 select { 153 // On timeout, abort and return an error. 154 case <-timeout: 155 return nil, errors.New("timed out waiting for package deployment to complete") 156 default: 157 // Wait for 1 second before checking the secret again 158 time.Sleep(1 * time.Second) 159 deployedPackage, err = c.GetDeployedPackage(deployedPackage.Name) 160 if err != nil { 161 return nil, err 162 } 163 packageNeedsWait, _, _ = c.PackageSecretNeedsWait(deployedPackage, component, skipWebhooks) 164 } 165 } 166 167 spinner.Success() 168 return deployedPackage, nil 169 } 170 171 // RecordPackageDeployment saves metadata about a package that has been deployed to the cluster. 172 func (c *Cluster) RecordPackageDeployment(pkg types.JackalPackage, components []types.DeployedComponent, connectStrings types.ConnectStrings, generation int) (deployedPackage *types.DeployedPackage, err error) { 173 packageName := pkg.Metadata.Name 174 175 // Generate a secret that describes the package that is being deployed 176 secretName := config.JackalPackagePrefix + packageName 177 deployedPackageSecret := c.GenerateSecret(JackalNamespaceName, secretName, corev1.SecretTypeOpaque) 178 deployedPackageSecret.Labels[JackalPackageInfoLabel] = packageName 179 180 // Attempt to load information about webhooks for the package 181 var componentWebhooks map[string]map[string]types.Webhook 182 existingPackageSecret, err := c.GetDeployedPackage(packageName) 183 if err != nil { 184 message.Debugf("Unable to fetch existing secret for package '%s': %s", packageName, err.Error()) 185 } 186 if existingPackageSecret != nil { 187 componentWebhooks = existingPackageSecret.ComponentWebhooks 188 } 189 190 deployedPackage = &types.DeployedPackage{ 191 Name: packageName, 192 CLIVersion: config.CLIVersion, 193 Data: pkg, 194 DeployedComponents: components, 195 ConnectStrings: connectStrings, 196 Generation: generation, 197 ComponentWebhooks: componentWebhooks, 198 } 199 200 packageData, err := json.Marshal(deployedPackage) 201 if err != nil { 202 return nil, err 203 } 204 205 // Update the package secret 206 deployedPackageSecret.Data = map[string][]byte{"data": packageData} 207 var updatedSecret *corev1.Secret 208 if updatedSecret, err = c.CreateOrUpdateSecret(deployedPackageSecret); err != nil { 209 return nil, fmt.Errorf("failed to record package deployment in secret '%s'", deployedPackageSecret.Name) 210 } 211 212 if err := json.Unmarshal(updatedSecret.Data["data"], &deployedPackage); err != nil { 213 return nil, err 214 } 215 216 return deployedPackage, nil 217 } 218 219 // EnableRegHPAScaleDown enables the HPA scale down for the Jackal Registry. 220 func (c *Cluster) EnableRegHPAScaleDown() error { 221 hpa, err := c.GetHPA(JackalNamespaceName, "jackal-docker-registry") 222 if err != nil { 223 return err 224 } 225 226 // Enable HPA scale down. 227 policy := autoscalingV2.MinChangePolicySelect 228 hpa.Spec.Behavior.ScaleDown.SelectPolicy = &policy 229 230 // Save the HPA changes. 231 if _, err = c.UpdateHPA(hpa); err != nil { 232 return err 233 } 234 235 return nil 236 } 237 238 // DisableRegHPAScaleDown disables the HPA scale down for the Jackal Registry. 239 func (c *Cluster) DisableRegHPAScaleDown() error { 240 hpa, err := c.GetHPA(JackalNamespaceName, "jackal-docker-registry") 241 if err != nil { 242 return err 243 } 244 245 // Disable HPA scale down. 246 policy := autoscalingV2.DisabledPolicySelect 247 hpa.Spec.Behavior.ScaleDown.SelectPolicy = &policy 248 249 // Save the HPA changes. 250 if _, err = c.UpdateHPA(hpa); err != nil { 251 return err 252 } 253 254 return nil 255 } 256 257 // GetInstalledChartsForComponent returns any installed Helm Charts for the provided package component. 258 func (c *Cluster) GetInstalledChartsForComponent(packageName string, component types.JackalComponent) (installedCharts []types.InstalledChart, err error) { 259 deployedPackage, err := c.GetDeployedPackage(packageName) 260 if err != nil { 261 return installedCharts, err 262 } 263 264 for _, deployedComponent := range deployedPackage.DeployedComponents { 265 if deployedComponent.Name == component.Name { 266 installedCharts = append(installedCharts, deployedComponent.InstalledCharts...) 267 } 268 } 269 270 return installedCharts, nil 271 }