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  }