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 }