github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/commands/compute/hashfiles.go (about) 1 package compute 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "crypto/sha512" 7 "errors" 8 "fmt" 9 "io" 10 "os" 11 "path/filepath" 12 "sort" 13 14 "github.com/kennygrant/sanitize" 15 "github.com/mholt/archiver/v3" 16 17 "github.com/fastly/cli/pkg/argparser" 18 fsterr "github.com/fastly/cli/pkg/errors" 19 "github.com/fastly/cli/pkg/global" 20 "github.com/fastly/cli/pkg/manifest" 21 "github.com/fastly/cli/pkg/text" 22 ) 23 24 // MaxPackageSize represents the max package size that can be uploaded to the 25 // Fastly Package API endpoint. 26 // 27 // NOTE: This is variable not a constant for the sake of test manipulations. 28 // https://developer.fastly.com/learning/compute/#limitations-and-constraints 29 var MaxPackageSize int64 = 100000000 // 100MB in bytes 30 31 // HashFilesCommand produces a deployable artifact from files on the local disk. 32 type HashFilesCommand struct { 33 argparser.Base 34 35 // Build fields 36 dir argparser.OptionalString 37 env argparser.OptionalString 38 includeSrc argparser.OptionalBool 39 lang argparser.OptionalString 40 metadataDisable argparser.OptionalBool 41 metadataFilterEnvVars argparser.OptionalString 42 metadataShow argparser.OptionalBool 43 packageName argparser.OptionalString 44 timeout argparser.OptionalInt 45 46 buildCmd *BuildCommand 47 Package string 48 SkipBuild bool 49 } 50 51 // NewHashFilesCommand returns a usable command registered under the parent. 52 func NewHashFilesCommand(parent argparser.Registerer, g *global.Data, build *BuildCommand) *HashFilesCommand { 53 var c HashFilesCommand 54 c.buildCmd = build 55 c.Globals = g 56 c.CmdClause = parent.Command("hash-files", "Generate a SHA512 digest from the contents of the Compute package") 57 c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').Action(c.dir.Set).StringVar(&c.dir.Value) 58 c.CmdClause.Flag("env", "The manifest environment config to use (e.g. 'stage' will attempt to read 'fastly.stage.toml')").Action(c.env.Set).StringVar(&c.env.Value) 59 c.CmdClause.Flag("include-source", "Include source code in built package").Action(c.includeSrc.Set).BoolVar(&c.includeSrc.Value) 60 c.CmdClause.Flag("language", "Language type").Action(c.lang.Set).StringVar(&c.lang.Value) 61 c.CmdClause.Flag("metadata-disable", "Disable Wasm binary metadata annotations").Action(c.metadataDisable.Set).BoolVar(&c.metadataDisable.Value) 62 c.CmdClause.Flag("metadata-filter-envvars", "Redact specified environment variables from [scripts.env_vars] using comma-separated list").Action(c.metadataFilterEnvVars.Set).StringVar(&c.metadataFilterEnvVars.Value) 63 c.CmdClause.Flag("metadata-show", "Inspect the Wasm binary metadata").Action(c.metadataShow.Set).BoolVar(&c.metadataShow.Value) 64 c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.Package) 65 c.CmdClause.Flag("package-name", "Package name").Action(c.packageName.Set).StringVar(&c.packageName.Value) 66 c.CmdClause.Flag("skip-build", "Skip the build step").BoolVar(&c.SkipBuild) 67 c.CmdClause.Flag("timeout", "Timeout, in seconds, for the build compilation step").Action(c.timeout.Set).IntVar(&c.timeout.Value) 68 69 return &c 70 } 71 72 // Exec implements the command interface. 73 func (c *HashFilesCommand) Exec(in io.Reader, out io.Writer) (err error) { 74 if !c.SkipBuild && c.Package == "" { 75 err = c.Build(in, out) 76 if err != nil { 77 return err 78 } 79 if c.Globals.Verbose() { 80 text.Break(out) 81 } 82 } 83 84 var pkgPath string 85 86 if c.Package == "" { 87 manifestFilename := EnvironmentManifest(c.env.Value) 88 wd, err := os.Getwd() 89 if err != nil { 90 return fmt.Errorf("failed to get current working directory: %w", err) 91 } 92 defer func() { 93 _ = os.Chdir(wd) 94 }() 95 manifestPath := filepath.Join(wd, manifestFilename) 96 97 projectDir, err := ChangeProjectDirectory(c.dir.Value) 98 if err != nil { 99 return err 100 } 101 if projectDir != "" { 102 if c.Globals.Verbose() { 103 text.Info(out, ProjectDirMsg, projectDir) 104 } 105 manifestPath = filepath.Join(projectDir, manifestFilename) 106 } 107 108 if projectDir != "" || c.env.WasSet { 109 err = c.Globals.Manifest.File.Read(manifestPath) 110 } else { 111 err = c.Globals.Manifest.File.ReadError() 112 } 113 if err != nil { 114 if errors.Is(err, os.ErrNotExist) { 115 err = fsterr.ErrReadingManifest 116 } 117 c.Globals.ErrLog.Add(err) 118 return err 119 } 120 121 projectName, source := c.Globals.Manifest.Name() 122 if source == manifest.SourceUndefined { 123 return fsterr.ErrReadingManifest 124 } 125 pkgPath = filepath.Join(projectDir, "pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) 126 } else { 127 pkgPath, err = filepath.Abs(c.Package) 128 if err != nil { 129 return fmt.Errorf("failed to locate package path '%s': %w", c.Package, err) 130 } 131 } 132 133 hash, err := getFilesHash(pkgPath) 134 if err != nil { 135 return err 136 } 137 138 text.Output(out, hash) 139 return nil 140 } 141 142 // Build constructs and executes the build logic. 143 func (c *HashFilesCommand) Build(in io.Reader, out io.Writer) error { 144 output := out 145 if !c.Globals.Verbose() { 146 output = io.Discard 147 } 148 if c.dir.WasSet { 149 c.buildCmd.Flags.Dir = c.dir.Value 150 } 151 if c.env.WasSet { 152 c.buildCmd.Flags.Env = c.env.Value 153 } 154 if c.includeSrc.WasSet { 155 c.buildCmd.Flags.IncludeSrc = c.includeSrc.Value 156 } 157 if c.lang.WasSet { 158 c.buildCmd.Flags.Lang = c.lang.Value 159 } 160 if c.packageName.WasSet { 161 c.buildCmd.Flags.PackageName = c.packageName.Value 162 } 163 if c.timeout.WasSet { 164 c.buildCmd.Flags.Timeout = c.timeout.Value 165 } 166 if c.metadataDisable.WasSet { 167 c.buildCmd.MetadataDisable = c.metadataDisable.Value 168 } 169 if c.metadataFilterEnvVars.WasSet { 170 c.buildCmd.MetadataFilterEnvVars = c.metadataFilterEnvVars.Value 171 } 172 if c.metadataShow.WasSet { 173 c.buildCmd.MetadataShow = c.metadataShow.Value 174 } 175 return c.buildCmd.Exec(in, output) 176 } 177 178 // getFilesHash returns a hash of all the files in the package in sorted filename order. 179 func getFilesHash(pkgPath string) (string, error) { 180 contents := make(map[string]*bytes.Buffer) 181 182 if err := packageFiles(pkgPath, func(f archiver.File) error { 183 // We want the full path here and not f.Name(), which is only the 184 // filename. 185 // 186 // This is safe to do - we already verified it in packageFiles(). 187 header, ok := f.Header.(*tar.Header) 188 if !ok { 189 return errors.New("failed to convert file type into *tar.Header") 190 } 191 entry := header.Name 192 contents[entry] = &bytes.Buffer{} 193 if _, err := io.Copy(contents[entry], f); err != nil { 194 return fmt.Errorf("error reading %s: %w", entry, err) 195 } 196 return nil 197 }); err != nil { 198 return "", err 199 } 200 201 keys := make([]string, 0, len(contents)) 202 for k := range contents { 203 keys = append(keys, k) 204 } 205 sort.Strings(keys) 206 207 h := sha512.New() 208 for _, entry := range keys { 209 if _, err := io.Copy(h, contents[entry]); err != nil { 210 return "", fmt.Errorf("failed to generate hash from package files: %w", err) 211 } 212 } 213 return fmt.Sprintf("%x", h.Sum(nil)), nil 214 }