Skip to content

Commit

Permalink
feat(compute): add metadata subcommand (#1013)
Browse files Browse the repository at this point in the history
* feat(telemetry): add telemetry command

* refactor(telemetry): enable by default

* doc(DEVELOP): clarify the app config flows

* doc(config): annotate File fields

* feat: display telemetry to user on first run after install or update

* fix(ci): add /dev/null equivalent for Windows

* refactor: rename telemetry wasm-metadata

* refactor: move `telemetry` command to `compute metadata`

* refactor(compute/metadata): display updated config settings

* fix(metadata): machine info must be opt-in

* fix(compute/metadata): reference correct error type

* remove(telemetry): command moved to compute metadata.

* fix(compute/build): remove unused arg

* fix(app): update commandCollectsData() list

* feat: only record metadata for items marked as enabled

* fix(app): hide metadata message unless the user has opted into data collection

* refactor: messaging to include additional links

* refactor(compute/metadata): make a hidden command for now

* refactor: move sdk data into package_info
  • Loading branch information
Integralist authored Oct 31, 2023
1 parent 2762cc9 commit 2aeed0f
Show file tree
Hide file tree
Showing 13 changed files with 439 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .fastly/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ config_version = 4
[fastly]
api_endpoint = "https://api.fastly.com"

[wasm-metadata]
build_info = "enable"
machine_info = "disable" # users have to opt-in for this (everything else they'll have to opt-out)
package_info = "enable"

[language]
[language.go]
tinygo_constraint = ">= 0.28.1-0" # NOTE -0 indicates to the CLI's semver package that we accept pre-releases (TinyGo users commonly use pre-releases).
Expand Down
8 changes: 7 additions & 1 deletion DEVELOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ CLI_CATEGORY=logging CLI_CATEGORY_COMMAND=logging CLI_PACKAGE=foobar CLI_COMMAND
### `.fastly/config.toml`

The CLI dynamically generates the `./pkg/config/config.toml` within the CI release process so it can be embedded into the CLI binary.
The CLI dynamically generates the `./pkg/config/config.toml` within the CI release process so it can be embedded into the CLI binary.

The file is added to `.gitignore` to avoid it being added to the git repository.

When compiling the CLI for a new release, it will execute [`./scripts/config.sh`](./scripts/config.sh). The script uses [`./.fastly/config.toml`](./.fastly/config.toml) as a template file to then dynamically inject a list of starter kits (pulling their data from their public repositories).

The resulting configuration is then saved to disk at `./pkg/config/config.toml` and embedded into the CLI when compiled.

When a user installs the CLI for the first time, they'll have no existing config and so the embedded config will be used. In the future, when the user updates their CLI, the existing config they have will be used.

If the config has changed in any way, then you (the CLI developer) should ensure the `config_version` number is bumped before publishing a new CLI release. This is because when the user updates to that new CLI version and the invoke the CLI, the CLI will identify a mismatch between the user's local config version and the embedded config version. This will cause the embedded config to be merged with the local config and consequently the user's config will be updated to include the new fields.

> **NOTE:** The CLI does provide a `fastly config --reset` option that resets the config to a version compatible with the user's current CLI version. This is fallback for users who run into issues for whatever reason.
### Running Compute commands locally

If you need to test the Fastly CLI locally while developing a Compute feature, then use the `--dir` flag (exposed on `compute build`, `compute deploy`, `compute serve` and `compute publish`) to ensure the CLI doesn't attempt to treat the repository directory as your project directory.
Expand Down
2 changes: 2 additions & 0 deletions pkg/app/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func defineCommands(
computeHashFiles := compute.NewHashFilesCommand(computeCmdRoot.CmdClause, g, computeBuild)
computeHashsum := compute.NewHashsumCommand(computeCmdRoot.CmdClause, g, computeBuild)
computeInit := compute.NewInitCommand(computeCmdRoot.CmdClause, g, m)
computeMetadata := compute.NewMetadataCommand(computeCmdRoot.CmdClause, g)
computePack := compute.NewPackCommand(computeCmdRoot.CmdClause, g, m)
computePublish := compute.NewPublishCommand(computeCmdRoot.CmdClause, g, computeBuild, computeDeploy)
computeServe := compute.NewServeCommand(computeCmdRoot.CmdClause, g, computeBuild, opts.Versioners.Viceroy)
Expand Down Expand Up @@ -481,6 +482,7 @@ func defineCommands(
computeHashFiles,
computeHashsum,
computeInit,
computeMetadata,
computePack,
computePublish,
computeServe,
Expand Down
35 changes: 31 additions & 4 deletions pkg/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"fmt"
"io"
"os"
"slices"
"strconv"
"strings"
"time"

"github.com/fastly/go-fastly/v8/fastly"
"github.com/fastly/kingpin"
Expand Down Expand Up @@ -96,15 +99,15 @@ func Run(opts RunOpts) error {
app.Flag("verbose", "Verbose logging").Short('v').BoolVar(&g.Flags.Verbose)

commands := defineCommands(app, &g, *opts.Manifest, opts)
command, name, err := processCommandInput(opts, app, &g, commands)
command, commandName, err := processCommandInput(opts, app, &g, commands)
if err != nil {
return err
}
// We short-circuit the execution for specific cases:
//
// - cmd.ArgsIsHelpJSON() == true
// - shell autocompletion flag provided
switch name {
switch commandName {
case "help--format=json":
fallthrough
case "help--formatjson":
Expand All @@ -113,6 +116,20 @@ func Run(opts RunOpts) error {
return nil
}

// FIXME: Tweak messaging before for 10.7.0
// To learn more about what data is being collected, why, and how to disable it: https://developer.fastly.com/reference/cli/
metadataDisable, _ := strconv.ParseBool(g.Env.WasmMetadataDisable)
if slices.Contains(opts.Args, "--metadata-enable") && !metadataDisable && !g.Config.CLI.MetadataNoticeDisplayed && commandCollectsData(commandName) {
text.Important(g.Output, "The Fastly CLI is configured to collect data related to Wasm builds (e.g. compilation times, resource usage, and other non-identifying data). To learn more about our data & privacy policies visit https://www.fastly.com/trust. Join the conversation https://bit.ly/wasm-metadata")
text.Break(g.Output)
g.Config.CLI.MetadataNoticeDisplayed = true
err := g.Config.Write(g.ConfigPath)
if err != nil {
return fmt.Errorf("failed to persist change to metadata notice: %w", err)
}
time.Sleep(5 * time.Second) // this message is only displayed once so give the user a chance to see it before it possibly scrolls off screen
}

if g.Flags.Quiet {
opts.Manifest.File.SetQuiet(true)
}
Expand All @@ -136,7 +153,7 @@ func Run(opts RunOpts) error {
// If we are using the token from config file, check the file's permissions
// to assert if they are not too open or have been altered outside of the
// application and warn if so.
segs := strings.Split(name, " ")
segs := strings.Split(commandName, " ")
if source == lookup.SourceFile && (len(segs) > 0 && segs[0] != "profile") {
if fi, err := os.Stat(config.FilePath); err == nil {
if mode := fi.Mode().Perm(); mode > config.FilePermissions {
Expand Down Expand Up @@ -177,7 +194,7 @@ func Run(opts RunOpts) error {
return fmt.Errorf("error constructing Fastly realtime stats client: %w", err)
}

if opts.Versioners.CLI != nil && name != "update" && !version.IsPreRelease(revision.AppVersion) {
if opts.Versioners.CLI != nil && commandName != "update" && !version.IsPreRelease(revision.AppVersion) {
f := update.CheckAsync(
revision.AppVersion,
opts.Versioners.CLI,
Expand Down Expand Up @@ -246,3 +263,13 @@ func determineProfile(manifestValue, flagValue string, profiles config.Profiles)
name, _ := profile.Default(profiles)
return name
}

// commandCollectsData determines if the command to be executed is one that
// collects data related to a Wasm binary.
func commandCollectsData(command string) bool {
switch command {
case "compute build", "compute hashsum", "compute hash-files", "compute publish", "compute serve":
return true
}
return false
}
2 changes: 1 addition & 1 deletion pkg/app/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ whoami

go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
_, _ = io.Copy(&buf, r)
outC <- buf.String()
}()

Expand Down
104 changes: 69 additions & 35 deletions pkg/commands/compute/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,34 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) {
// FIXME: For feature launch replace enable flag with disable equivalent.
// e.g. define --metadata-disable and check for that first with env var.
// Also make sure hidden flags (across all composite commands) aren't hidden.
// Also update the run.go app to remove the message which displays a warning.
// Also one final release un-hide the metadata command and add metadata.json examples
// e.g.
/*
"metadata": {
"examples": [
{
"cmd": "fastly compute metadata --enable",
"title": "Enable all metadata collection information"
},
{
"cmd": "fastly compute metadata --disable",
"title": "Disable all metadata collection information"
},
{
"cmd": "fastly compute metadata --enable-build --enable-machine --enable-package",
"title": "Enable specific metadata collection information"
},
{
"cmd": "fastly compute metadata --disable-build --disable-machine --disable-package",
"title": "Disable specific metadata collection information"
}
]
},
*/
metadataDisable, _ := strconv.ParseBool(c.Globals.Env.WasmMetadataDisable)
if c.MetadataEnable && !metadataDisable {
if err := c.AnnotateWasmBinaryLong(wasmtools, metadataArgs, language, out); err != nil {
if err := c.AnnotateWasmBinaryLong(wasmtools, metadataArgs, language); err != nil {
return err
}
} else {
Expand Down Expand Up @@ -327,24 +352,11 @@ func (c *BuildCommand) AnnotateWasmBinaryShort(wasmtools string, args []string)
}

// AnnotateWasmBinaryLong annotates the Wasm binary will all available data.
func (c *BuildCommand) AnnotateWasmBinaryLong(wasmtools string, args []string, language *Language, out io.Writer) error {
func (c *BuildCommand) AnnotateWasmBinaryLong(wasmtools string, args []string, language *Language) error {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)

dc := DataCollection{
BuildInfo: DataCollectionBuildInfo{
MemoryHeapAlloc: ms.HeapAlloc,
},
MachineInfo: DataCollectionMachineInfo{
Arch: runtime.GOARCH,
CPUs: runtime.NumCPU(),
Compiler: runtime.Compiler,
GoVersion: runtime.Version(),
OS: runtime.GOOS,
},
PackageInfo: DataCollectionPackageInfo{
ClonedFrom: c.Globals.Manifest.File.ClonedFrom,
},
ScriptInfo: DataCollectionScriptInfo{
DefaultBuildUsed: language.DefaultBuildScript(),
BuildScript: c.Globals.Manifest.File.Scripts.Build,
Expand All @@ -354,6 +366,27 @@ func (c *BuildCommand) AnnotateWasmBinaryLong(wasmtools string, args []string, l
},
}

if c.Globals.Config.WasmMetadata.BuildInfo == "enable" {
dc.BuildInfo = DataCollectionBuildInfo{
MemoryHeapAlloc: ms.HeapAlloc,
}
}
if c.Globals.Config.WasmMetadata.MachineInfo == "enable" {
dc.MachineInfo = DataCollectionMachineInfo{
Arch: runtime.GOARCH,
CPUs: runtime.NumCPU(),
Compiler: runtime.Compiler,
GoVersion: runtime.Version(),
OS: runtime.GOOS,
}
}
if c.Globals.Config.WasmMetadata.PackageInfo == "enable" {
dc.PackageInfo = DataCollectionPackageInfo{
ClonedFrom: c.Globals.Manifest.File.ClonedFrom,
Packages: language.Dependencies(),
}
}

// NOTE: There's an open issue (2023.10.13) with ResultsChan().
// https://github.com/trufflesecurity/trufflehog/issues/1881
// As a workaround: I've implemented a custom printer to track results.
Expand Down Expand Up @@ -438,10 +471,6 @@ func (c *BuildCommand) AnnotateWasmBinaryLong(wasmtools string, args []string, l

args = append(args, fmt.Sprintf("--processed-by=fastly_data=%s", data))

for k, v := range language.Dependencies() {
args = append(args, fmt.Sprintf("--sdk=%s=%s", k, v))
}

return c.Globals.ExecuteWasmTools(wasmtools, args)
}

Expand Down Expand Up @@ -942,38 +971,43 @@ func GetNonIgnoredFiles(base string, ignoredFiles map[string]bool) ([]string, er

// DataCollection represents data annotated onto the Wasm binary.
type DataCollection struct {
BuildInfo DataCollectionBuildInfo `json:"build_info"`
MachineInfo DataCollectionMachineInfo `json:"machine_info"`
PackageInfo DataCollectionPackageInfo `json:"package_info"`
ScriptInfo DataCollectionScriptInfo `json:"script_info"`
BuildInfo DataCollectionBuildInfo `json:"build_info,omitempty"`
MachineInfo DataCollectionMachineInfo `json:"machine_info,omitempty"`
PackageInfo DataCollectionPackageInfo `json:"package_info,omitempty"`
ScriptInfo DataCollectionScriptInfo `json:"script_info,omitempty"`
}

// DataCollectionBuildInfo represents build data annotated onto the Wasm binary.
type DataCollectionBuildInfo struct {
MemoryHeapAlloc uint64 `json:"mem_heap_alloc"`
MemoryHeapAlloc uint64 `json:"mem_heap_alloc,omitempty"`
}

// DataCollectionMachineInfo represents machine data annotated onto the Wasm binary.
type DataCollectionMachineInfo struct {
Arch string `json:"arch"`
CPUs int `json:"cpus"`
Compiler string `json:"compiler"`
GoVersion string `json:"go_version"`
OS string `json:"os"`
Arch string `json:"arch,omitempty"`
CPUs int `json:"cpus,omitempty"`
Compiler string `json:"compiler,omitempty"`
GoVersion string `json:"go_version,omitempty"`
OS string `json:"os,omitempty"`
}

// DataCollectionPackageInfo represents package data annotated onto the Wasm binary.
type DataCollectionPackageInfo struct {
ClonedFrom string `json:"cloned_from"`
// ClonedFrom indicates if the Starter Kit used was cloned from a specific
// repository (e.g. using the `compute init` --from flag).
ClonedFrom string `json:"cloned_from,omitempty"`
// Packages is a map where the key is the name of the package and the value is
// the package version.
Packages map[string]string `json:"packages,omitempty"`
}

// DataCollectionScriptInfo represents script data annotated onto the Wasm binary.
type DataCollectionScriptInfo struct {
DefaultBuildUsed bool `json:"default_build_used"`
BuildScript string `json:"build_script"`
EnvVars []string `json:"env_vars"`
PostInitScript string `json:"post_init_script"`
PostBuildScript string `json:"post_build_script"`
DefaultBuildUsed bool `json:"default_build_used,omitempty"`
BuildScript string `json:"build_script,omitempty"`
EnvVars []string `json:"env_vars,omitempty"`
PostInitScript string `json:"post_init_script,omitempty"`
PostBuildScript string `json:"post_build_script,omitempty"`
}

// Result represents an identified secret.
Expand Down
Loading

0 comments on commit 2aeed0f

Please sign in to comment.