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 }