github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/layout/package.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package layout contains functions for interacting with Jackal's package layout on disk. 5 package layout 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "slices" 12 "strings" 13 14 "github.com/Masterminds/semver/v3" 15 "github.com/Racer159/jackal/src/pkg/interactive" 16 "github.com/Racer159/jackal/src/pkg/message" 17 "github.com/Racer159/jackal/src/pkg/packager/deprecated" 18 "github.com/Racer159/jackal/src/pkg/utils" 19 "github.com/Racer159/jackal/src/types" 20 "github.com/defenseunicorns/pkg/helpers" 21 "github.com/google/go-containerregistry/pkg/crane" 22 "github.com/mholt/archiver/v3" 23 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 24 ) 25 26 // PackagePaths is the default package layout. 27 type PackagePaths struct { 28 Base string 29 JackalYAML string 30 Checksums string 31 32 Signature string 33 34 Components Components 35 SBOMs SBOMs 36 Images Images 37 38 isLegacyLayout bool 39 } 40 41 // InjectionMadnessPaths contains paths for injection madness. 42 type InjectionMadnessPaths struct { 43 InjectionBinary string 44 SeedImagesDir string 45 InjectorPayloadTarGz string 46 } 47 48 // New returns a new PackagePaths struct. 49 func New(baseDir string) *PackagePaths { 50 return &PackagePaths{ 51 Base: baseDir, 52 JackalYAML: filepath.Join(baseDir, JackalYAML), 53 Checksums: filepath.Join(baseDir, Checksums), 54 Components: Components{ 55 Base: filepath.Join(baseDir, ComponentsDir), 56 }, 57 } 58 } 59 60 // ReadJackalYAML reads a jackal.yaml file into memory, 61 // checks if it's using the legacy layout, and migrates deprecated component configs. 62 func (pp *PackagePaths) ReadJackalYAML() (pkg types.JackalPackage, warnings []string, err error) { 63 if err := utils.ReadYaml(pp.JackalYAML, &pkg); err != nil { 64 return types.JackalPackage{}, nil, fmt.Errorf("unable to read jackal.yaml: %w", err) 65 } 66 67 if pp.IsLegacyLayout() { 68 warnings = append(warnings, "Detected deprecated package layout, migrating to new layout - support for this package will be dropped in v1.0.0") 69 } 70 71 if len(pkg.Build.Migrations) > 0 { 72 var componentWarnings []string 73 for idx, component := range pkg.Components { 74 // Handle component configuration deprecations 75 pkg.Components[idx], componentWarnings = deprecated.MigrateComponent(pkg.Build, component) 76 warnings = append(warnings, componentWarnings...) 77 } 78 } 79 80 return pkg, warnings, nil 81 } 82 83 // MigrateLegacy migrates a legacy package layout to the new layout. 84 func (pp *PackagePaths) MigrateLegacy() (err error) { 85 var pkg types.JackalPackage 86 base := pp.Base 87 88 // legacy layout does not contain a checksums file, nor a signature 89 if helpers.InvalidPath(pp.Checksums) && pp.Signature == "" { 90 if err := utils.ReadYaml(pp.JackalYAML, &pkg); err != nil { 91 return err 92 } 93 buildVer, err := semver.NewVersion(pkg.Build.Version) 94 if err != nil { 95 return err 96 } 97 if !buildVer.LessThan(semver.MustParse("v0.25.0")) { 98 return nil 99 } 100 pp.isLegacyLayout = true 101 } else { 102 return nil 103 } 104 105 // Migrate legacy sboms 106 legacySBOMs := filepath.Join(base, "sboms") 107 if !helpers.InvalidPath(legacySBOMs) { 108 pp = pp.AddSBOMs() 109 message.Debugf("Migrating %q to %q", legacySBOMs, pp.SBOMs.Path) 110 if err := os.Rename(legacySBOMs, pp.SBOMs.Path); err != nil { 111 return err 112 } 113 } 114 115 // Migrate legacy images 116 legacyImagesTar := filepath.Join(base, "images.tar") 117 if !helpers.InvalidPath(legacyImagesTar) { 118 pp = pp.AddImages() 119 message.Debugf("Migrating %q to %q", legacyImagesTar, pp.Images.Base) 120 defer os.Remove(legacyImagesTar) 121 imgTags := []string{} 122 for _, component := range pkg.Components { 123 imgTags = append(imgTags, component.Images...) 124 } 125 // convert images to oci layout 126 // until this for-loop is complete, there will be a duplication of images, resulting in some wasted space 127 tagToDigest := make(map[string]string) 128 for _, tag := range imgTags { 129 img, err := crane.LoadTag(legacyImagesTar, tag) 130 if err != nil { 131 return err 132 } 133 if err := crane.SaveOCI(img, pp.Images.Base); err != nil { 134 return err 135 } 136 // Get the image digest so we can set an annotation in the image.json later 137 imgDigest, err := img.Digest() 138 if err != nil { 139 return err 140 } 141 tagToDigest[tag] = imgDigest.String() 142 143 if err := pp.Images.AddV1Image(img); err != nil { 144 return err 145 } 146 } 147 if err := utils.AddImageNameAnnotation(pp.Images.Base, tagToDigest); err != nil { 148 return err 149 } 150 } 151 152 // Migrate legacy components 153 // 154 // Migration of paths within components occurs during `deploy` 155 // no other operation should need to know about legacy component paths 156 for _, component := range pkg.Components { 157 _, err := pp.Components.Create(component) 158 if err != nil { 159 return err 160 } 161 } 162 163 return nil 164 } 165 166 // IsLegacyLayout returns true if the package is using the legacy layout. 167 func (pp *PackagePaths) IsLegacyLayout() bool { 168 return pp.isLegacyLayout 169 } 170 171 // SignPackage signs the jackal.yaml in a Jackal package. 172 func (pp *PackagePaths) SignPackage(signingKeyPath, signingKeyPassword string, isInteractive bool) error { 173 if signingKeyPath == "" { 174 return nil 175 } 176 177 pp.Signature = filepath.Join(pp.Base, Signature) 178 179 passwordFunc := func(_ bool) ([]byte, error) { 180 if signingKeyPassword != "" { 181 return []byte(signingKeyPassword), nil 182 } 183 if !isInteractive { 184 return nil, nil 185 } 186 return interactive.PromptSigPassword() 187 } 188 _, err := utils.CosignSignBlob(pp.JackalYAML, pp.Signature, signingKeyPath, passwordFunc) 189 if err != nil { 190 return fmt.Errorf("unable to sign the package: %w", err) 191 } 192 193 return nil 194 } 195 196 // GenerateChecksums walks through all of the files starting at the base path and generates a checksum file. 197 // 198 // Each file within the basePath represents a layer within the Jackal package. 199 // 200 // Returns a SHA256 checksum of the checksums.txt file. 201 func (pp *PackagePaths) GenerateChecksums() (string, error) { 202 var checksumsData = []string{} 203 204 for rel, abs := range pp.Files() { 205 if rel == JackalYAML || rel == Checksums { 206 continue 207 } 208 209 sum, err := helpers.GetSHA256OfFile(abs) 210 if err != nil { 211 return "", err 212 } 213 checksumsData = append(checksumsData, fmt.Sprintf("%s %s", sum, rel)) 214 } 215 slices.Sort(checksumsData) 216 217 // Create the checksums file 218 if err := os.WriteFile(pp.Checksums, []byte(strings.Join(checksumsData, "\n")+"\n"), helpers.ReadWriteUser); err != nil { 219 return "", err 220 } 221 222 // Calculate the checksum of the checksum file 223 return helpers.GetSHA256OfFile(pp.Checksums) 224 } 225 226 // ArchivePackage creates an archive for a Jackal package. 227 func (pp *PackagePaths) ArchivePackage(destinationTarball string, maxPackageSizeMB int) error { 228 spinner := message.NewProgressSpinner("Writing %s to %s", pp.Base, destinationTarball) 229 defer spinner.Stop() 230 231 // Make the archive 232 archiveSrc := []string{pp.Base + string(os.PathSeparator)} 233 if err := archiver.Archive(archiveSrc, destinationTarball); err != nil { 234 return fmt.Errorf("unable to create package: %w", err) 235 } 236 spinner.Updatef("Wrote %s to %s", pp.Base, destinationTarball) 237 238 fi, err := os.Stat(destinationTarball) 239 if err != nil { 240 return fmt.Errorf("unable to read the package archive: %w", err) 241 } 242 spinner.Successf("Package saved to %q", destinationTarball) 243 244 // Convert Megabytes to bytes. 245 chunkSize := maxPackageSizeMB * 1000 * 1000 246 247 // If a chunk size was specified and the package is larger than the chunk size, split it into chunks. 248 if maxPackageSizeMB > 0 && fi.Size() > int64(chunkSize) { 249 if fi.Size()/int64(chunkSize) > 999 { 250 return fmt.Errorf("unable to split the package archive into multiple files: must be less than 1,000 files") 251 } 252 message.Notef("Package is larger than %dMB, splitting into multiple files", maxPackageSizeMB) 253 err := utils.SplitFile(destinationTarball, chunkSize) 254 if err != nil { 255 return fmt.Errorf("unable to split the package archive into multiple files: %w", err) 256 } 257 } 258 return nil 259 } 260 261 // AddImages sets the default image paths. 262 func (pp *PackagePaths) AddImages() *PackagePaths { 263 pp.Images.Base = filepath.Join(pp.Base, ImagesDir) 264 pp.Images.OCILayout = filepath.Join(pp.Images.Base, OCILayout) 265 pp.Images.Index = filepath.Join(pp.Images.Base, IndexJSON) 266 return pp 267 } 268 269 // AddSBOMs sets the default sbom paths. 270 func (pp *PackagePaths) AddSBOMs() *PackagePaths { 271 pp.SBOMs = SBOMs{ 272 Path: filepath.Join(pp.Base, SBOMDir), 273 } 274 return pp 275 } 276 277 // SetFromLayers maps layers to package paths. 278 func (pp *PackagePaths) SetFromLayers(layers []ocispec.Descriptor) { 279 paths := []string{} 280 for _, layer := range layers { 281 if layer.Annotations[ocispec.AnnotationTitle] != "" { 282 paths = append(paths, layer.Annotations[ocispec.AnnotationTitle]) 283 } 284 } 285 pp.SetFromPaths(paths) 286 } 287 288 // SetFromPaths maps paths to package paths. 289 func (pp *PackagePaths) SetFromPaths(paths []string) { 290 for _, rel := range paths { 291 // Convert from the standard '/' to the OS path separator for Windows support 292 switch path := filepath.FromSlash(rel); { 293 case path == JackalYAML: 294 pp.JackalYAML = filepath.Join(pp.Base, path) 295 case path == Signature: 296 pp.Signature = filepath.Join(pp.Base, path) 297 case path == Checksums: 298 pp.Checksums = filepath.Join(pp.Base, path) 299 case path == SBOMTar: 300 pp.SBOMs.Path = filepath.Join(pp.Base, path) 301 case path == OCILayoutPath: 302 pp.Images.OCILayout = filepath.Join(pp.Base, path) 303 case path == IndexPath: 304 pp.Images.Index = filepath.Join(pp.Base, path) 305 case strings.HasPrefix(path, ImagesBlobsDir): 306 if pp.Images.Base == "" { 307 pp.Images.Base = filepath.Join(pp.Base, ImagesDir) 308 } 309 pp.Images.AddBlob(filepath.Base(path)) 310 case strings.HasPrefix(path, ComponentsDir) && filepath.Ext(path) == ".tar": 311 if pp.Components.Base == "" { 312 pp.Components.Base = filepath.Join(pp.Base, ComponentsDir) 313 } 314 componentName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 315 if pp.Components.Tarballs == nil { 316 pp.Components.Tarballs = make(map[string]string) 317 } 318 pp.Components.Tarballs[componentName] = filepath.Join(pp.Base, path) 319 default: 320 message.Debug("ignoring path", path) 321 } 322 } 323 } 324 325 // Files returns a map of all the files in the package. 326 func (pp *PackagePaths) Files() map[string]string { 327 pathMap := make(map[string]string) 328 329 stripBase := func(path string) string { 330 rel, _ := filepath.Rel(pp.Base, path) 331 // Convert from the OS path separator to the standard '/' for Windows support 332 return filepath.ToSlash(rel) 333 } 334 335 add := func(path string) { 336 if path == "" { 337 return 338 } 339 pathMap[stripBase(path)] = path 340 } 341 342 add(pp.JackalYAML) 343 add(pp.Signature) 344 add(pp.Checksums) 345 346 add(pp.Images.OCILayout) 347 add(pp.Images.Index) 348 for _, blob := range pp.Images.Blobs { 349 add(blob) 350 } 351 352 for _, tarball := range pp.Components.Tarballs { 353 add(tarball) 354 } 355 356 if pp.SBOMs.IsTarball() { 357 add(pp.SBOMs.Path) 358 } 359 return pathMap 360 }