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 }