diff --git a/RELEASE.md b/RELEASE.md index d9e515a51..70b3c2b7b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,7 @@ # Release Process 1. Merge all PRs intended for the release. +1. Ensure any relevant `FIXME` notes in the code are addressed (e.g. `FIXME: remove this feature before next major release`). 1. Rebase latest remote main branch locally (`git pull --rebase origin main`). 1. Ensure all analysis checks and tests are passing (`time TEST_COMPUTE_INIT=1 TEST_COMPUTE_BUILD=1 TEST_COMPUTE_DEPLOY=1 make all`). 1. Ensure goreleaser builds locally (`make release GORELEASER_ARGS="--skip-validate --skip-post-hooks --clean"`). diff --git a/pkg/app/commands.go b/pkg/app/commands.go index 5d7d78b8b..c69bc8848 100644 --- a/pkg/app/commands.go +++ b/pkg/app/commands.go @@ -111,16 +111,16 @@ func defineCommands( backendList := backend.NewListCommand(backendCmdRoot.CmdClause, g, m) backendUpdate := backend.NewUpdateCommand(backendCmdRoot.CmdClause, g, m) computeCmdRoot := compute.NewRootCommand(app, g) - computeBuild := compute.NewBuildCommand(computeCmdRoot.CmdClause, g, m) - computeDeploy := compute.NewDeployCommand(computeCmdRoot.CmdClause, g, m) - computeHashFiles := compute.NewHashFilesCommand(computeCmdRoot.CmdClause, g, computeBuild, m) - computeHashsum := compute.NewHashsumCommand(computeCmdRoot.CmdClause, g, computeBuild, m) + computeBuild := compute.NewBuildCommand(computeCmdRoot.CmdClause, g) + computeDeploy := compute.NewDeployCommand(computeCmdRoot.CmdClause, g) + computeHashFiles := compute.NewHashFilesCommand(computeCmdRoot.CmdClause, g, computeBuild) + computeHashsum := compute.NewHashsumCommand(computeCmdRoot.CmdClause, g, computeBuild) computeInit := compute.NewInitCommand(computeCmdRoot.CmdClause, g, m) computePack := compute.NewPackCommand(computeCmdRoot.CmdClause, g, m) - computePublish := compute.NewPublishCommand(computeCmdRoot.CmdClause, g, computeBuild, computeDeploy, m) - computeServe := compute.NewServeCommand(computeCmdRoot.CmdClause, g, computeBuild, opts.Versioners.Viceroy, m) + computePublish := compute.NewPublishCommand(computeCmdRoot.CmdClause, g, computeBuild, computeDeploy) + computeServe := compute.NewServeCommand(computeCmdRoot.CmdClause, g, computeBuild, opts.Versioners.Viceroy) computeUpdate := compute.NewUpdateCommand(computeCmdRoot.CmdClause, g, m) - computeValidate := compute.NewValidateCommand(computeCmdRoot.CmdClause, g, m) + computeValidate := compute.NewValidateCommand(computeCmdRoot.CmdClause, g) configCmdRoot := config.NewRootCommand(app, g) configstoreCmdRoot := configstore.NewRootCommand(app, g) configstoreCreate := configstore.NewCreateCommand(configstoreCmdRoot.CmdClause, g, m) diff --git a/pkg/commands/compute/build.go b/pkg/commands/compute/build.go index c69302d0a..f69e2273f 100644 --- a/pkg/commands/compute/build.go +++ b/pkg/commands/compute/build.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/kennygrant/sanitize" "github.com/mholt/archiver/v3" @@ -26,11 +27,12 @@ const IgnoreFilePath = ".fastlyignore" // CustomPostScriptMessage is the message displayed to a user when there is // either a post_init or post_build script defined. -const CustomPostScriptMessage = "This project has a custom post_%s script defined in the fastly.toml manifest" +const CustomPostScriptMessage = "This project has a custom post_%s script defined in the %s manifest" // Flags represents the flags defined for the command. type Flags struct { Dir string + Env string IncludeSrc bool Lang string PackageName string @@ -43,20 +45,19 @@ type BuildCommand struct { // NOTE: these are public so that the "serve" and "publish" composite // commands can set the values appropriately before calling Exec(). - Flags Flags - Manifest manifest.Data + Flags Flags } // NewBuildCommand returns a usable command registered under the parent. -func NewBuildCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *BuildCommand { +func NewBuildCommand(parent cmd.Registerer, g *global.Data) *BuildCommand { var c BuildCommand c.Globals = g - c.Manifest = m // TODO: Stop passing a non-mutable 'copy' in any commands. c.CmdClause = parent.Command("build", "Build a Compute package locally") // NOTE: when updating these flags, be sure to update the composite commands: // `compute publish` and `compute serve`. c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').StringVar(&c.Flags.Dir) + c.CmdClause.Flag("env", "The manifest environment config to use (e.g. 'stage' will attempt to read 'fastly.stage.toml')").StringVar(&c.Flags.Env) c.CmdClause.Flag("include-source", "Include source code in built package").BoolVar(&c.Flags.IncludeSrc) c.CmdClause.Flag("language", "Language type").StringVar(&c.Flags.Lang) c.CmdClause.Flag("package-name", "Package name").StringVar(&c.Flags.PackageName) @@ -69,28 +70,32 @@ func NewBuildCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *Bu func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { // We'll restore this at the end to print a final successful build output. originalOut := out - if c.Globals.Flags.Quiet { out = io.Discard } - var projectDir string - if c.Flags.Dir != "" { - projectDir, err = filepath.Abs(c.Flags.Dir) - if err != nil { - return fmt.Errorf("failed to construct absolute path to directory '%s': %w", c.Flags.Dir, err) - } - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - if err := os.Chdir(projectDir); err != nil { - return fmt.Errorf("failed to change working directory to '%s': %w", projectDir, err) + manifestFilename := EnvironmentManifest(c.Flags.Env) + if c.Flags.Env != "" { + if c.Globals.Verbose() { + text.Info(out, EnvManifestMsg, manifestFilename, manifest.Filename) } - defer os.Chdir(wd) + } + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + manifestPath := filepath.Join(wd, manifestFilename) + + projectDir, err := ChangeProjectDirectory(c.Flags.Dir) + if err != nil { + return err + } + if projectDir != "" { if c.Globals.Verbose() { - text.Info(out, "Changed project directory to '%s'\n\n", projectDir) + text.Info(out, ProjectDirMsg, projectDir) } + manifestPath = filepath.Join(projectDir, manifestFilename) } spinner, err := text.NewSpinner(out) @@ -104,11 +109,11 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { } }(c.Globals.ErrLog) - err = spinner.Process("Verifying fastly.toml", func(_ *text.SpinnerWrapper) error { - if projectDir == "" { - err = c.Globals.Manifest.File.ReadError() + err = spinner.Process(fmt.Sprintf("Verifying %s", manifestFilename), func(_ *text.SpinnerWrapper) error { + if projectDir != "" || c.Flags.Env != "" { + err = c.Globals.Manifest.File.Read(manifestPath) } else { - err = c.Globals.Manifest.File.Read(filepath.Join(projectDir, manifest.Filename)) + err = c.Globals.Manifest.File.ReadError() } if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -125,7 +130,7 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { var pkgName string err = spinner.Process("Identifying package name", func(_ *text.SpinnerWrapper) error { - pkgName, err = packageName(c) + pkgName, err = c.PackageName(manifestFilename) if err != nil { return err } @@ -147,7 +152,7 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { return err } - language, err := language(toolchain, c, in, out, spinner) + language, err := language(toolchain, manifestFilename, c, in, out, spinner) if err != nil { return err } @@ -167,7 +172,57 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { dest := filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", pkgName)) err = spinner.Process("Creating package archive", func(_ *text.SpinnerWrapper) error { - // NOTE: The minimum package requirement is `fastly.toml` and `main.wasm`. + // IMPORTANT: The minimum package requirement is `fastly.toml` and `main.wasm`. + // + // The Fastly platform will reject a package that doesn't have a manifest + // named exactly fastly.toml which means if the user is building and + // deploying a package with an environment manifest (e.g. fastly.stage.toml) + // then we need to: + // + // 1. Rename any existing fastly.toml to fastly.toml.backup. + // 2. Make a temp copy of the environment manifest and name it fastly.toml + // 3. Remove the newly created fastly.toml once the packaging is done + // 4. Rename the fastly.toml.backup back to fastly.toml + if c.Flags.Env != "" { + // 1. Rename any existing fastly.toml to fastly.toml.backup. + // + // For example, the user is trying to deploy a fastly.stage.toml rather + // than the standard fastly.toml manifest. + if _, err := os.Stat(manifest.Filename); err == nil { + backup := fmt.Sprintf("%s.backup.%d", manifest.Filename, time.Now().Unix()) + if err := os.Rename(manifest.Filename, backup); err != nil { + return fmt.Errorf("failed to backup primary manifest file: %w", err) + } + defer func() { + // 4. Rename the fastly.toml.backup back to fastly.toml + if err = os.Rename(backup, manifest.Filename); err != nil { + text.Error(out, err.Error()) + } + }() + } else { + // 3. Remove the newly created fastly.toml once the packaging is done + // + // If there wasn't an existing fastly.toml because the user only wants + // to work with environment manifests (e.g. fastly.stage.toml and + // fastly.production.toml) then we should remove the fastly.toml that we + // created just for the packaging process (see step 2. below). + defer func() { + if err = os.Remove(manifest.Filename); err != nil { + text.Error(out, err.Error()) + } + }() + } + // 2. Make a temp copy of the environment manifest and name it fastly.toml + // + // If there was no existing fastly.toml then this step will create one, so + // we need to make sure we remove it after packaging has finished so as to + // not confuse the user with a fastly.toml that has suddenly appeared (see + // step 3. above). + if err := filesystem.CopyFile(manifestFilename, manifest.Filename); err != nil { + return fmt.Errorf("failed to copy environment manifest file: %w", err) + } + } + files := []string{ manifest.Filename, "bin/main.wasm", @@ -236,9 +291,9 @@ func (c *BuildCommand) includeSourceCode(files []string, srcDir string) ([]strin return files, nil } -// packageName acquires the package name from either a flag or manifest. +// PackageName acquires the package name from either a flag or manifest. // Additionally it will sanitize the name. -func packageName(c *BuildCommand) (string, error) { +func (c *BuildCommand) PackageName(manifestFilename string) (string, error) { var name string switch { @@ -249,7 +304,7 @@ func packageName(c *BuildCommand) (string, error) { default: return "", fsterr.RemediationError{ Inner: fmt.Errorf("package name is missing"), - Remediation: "Add a name to the fastly.toml 'name' field. Reference: https://developer.fastly.com/reference/compute/fastly-toml/", + Remediation: fmt.Sprintf("Add a name to the %s 'name' field. Reference: https://developer.fastly.com/reference/compute/fastly-toml/", manifestFilename), } } @@ -277,7 +332,7 @@ func identifyToolchain(c *BuildCommand) (string, error) { } // language returns a pointer to a supported language. -func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, spinner text.Spinner) (*Language, error) { +func language(toolchain, manifestFilename string, c *BuildCommand, in io.Reader, out io.Writer, spinner text.Spinner) (*Language, error) { var language *Language switch toolchain { case "assemblyscript": @@ -289,6 +344,7 @@ func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, sp c.Globals, c.Flags, in, + manifestFilename, out, spinner, ), @@ -302,6 +358,7 @@ func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, sp c.Globals, c.Flags, in, + manifestFilename, out, spinner, ), @@ -315,6 +372,7 @@ func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, sp c.Globals, c.Flags, in, + manifestFilename, out, spinner, ), @@ -328,6 +386,7 @@ func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, sp c.Globals, c.Flags, in, + manifestFilename, out, spinner, ), @@ -340,6 +399,7 @@ func language(toolchain string, c *BuildCommand, in io.Reader, out io.Writer, sp c.Globals, c.Flags, in, + manifestFilename, out, spinner, ), diff --git a/pkg/commands/compute/compute_test.go b/pkg/commands/compute/compute_test.go index b7facdf24..5de982a2e 100644 --- a/pkg/commands/compute/compute_test.go +++ b/pkg/commands/compute/compute_test.go @@ -12,7 +12,6 @@ import ( "github.com/fastly/cli/pkg/commands/compute" "github.com/fastly/cli/pkg/github" "github.com/fastly/cli/pkg/global" - "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/testutil" ) @@ -20,16 +19,13 @@ import ( // within the `compute publish` command doesn't fall out of sync with the // `compute build` and `compute deploy` commands from which publish is composed. func TestPublishFlagDivergence(t *testing.T) { - var ( - g global.Data - data manifest.Data - ) + var g global.Data acmd := kingpin.New("foo", "bar") rcmd := compute.NewRootCommand(acmd, &g) - bcmd := compute.NewBuildCommand(rcmd.CmdClause, &g, data) - dcmd := compute.NewDeployCommand(rcmd.CmdClause, &g, data) - pcmd := compute.NewPublishCommand(rcmd.CmdClause, &g, bcmd, dcmd, data) + bcmd := compute.NewBuildCommand(rcmd.CmdClause, &g) + dcmd := compute.NewDeployCommand(rcmd.CmdClause, &g) + pcmd := compute.NewPublishCommand(rcmd.CmdClause, &g, bcmd, dcmd) buildFlags := getFlags(bcmd.CmdClause) deployFlags := getFlags(dcmd.CmdClause) @@ -63,10 +59,7 @@ func TestPublishFlagDivergence(t *testing.T) { // within the `compute serve` command doesn't fall out of sync with the // `compute build` command as `compute serve` delegates to build. func TestServeFlagDivergence(t *testing.T) { - var ( - cfg global.Data - data manifest.Data - ) + var cfg global.Data versioner := github.New(github.Opts{ Org: "fastly", Repo: "viceroy", @@ -75,8 +68,8 @@ func TestServeFlagDivergence(t *testing.T) { acmd := kingpin.New("foo", "bar") rcmd := compute.NewRootCommand(acmd, &cfg) - bcmd := compute.NewBuildCommand(rcmd.CmdClause, &cfg, data) - scmd := compute.NewServeCommand(rcmd.CmdClause, &cfg, bcmd, versioner, data) + bcmd := compute.NewBuildCommand(rcmd.CmdClause, &cfg) + scmd := compute.NewServeCommand(rcmd.CmdClause, &cfg, bcmd, versioner) buildFlags := getFlags(bcmd.CmdClause) serveFlags := getFlags(scmd.CmdClause) @@ -96,7 +89,6 @@ func TestServeFlagDivergence(t *testing.T) { ignoreServeFlags := []string{ "addr", "debug", - "env", "file", "profile-guest", "profile-guest-dir", @@ -120,6 +112,94 @@ func TestServeFlagDivergence(t *testing.T) { } } +// TestHashsumFlagDivergence validates that the manually curated list of flags +// within the `compute hashsum` command doesn't fall out of sync with the +// `compute build` command as `compute hashsum` delegates to build. +func TestHashsumFlagDivergence(t *testing.T) { + var cfg global.Data + acmd := kingpin.New("foo", "bar") + + rcmd := compute.NewRootCommand(acmd, &cfg) + bcmd := compute.NewBuildCommand(rcmd.CmdClause, &cfg) + hcmd := compute.NewHashsumCommand(rcmd.CmdClause, &cfg, bcmd) + + buildFlags := getFlags(bcmd.CmdClause) + hashsumFlags := getFlags(hcmd.CmdClause) + + var ( + expect = make(map[string]int) + have = make(map[string]int) + ) + + iter := buildFlags.MapRange() + for iter.Next() { + expect[iter.Key().String()] = 1 + } + + // Some flags on `compute hashsum` are unique to it. + // We only want to be sure hashsum contains all build flags. + ignoreServeFlags := []string{ + "package", + "skip-build", + } + + iter = hashsumFlags.MapRange() + for iter.Next() { + flag := iter.Key().String() + if !ignoreFlag(ignoreServeFlags, flag) { + have[flag] = 1 + } + } + + if !reflect.DeepEqual(expect, have) { + t.Fatalf("the flags between build and hashsum don't match\n\nexpect: %+v\nhave: %+v\n\n", expect, have) + } +} + +// TestHashfilesFlagDivergence validates that the manually curated list of flags +// within the `compute hashsum` command doesn't fall out of sync with the +// `compute build` command as `compute hashsum` delegates to build. +func TestHashfilesFlagDivergence(t *testing.T) { + var cfg global.Data + acmd := kingpin.New("foo", "bar") + + rcmd := compute.NewRootCommand(acmd, &cfg) + bcmd := compute.NewBuildCommand(rcmd.CmdClause, &cfg) + hcmd := compute.NewHashFilesCommand(rcmd.CmdClause, &cfg, bcmd) + + buildFlags := getFlags(bcmd.CmdClause) + hashfilesFlags := getFlags(hcmd.CmdClause) + + var ( + expect = make(map[string]int) + have = make(map[string]int) + ) + + iter := buildFlags.MapRange() + for iter.Next() { + expect[iter.Key().String()] = 1 + } + + // Some flags on `compute hashsum` are unique to it. + // We only want to be sure hashsum contains all build flags. + ignoreServeFlags := []string{ + "package", + "skip-build", + } + + iter = hashfilesFlags.MapRange() + for iter.Next() { + flag := iter.Key().String() + if !ignoreFlag(ignoreServeFlags, flag) { + have[flag] = 1 + } + } + + if !reflect.DeepEqual(expect, have) { + t.Fatalf("the flags between build and hash-files don't match\n\nexpect: %+v\nhave: %+v\n\n", expect, have) + } +} + // ignoreFlag indicates if needle should be omitted from comparison. func ignoreFlag(ignore []string, flag string) bool { for _, i := range ignore { diff --git a/pkg/commands/compute/deploy.go b/pkg/commands/compute/deploy.go index e849c4df3..385602d7e 100644 --- a/pkg/commands/compute/deploy.go +++ b/pkg/commands/compute/deploy.go @@ -47,7 +47,7 @@ type DeployCommand struct { Comment cmd.OptionalString Dir string Domain string - Manifest manifest.Data + Env string PackagePath string ServiceName cmd.OptionalServiceNameID ServiceVersion cmd.OptionalServiceVersion @@ -58,10 +58,9 @@ type DeployCommand struct { } // NewDeployCommand returns a usable command registered under the parent. -func NewDeployCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *DeployCommand { +func NewDeployCommand(parent cmd.Registerer, g *global.Data) *DeployCommand { var c DeployCommand c.Globals = g - c.Manifest = m c.CmdClause = parent.Command("deploy", "Deploy a package to a Fastly Compute service") // NOTE: when updating these flags, be sure to update the composite command: @@ -87,6 +86,7 @@ func NewDeployCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *D c.CmdClause.Flag("comment", "Human-readable comment").Action(c.Comment.Set).StringVar(&c.Comment.Value) c.CmdClause.Flag("dir", "Project directory (default: current directory)").Short('C').StringVar(&c.Dir) c.CmdClause.Flag("domain", "The name of the domain associated to the package").StringVar(&c.Domain) + c.CmdClause.Flag("env", "The manifest environment config to use (e.g. 'stage' will attempt to read 'fastly.stage.toml')").StringVar(&c.Env) c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.PackagePath) c.CmdClause.Flag("status-check-code", "Set the expected status response for the service availability check").IntVar(&c.StatusCheckCode) c.CmdClause.Flag("status-check-off", "Disable the service availability check").BoolVar(&c.StatusCheckOff) @@ -97,24 +97,28 @@ func NewDeployCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *D // Exec implements the command interface. func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { - var projectDir string - if c.Dir != "" { - projectDir, err = filepath.Abs(c.Dir) - if err != nil { - return fmt.Errorf("failed to construct absolute path to directory '%s': %w", c.Dir, err) - } - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - if err := os.Chdir(projectDir); err != nil { - return fmt.Errorf("failed to change working directory to '%s': %w", projectDir, err) + manifestFilename := EnvironmentManifest(c.Env) + if c.Env != "" { + if c.Globals.Verbose() { + text.Info(out, EnvManifestMsg, manifestFilename, manifest.Filename) } - defer os.Chdir(wd) + } + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + c.manifestPath = filepath.Join(wd, manifestFilename) + + projectDir, err := ChangeProjectDirectory(c.Dir) + if err != nil { + return err + } + if projectDir != "" { if c.Globals.Verbose() { - text.Info(out, "Changed project directory to '%s'\n\n", projectDir) + text.Info(out, ProjectDirMsg, projectDir) } - c.manifestPath = filepath.Join(projectDir, manifest.Filename) + c.manifestPath = filepath.Join(projectDir, manifestFilename) } spinner, err := text.NewSpinner(out) @@ -122,11 +126,11 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { return err } - err = spinner.Process("Verifying fastly.toml", func(_ *text.SpinnerWrapper) error { - if projectDir == "" { - err = c.Globals.Manifest.File.ReadError() - } else { + err = spinner.Process(fmt.Sprintf("Verifying %s", manifestFilename), func(_ *text.SpinnerWrapper) error { + if projectDir != "" || c.Env != "" { err = c.Globals.Manifest.File.Read(c.manifestPath) + } else { + err = c.Globals.Manifest.File.ReadError() } if err != nil { // If the user hasn't specified a package to deploy, then we'll just check @@ -140,11 +144,11 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { } // Otherwise, we'll attempt to read the manifest from within the given // package archive. - if err := readManifestFromPackageArchive(&c.Globals.Manifest, c.PackagePath); err != nil { + if err := readManifestFromPackageArchive(&c.Globals.Manifest, c.PackagePath, manifestFilename); err != nil { return err } if c.Globals.Verbose() { - text.Info(out, "Using fastly.toml within --package archive: %s\n\n", c.PackagePath) + text.Info(out, "Using %s within --package archive: %s\n\n", manifestFilename, c.PackagePath) } } return nil @@ -165,7 +169,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { undoStack := undo.NewStack() undoStack.Push(func() error { if noExistingService { - return c.CleanupNewService(serviceID, out) + return c.CleanupNewService(serviceID, manifestFilename, out) } return nil }) @@ -183,7 +187,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { var serviceVersion *fastly.Version if noExistingService { - serviceID, serviceVersion, err = c.NewService(fnActivateTrial, spinner, in, out) + serviceID, serviceVersion, err = c.NewService(manifestFilename, fnActivateTrial, spinner, in, out) if err != nil { return err } @@ -396,8 +400,8 @@ func validatePackage(pkgPath string) error { // readManifestFromPackageArchive extracts the manifest file from the given // package archive file and reads it into memory. -func readManifestFromPackageArchive(data *manifest.Data, packageFlag string) error { - dst, err := os.MkdirTemp("", fmt.Sprintf("%s-*", manifest.Filename)) +func readManifestFromPackageArchive(data *manifest.Data, packageFlag, manifestFilename string) error { + dst, err := os.MkdirTemp("", fmt.Sprintf("%s-*", manifestFilename)) if err != nil { return err } @@ -413,7 +417,7 @@ func readManifestFromPackageArchive(data *manifest.Data, packageFlag string) err } extractedDirName := files[0].Name() - manifestPath, err := locateManifest(filepath.Join(dst, extractedDirName)) + manifestPath, err := locateManifest(filepath.Join(dst, extractedDirName), manifestFilename) if err != nil { return err } @@ -431,7 +435,7 @@ func readManifestFromPackageArchive(data *manifest.Data, packageFlag string) err // locateManifest attempts to find the manifest within the given path's // directory tree. -func locateManifest(path string) (string, error) { +func locateManifest(path, manifestFilename string) (string, error) { root, err := filepath.Abs(path) if err != nil { return "", err @@ -443,7 +447,7 @@ func locateManifest(path string) (string, error) { if err != nil { return err } - if !entry.IsDir() && filepath.Base(path) == manifest.Filename { + if !entry.IsDir() && filepath.Base(path) == manifestFilename { foundManifest = path return fsterr.ErrStopWalk } @@ -510,7 +514,7 @@ func preconfigureActivateTrial(endpoint, token string, httpClient api.HTTPClient } // NewService handles creating a new service when no Service ID is found. -func (c *DeployCommand) NewService(fnActivateTrial Activator, spinner text.Spinner, in io.Reader, out io.Writer) (string, *fastly.Version, error) { +func (c *DeployCommand) NewService(manifestFilename string, fnActivateTrial Activator, spinner text.Spinner, in io.Reader, out io.Writer) (string, *fastly.Version, error) { var ( err error serviceID string @@ -518,12 +522,11 @@ func (c *DeployCommand) NewService(fnActivateTrial Activator, spinner text.Spinn ) if !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive { - text.Output(out, "There is no Fastly service associated with this package. To connect to an existing service add the Service ID to the fastly.toml file, otherwise follow the prompts to create a service now.") - text.Break(out) + text.Output(out, "There is no Fastly service associated with this package. To connect to an existing service add the Service ID to the %s file, otherwise follow the prompts to create a service now.\n\n", manifestFilename) text.Output(out, "Press ^C at any time to quit.") if c.Globals.Manifest.File.Setup.Defined() { - text.Info(out, "\nProcessing of the fastly.toml [setup] configuration happens only when there is no existing service. Once a service is created, any further changes to the service or its resources must be made manually.") + text.Info(out, "\nProcessing of the %s [setup] configuration happens only when there is no existing service. Once a service is created, any further changes to the service or its resources must be made manually.", manifestFilename) } text.Break(out) @@ -682,7 +685,7 @@ func createService( // CleanupNewService is executed if a new service flow has errors. // It deletes the service, which will cause any contained resources to be deleted. // It will also strip the Service ID from the fastly.toml manifest file. -func (c *DeployCommand) CleanupNewService(serviceID string, out io.Writer) error { +func (c *DeployCommand) CleanupNewService(serviceID, manifestFilename string, out io.Writer) error { text.Info(out, "\nCleaning up service\n\n") err := c.Globals.APIClient.DeleteService(&fastly.DeleteServiceInput{ ID: serviceID, @@ -691,7 +694,7 @@ func (c *DeployCommand) CleanupNewService(serviceID string, out io.Writer) error return err } - text.Info(out, "Removing Service ID from fastly.toml\n\n") + text.Info(out, "Removing Service ID from %s\n\n", manifestFilename) err = c.UpdateManifestServiceID("", c.manifestPath) if err != nil { return err @@ -709,15 +712,12 @@ func (c *DeployCommand) CleanupNewService(serviceID string, out io.Writer) error // empty string (otherwise the service itself will be deleted while the // manifest will continue to hold a reference to it). func (c *DeployCommand) UpdateManifestServiceID(serviceID, manifestPath string) error { - if manifestPath == "" { - manifestPath = manifest.Filename - } if err := c.Globals.Manifest.File.Read(manifestPath); err != nil { - return fmt.Errorf("error reading fastly.toml: %w", err) + return fmt.Errorf("error reading %s: %w", manifestPath, err) } c.Globals.Manifest.File.ServiceID = serviceID if err := c.Globals.Manifest.File.Write(manifestPath); err != nil { - return fmt.Errorf("error saving fastly.toml: %w", err) + return fmt.Errorf("error saving %s: %w", manifestPath, err) } return nil } diff --git a/pkg/commands/compute/dir.go b/pkg/commands/compute/dir.go new file mode 100644 index 000000000..46609f706 --- /dev/null +++ b/pkg/commands/compute/dir.go @@ -0,0 +1,39 @@ +package compute + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/fastly/cli/pkg/manifest" +) + +// EnvManifestMsg informs the user that an environment manifest is being used. +const EnvManifestMsg = "Using the '%s' environment manifest (it will be packaged up as %s)\n\n" + +// ProjectDirMsg informs the user that we've changed the project directory. +const ProjectDirMsg = "Changed project directory to '%s'\n\n" + +// EnvironmentManifest returns the relevant manifest filename, taking into +// account the user passing an --env flag. +func EnvironmentManifest(env string) (manifestFilename string) { + manifestFilename = manifest.Filename + if env != "" { + manifestFilename = fmt.Sprintf("fastly.%s.toml", env) + } + return manifestFilename +} + +// ChangeProjectDirectory moves into `dir` and returns its absolute path. +func ChangeProjectDirectory(dir string) (projectDirectory string, err error) { + if dir != "" { + projectDirectory, err = filepath.Abs(dir) + if err != nil { + return "", fmt.Errorf("failed to construct absolute path to directory '%s': %w", dir, err) + } + if err := os.Chdir(projectDirectory); err != nil { + return "", fmt.Errorf("failed to change working directory to '%s': %w", projectDirectory, err) + } + } + return projectDirectory, nil +} diff --git a/pkg/commands/compute/hashfiles.go b/pkg/commands/compute/hashfiles.go index 1582b7db0..2c2a7cdcd 100644 --- a/pkg/commands/compute/hashfiles.go +++ b/pkg/commands/compute/hashfiles.go @@ -4,8 +4,10 @@ import ( "archive/tar" "bytes" "crypto/sha512" + "errors" "fmt" "io" + "os" "path/filepath" "sort" @@ -13,6 +15,7 @@ import ( "github.com/mholt/archiver/v3" "github.com/fastly/cli/pkg/cmd" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" @@ -29,44 +32,96 @@ var MaxPackageSize int64 = 100000000 // 100MB in bytes type HashFilesCommand struct { cmd.Base + // Build fields + dir cmd.OptionalString + env cmd.OptionalString + includeSrc cmd.OptionalBool + lang cmd.OptionalString + packageName cmd.OptionalString + timeout cmd.OptionalInt + buildCmd *BuildCommand - Manifest manifest.Data Package string SkipBuild bool } // NewHashFilesCommand returns a usable command registered under the parent. -func NewHashFilesCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, m manifest.Data) *HashFilesCommand { +func NewHashFilesCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand) *HashFilesCommand { var c HashFilesCommand c.buildCmd = build c.Globals = g - c.Manifest = m c.CmdClause = parent.Command("hash-files", "Generate a SHA512 digest from the contents of the Compute package") + c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').Action(c.dir.Set).StringVar(&c.dir.Value) + 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) + c.CmdClause.Flag("include-source", "Include source code in built package").Action(c.includeSrc.Set).BoolVar(&c.includeSrc.Value) + c.CmdClause.Flag("language", "Language type").Action(c.lang.Set).StringVar(&c.lang.Value) c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.Package) + c.CmdClause.Flag("package-name", "Package name").Action(c.packageName.Set).StringVar(&c.packageName.Value) c.CmdClause.Flag("skip-build", "Skip the build step").BoolVar(&c.SkipBuild) + c.CmdClause.Flag("timeout", "Timeout, in seconds, for the build compilation step").Action(c.timeout.Set).IntVar(&c.timeout.Value) return &c } // Exec implements the command interface. func (c *HashFilesCommand) Exec(in io.Reader, out io.Writer) (err error) { - if !c.SkipBuild { + if !c.SkipBuild && c.Package == "" { err = c.Build(in, out) if err != nil { return err } + if c.Globals.Verbose() { + text.Break(out) + } } - pkgName := fmt.Sprintf("%s.tar.gz", sanitize.BaseName(c.Globals.Manifest.File.Name)) - pkg := filepath.Join("pkg", pkgName) + var pkgPath string + + if c.Package == "" { + manifestFilename := EnvironmentManifest(c.env.Value) + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + manifestPath := filepath.Join(wd, manifestFilename) + + projectDir, err := ChangeProjectDirectory(c.dir.Value) + if err != nil { + return err + } + if projectDir != "" { + if c.Globals.Verbose() { + text.Info(out, ProjectDirMsg, projectDir) + } + manifestPath = filepath.Join(projectDir, manifestFilename) + } + + if projectDir != "" || c.env.WasSet { + err = c.Globals.Manifest.File.Read(manifestPath) + } else { + err = c.Globals.Manifest.File.ReadError() + } + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = fsterr.ErrReadingManifest + } + c.Globals.ErrLog.Add(err) + return err + } - if c.Package != "" { - pkg, err = filepath.Abs(c.Package) + projectName, source := c.Globals.Manifest.Name() + if source == manifest.SourceUndefined { + return fsterr.ErrReadingManifest + } + pkgPath = filepath.Join(projectDir, "pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) + } else { + pkgPath, err = filepath.Abs(c.Package) if err != nil { return fmt.Errorf("failed to locate package path '%s': %w", c.Package, err) } } - hash, err := getFilesHash(pkg) + hash, err := getFilesHash(pkgPath) if err != nil { return err } @@ -81,6 +136,24 @@ func (c *HashFilesCommand) Build(in io.Reader, out io.Writer) error { if !c.Globals.Verbose() { output = io.Discard } + if c.dir.WasSet { + c.buildCmd.Flags.Dir = c.dir.Value + } + if c.env.WasSet { + c.buildCmd.Flags.Env = c.env.Value + } + if c.includeSrc.WasSet { + c.buildCmd.Flags.IncludeSrc = c.includeSrc.Value + } + if c.lang.WasSet { + c.buildCmd.Flags.Lang = c.lang.Value + } + if c.packageName.WasSet { + c.buildCmd.Flags.PackageName = c.packageName.Value + } + if c.timeout.WasSet { + c.buildCmd.Flags.Timeout = c.timeout.Value + } return c.buildCmd.Exec(in, output) } diff --git a/pkg/commands/compute/hashsum.go b/pkg/commands/compute/hashsum.go index e25fb32d3..cc9292a59 100644 --- a/pkg/commands/compute/hashsum.go +++ b/pkg/commands/compute/hashsum.go @@ -2,6 +2,7 @@ package compute import ( "crypto/sha512" + "errors" "fmt" "io" "os" @@ -20,22 +21,34 @@ import ( type HashsumCommand struct { cmd.Base + // Build fields + dir cmd.OptionalString + env cmd.OptionalString + includeSrc cmd.OptionalBool + lang cmd.OptionalString + packageName cmd.OptionalString + timeout cmd.OptionalInt + buildCmd *BuildCommand - Manifest manifest.Data PackagePath string SkipBuild bool } // NewHashsumCommand returns a usable command registered under the parent. // Deprecated: Use NewHashFilesCommand instead. -func NewHashsumCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, m manifest.Data) *HashsumCommand { +func NewHashsumCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand) *HashsumCommand { var c HashsumCommand c.buildCmd = build c.Globals = g - c.Manifest = m c.CmdClause = parent.Command("hashsum", "Generate a SHA512 digest from a Compute package").Hidden() + c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').Action(c.dir.Set).StringVar(&c.dir.Value) + 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) + c.CmdClause.Flag("include-source", "Include source code in built package").Action(c.includeSrc.Set).BoolVar(&c.includeSrc.Value) + c.CmdClause.Flag("language", "Language type").Action(c.lang.Set).StringVar(&c.lang.Value) c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.PackagePath) + c.CmdClause.Flag("package-name", "Package name").Action(c.packageName.Set).StringVar(&c.packageName.Value) c.CmdClause.Flag("skip-build", "Skip the build step").BoolVar(&c.SkipBuild) + c.CmdClause.Flag("timeout", "Timeout, in seconds, for the build compilation step").Action(c.timeout.Set).IntVar(&c.timeout.Value) return &c } @@ -49,7 +62,8 @@ func (c *HashsumCommand) Exec(in io.Reader, out io.Writer) (err error) { } } - if !c.SkipBuild { + // No point in building a package if the user provides a package path. + if !c.SkipBuild && c.PackagePath == "" { err = c.Build(in, out) if err != nil { return err @@ -57,15 +71,48 @@ func (c *HashsumCommand) Exec(in io.Reader, out io.Writer) (err error) { text.Break(out) } - if c.PackagePath == "" { - projectName, source := c.Manifest.Name() + pkgPath := c.PackagePath + if pkgPath == "" { + manifestFilename := EnvironmentManifest(c.env.Value) + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + manifestPath := filepath.Join(wd, manifestFilename) + + projectDir, err := ChangeProjectDirectory(c.dir.Value) + if err != nil { + return err + } + if projectDir != "" { + if c.Globals.Verbose() { + text.Info(out, ProjectDirMsg, projectDir) + } + manifestPath = filepath.Join(projectDir, manifestFilename) + } + + if projectDir != "" || c.env.WasSet { + err = c.Globals.Manifest.File.Read(manifestPath) + } else { + err = c.Globals.Manifest.File.ReadError() + } + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = fsterr.ErrReadingManifest + } + c.Globals.ErrLog.Add(err) + return err + } + + projectName, source := c.Globals.Manifest.Name() if source == manifest.SourceUndefined { return fsterr.ErrReadingManifest } - c.PackagePath = filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) + pkgPath = filepath.Join(projectDir, "pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) } - err = validatePackage(c.PackagePath) + err = validatePackage(pkgPath) if err != nil { var skipBuildMsg string if c.SkipBuild { @@ -92,6 +139,24 @@ func (c *HashsumCommand) Build(in io.Reader, out io.Writer) error { if !c.Globals.Verbose() { output = io.Discard } + if c.dir.WasSet { + c.buildCmd.Flags.Dir = c.dir.Value + } + if c.env.WasSet { + c.buildCmd.Flags.Env = c.env.Value + } + if c.includeSrc.WasSet { + c.buildCmd.Flags.IncludeSrc = c.includeSrc.Value + } + if c.lang.WasSet { + c.buildCmd.Flags.Lang = c.lang.Value + } + if c.packageName.WasSet { + c.buildCmd.Flags.PackageName = c.packageName.Value + } + if c.timeout.WasSet { + c.buildCmd.Flags.Timeout = c.timeout.Value + } return c.buildCmd.Exec(in, output) } diff --git a/pkg/commands/compute/init.go b/pkg/commands/compute/init.go index b3001149d..55577cdc6 100644 --- a/pkg/commands/compute/init.go +++ b/pkg/commands/compute/init.go @@ -251,7 +251,7 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { postInit := md.File.Scripts.PostInit if postInit != "" { if !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive { - msg := fmt.Sprintf(CustomPostScriptMessage, "init") + msg := fmt.Sprintf(CustomPostScriptMessage, "init", manifest.Filename) err := promptForPostInitContinue(msg, postInit, out, in) if err != nil { if errors.Is(err, fsterr.ErrPostInitStopped) { diff --git a/pkg/commands/compute/language_assemblyscript.go b/pkg/commands/compute/language_assemblyscript.go index 7575a129e..24465ea4a 100644 --- a/pkg/commands/compute/language_assemblyscript.go +++ b/pkg/commands/compute/language_assemblyscript.go @@ -41,20 +41,22 @@ func NewAssemblyScript( globals *global.Data, flags Flags, in io.Reader, + manifestFilename string, out io.Writer, spinner text.Spinner, ) *AssemblyScript { return &AssemblyScript{ Shell: Shell{}, - build: fastlyManifest.Scripts.Build, - errlog: globals.ErrLog, - input: in, - output: out, - postBuild: fastlyManifest.Scripts.PostBuild, - spinner: spinner, - timeout: flags.Timeout, - verbose: globals.Verbose(), + build: fastlyManifest.Scripts.Build, + errlog: globals.ErrLog, + input: in, + manifestFilename: manifestFilename, + output: out, + postBuild: fastlyManifest.Scripts.PostBuild, + spinner: spinner, + timeout: flags.Timeout, + verbose: globals.Verbose(), } } @@ -70,6 +72,8 @@ type AssemblyScript struct { errlog fsterr.LogInterface // input is the user's terminal stdin stream input io.Reader + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // output is the users terminal stdout stream @@ -107,21 +111,22 @@ func (a *AssemblyScript) Build() error { } if noBuildScript && a.verbose { - text.Info(a.output, "No [scripts.build] found in fastly.toml. The following default build command for AssemblyScript will be used: `%s`\n\n", a.build) + text.Info(a.output, "No [scripts.build] found in %s. The following default build command for AssemblyScript will be used: `%s`\n\n", a.manifestFilename, a.build) } bt := BuildToolchain{ - autoYes: a.autoYes, - buildFn: a.Shell.Build, - buildScript: a.build, - errlog: a.errlog, - in: a.input, - nonInteractive: a.nonInteractive, - out: a.output, - postBuild: a.postBuild, - spinner: a.spinner, - timeout: a.timeout, - verbose: a.verbose, + autoYes: a.autoYes, + buildFn: a.Shell.Build, + buildScript: a.build, + errlog: a.errlog, + in: a.input, + manifestFilename: a.manifestFilename, + nonInteractive: a.nonInteractive, + out: a.output, + postBuild: a.postBuild, + spinner: a.spinner, + timeout: a.timeout, + verbose: a.verbose, } return bt.Build() diff --git a/pkg/commands/compute/language_go.go b/pkg/commands/compute/language_go.go index cf3877468..509d8f72d 100644 --- a/pkg/commands/compute/language_go.go +++ b/pkg/commands/compute/language_go.go @@ -38,24 +38,26 @@ func NewGo( globals *global.Data, flags Flags, in io.Reader, + manifestFilename string, out io.Writer, spinner text.Spinner, ) *Go { return &Go{ Shell: Shell{}, - autoYes: globals.Flags.AutoYes, - build: fastlyManifest.Scripts.Build, - config: globals.Config.Language.Go, - env: fastlyManifest.Scripts.EnvVars, - errlog: globals.ErrLog, - input: in, - nonInteractive: globals.Flags.NonInteractive, - output: out, - postBuild: fastlyManifest.Scripts.PostBuild, - spinner: spinner, - timeout: flags.Timeout, - verbose: globals.Verbose(), + autoYes: globals.Flags.AutoYes, + build: fastlyManifest.Scripts.Build, + config: globals.Config.Language.Go, + env: fastlyManifest.Scripts.EnvVars, + errlog: globals.ErrLog, + input: in, + manifestFilename: manifestFilename, + nonInteractive: globals.Flags.NonInteractive, + output: out, + postBuild: fastlyManifest.Scripts.PostBuild, + spinner: spinner, + timeout: flags.Timeout, + verbose: globals.Verbose(), } } @@ -80,6 +82,8 @@ type Go struct { errlog fsterr.LogInterface // input is the user's terminal stdin stream input io.Reader + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // output is the users terminal stdout stream @@ -109,7 +113,7 @@ func (g *Go) Build() error { if !g.verbose { text.Break(g.output) } - text.Info(g.output, "No [scripts.build] found in fastly.toml. Visit https://developer.fastly.com/learning/compute/go/ to learn how to target standard Go vs TinyGo.\n\n") + text.Info(g.output, "No [scripts.build] found in %s. Visit https://developer.fastly.com/learning/compute/go/ to learn how to target standard Go vs TinyGo.\n\n", g.manifestFilename) text.Description(g.output, "The following default build command for TinyGo will be used", g.build) } @@ -144,18 +148,19 @@ func (g *Go) Build() error { } bt := BuildToolchain{ - autoYes: g.autoYes, - buildFn: g.Shell.Build, - buildScript: g.build, - env: g.env, - errlog: g.errlog, - in: g.input, - nonInteractive: g.nonInteractive, - out: g.output, - postBuild: g.postBuild, - spinner: g.spinner, - timeout: g.timeout, - verbose: g.verbose, + autoYes: g.autoYes, + buildFn: g.Shell.Build, + buildScript: g.build, + env: g.env, + errlog: g.errlog, + in: g.input, + manifestFilename: g.manifestFilename, + nonInteractive: g.nonInteractive, + out: g.output, + postBuild: g.postBuild, + spinner: g.spinner, + timeout: g.timeout, + verbose: g.verbose, } return bt.Build() diff --git a/pkg/commands/compute/language_javascript.go b/pkg/commands/compute/language_javascript.go index d4f0bcecf..811aa8c91 100644 --- a/pkg/commands/compute/language_javascript.go +++ b/pkg/commands/compute/language_javascript.go @@ -43,23 +43,25 @@ func NewJavaScript( globals *global.Data, flags Flags, in io.Reader, + manifestFilename string, out io.Writer, spinner text.Spinner, ) *JavaScript { return &JavaScript{ Shell: Shell{}, - autoYes: globals.Flags.AutoYes, - build: fastlyManifest.Scripts.Build, - env: fastlyManifest.Scripts.EnvVars, - errlog: globals.ErrLog, - input: in, - nonInteractive: globals.Flags.NonInteractive, - output: out, - postBuild: fastlyManifest.Scripts.PostBuild, - spinner: spinner, - timeout: flags.Timeout, - verbose: globals.Verbose(), + autoYes: globals.Flags.AutoYes, + build: fastlyManifest.Scripts.Build, + env: fastlyManifest.Scripts.EnvVars, + errlog: globals.ErrLog, + input: in, + manifestFilename: manifestFilename, + nonInteractive: globals.Flags.NonInteractive, + output: out, + postBuild: fastlyManifest.Scripts.PostBuild, + spinner: spinner, + timeout: flags.Timeout, + verbose: globals.Verbose(), } } @@ -77,6 +79,8 @@ type JavaScript struct { errlog fsterr.LogInterface // input is the user's terminal stdin stream input io.Reader + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // output is the users terminal stdout stream @@ -110,22 +114,23 @@ func (j *JavaScript) Build() error { } if noBuildScript && j.verbose { - text.Info(j.output, "No [scripts.build] found in fastly.toml. The following default build command for JavaScript will be used: `%s`\n\n", j.build) + text.Info(j.output, "No [scripts.build] found in %s. The following default build command for JavaScript will be used: `%s`\n\n", j.manifestFilename, j.build) } bt := BuildToolchain{ - autoYes: j.autoYes, - buildFn: j.Shell.Build, - buildScript: j.build, - env: j.env, - errlog: j.errlog, - in: j.input, - nonInteractive: j.nonInteractive, - out: j.output, - postBuild: j.postBuild, - spinner: j.spinner, - timeout: j.timeout, - verbose: j.verbose, + autoYes: j.autoYes, + buildFn: j.Shell.Build, + buildScript: j.build, + env: j.env, + errlog: j.errlog, + in: j.input, + manifestFilename: j.manifestFilename, + nonInteractive: j.nonInteractive, + out: j.output, + postBuild: j.postBuild, + spinner: j.spinner, + timeout: j.timeout, + verbose: j.verbose, } return bt.Build() diff --git a/pkg/commands/compute/language_other.go b/pkg/commands/compute/language_other.go index 00e4074be..051d3f51f 100644 --- a/pkg/commands/compute/language_other.go +++ b/pkg/commands/compute/language_other.go @@ -15,23 +15,25 @@ func NewOther( globals *global.Data, flags Flags, in io.Reader, + manifestFilename string, out io.Writer, spinner text.Spinner, ) *Other { return &Other{ Shell: Shell{}, - autoYes: globals.Flags.AutoYes, - build: fastlyManifest.Scripts.Build, - env: fastlyManifest.Scripts.EnvVars, - errlog: globals.ErrLog, - input: in, - nonInteractive: globals.Flags.NonInteractive, - output: out, - postBuild: fastlyManifest.Scripts.PostBuild, - spinner: spinner, - timeout: flags.Timeout, - verbose: globals.Verbose(), + autoYes: globals.Flags.AutoYes, + build: fastlyManifest.Scripts.Build, + env: fastlyManifest.Scripts.EnvVars, + errlog: globals.ErrLog, + input: in, + manifestFilename: manifestFilename, + nonInteractive: globals.Flags.NonInteractive, + output: out, + postBuild: fastlyManifest.Scripts.PostBuild, + spinner: spinner, + timeout: flags.Timeout, + verbose: globals.Verbose(), } } @@ -49,6 +51,8 @@ type Other struct { errlog fsterr.LogInterface // input is the user's terminal stdin stream input io.Reader + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // output is the users terminal stdout stream @@ -68,18 +72,19 @@ type Other struct { // source to a Wasm binary. func (o Other) Build() error { bt := BuildToolchain{ - autoYes: o.autoYes, - buildFn: o.Shell.Build, - buildScript: o.build, - env: o.env, - errlog: o.errlog, - in: o.input, - nonInteractive: o.nonInteractive, - out: o.output, - postBuild: o.postBuild, - spinner: o.spinner, - timeout: o.timeout, - verbose: o.verbose, + autoYes: o.autoYes, + buildFn: o.Shell.Build, + buildScript: o.build, + env: o.env, + errlog: o.errlog, + in: o.input, + manifestFilename: o.manifestFilename, + nonInteractive: o.nonInteractive, + out: o.output, + postBuild: o.postBuild, + spinner: o.spinner, + timeout: o.timeout, + verbose: o.verbose, } return bt.Build() } diff --git a/pkg/commands/compute/language_rust.go b/pkg/commands/compute/language_rust.go index 1dfa8652e..d490ebb01 100644 --- a/pkg/commands/compute/language_rust.go +++ b/pkg/commands/compute/language_rust.go @@ -48,24 +48,26 @@ func NewRust( globals *global.Data, flags Flags, in io.Reader, + manifestFilename string, out io.Writer, spinner text.Spinner, ) *Rust { return &Rust{ Shell: Shell{}, - autoYes: globals.Flags.AutoYes, - build: fastlyManifest.Scripts.Build, - config: globals.Config.Language.Rust, - env: fastlyManifest.Scripts.EnvVars, - errlog: globals.ErrLog, - input: in, - nonInteractive: globals.Flags.NonInteractive, - output: out, - postBuild: fastlyManifest.Scripts.PostBuild, - spinner: spinner, - timeout: flags.Timeout, - verbose: globals.Verbose(), + autoYes: globals.Flags.AutoYes, + build: fastlyManifest.Scripts.Build, + config: globals.Config.Language.Rust, + env: fastlyManifest.Scripts.EnvVars, + errlog: globals.ErrLog, + input: in, + manifestFilename: manifestFilename, + nonInteractive: globals.Flags.NonInteractive, + output: out, + postBuild: fastlyManifest.Scripts.PostBuild, + spinner: spinner, + timeout: flags.Timeout, + verbose: globals.Verbose(), } } @@ -85,6 +87,8 @@ type Rust struct { errlog fsterr.LogInterface // input is the user's terminal stdin stream input io.Reader + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // output is the users terminal stdout stream @@ -118,7 +122,7 @@ func (r *Rust) Build() error { } if noBuildScript && r.verbose { - text.Info(r.output, "No [scripts.build] found in fastly.toml. The following default build command for Rust will be used: `%s`\n\n", r.build) + text.Info(r.output, "No [scripts.build] found in %s. The following default build command for Rust will be used: `%s`\n\n", r.manifestFilename, r.build) } r.toolchainConstraint() @@ -131,6 +135,7 @@ func (r *Rust) Build() error { errlog: r.errlog, in: r.input, internalPostBuildCallback: r.ProcessLocation, + manifestFilename: r.manifestFilename, nonInteractive: r.nonInteractive, out: r.output, postBuild: r.postBuild, diff --git a/pkg/commands/compute/language_toolchain.go b/pkg/commands/compute/language_toolchain.go index b795b0184..516e46b33 100644 --- a/pkg/commands/compute/language_toolchain.go +++ b/pkg/commands/compute/language_toolchain.go @@ -11,6 +11,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" fstexec "github.com/fastly/cli/pkg/exec" + "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" ) @@ -60,6 +61,8 @@ type BuildToolchain struct { in io.Reader // internalPostBuildCallback is run after the build but before post build. internalPostBuildCallback func() error + // manifestFilename is the name of the manifest file. + manifestFilename string // nonInteractive is the --non-interactive flag. nonInteractive bool // out is the users terminal stdout stream @@ -166,7 +169,10 @@ func (bt BuildToolchain) Build() error { if bt.postBuild != "" { if !bt.autoYes && !bt.nonInteractive { - msg := fmt.Sprintf(CustomPostScriptMessage, "build") + if bt.manifestFilename == "" { + bt.manifestFilename = manifest.Filename + } + msg := fmt.Sprintf(CustomPostScriptMessage, "build", bt.manifestFilename) err := bt.promptForPostBuildContinue(msg, bt.postBuild, bt.out, bt.in) if err != nil { return err diff --git a/pkg/commands/compute/publish.go b/pkg/commands/compute/publish.go index 0c83affad..9434db38c 100644 --- a/pkg/commands/compute/publish.go +++ b/pkg/commands/compute/publish.go @@ -4,20 +4,17 @@ import ( "fmt" "io" "os" - "path/filepath" "github.com/fastly/cli/pkg/cmd" "github.com/fastly/cli/pkg/global" - "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" ) // PublishCommand produces and deploys an artifact from files on the local disk. type PublishCommand struct { cmd.Base - manifest manifest.Data - build *BuildCommand - deploy *DeployCommand + build *BuildCommand + deploy *DeployCommand // Build fields dir cmd.OptionalString @@ -29,6 +26,7 @@ type PublishCommand struct { // Deploy fields comment cmd.OptionalString domain cmd.OptionalString + env cmd.OptionalString pkg cmd.OptionalString serviceName cmd.OptionalServiceNameID serviceVersion cmd.OptionalServiceVersion @@ -39,10 +37,9 @@ type PublishCommand struct { } // NewPublishCommand returns a usable command registered under the parent. -func NewPublishCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, deploy *DeployCommand, m manifest.Data) *PublishCommand { +func NewPublishCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, deploy *DeployCommand) *PublishCommand { var c PublishCommand c.Globals = g - c.manifest = m c.build = build c.deploy = deploy c.CmdClause = parent.Command("publish", "Build and deploy a Compute package to a Fastly service") @@ -50,6 +47,7 @@ func NewPublishCommand(parent cmd.Registerer, g *global.Data, build *BuildComman c.CmdClause.Flag("comment", "Human-readable comment").Action(c.comment.Set).StringVar(&c.comment.Value) c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').Action(c.dir.Set).StringVar(&c.dir.Value) c.CmdClause.Flag("domain", "The name of the domain associated to the package").Action(c.domain.Set).StringVar(&c.domain.Value) + 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) c.CmdClause.Flag("include-source", "Include source code in built package").Action(c.includeSrc.Set).BoolVar(&c.includeSrc.Value) c.CmdClause.Flag("language", "Language type").Action(c.lang.Set).StringVar(&c.lang.Value) c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').Action(c.pkg.Set).StringVar(&c.pkg.Value) @@ -57,7 +55,7 @@ func NewPublishCommand(parent cmd.Registerer, g *global.Data, build *BuildComman c.RegisterFlag(cmd.StringFlagOpts{ Name: cmd.FlagServiceIDName, Description: cmd.FlagServiceIDDesc, - Dst: &c.manifest.Flag.ServiceID, + Dst: &c.Globals.Manifest.Flag.ServiceID, Short: 's', }) c.RegisterFlag(cmd.StringFlagOpts{ @@ -93,6 +91,9 @@ func (c *PublishCommand) Exec(in io.Reader, out io.Writer) (err error) { if c.dir.WasSet { c.build.Flags.Dir = c.dir.Value } + if c.env.WasSet { + c.build.Flags.Env = c.env.Value + } if c.includeSrc.WasSet { c.build.Flags.IncludeSrc = c.includeSrc.Value } @@ -105,7 +106,6 @@ func (c *PublishCommand) Exec(in io.Reader, out io.Writer) (err error) { if c.timeout.WasSet { c.build.Flags.Timeout = c.timeout.Value } - c.build.Manifest = c.manifest err = c.build.Exec(in, out) if err != nil { @@ -115,21 +115,19 @@ func (c *PublishCommand) Exec(in io.Reader, out io.Writer) (err error) { text.Break(out) - if c.dir.WasSet { - projectDir, err := filepath.Abs(c.dir.Value) - if err != nil { - return fmt.Errorf("failed to construct absolute path to directory '%s': %w", c.dir.Value, err) - } - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - if err := os.Chdir(projectDir); err != nil { - return fmt.Errorf("failed to change working directory to '%s': %w", projectDir, err) - } - defer os.Chdir(wd) + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + + projectDir, err := ChangeProjectDirectory(c.dir.Value) + if err != nil { + return err + } + if projectDir != "" { if c.Globals.Verbose() { - text.Info(out, "Changed project directory to '%s'\n\n", projectDir) + text.Info(out, ProjectDirMsg, projectDir) } } @@ -149,10 +147,12 @@ func (c *PublishCommand) Exec(in io.Reader, out io.Writer) (err error) { if c.domain.WasSet { c.deploy.Domain = c.domain.Value } + if c.env.WasSet { + c.deploy.Env = c.env.Value + } if c.comment.WasSet { c.deploy.Comment = c.comment } - c.deploy.Manifest = c.manifest if c.statusCheckCode > 0 { c.deploy.StatusCheckCode = c.statusCheckCode } diff --git a/pkg/commands/compute/serve.go b/pkg/commands/compute/serve.go index 0838e1387..9ea76d378 100644 --- a/pkg/commands/compute/serve.go +++ b/pkg/commands/compute/serve.go @@ -42,9 +42,8 @@ var viceroyError = fsterr.RemediationError{ // ServeCommand produces and runs an artifact from files on the local disk. type ServeCommand struct { cmd.Base - manifest manifest.Data - build *BuildCommand - av github.AssetVersioner + build *BuildCommand + av github.AssetVersioner // Build fields dir cmd.OptionalString @@ -68,7 +67,7 @@ type ServeCommand struct { } // NewServeCommand returns a usable command registered under the parent. -func NewServeCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, av github.AssetVersioner, m manifest.Data) *ServeCommand { +func NewServeCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, av github.AssetVersioner) *ServeCommand { var c ServeCommand c.build = build @@ -76,12 +75,11 @@ func NewServeCommand(parent cmd.Registerer, g *global.Data, build *BuildCommand, c.Globals = g c.CmdClause = parent.Command("serve", "Build and run a Compute package locally") - c.manifest = m c.CmdClause.Flag("addr", "The IPv4 address and port to listen on").Default("127.0.0.1:7676").StringVar(&c.addr) c.CmdClause.Flag("debug", "Run the server in Debug Adapter mode").Hidden().BoolVar(&c.debug) c.CmdClause.Flag("dir", "Project directory to build (default: current directory)").Short('C').Action(c.dir.Set).StringVar(&c.dir.Value) - c.CmdClause.Flag("env", "The environment configuration to use (e.g. stage)").Action(c.env.Set).StringVar(&c.env.Value) + 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) c.CmdClause.Flag("file", "The Wasm file to run").Default("bin/main.wasm").StringVar(&c.file) c.CmdClause.Flag("include-source", "Include source code in built package").Action(c.includeSrc.Set).BoolVar(&c.includeSrc.Value) c.CmdClause.Flag("language", "Language type").Action(c.lang.Set).StringVar(&c.lang.Value) @@ -116,26 +114,32 @@ func (c *ServeCommand) Exec(in io.Reader, out io.Writer) (err error) { if err != nil { return err } + text.Break(out) } - text.Break(out) - - if c.dir.WasSet { - projectDir, err := filepath.Abs(c.dir.Value) - if err != nil { - return fmt.Errorf("failed to construct absolute path to directory '%s': %w", c.dir.Value, err) - } - wd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current working directory: %w", err) - } - if err := os.Chdir(projectDir); err != nil { - return fmt.Errorf("failed to change working directory to '%s': %w", projectDir, err) + manifestFilename := EnvironmentManifest(c.env.Value) + if c.env.Value != "" { + if c.Globals.Verbose() { + text.Info(out, EnvManifestMsg, manifestFilename, manifest.Filename) } - defer os.Chdir(wd) + } + wd, err := os.Getwd() + if err != nil { + c.Globals.ErrLog.Add(err) + return fmt.Errorf("failed to get current working directory: %w", err) + } + defer os.Chdir(wd) + manifestPath := filepath.Join(wd, manifestFilename) + + projectDir, err := ChangeProjectDirectory(c.dir.Value) + if err != nil { + return err + } + if projectDir != "" { if c.Globals.Verbose() { - text.Info(out, "Changed project directory to '%s'\n\n", projectDir) + text.Info(out, ProjectDirMsg, projectDir) } + manifestPath = filepath.Join(projectDir, manifestFilename) } c.setBackendsWithDefaultOverrideHostIfMissing(out) @@ -145,22 +149,21 @@ func (c *ServeCommand) Exec(in io.Reader, out io.Writer) (err error) { return err } - wd, err := os.Getwd() - if err != nil { - c.Globals.ErrLog.Add(err) - return err - } - filename := "fastly.toml" - if c.env.Value != "" { - filename = fmt.Sprintf("fastly.%s.toml", c.env.Value) - } - manifestPath := filepath.Join(wd, filename) - if c.env.Value != "" { - err := c.manifest.File.Read(manifestPath) + // NOTE: We read again the manifest to catch a skip-build scenario. + // + // For example, a user runs `compute build` then `compute serve --skip-build`. + // In that scenario our in-memory manifest could be invalid as the user might + // have also called `compute serve --skip-build --env <...> --dir <...>`. + // + // If the user doesn't set --skip-build then `compute serve` will call + // `compute build` and the logic there will update the manifest in-memory data + // with the relevant project directory and environment manifest content. + if c.skipBuild { + err := c.Globals.Manifest.File.Read(manifestPath) if err != nil { return fmt.Errorf("failed to parse manifest '%s': %w", manifestPath, err) } - c.av.SetRequestedVersion(c.manifest.File.LocalServer.ViceroyVersion) + c.av.SetRequestedVersion(c.Globals.Manifest.File.LocalServer.ViceroyVersion) if c.Globals.Verbose() { text.Info(out, "Fastly manifest set to: %s\n\n", manifestPath) } @@ -234,6 +237,9 @@ func (c *ServeCommand) Build(in io.Reader, out io.Writer) error { if c.dir.WasSet { c.build.Flags.Dir = c.dir.Value } + if c.env.WasSet { + c.build.Flags.Env = c.env.Value + } if c.includeSrc.WasSet { c.build.Flags.IncludeSrc = c.includeSrc.Value } @@ -456,7 +462,7 @@ func installViceroy( // Viceroy is already installed, so we check if the installed version matches the latest. // But we'll skip that check if the TTL for the Viceroy LastChecked hasn't expired. - stale := g.Config.Viceroy.LastChecked != "" && g.Config.Viceroy.LatestVersion != "" && check.Stale(g.Config.Viceroy.LastChecked, g.Config.Viceroy.TTL) + stale := check.Stale(g.Config.Viceroy.LastChecked, g.Config.Viceroy.TTL) if !stale && !forceCheckViceroyLatest { if g.Verbose() { text.Info(g.Output, "Viceroy is installed but the CLI config (`fastly config`) shows the TTL, checking for a newer version, hasn't expired. To force a refresh, re-run the command with the `--viceroy-check` flag.\n\n") @@ -464,32 +470,19 @@ func installViceroy( return nil } - err = spinner.Start() - if err != nil { - return err - } - msg = "Checking latest Viceroy release" - spinner.Message(msg + "...") - // IMPORTANT: We declare separately so to shadow `err` from parent scope. var latestVersion string - latestVersion, err = av.LatestVersion() - if err != nil { - err = fmt.Errorf("error fetching latest version: %w", err) - spinner.StopFailMessage(msg) - spinErr := spinner.StopFail() - if spinErr != nil { - return fmt.Errorf(text.SpinnerErrWrapper, spinErr, err) - } - return fsterr.RemediationError{ - Inner: err, - Remediation: fsterr.NetworkRemediation, + err = spinner.Process("Checking latest Viceroy release", func(_ *text.SpinnerWrapper) error { + latestVersion, err = av.LatestVersion() + if err != nil { + return fsterr.RemediationError{ + Inner: fmt.Errorf("error fetching latest version: %w", err), + Remediation: fsterr.NetworkRemediation, + } } - } - - spinner.StopMessage(msg) - err = spinner.Stop() + return nil + }) if err != nil { return err } @@ -508,7 +501,7 @@ func installViceroy( } if g.Verbose() { - text.Info(g.Output, "The CLI config (`fastly config`) has been updated with the latest Viceroy version: %s\n\n", latestVersion) + text.Info(g.Output, "\nThe CLI config (`fastly config`) has been updated with the latest Viceroy version: %s\n\n", latestVersion) } if installedVersion != "" && installedVersion == latestVersion { @@ -525,6 +518,7 @@ func installViceroy( tmpBin, err = av.DownloadLatest() } + // NOTE: The above `switch` needs to shadow the function-level `err` variable. if err != nil { err = fmt.Errorf("error downloading Viceroy release: %w", err) spinner.StopFailMessage(msg) diff --git a/pkg/commands/compute/validate.go b/pkg/commands/compute/validate.go index c0c258395..977270b0a 100644 --- a/pkg/commands/compute/validate.go +++ b/pkg/commands/compute/validate.go @@ -18,12 +18,12 @@ import ( ) // NewValidateCommand returns a usable command registered under the parent. -func NewValidateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *ValidateCommand { +func NewValidateCommand(parent cmd.Registerer, g *global.Data) *ValidateCommand { var c ValidateCommand c.Globals = g - c.manifest = m c.CmdClause = parent.Command("validate", "Validate a Compute package") c.CmdClause.Flag("package", "Path to a package tar.gz").Short('p').StringVar(&c.path) + 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) return &c } @@ -31,7 +31,7 @@ func NewValidateCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { packagePath := c.path if packagePath == "" { - projectName, source := c.manifest.Name() + projectName, source := c.Globals.Manifest.Name() if source == manifest.SourceUndefined { return fsterr.RemediationError{ Inner: fmt.Errorf("failed to read project name: %w", fsterr.ErrReadingManifest), @@ -49,6 +49,14 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { return fmt.Errorf("error reading file path: %w", err) } + manifestFilename := manifest.Filename + if c.env != "" { + manifestFilename = fmt.Sprintf("fastly.%s.toml", c.env) + if c.Globals.Verbose() { + text.Info(out, "Using the '%s' environment manifest (it will be packaged up as %s)\n\n", manifestFilename, manifest.Filename) + } + } + if err := validatePackageContent(p); err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Path": c.path, @@ -66,8 +74,8 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { // ValidateCommand validates a package archive. type ValidateCommand struct { cmd.Base - manifest manifest.Data - path string + env string + path string } // validatePackageContent is a utility function to determine whether a package @@ -77,8 +85,8 @@ type ValidateCommand struct { // NOTE: This function is also called by the `deploy` command. func validatePackageContent(pkgPath string) error { files := map[string]bool{ - "fastly.toml": false, - "main.wasm": false, + manifest.Filename: false, + "main.wasm": false, } if err := packageFiles(pkgPath, func(f archiver.File) error {