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  }