github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/common.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  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"strings"
    12  	"time"
    13  
    14  	"slices"
    15  
    16  	"github.com/Masterminds/semver/v3"
    17  	"github.com/Racer159/jackal/src/config/lang"
    18  	"github.com/Racer159/jackal/src/internal/packager/template"
    19  	"github.com/Racer159/jackal/src/pkg/cluster"
    20  	"github.com/Racer159/jackal/src/types"
    21  
    22  	"github.com/Racer159/jackal/src/config"
    23  	"github.com/Racer159/jackal/src/pkg/layout"
    24  	"github.com/Racer159/jackal/src/pkg/message"
    25  	"github.com/Racer159/jackal/src/pkg/packager/deprecated"
    26  	"github.com/Racer159/jackal/src/pkg/packager/sources"
    27  	"github.com/Racer159/jackal/src/pkg/utils"
    28  )
    29  
    30  // Packager is the main struct for managing packages.
    31  type Packager struct {
    32  	cfg            *types.PackagerConfig
    33  	cluster        *cluster.Cluster
    34  	layout         *layout.PackagePaths
    35  	warnings       []string
    36  	valueTemplate  *template.Values
    37  	hpaModified    bool
    38  	connectStrings types.ConnectStrings
    39  	sbomViewFiles  []string
    40  	source         sources.PackageSource
    41  	generation     int
    42  }
    43  
    44  // Modifier is a function that modifies the packager.
    45  type Modifier func(*Packager)
    46  
    47  // WithSource sets the source for the packager.
    48  func WithSource(source sources.PackageSource) Modifier {
    49  	return func(p *Packager) {
    50  		p.source = source
    51  	}
    52  }
    53  
    54  // WithCluster sets the cluster client for the packager.
    55  func WithCluster(cluster *cluster.Cluster) Modifier {
    56  	return func(p *Packager) {
    57  		p.cluster = cluster
    58  	}
    59  }
    60  
    61  // WithTemp sets the temp directory for the packager.
    62  //
    63  // This temp directory is used as the destination where p.source loads the package.
    64  func WithTemp(base string) Modifier {
    65  	return func(p *Packager) {
    66  		p.layout = layout.New(base)
    67  	}
    68  }
    69  
    70  /*
    71  New creates a new package instance with the provided config.
    72  
    73  Note: This function creates a tmp directory that should be cleaned up with p.ClearTempPaths().
    74  */
    75  func New(cfg *types.PackagerConfig, mods ...Modifier) (*Packager, error) {
    76  	if cfg == nil {
    77  		return nil, fmt.Errorf("no config provided")
    78  	}
    79  
    80  	if cfg.SetVariableMap == nil {
    81  		cfg.SetVariableMap = make(map[string]*types.JackalSetVariable)
    82  	}
    83  
    84  	var (
    85  		err  error
    86  		pkgr = &Packager{
    87  			cfg: cfg,
    88  		}
    89  	)
    90  
    91  	if config.CommonOptions.TempDirectory != "" {
    92  		// If the cache directory is within the temp directory, warn the user
    93  		if strings.HasPrefix(config.CommonOptions.CachePath, config.CommonOptions.TempDirectory) {
    94  			message.Warnf("The cache directory (%q) is within the temp directory (%q) and will be removed when the temp directory is cleaned up", config.CommonOptions.CachePath, config.CommonOptions.TempDirectory)
    95  		}
    96  	}
    97  
    98  	for _, mod := range mods {
    99  		mod(pkgr)
   100  	}
   101  
   102  	// Fill the source if it wasn't provided - note source can be nil if the package is being created
   103  	if pkgr.source == nil && pkgr.cfg.CreateOpts.BaseDir == "" {
   104  		pkgr.source, err = sources.New(&pkgr.cfg.PkgOpts)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  	}
   109  
   110  	// If the temp directory is not set, set it to the default
   111  	if pkgr.layout == nil {
   112  		if err = pkgr.setTempDirectory(config.CommonOptions.TempDirectory); err != nil {
   113  			return nil, fmt.Errorf("unable to create package temp paths: %w", err)
   114  		}
   115  	}
   116  
   117  	return pkgr, nil
   118  }
   119  
   120  /*
   121  NewOrDie creates a new package instance with the provided config or throws a fatal error.
   122  
   123  Note: This function creates a tmp directory that should be cleaned up with p.ClearTempPaths().
   124  */
   125  func NewOrDie(config *types.PackagerConfig, mods ...Modifier) *Packager {
   126  	var (
   127  		err  error
   128  		pkgr *Packager
   129  	)
   130  
   131  	if pkgr, err = New(config, mods...); err != nil {
   132  		message.Fatalf(err, "Unable to setup the package config: %s", err.Error())
   133  	}
   134  
   135  	return pkgr
   136  }
   137  
   138  // setTempDirectory sets the temp directory for the packager.
   139  func (p *Packager) setTempDirectory(path string) error {
   140  	dir, err := utils.MakeTempDir(path)
   141  	if err != nil {
   142  		return fmt.Errorf("unable to create package temp paths: %w", err)
   143  	}
   144  
   145  	p.layout = layout.New(dir)
   146  	return nil
   147  }
   148  
   149  // ClearTempPaths removes the temp directory and any files within it.
   150  func (p *Packager) ClearTempPaths() {
   151  	// Remove the temp directory, but don't throw an error if it fails
   152  	_ = os.RemoveAll(p.layout.Base)
   153  	_ = os.RemoveAll(layout.SBOMDir)
   154  }
   155  
   156  // connectToCluster attempts to connect to a cluster if a connection is not already established
   157  func (p *Packager) connectToCluster(timeout time.Duration) (err error) {
   158  	if p.isConnectedToCluster() {
   159  		return nil
   160  	}
   161  
   162  	p.cluster, err = cluster.NewClusterWithWait(timeout)
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	return p.attemptClusterChecks()
   168  }
   169  
   170  // isConnectedToCluster returns whether the current packager instance is connected to a cluster
   171  func (p *Packager) isConnectedToCluster() bool {
   172  	return p.cluster != nil
   173  }
   174  
   175  // hasImages returns whether the current package contains images
   176  func (p *Packager) hasImages() bool {
   177  	for _, component := range p.cfg.Pkg.Components {
   178  		if len(component.Images) > 0 {
   179  			return true
   180  		}
   181  	}
   182  	return false
   183  }
   184  
   185  // attemptClusterChecks attempts to connect to the cluster and check for useful metadata and config mismatches.
   186  // NOTE: attemptClusterChecks should only return an error if there is a problem significant enough to halt a deployment, otherwise it should return nil and print a warning message.
   187  func (p *Packager) attemptClusterChecks() (err error) {
   188  
   189  	spinner := message.NewProgressSpinner("Gathering additional cluster information (if available)")
   190  	defer spinner.Stop()
   191  
   192  	// Check if the package has already been deployed and get its generation
   193  	if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil {
   194  		// If this package has been deployed before, increment the package generation within the secret
   195  		p.generation = existingDeployedPackage.Generation + 1
   196  	}
   197  
   198  	// Check the clusters architecture matches the package spec
   199  	if err := p.validatePackageArchitecture(); err != nil {
   200  		if errors.Is(err, lang.ErrUnableToCheckArch) {
   201  			message.Warnf("Unable to validate package architecture: %s", err.Error())
   202  		} else {
   203  			return err
   204  		}
   205  	}
   206  
   207  	// Check for any breaking changes between the initialized Jackal version and this CLI
   208  	if existingInitPackage, _ := p.cluster.GetDeployedPackage("init"); existingInitPackage != nil {
   209  		// Use the build version instead of the metadata since this will support older Jackal versions
   210  		deprecated.PrintBreakingChanges(existingInitPackage.Data.Build.Version)
   211  	}
   212  
   213  	spinner.Success()
   214  
   215  	return nil
   216  }
   217  
   218  // validatePackageArchitecture validates that the package architecture matches the target cluster architecture.
   219  func (p *Packager) validatePackageArchitecture() error {
   220  	// Ignore this check if we don't have a cluster connection, or the package contains no images
   221  	if !p.isConnectedToCluster() || !p.hasImages() {
   222  		return nil
   223  	}
   224  
   225  	clusterArchitectures, err := p.cluster.GetArchitectures()
   226  	if err != nil {
   227  		return lang.ErrUnableToCheckArch
   228  	}
   229  
   230  	// Check if the package architecture and the cluster architecture are the same.
   231  	if !slices.Contains(clusterArchitectures, p.cfg.Pkg.Metadata.Architecture) {
   232  		return fmt.Errorf(lang.CmdPackageDeployValidateArchitectureErr, p.cfg.Pkg.Metadata.Architecture, strings.Join(clusterArchitectures, ", "))
   233  	}
   234  
   235  	return nil
   236  }
   237  
   238  // validateLastNonBreakingVersion validates the Jackal CLI version against a package's LastNonBreakingVersion.
   239  func (p *Packager) validateLastNonBreakingVersion() (err error) {
   240  	cliVersion := config.CLIVersion
   241  	lastNonBreakingVersion := p.cfg.Pkg.Build.LastNonBreakingVersion
   242  
   243  	if lastNonBreakingVersion == "" {
   244  		return nil
   245  	}
   246  
   247  	lastNonBreakingSemVer, err := semver.NewVersion(lastNonBreakingVersion)
   248  	if err != nil {
   249  		return fmt.Errorf("unable to parse lastNonBreakingVersion '%s' from Jackal package build data : %w", lastNonBreakingVersion, err)
   250  	}
   251  
   252  	cliSemVer, err := semver.NewVersion(cliVersion)
   253  	if err != nil {
   254  		warning := fmt.Sprintf(lang.CmdPackageDeployInvalidCLIVersionWarn, config.CLIVersion)
   255  		p.warnings = append(p.warnings, warning)
   256  		return nil
   257  	}
   258  
   259  	if cliSemVer.LessThan(lastNonBreakingSemVer) {
   260  		warning := fmt.Sprintf(
   261  			lang.CmdPackageDeployValidateLastNonBreakingVersionWarn,
   262  			cliVersion,
   263  			lastNonBreakingVersion,
   264  			lastNonBreakingVersion,
   265  		)
   266  		p.warnings = append(p.warnings, warning)
   267  	}
   268  
   269  	return nil
   270  }