github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/compute/validate.go (about)

     1  package compute
     2  
     3  import (
     4  	"archive/tar"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/kennygrant/sanitize"
    11  	"github.com/mholt/archiver/v3"
    12  
    13  	"github.com/fastly/cli/pkg/argparser"
    14  	fsterr "github.com/fastly/cli/pkg/errors"
    15  	"github.com/fastly/cli/pkg/global"
    16  	"github.com/fastly/cli/pkg/manifest"
    17  	"github.com/fastly/cli/pkg/text"
    18  )
    19  
    20  // NewValidateCommand returns a usable command registered under the parent.
    21  func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCommand {
    22  	var c ValidateCommand
    23  	c.Globals = g
    24  	c.CmdClause = parent.Command("validate", "Validate a Compute package")
    25  	c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.path)
    26  	c.CmdClause.Flag("env", "The manifest environment config to validate (e.g. 'stage' will attempt to read 'fastly.stage.toml' inside the package)").StringVar(&c.env)
    27  	return &c
    28  }
    29  
    30  // Exec implements the command interface.
    31  func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error {
    32  	packagePath := c.path
    33  	if packagePath == "" {
    34  		projectName, source := c.Globals.Manifest.Name()
    35  		if source == manifest.SourceUndefined {
    36  			return fsterr.RemediationError{
    37  				Inner:       fmt.Errorf("failed to read project name: %w", fsterr.ErrReadingManifest),
    38  				Remediation: "Run `fastly compute build` to produce a Compute package, alternatively use the --package flag to reference a package outside of the current project.",
    39  			}
    40  		}
    41  		packagePath = filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName)))
    42  	}
    43  
    44  	p, err := filepath.Abs(packagePath)
    45  	if err != nil {
    46  		c.Globals.ErrLog.AddWithContext(err, map[string]any{
    47  			"Path": c.path,
    48  		})
    49  		return fmt.Errorf("error reading file path: %w", err)
    50  	}
    51  
    52  	if c.env != "" {
    53  		manifestFilename := fmt.Sprintf("fastly.%s.toml", c.env)
    54  		if c.Globals.Verbose() {
    55  			text.Info(out, "Using the '%s' environment manifest (it will be packaged up as %s)\n\n", manifestFilename, manifest.Filename)
    56  		}
    57  	}
    58  
    59  	if err := validatePackageContent(p); err != nil {
    60  		c.Globals.ErrLog.AddWithContext(err, map[string]any{
    61  			"Path": c.path,
    62  		})
    63  		return fsterr.RemediationError{
    64  			Inner:       fmt.Errorf("failed to validate package: %w", err),
    65  			Remediation: "Run `fastly compute build` to produce a Compute package, alternatively use the --package flag to reference a package outside of the current project.",
    66  		}
    67  	}
    68  
    69  	text.Success(out, "Validated package %s", p)
    70  	return nil
    71  }
    72  
    73  // ValidateCommand validates a package archive.
    74  type ValidateCommand struct {
    75  	argparser.Base
    76  	env  string
    77  	path string
    78  }
    79  
    80  // validatePackageContent is a utility function to determine whether a package
    81  // is valid. It walks through the package files checking the filename against a
    82  // list of required files. If one of the files doesn't exist it returns an error.
    83  //
    84  // NOTE: This function is also called by the `deploy` command.
    85  func validatePackageContent(pkgPath string) error {
    86  	// False positive https://github.com/semgrep/semgrep/issues/8593
    87  	// nosemgrep: trailofbits.go.iterate-over-empty-map.iterate-over-empty-map
    88  	files := map[string]bool{
    89  		manifest.Filename: false,
    90  		"main.wasm":       false,
    91  	}
    92  
    93  	if err := packageFiles(pkgPath, func(f archiver.File) error {
    94  		for k := range files {
    95  			if k == f.Name() {
    96  				files[k] = true
    97  			}
    98  		}
    99  		return nil
   100  	}); err != nil {
   101  		return err
   102  	}
   103  
   104  	for k, found := range files {
   105  		if !found {
   106  			return fmt.Errorf("error validating package: package must contain a %s file", k)
   107  		}
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  // packageFiles is a utility function to iterate over the package content.
   114  // It attempts to unarchive and read a tar.gz file from a specific path,
   115  // calling fn on each file in the archive.
   116  func packageFiles(path string, fn func(archiver.File) error) error {
   117  	file, err := os.Open(filepath.Clean(path))
   118  	if err != nil {
   119  		return fmt.Errorf("error reading package: %w", err)
   120  	}
   121  	defer file.Close() // #nosec G307
   122  
   123  	tr := archiver.NewTarGz()
   124  	err = tr.Open(file, 0)
   125  	if err != nil {
   126  		return fmt.Errorf("error unarchiving package: %w", err)
   127  	}
   128  	defer tr.Close()
   129  
   130  	for {
   131  		f, err := tr.Read()
   132  		if err == io.EOF {
   133  			break
   134  		}
   135  		if err != nil {
   136  			return fmt.Errorf("error reading package: %w", err)
   137  		}
   138  
   139  		header, ok := f.Header.(*tar.Header)
   140  		if !ok || header.Typeflag != tar.TypeReg {
   141  			f.Close()
   142  			continue
   143  		}
   144  
   145  		if err = fn(f); err != nil {
   146  			f.Close()
   147  			return err
   148  		}
   149  
   150  		err = f.Close()
   151  		if err != nil {
   152  			return fmt.Errorf("error closing file: %w", err)
   153  		}
   154  	}
   155  
   156  	return nil
   157  }