github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/deploy.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package packager contains functions for interacting with, managing and deploying Jackal packages.
     5  package packager
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/Racer159/jackal/src/config"
    18  	"github.com/Racer159/jackal/src/config/lang"
    19  	"github.com/Racer159/jackal/src/internal/packager/git"
    20  	"github.com/Racer159/jackal/src/internal/packager/helm"
    21  	"github.com/Racer159/jackal/src/internal/packager/images"
    22  	"github.com/Racer159/jackal/src/internal/packager/template"
    23  	"github.com/Racer159/jackal/src/pkg/cluster"
    24  	"github.com/Racer159/jackal/src/pkg/k8s"
    25  	"github.com/Racer159/jackal/src/pkg/layout"
    26  	"github.com/Racer159/jackal/src/pkg/message"
    27  	"github.com/Racer159/jackal/src/pkg/packager/actions"
    28  	"github.com/Racer159/jackal/src/pkg/packager/filters"
    29  	"github.com/Racer159/jackal/src/pkg/packager/variables"
    30  	"github.com/Racer159/jackal/src/pkg/transform"
    31  	"github.com/Racer159/jackal/src/types"
    32  	"github.com/defenseunicorns/pkg/helpers"
    33  	corev1 "k8s.io/api/core/v1"
    34  )
    35  
    36  func (p *Packager) resetRegistryHPA() {
    37  	if p.isConnectedToCluster() && p.hpaModified {
    38  		if err := p.cluster.EnableRegHPAScaleDown(); err != nil {
    39  			message.Debugf("unable to reenable the registry HPA scale down: %s", err.Error())
    40  		}
    41  	}
    42  }
    43  
    44  // Deploy attempts to deploy the given PackageConfig.
    45  func (p *Packager) Deploy() (err error) {
    46  
    47  	isInteractive := !config.CommonOptions.Confirm
    48  
    49  	deployFilter := filters.Combine(
    50  		filters.ByLocalOS(runtime.GOOS),
    51  		filters.ForDeploy(p.cfg.PkgOpts.OptionalComponents, isInteractive),
    52  	)
    53  
    54  	if isInteractive {
    55  		filter := filters.Empty()
    56  
    57  		p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, filter, true)
    58  		if err != nil {
    59  			return fmt.Errorf("unable to load the package: %w", err)
    60  		}
    61  	} else {
    62  		p.cfg.Pkg, p.warnings, err = p.source.LoadPackage(p.layout, deployFilter, true)
    63  		if err != nil {
    64  			return fmt.Errorf("unable to load the package: %w", err)
    65  		}
    66  
    67  		if err := variables.SetVariableMapInConfig(p.cfg); err != nil {
    68  			return err
    69  		}
    70  	}
    71  
    72  	if err := p.validateLastNonBreakingVersion(); err != nil {
    73  		return err
    74  	}
    75  
    76  	var sbomWarnings []string
    77  	p.sbomViewFiles, sbomWarnings, err = p.layout.SBOMs.StageSBOMViewFiles()
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	p.warnings = append(p.warnings, sbomWarnings...)
    83  
    84  	// Confirm the overall package deployment
    85  	if !p.confirmAction(config.JackalDeployStage) {
    86  		return fmt.Errorf("deployment cancelled")
    87  	}
    88  
    89  	if isInteractive {
    90  		p.cfg.Pkg.Components, err = deployFilter.Apply(p.cfg.Pkg)
    91  		if err != nil {
    92  			return err
    93  		}
    94  
    95  		// Set variables and prompt if --confirm is not set
    96  		if err := variables.SetVariableMapInConfig(p.cfg); err != nil {
    97  			return err
    98  		}
    99  	}
   100  
   101  	p.hpaModified = false
   102  	p.connectStrings = make(types.ConnectStrings)
   103  	// Reset registry HPA scale down whether an error occurs or not
   104  	defer p.resetRegistryHPA()
   105  
   106  	// Get a list of all the components we are deploying and actually deploy them
   107  	deployedComponents, err := p.deployComponents()
   108  	if err != nil {
   109  		return err
   110  	}
   111  	if len(deployedComponents) == 0 {
   112  		message.Warn("No components were selected for deployment.  Inspect the package to view the available components and select components interactively or by name with \"--components\"")
   113  	}
   114  
   115  	// Notify all the things about the successful deployment
   116  	message.Successf("Jackal deployment complete")
   117  
   118  	p.printTablesForDeployment(deployedComponents)
   119  
   120  	return nil
   121  }
   122  
   123  // deployComponents loops through a list of JackalComponents and deploys them.
   124  func (p *Packager) deployComponents() (deployedComponents []types.DeployedComponent, err error) {
   125  	// Generate a value template
   126  	if p.valueTemplate, err = template.Generate(p.cfg); err != nil {
   127  		return deployedComponents, fmt.Errorf("unable to generate the value template: %w", err)
   128  	}
   129  
   130  	// Check if this package has been deployed before and grab relevant information about already deployed components
   131  	if p.generation == 0 {
   132  		p.generation = 1 // If this is the first deployment, set the generation to 1
   133  	}
   134  
   135  	// Process all the components we are deploying
   136  	for _, component := range p.cfg.Pkg.Components {
   137  
   138  		deployedComponent := types.DeployedComponent{
   139  			Name:               component.Name,
   140  			Status:             types.ComponentStatusDeploying,
   141  			ObservedGeneration: p.generation,
   142  		}
   143  
   144  		// If this component requires a cluster, connect to one
   145  		if component.RequiresCluster() {
   146  			timeout := cluster.DefaultTimeout
   147  			if p.cfg.Pkg.IsInitConfig() {
   148  				timeout = 5 * time.Minute
   149  			}
   150  
   151  			if err := p.connectToCluster(timeout); err != nil {
   152  				return deployedComponents, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err)
   153  			}
   154  		}
   155  
   156  		// Ensure we don't overwrite any installedCharts data when updating the package secret
   157  		if p.isConnectedToCluster() {
   158  			deployedComponent.InstalledCharts, err = p.cluster.GetInstalledChartsForComponent(p.cfg.Pkg.Metadata.Name, component)
   159  			if err != nil {
   160  				message.Debugf("Unable to fetch installed Helm charts for component '%s': %s", component.Name, err.Error())
   161  			}
   162  		}
   163  
   164  		deployedComponents = append(deployedComponents, deployedComponent)
   165  		idx := len(deployedComponents) - 1
   166  
   167  		// Update the package secret to indicate that we are attempting to deploy this component
   168  		if p.isConnectedToCluster() {
   169  			if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil {
   170  				message.Debugf("Unable to record package deployment for component %s: this will affect features like `jackal package remove`: %s", component.Name, err.Error())
   171  			}
   172  		}
   173  
   174  		// Deploy the component
   175  		var charts []types.InstalledChart
   176  		var deployErr error
   177  		if p.cfg.Pkg.IsInitConfig() {
   178  			charts, deployErr = p.deployInitComponent(component)
   179  		} else {
   180  			charts, deployErr = p.deployComponent(component, false /* keep img checksum */, false /* always push images */)
   181  		}
   182  
   183  		onDeploy := component.Actions.OnDeploy
   184  
   185  		onFailure := func() {
   186  			if err := actions.Run(p.cfg, onDeploy.Defaults, onDeploy.OnFailure, p.valueTemplate); err != nil {
   187  				message.Debugf("unable to run component failure action: %s", err.Error())
   188  			}
   189  		}
   190  
   191  		if deployErr != nil {
   192  			onFailure()
   193  
   194  			// Update the package secret to indicate that we failed to deploy this component
   195  			deployedComponents[idx].Status = types.ComponentStatusFailed
   196  			if p.isConnectedToCluster() {
   197  				if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil {
   198  					message.Debugf("Unable to record package deployment for component %q: this will affect features like `jackal package remove`: %s", component.Name, err.Error())
   199  				}
   200  			}
   201  
   202  			return deployedComponents, fmt.Errorf("unable to deploy component %q: %w", component.Name, deployErr)
   203  		}
   204  
   205  		// Update the package secret to indicate that we successfully deployed this component
   206  		deployedComponents[idx].InstalledCharts = charts
   207  		deployedComponents[idx].Status = types.ComponentStatusSucceeded
   208  		if p.isConnectedToCluster() {
   209  			if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil {
   210  				message.Debugf("Unable to record package deployment for component %q: this will affect features like `jackal package remove`: %s", component.Name, err.Error())
   211  			}
   212  		}
   213  
   214  		if err := actions.Run(p.cfg, onDeploy.Defaults, onDeploy.OnSuccess, p.valueTemplate); err != nil {
   215  			onFailure()
   216  			return deployedComponents, fmt.Errorf("unable to run component success action: %w", err)
   217  		}
   218  	}
   219  
   220  	return deployedComponents, nil
   221  }
   222  
   223  func (p *Packager) deployInitComponent(component types.JackalComponent) (charts []types.InstalledChart, err error) {
   224  	hasExternalRegistry := p.cfg.InitOpts.RegistryInfo.Address != ""
   225  	isSeedRegistry := component.Name == "jackal-seed-registry"
   226  	isRegistry := component.Name == "jackal-registry"
   227  	isInjector := component.Name == "jackal-injector"
   228  	isAgent := component.Name == "jackal-agent"
   229  	isK3s := component.Name == "k3s"
   230  
   231  	if isK3s {
   232  		p.cfg.InitOpts.ApplianceMode = true
   233  	}
   234  
   235  	// Always init the state before the first component that requires the cluster (on most deployments, the jackal-seed-registry)
   236  	if component.RequiresCluster() && p.cfg.State == nil {
   237  		err = p.cluster.InitJackalState(p.cfg.InitOpts)
   238  		if err != nil {
   239  			return charts, fmt.Errorf("unable to initialize Jackal state: %w", err)
   240  		}
   241  	}
   242  
   243  	if hasExternalRegistry && (isSeedRegistry || isInjector || isRegistry) {
   244  		message.Notef("Not deploying the component (%s) since external registry information was provided during `jackal init`", component.Name)
   245  		return charts, nil
   246  	}
   247  
   248  	if isRegistry {
   249  		// If we are deploying the registry then mark the HPA as "modified" to set it to Min later
   250  		p.hpaModified = true
   251  	}
   252  
   253  	// Before deploying the seed registry, start the injector
   254  	if isSeedRegistry {
   255  		p.cluster.StartInjectionMadness(p.layout.Base, p.layout.Images.Base, component.Images)
   256  	}
   257  
   258  	charts, err = p.deployComponent(component, isAgent /* skip img checksum if isAgent */, isSeedRegistry /* skip image push if isSeedRegistry */)
   259  	if err != nil {
   260  		return charts, err
   261  	}
   262  
   263  	// Do cleanup for when we inject the seed registry during initialization
   264  	if isSeedRegistry {
   265  		if err := p.cluster.StopInjectionMadness(); err != nil {
   266  			return charts, fmt.Errorf("unable to seed the Jackal Registry: %w", err)
   267  		}
   268  	}
   269  
   270  	return charts, nil
   271  }
   272  
   273  // Deploy a Jackal Component.
   274  func (p *Packager) deployComponent(component types.JackalComponent, noImgChecksum bool, noImgPush bool) (charts []types.InstalledChart, err error) {
   275  	// Toggles for general deploy operations
   276  	componentPath := p.layout.Components.Dirs[component.Name]
   277  
   278  	// All components now require a name
   279  	message.HeaderInfof("📦 %s COMPONENT", strings.ToUpper(component.Name))
   280  
   281  	hasImages := len(component.Images) > 0 && !noImgPush
   282  	hasCharts := len(component.Charts) > 0
   283  	hasManifests := len(component.Manifests) > 0
   284  	hasRepos := len(component.Repos) > 0
   285  	hasDataInjections := len(component.DataInjections) > 0
   286  	hasFiles := len(component.Files) > 0
   287  
   288  	onDeploy := component.Actions.OnDeploy
   289  
   290  	if !p.valueTemplate.Ready() && component.RequiresCluster() {
   291  		// Setup the state in the config and get the valuesTemplate
   292  		p.valueTemplate, err = p.setupStateValuesTemplate()
   293  		if err != nil {
   294  			return charts, err
   295  		}
   296  
   297  		// Disable the registry HPA scale down if we are deploying images and it is not already disabled
   298  		if hasImages && !p.hpaModified && p.cfg.State.RegistryInfo.InternalRegistry {
   299  			if err := p.cluster.DisableRegHPAScaleDown(); err != nil {
   300  				message.Debugf("unable to disable the registry HPA scale down: %s", err.Error())
   301  			} else {
   302  				p.hpaModified = true
   303  			}
   304  		}
   305  	}
   306  
   307  	if err = actions.Run(p.cfg, onDeploy.Defaults, onDeploy.Before, p.valueTemplate); err != nil {
   308  		return charts, fmt.Errorf("unable to run component before action: %w", err)
   309  	}
   310  
   311  	if hasFiles {
   312  		if err := p.processComponentFiles(component, componentPath.Files); err != nil {
   313  			return charts, fmt.Errorf("unable to process the component files: %w", err)
   314  		}
   315  	}
   316  
   317  	if hasImages {
   318  		if err := p.pushImagesToRegistry(component.Images, noImgChecksum); err != nil {
   319  			return charts, fmt.Errorf("unable to push images to the registry: %w", err)
   320  		}
   321  	}
   322  
   323  	if hasRepos {
   324  		if err = p.pushReposToRepository(componentPath.Repos, component.Repos); err != nil {
   325  			return charts, fmt.Errorf("unable to push the repos to the repository: %w", err)
   326  		}
   327  	}
   328  
   329  	if hasDataInjections {
   330  		waitGroup := sync.WaitGroup{}
   331  		defer waitGroup.Wait()
   332  
   333  		for idx, data := range component.DataInjections {
   334  			waitGroup.Add(1)
   335  			go p.cluster.HandleDataInjection(&waitGroup, data, componentPath, idx)
   336  		}
   337  	}
   338  
   339  	if hasCharts || hasManifests {
   340  		if charts, err = p.installChartAndManifests(componentPath, component); err != nil {
   341  			return charts, fmt.Errorf("unable to install helm chart(s): %w", err)
   342  		}
   343  	}
   344  
   345  	if err = actions.Run(p.cfg, onDeploy.Defaults, onDeploy.After, p.valueTemplate); err != nil {
   346  		return charts, fmt.Errorf("unable to run component after action: %w", err)
   347  	}
   348  
   349  	return charts, nil
   350  }
   351  
   352  // Move files onto the host of the machine performing the deployment.
   353  func (p *Packager) processComponentFiles(component types.JackalComponent, pkgLocation string) error {
   354  	spinner := message.NewProgressSpinner("Copying %d files", len(component.Files))
   355  	defer spinner.Stop()
   356  
   357  	for fileIdx, file := range component.Files {
   358  		spinner.Updatef("Loading %s", file.Target)
   359  
   360  		fileLocation := filepath.Join(pkgLocation, strconv.Itoa(fileIdx), filepath.Base(file.Target))
   361  		if helpers.InvalidPath(fileLocation) {
   362  			fileLocation = filepath.Join(pkgLocation, strconv.Itoa(fileIdx))
   363  		}
   364  
   365  		// If a shasum is specified check it again on deployment as well
   366  		if file.Shasum != "" {
   367  			spinner.Updatef("Validating SHASUM for %s", file.Target)
   368  			if err := helpers.SHAsMatch(fileLocation, file.Shasum); err != nil {
   369  				return err
   370  			}
   371  		}
   372  
   373  		// Replace temp target directory and home directory
   374  		file.Target = strings.Replace(file.Target, "###JACKAL_TEMP###", p.layout.Base, 1)
   375  		file.Target = config.GetAbsHomePath(file.Target)
   376  
   377  		fileList := []string{}
   378  		if helpers.IsDir(fileLocation) {
   379  			files, _ := helpers.RecursiveFileList(fileLocation, nil, false)
   380  			fileList = append(fileList, files...)
   381  		} else {
   382  			fileList = append(fileList, fileLocation)
   383  		}
   384  
   385  		for _, subFile := range fileList {
   386  			// Check if the file looks like a text file
   387  			isText, err := helpers.IsTextFile(subFile)
   388  			if err != nil {
   389  				message.Debugf("unable to determine if file %s is a text file: %s", subFile, err)
   390  			}
   391  
   392  			// If the file is a text file, template it
   393  			if isText {
   394  				spinner.Updatef("Templating %s", file.Target)
   395  				if err := p.valueTemplate.Apply(component, subFile, true); err != nil {
   396  					return fmt.Errorf("unable to template file %s: %w", subFile, err)
   397  				}
   398  			}
   399  		}
   400  
   401  		// Copy the file to the destination
   402  		spinner.Updatef("Saving %s", file.Target)
   403  		err := helpers.CreatePathAndCopy(fileLocation, file.Target)
   404  		if err != nil {
   405  			return fmt.Errorf("unable to copy file %s to %s: %w", fileLocation, file.Target, err)
   406  		}
   407  
   408  		// Loop over all symlinks and create them
   409  		for _, link := range file.Symlinks {
   410  			spinner.Updatef("Adding symlink %s->%s", link, file.Target)
   411  			// Try to remove the filepath if it exists
   412  			_ = os.RemoveAll(link)
   413  			// Make sure the parent directory exists
   414  			_ = helpers.CreateParentDirectory(link)
   415  			// Create the symlink
   416  			err := os.Symlink(file.Target, link)
   417  			if err != nil {
   418  				return fmt.Errorf("unable to create symlink %s->%s: %w", link, file.Target, err)
   419  			}
   420  		}
   421  
   422  		// Cleanup now to reduce disk pressure
   423  		_ = os.RemoveAll(fileLocation)
   424  	}
   425  
   426  	spinner.Success()
   427  
   428  	return nil
   429  }
   430  
   431  // setupStateValuesTemplate fetched the current JackalState from the k8s cluster and generate a p.valueTemplate from the state values.
   432  func (p *Packager) setupStateValuesTemplate() (values *template.Values, err error) {
   433  	// If we are touching K8s, make sure we can talk to it once per deployment
   434  	spinner := message.NewProgressSpinner("Loading the Jackal State from the Kubernetes cluster")
   435  	defer spinner.Stop()
   436  
   437  	state, err := p.cluster.LoadJackalState()
   438  	// Return on error if we are not in YOLO mode
   439  	if err != nil && !p.cfg.Pkg.Metadata.YOLO {
   440  		return nil, fmt.Errorf("%s %w", lang.ErrLoadState, err)
   441  	} else if state == nil && p.cfg.Pkg.Metadata.YOLO {
   442  		state = &types.JackalState{}
   443  		// YOLO mode, so minimal state needed
   444  		state.Distro = "YOLO"
   445  
   446  		// Try to create the jackal namespace
   447  		spinner.Updatef("Creating the Jackal namespace")
   448  		jackalNamespace := p.cluster.NewJackalManagedNamespace(cluster.JackalNamespaceName)
   449  		if _, err := p.cluster.CreateNamespace(jackalNamespace); err != nil {
   450  			spinner.Fatalf(err, "Unable to create the jackal namespace")
   451  		}
   452  	}
   453  
   454  	if p.cfg.Pkg.Metadata.YOLO && state.Distro != "YOLO" {
   455  		message.Warn("This package is in YOLO mode, but the cluster was already initialized with 'jackal init'. " +
   456  			"This may cause issues if the package does not exclude any charts or manifests from the Jackal Agent using " +
   457  			"the pod or namespace label `jackal.dev/agent: ignore'.")
   458  	}
   459  
   460  	p.cfg.State = state
   461  
   462  	// Continue loading state data if it is valid
   463  	values, err = template.Generate(p.cfg)
   464  	if err != nil {
   465  		return values, err
   466  	}
   467  
   468  	spinner.Success()
   469  	return values, nil
   470  }
   471  
   472  // Push all of the components images to the configured container registry.
   473  func (p *Packager) pushImagesToRegistry(componentImages []string, noImgChecksum bool) error {
   474  	if len(componentImages) == 0 {
   475  		return nil
   476  	}
   477  
   478  	var combinedImageList []transform.Image
   479  	for _, src := range componentImages {
   480  		ref, err := transform.ParseImageRef(src)
   481  		if err != nil {
   482  			return fmt.Errorf("failed to create ref for image %s: %w", src, err)
   483  		}
   484  		combinedImageList = append(combinedImageList, ref)
   485  	}
   486  
   487  	imageList := helpers.Unique(combinedImageList)
   488  
   489  	imgConfig := images.ImageConfig{
   490  		ImagesPath:    p.layout.Images.Base,
   491  		ImageList:     imageList,
   492  		NoChecksum:    noImgChecksum,
   493  		RegInfo:       p.cfg.State.RegistryInfo,
   494  		Insecure:      config.CommonOptions.Insecure,
   495  		Architectures: []string{p.cfg.Pkg.Build.Architecture},
   496  	}
   497  
   498  	return helpers.Retry(func() error {
   499  		return imgConfig.PushToJackalRegistry()
   500  	}, p.cfg.PkgOpts.Retries, 5*time.Second, message.Warnf)
   501  }
   502  
   503  // Push all of the components git repos to the configured git server.
   504  func (p *Packager) pushReposToRepository(reposPath string, repos []string) error {
   505  	for _, repoURL := range repos {
   506  		// Create an anonymous function to push the repo to the Jackal git server
   507  		tryPush := func() error {
   508  			gitClient := git.New(p.cfg.State.GitServer)
   509  			svcInfo, _ := k8s.ServiceInfoFromServiceURL(gitClient.Server.Address)
   510  
   511  			var err error
   512  			var tunnel *k8s.Tunnel
   513  
   514  			// If this is a service (svcInfo is not nil), create a port-forward tunnel to that resource
   515  			if svcInfo != nil {
   516  				if !p.isConnectedToCluster() {
   517  					err := p.connectToCluster(5 * time.Second)
   518  					if err != nil {
   519  						return err
   520  					}
   521  				}
   522  
   523  				tunnel, err = p.cluster.NewTunnel(svcInfo.Namespace, k8s.SvcResource, svcInfo.Name, "", 0, svcInfo.Port)
   524  				if err != nil {
   525  					return err
   526  				}
   527  
   528  				_, err = tunnel.Connect()
   529  				if err != nil {
   530  					return err
   531  				}
   532  				defer tunnel.Close()
   533  				gitClient.Server.Address = tunnel.HTTPEndpoint()
   534  
   535  				return tunnel.Wrap(func() error { return gitClient.PushRepo(repoURL, reposPath) })
   536  			}
   537  
   538  			return gitClient.PushRepo(repoURL, reposPath)
   539  		}
   540  
   541  		// Try repo push up to retry limit
   542  		if err := helpers.Retry(tryPush, p.cfg.PkgOpts.Retries, 5*time.Second, message.Warnf); err != nil {
   543  			return fmt.Errorf("unable to push repo %s to the Git Server: %w", repoURL, err)
   544  		}
   545  	}
   546  
   547  	return nil
   548  }
   549  
   550  // Install all Helm charts and raw k8s manifests into the k8s cluster.
   551  func (p *Packager) installChartAndManifests(componentPaths *layout.ComponentPaths, component types.JackalComponent) (installedCharts []types.InstalledChart, err error) {
   552  	for _, chart := range component.Charts {
   553  
   554  		// jackal magic for the value file
   555  		for idx := range chart.ValuesFiles {
   556  			chartValueName := helm.StandardValuesName(componentPaths.Values, chart, idx)
   557  			if err := p.valueTemplate.Apply(component, chartValueName, false); err != nil {
   558  				return installedCharts, err
   559  			}
   560  		}
   561  
   562  		// TODO (@WSTARR): Currently this logic is library-only and is untested while it is in an experimental state - it may eventually get added as shorthand in Jackal Variables though
   563  		var valuesOverrides map[string]any
   564  		if componentChartValuesOverrides, ok := p.cfg.DeployOpts.ValuesOverridesMap[component.Name]; ok {
   565  			if chartValuesOverrides, ok := componentChartValuesOverrides[chart.Name]; ok {
   566  				valuesOverrides = chartValuesOverrides
   567  			}
   568  		}
   569  
   570  		helmCfg := helm.New(
   571  			chart,
   572  			componentPaths.Charts,
   573  			componentPaths.Values,
   574  			helm.WithDeployInfo(
   575  				component,
   576  				p.cfg,
   577  				p.cluster,
   578  				valuesOverrides,
   579  				p.cfg.DeployOpts.Timeout,
   580  				p.cfg.PkgOpts.Retries),
   581  		)
   582  
   583  		addedConnectStrings, installedChartName, err := helmCfg.InstallOrUpgradeChart()
   584  		if err != nil {
   585  			return installedCharts, err
   586  		}
   587  		installedCharts = append(installedCharts, types.InstalledChart{Namespace: chart.Namespace, ChartName: installedChartName})
   588  
   589  		// Iterate over any connectStrings and add to the main map
   590  		for name, description := range addedConnectStrings {
   591  			p.connectStrings[name] = description
   592  		}
   593  	}
   594  
   595  	for _, manifest := range component.Manifests {
   596  		for idx := range manifest.Files {
   597  			if helpers.InvalidPath(filepath.Join(componentPaths.Manifests, manifest.Files[idx])) {
   598  				// The path is likely invalid because of how we compose OCI components, add an index suffix to the filename
   599  				manifest.Files[idx] = fmt.Sprintf("%s-%d.yaml", manifest.Name, idx)
   600  				if helpers.InvalidPath(filepath.Join(componentPaths.Manifests, manifest.Files[idx])) {
   601  					return installedCharts, fmt.Errorf("unable to find manifest file %s", manifest.Files[idx])
   602  				}
   603  			}
   604  		}
   605  		// Move kustomizations to files now
   606  		for idx := range manifest.Kustomizations {
   607  			kustomization := fmt.Sprintf("kustomization-%s-%d.yaml", manifest.Name, idx)
   608  			manifest.Files = append(manifest.Files, kustomization)
   609  		}
   610  
   611  		if manifest.Namespace == "" {
   612  			// Helm gets sad when you don't provide a namespace even though we aren't using helm templating
   613  			manifest.Namespace = corev1.NamespaceDefault
   614  		}
   615  
   616  		// Create a chart and helm cfg from a given Jackal Manifest.
   617  		helmCfg, err := helm.NewFromJackalManifest(
   618  			manifest,
   619  			componentPaths.Manifests,
   620  			p.cfg.Pkg.Metadata.Name,
   621  			component.Name,
   622  			helm.WithDeployInfo(
   623  				component,
   624  				p.cfg,
   625  				p.cluster,
   626  				nil,
   627  				p.cfg.DeployOpts.Timeout,
   628  				p.cfg.PkgOpts.Retries),
   629  		)
   630  		if err != nil {
   631  			return installedCharts, err
   632  		}
   633  
   634  		// Install the chart.
   635  		addedConnectStrings, installedChartName, err := helmCfg.InstallOrUpgradeChart()
   636  		if err != nil {
   637  			return installedCharts, err
   638  		}
   639  
   640  		installedCharts = append(installedCharts, types.InstalledChart{Namespace: manifest.Namespace, ChartName: installedChartName})
   641  
   642  		// Iterate over any connectStrings and add to the main map
   643  		for name, description := range addedConnectStrings {
   644  			p.connectStrings[name] = description
   645  		}
   646  	}
   647  
   648  	return installedCharts, nil
   649  }
   650  
   651  func (p *Packager) printTablesForDeployment(componentsToDeploy []types.DeployedComponent) {
   652  
   653  	// If not init config, print the application connection table
   654  	if !p.cfg.Pkg.IsInitConfig() {
   655  		message.PrintConnectStringTable(p.connectStrings)
   656  	} else {
   657  		if p.cluster != nil {
   658  			// Grab a fresh copy of the state (if we are able) to print the most up-to-date version of the creds
   659  			freshState, err := p.cluster.LoadJackalState()
   660  			if err != nil {
   661  				freshState = p.cfg.State
   662  			}
   663  			// otherwise, print the init config connection and passwords
   664  			message.PrintCredentialTable(freshState, componentsToDeploy)
   665  		}
   666  	}
   667  }