github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/creator/skeleton.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package creator contains functions for creating Jackal packages. 5 package creator 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "strconv" 12 "strings" 13 14 "github.com/Racer159/jackal/src/config" 15 "github.com/Racer159/jackal/src/config/lang" 16 "github.com/Racer159/jackal/src/extensions/bigbang" 17 "github.com/Racer159/jackal/src/internal/packager/helm" 18 "github.com/Racer159/jackal/src/internal/packager/kustomize" 19 "github.com/Racer159/jackal/src/pkg/layout" 20 "github.com/Racer159/jackal/src/pkg/message" 21 "github.com/Racer159/jackal/src/pkg/utils" 22 "github.com/Racer159/jackal/src/pkg/zoci" 23 "github.com/Racer159/jackal/src/types" 24 "github.com/defenseunicorns/pkg/helpers" 25 "github.com/mholt/archiver/v3" 26 ) 27 28 var ( 29 // verify that SkeletonCreator implements Creator 30 _ Creator = (*SkeletonCreator)(nil) 31 ) 32 33 // SkeletonCreator provides methods for creating skeleton Jackal packages. 34 type SkeletonCreator struct { 35 createOpts types.JackalCreateOptions 36 publishOpts types.JackalPublishOptions 37 } 38 39 // NewSkeletonCreator returns a new SkeletonCreator. 40 func NewSkeletonCreator(createOpts types.JackalCreateOptions, publishOpts types.JackalPublishOptions) *SkeletonCreator { 41 return &SkeletonCreator{createOpts, publishOpts} 42 } 43 44 // LoadPackageDefinition loads and configure a jackal.yaml file when creating and publishing a skeleton package. 45 func (sc *SkeletonCreator) LoadPackageDefinition(dst *layout.PackagePaths) (pkg types.JackalPackage, warnings []string, err error) { 46 pkg, warnings, err = dst.ReadJackalYAML() 47 if err != nil { 48 return types.JackalPackage{}, nil, err 49 } 50 51 pkg.Metadata.Architecture = config.GetArch() 52 53 // Compose components into a single jackal.yaml file 54 pkg, composeWarnings, err := ComposeComponents(pkg, sc.createOpts.Flavor) 55 if err != nil { 56 return types.JackalPackage{}, nil, err 57 } 58 59 pkg.Metadata.Architecture = zoci.SkeletonArch 60 61 warnings = append(warnings, composeWarnings...) 62 63 pkg.Components, err = sc.processExtensions(pkg.Components, dst) 64 if err != nil { 65 return types.JackalPackage{}, nil, err 66 } 67 68 for _, warning := range warnings { 69 message.Warn(warning) 70 } 71 72 return pkg, warnings, nil 73 } 74 75 // Assemble updates all components of the loaded Jackal package with necessary modifications for package assembly. 76 // 77 // It processes each component to ensure correct structure and resource locations. 78 func (sc *SkeletonCreator) Assemble(dst *layout.PackagePaths, components []types.JackalComponent, _ string) error { 79 for _, component := range components { 80 c, err := sc.addComponent(component, dst) 81 if err != nil { 82 return err 83 } 84 components = append(components, *c) 85 } 86 87 return nil 88 } 89 90 // Output does the following: 91 // 92 // - archives components 93 // 94 // - generates checksums for all package files 95 // 96 // - writes the loaded jackal.yaml to disk 97 // 98 // - signs the package 99 func (sc *SkeletonCreator) Output(dst *layout.PackagePaths, pkg *types.JackalPackage) (err error) { 100 for _, component := range pkg.Components { 101 if err := dst.Components.Archive(component, false); err != nil { 102 return err 103 } 104 } 105 106 // Calculate all the checksums 107 pkg.Metadata.AggregateChecksum, err = dst.GenerateChecksums() 108 if err != nil { 109 return fmt.Errorf("unable to generate checksums for the package: %w", err) 110 } 111 112 if err := recordPackageMetadata(pkg, sc.createOpts); err != nil { 113 return err 114 } 115 116 if err := utils.WriteYaml(dst.JackalYAML, pkg, helpers.ReadUser); err != nil { 117 return fmt.Errorf("unable to write jackal.yaml: %w", err) 118 } 119 120 return dst.SignPackage(sc.publishOpts.SigningKeyPath, sc.publishOpts.SigningKeyPassword, !config.CommonOptions.Confirm) 121 } 122 123 func (sc *SkeletonCreator) processExtensions(components []types.JackalComponent, layout *layout.PackagePaths) (processedComponents []types.JackalComponent, err error) { 124 // Create component paths and process extensions for each component. 125 for _, c := range components { 126 componentPaths, err := layout.Components.Create(c) 127 if err != nil { 128 return nil, err 129 } 130 131 // Big Bang 132 if c.Extensions.BigBang != nil { 133 if c, err = bigbang.Skeletonize(componentPaths, c); err != nil { 134 return nil, fmt.Errorf("unable to process bigbang extension: %w", err) 135 } 136 } 137 138 processedComponents = append(processedComponents, c) 139 } 140 141 return processedComponents, nil 142 } 143 144 func (sc *SkeletonCreator) addComponent(component types.JackalComponent, dst *layout.PackagePaths) (updatedComponent *types.JackalComponent, err error) { 145 message.HeaderInfof("📦 %s COMPONENT", strings.ToUpper(component.Name)) 146 147 updatedComponent = &component 148 149 componentPaths, err := dst.Components.Create(component) 150 if err != nil { 151 return nil, err 152 } 153 154 if component.DeprecatedCosignKeyPath != "" { 155 dst := filepath.Join(componentPaths.Base, "cosign.pub") 156 err := helpers.CreatePathAndCopy(component.DeprecatedCosignKeyPath, dst) 157 if err != nil { 158 return nil, err 159 } 160 updatedComponent.DeprecatedCosignKeyPath = "cosign.pub" 161 } 162 163 // TODO: (@WSTARR) Shim the skeleton component's create action dirs to be empty. This prevents actions from failing by cd'ing into directories that will be flattened. 164 updatedComponent.Actions.OnCreate.Defaults.Dir = "" 165 166 resetActions := func(actions []types.JackalComponentAction) []types.JackalComponentAction { 167 for idx := range actions { 168 actions[idx].Dir = nil 169 } 170 return actions 171 } 172 173 updatedComponent.Actions.OnCreate.Before = resetActions(component.Actions.OnCreate.Before) 174 updatedComponent.Actions.OnCreate.After = resetActions(component.Actions.OnCreate.After) 175 updatedComponent.Actions.OnCreate.OnSuccess = resetActions(component.Actions.OnCreate.OnSuccess) 176 updatedComponent.Actions.OnCreate.OnFailure = resetActions(component.Actions.OnCreate.OnFailure) 177 178 // If any helm charts are defined, process them. 179 for chartIdx, chart := range component.Charts { 180 181 if chart.LocalPath != "" { 182 rel := filepath.Join(layout.ChartsDir, fmt.Sprintf("%s-%d", chart.Name, chartIdx)) 183 dst := filepath.Join(componentPaths.Base, rel) 184 185 err := helpers.CreatePathAndCopy(chart.LocalPath, dst) 186 if err != nil { 187 return nil, err 188 } 189 190 updatedComponent.Charts[chartIdx].LocalPath = rel 191 } 192 193 for valuesIdx, path := range chart.ValuesFiles { 194 if helpers.IsURL(path) { 195 continue 196 } 197 198 rel := fmt.Sprintf("%s-%d", helm.StandardName(layout.ValuesDir, chart), valuesIdx) 199 updatedComponent.Charts[chartIdx].ValuesFiles[valuesIdx] = rel 200 201 if err := helpers.CreatePathAndCopy(path, filepath.Join(componentPaths.Base, rel)); err != nil { 202 return nil, fmt.Errorf("unable to copy chart values file %s: %w", path, err) 203 } 204 } 205 } 206 207 for filesIdx, file := range component.Files { 208 message.Debugf("Loading %#v", file) 209 210 if helpers.IsURL(file.Source) { 211 continue 212 } 213 214 rel := filepath.Join(layout.FilesDir, strconv.Itoa(filesIdx), filepath.Base(file.Target)) 215 dst := filepath.Join(componentPaths.Base, rel) 216 destinationDir := filepath.Dir(dst) 217 218 if file.ExtractPath != "" { 219 if err := archiver.Extract(file.Source, file.ExtractPath, destinationDir); err != nil { 220 return nil, fmt.Errorf(lang.ErrFileExtract, file.ExtractPath, file.Source, err.Error()) 221 } 222 223 // Make sure dst reflects the actual file or directory. 224 updatedExtractedFileOrDir := filepath.Join(destinationDir, file.ExtractPath) 225 if updatedExtractedFileOrDir != dst { 226 if err := os.Rename(updatedExtractedFileOrDir, dst); err != nil { 227 return nil, fmt.Errorf(lang.ErrWritingFile, dst, err) 228 } 229 } 230 } else { 231 if err := helpers.CreatePathAndCopy(file.Source, dst); err != nil { 232 return nil, fmt.Errorf("unable to copy file %s: %w", file.Source, err) 233 } 234 } 235 236 // Change the source to the new relative source directory (any remote files will have been skipped above) 237 updatedComponent.Files[filesIdx].Source = rel 238 239 // Remove the extractPath from a skeleton since it will already extract it 240 updatedComponent.Files[filesIdx].ExtractPath = "" 241 242 // Abort packaging on invalid shasum (if one is specified). 243 if file.Shasum != "" { 244 if err := helpers.SHAsMatch(dst, file.Shasum); err != nil { 245 return nil, err 246 } 247 } 248 249 if file.Executable || helpers.IsDir(dst) { 250 _ = os.Chmod(dst, helpers.ReadWriteExecuteUser) 251 } else { 252 _ = os.Chmod(dst, helpers.ReadWriteUser) 253 } 254 } 255 256 if len(component.DataInjections) > 0 { 257 spinner := message.NewProgressSpinner("Loading data injections") 258 defer spinner.Stop() 259 260 for dataIdx, data := range component.DataInjections { 261 spinner.Updatef("Copying data injection %s for %s", data.Target.Path, data.Target.Selector) 262 263 rel := filepath.Join(layout.DataInjectionsDir, strconv.Itoa(dataIdx), filepath.Base(data.Target.Path)) 264 dst := filepath.Join(componentPaths.Base, rel) 265 266 if err := helpers.CreatePathAndCopy(data.Source, dst); err != nil { 267 return nil, fmt.Errorf("unable to copy data injection %s: %s", data.Source, err.Error()) 268 } 269 270 updatedComponent.DataInjections[dataIdx].Source = rel 271 } 272 273 spinner.Success() 274 } 275 276 if len(component.Manifests) > 0 { 277 // Get the proper count of total manifests to add. 278 manifestCount := 0 279 280 for _, manifest := range component.Manifests { 281 manifestCount += len(manifest.Files) 282 manifestCount += len(manifest.Kustomizations) 283 } 284 285 spinner := message.NewProgressSpinner("Loading %d K8s manifests", manifestCount) 286 defer spinner.Stop() 287 288 // Iterate over all manifests. 289 for manifestIdx, manifest := range component.Manifests { 290 for fileIdx, path := range manifest.Files { 291 rel := filepath.Join(layout.ManifestsDir, fmt.Sprintf("%s-%d.yaml", manifest.Name, fileIdx)) 292 dst := filepath.Join(componentPaths.Base, rel) 293 294 // Copy manifests without any processing. 295 spinner.Updatef("Copying manifest %s", path) 296 297 if err := helpers.CreatePathAndCopy(path, dst); err != nil { 298 return nil, fmt.Errorf("unable to copy manifest %s: %w", path, err) 299 } 300 301 updatedComponent.Manifests[manifestIdx].Files[fileIdx] = rel 302 } 303 304 for kustomizeIdx, path := range manifest.Kustomizations { 305 // Generate manifests from kustomizations and place in the package. 306 spinner.Updatef("Building kustomization for %s", path) 307 308 kname := fmt.Sprintf("kustomization-%s-%d.yaml", manifest.Name, kustomizeIdx) 309 rel := filepath.Join(layout.ManifestsDir, kname) 310 dst := filepath.Join(componentPaths.Base, rel) 311 312 if err := kustomize.Build(path, dst, manifest.KustomizeAllowAnyDirectory); err != nil { 313 return nil, fmt.Errorf("unable to build kustomization %s: %w", path, err) 314 } 315 } 316 317 // remove kustomizations 318 updatedComponent.Manifests[manifestIdx].Kustomizations = nil 319 } 320 321 spinner.Success() 322 } 323 324 return updatedComponent, nil 325 }