Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_PULUMI_DIFF` - If set to `true`, shows the Pulumi diff during deployments; defaults to `false`
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_RECIPE` - The deployment mode / recipe to use; defaults to `AFFORDABLE`
- `DEFANG_SUFFIX` - The suffix to use for all BYOC resources; defaults to the stack name, or `beta` if unset.
- `DEFANG_WORKSPACE` - The workspace (name or ID) to use; preferred way to select which workspace the CLI uses
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
Expand Down
2 changes: 1 addition & 1 deletion pkgs/defang/cli.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildGo125Module {
pname = "defang-cli";
version = "git";
src = lib.cleanSource ../../src;
vendorHash = "sha256-qNsk3rEco0mzBIPodsp3GZpzgaIzthAPSIMmVCja50I=";
vendorHash = "sha256-Qyi3P35xTsY98tSH7VrMMj1LeX9dRMmI4ANX9E2e+nU=";

subPackages = [ "cmd/cli" ];

Expand Down
1 change: 1 addition & 0 deletions pkgs/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_PULUMI_DIFF` - If set to `true`, shows the Pulumi diff during deployments; defaults to `false`
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_RECIPE` - The deployment mode / recipe to use; defaults to `AFFORDABLE`
- `DEFANG_SUFFIX` - The suffix to use for all BYOC resources; defaults to the stack name, or `beta` if unset.
- `DEFANG_WORKSPACE` - The workspace (name or ID) to use; preferred way to select which workspace the CLI uses
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
Expand Down
1 change: 1 addition & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ The Defang CLI recognizes the following environment variables:
- `DEFANG_PULUMI_DIFF` - If set to `true`, shows the Pulumi diff during deployments; defaults to `false`
- `DEFANG_PULUMI_DIR` - Run Pulumi from this folder, instead of spawning a cloud task; requires `--debug` (BYOC only)
- `DEFANG_PULUMI_VERSION` - Override the version of the Pulumi image to use (`aws` provider only)
- `DEFANG_RECIPE` - The deployment mode / recipe to use; defaults to `AFFORDABLE`
- `DEFANG_SUFFIX` - The suffix to use for all BYOC resources; defaults to the stack name, or `beta` if unset.
- `DEFANG_WORKSPACE` - The workspace (name or ID) to use; preferred way to select which workspace the CLI uses
- `NO_COLOR` - If set to any value, disables color output; by default, color output is enabled depending on the terminal
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/cli/command/cd.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ var cdPreviewCmd = &cobra.Command{
}

return cli.Preview(ctx, project, global.Client, session.Provider, cli.ComposeUpParams{
Mode: session.Stack.Mode,
Recipe: session.Stack.Recipe,
Project: project,
UploadMode: compose.UploadModePreview,
})
Expand Down
7 changes: 3 additions & 4 deletions src/cmd/cli/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"github.com/DefangLabs/defang/src/pkg/login"
"github.com/DefangLabs/defang/src/pkg/mcp"
"github.com/DefangLabs/defang/src/pkg/migrate"
"github.com/DefangLabs/defang/src/pkg/modes"
"github.com/DefangLabs/defang/src/pkg/scope"
"github.com/DefangLabs/defang/src/pkg/stacks"
"github.com/DefangLabs/defang/src/pkg/term"
Expand Down Expand Up @@ -186,8 +185,6 @@ func SetupCommands(version string) {
cdCmd.AddCommand(cdListCmd)
cdCmd.AddCommand(cdCancelCmd)
cdCmd.AddCommand(cdOutputsCmd)
cdPreviewCmd.Flags().VarP(&global.Stack.Mode, "mode", "m", fmt.Sprintf("deployment mode; one of %v", modes.AllDeploymentModes()))
cdPreviewCmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions(modes.AllDeploymentModes(), cobra.ShellCompDirectiveNoFileComp))
cdCmd.AddCommand(cdPreviewCmd)
cdInstallCmd.Flags().Bool("force", false, "force the installation of the CD resources into the cluster (allow downgrades)")
cdCmd.AddCommand(cdInstallCmd)
Expand Down Expand Up @@ -319,6 +316,8 @@ func SetupCommands(version string) {
// TODO: Add list, renew etc.
certCmd.AddCommand(certGenerateCmd)
RootCmd.AddCommand(certCmd)
recipeCmd := makeRecipeCmd()
RootCmd.AddCommand(recipeCmd)

stackCmd := makeStackCmd()
RootCmd.AddCommand(stackCmd)
Expand Down Expand Up @@ -500,7 +499,7 @@ func canIUseProvider(ctx context.Context, provider client.Provider, projectName
// provider-specific login paths such as GitHub Actions OIDC federation run instead of
// falling through to the plain default credential chain.
func authenticateProvider(ctx context.Context, provider client.Provider) error {
if err := provider.Authenticate(ctx, !global.NonInteractive); err != nil {
if err := provider.Authenticate(ctx, global.Interactive()); err != nil {
return fmt.Errorf("failed to authenticate with provider: %w", err)
}
return nil
Expand Down
9 changes: 4 additions & 5 deletions src/cmd/cli/command/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func makeComposeUpCmd() *cobra.Command {
Name: stacks.MakeDefaultName(accountInfo.Provider, accountInfo.Region),
Provider: accountInfo.Provider,
Region: accountInfo.Region,
Mode: session.Stack.Mode,
Recipe: session.Stack.Recipe,
})
if err != nil {
term.Debug("Failed to create stack:", err)
Expand All @@ -133,7 +133,7 @@ func makeComposeUpCmd() *cobra.Command {
deploy, project, err := cli.ComposeUp(ctx, global.Client, session.Provider, session.Stack, cli.ComposeUpParams{
Project: project,
UploadMode: upload,
Mode: session.Stack.Mode,
Recipe: session.Stack.Recipe,
})
if err != nil {
composeErr := err
Expand Down Expand Up @@ -206,7 +206,6 @@ func makeComposeUpCmd() *cobra.Command {
composeUpCmd.Flags().Bool("force", false, "force a build of the image even if nothing has changed; implies --build")
composeUpCmd.Flags().Bool("tail", false, "tail the service logs after updating") // no-op, but keep for backwards compatibility
_ = composeUpCmd.Flags().MarkHidden("tail")
composeUpCmd.Flags().VarP(&global.Stack.Mode, "mode", "m", fmt.Sprintf("deployment mode; one of %v", modes.AllDeploymentModes()))
composeUpCmd.Flags().Bool("build", false, "build images before starting services") // docker-compose compatibility
composeUpCmd.Flags().Bool("wait", true, "wait for services to be running|healthy") // docker-compose compatibility
_ = composeUpCmd.Flags().MarkHidden("wait")
Expand Down Expand Up @@ -243,7 +242,7 @@ func confirmDeployment(targetDirectory string, existingDeployments []*defangv1.D
Name: stackName,
Provider: accountInfo.Provider,
Region: accountInfo.Region,
Mode: global.Stack.Mode,
Recipe: global.Stack.Recipe,
})
if err != nil {
term.Debugf("Failed to create stack %v", err)
Expand Down Expand Up @@ -571,7 +570,7 @@ func makeComposeConfigCmd() *cobra.Command {
_, _, err = cli.ComposeUp(ctx, global.Client, sessionx.Provider, sessionx.Stack, cli.ComposeUpParams{
Project: project,
UploadMode: compose.UploadModeIgnore,
Mode: modes.ModeUnspecified,
Recipe: modes.RecipeUnspecified,
})
if !errors.Is(err, dryrun.ErrDryRun) {
return err
Expand Down
13 changes: 8 additions & 5 deletions src/cmd/cli/command/estimate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,32 +43,33 @@ func makeEstimateCmd() *cobra.Command {
var previewProvider client.Provider = &client.PlaygroundProvider{FabricClient: global.Client}

// default to development mode if not specified; TODO: when mode is not specified, show an interactive prompt
if global.Stack.Mode == modes.ModeUnspecified {
global.Stack.Mode = modes.ModeAffordable
if global.Stack.Recipe == modes.RecipeUnspecified {
global.Stack.Recipe = modes.RecipeAffordable
}
if region == "" {
region = client.GetRegion(global.Stack.Provider) // This sets the default region based on the provider
}

estimate, err := cli.RunEstimate(ctx, project, global.Client, previewProvider, global.Stack.Provider, region, global.Stack.Mode)
estimate, err := cli.RunEstimate(ctx, project, global.Client, previewProvider, global.Stack.Provider, region, global.Stack.Recipe)
if err != nil {
return fmt.Errorf("failed to run estimate: %w", err)
}
term.Debugf("Estimate: %+v", estimate)

cli.PrintEstimate(global.Stack.Mode, estimate, term.DefaultTerm)
cli.PrintEstimate(global.Stack.Recipe, estimate, term.DefaultTerm)

return nil
},
}

estimateCmd.Flags().VarP(&global.Stack.Mode, "mode", "m", fmt.Sprintf("deployment mode; one of %v", modes.AllDeploymentModes()))
estimateCmd.Flags().VarP(&global.Stack.Recipe, "mode", "m", fmt.Sprintf("deployment mode; one of %v", modes.AllDeploymentModes()))
estimateCmd.Flags().StringVarP(&global.Stack.Region, "region", "r", global.Stack.Region, "which cloud region to estimate")
return estimateCmd
}

var providerDescription = map[client.ProviderID]string{
client.ProviderDefang: "The Defang Playground is a free platform intended for testing purposes only.",
client.ProviderAzure: "Deploy to Azure using the AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET environment variables or the Azure CLI configuration.",
client.ProviderAWS: "Deploy to AWS using the AWS_* environment variables or the AWS CLI configuration.",
client.ProviderDO: "Deploy to DigitalOcean using the DIGITALOCEAN_TOKEN, SPACES_ACCESS_KEY_ID, and SPACES_SECRET_ACCESS_KEY environment variables.",
client.ProviderGCP: "Deploy to Google Cloud Platform using gcloud Application Default Credentials.",
Expand All @@ -94,6 +95,8 @@ func interactiveSelectProvider(providers []client.ProviderID) (client.ProviderID
defaultOption = client.ProviderDO.String()
case pkg.GcpInEnv() != "":
defaultOption = client.ProviderGCP.String()
case pkg.AzureInEnv() != "":
defaultOption = client.ProviderAzure.String()
}

var optionValue string
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/cli/command/estimate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TestPrintEstimate(t *testing.T) {
}

stdout, _ := term.SetupTestTerm(t)
cli.PrintEstimate(modes.ModeAffordable, estimate, term.DefaultTerm)
cli.PrintEstimate(modes.RecipeAffordable, estimate, term.DefaultTerm)

expectedOutput := `
Estimate for Deployment Mode: AFFORDABLE
Expand Down
14 changes: 7 additions & 7 deletions src/cmd/cli/command/globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ func NewGlobalConfig() *GlobalConfig {
}
}

mode := modes.ModeUnspecified
if fromEnv, ok := os.LookupEnv("DEFANG_MODE"); ok {
err := mode.Set(fromEnv)
recipe := modes.RecipeUnspecified
if key, fromEnv := pkg.GetFirstEnv("DEFANG_RECIPE", "DEFANG_MODE"); key != "" {
err := recipe.Set(fromEnv)
if err != nil {
term.Debugf("invalid DEFANG_MODE value: %v", err)
term.Debugf("invalid %s value: %v", key, err)
}
}

Expand All @@ -137,7 +137,7 @@ func NewGlobalConfig() *GlobalConfig {
Stack: stacks.Parameters{
Name: pkg.Getenv("DEFANG_STACK", ""),
Provider: provider,
Mode: mode,
Recipe: recipe,
Region: client.GetRegion(provider),
},
Verbose: pkg.GetenvBool("DEFANG_VERBOSE"),
Expand All @@ -158,8 +158,8 @@ func (global *GlobalConfig) ToMap() map[string]string {
if regionVarName != "" && global.Stack.Region != "" {
m[regionVarName] = global.Stack.Region
}
if global.Stack.Mode != modes.ModeUnspecified {
m["DEFANG_MODE"] = global.Stack.Mode.String()
if global.Stack.Recipe != modes.RecipeUnspecified {
m["DEFANG_RECIPE"] = global.Stack.Recipe.String()
}
m["DEFANG_VERBOSE"] = strconv.FormatBool(global.Verbose)
return m
Expand Down
7 changes: 3 additions & 4 deletions src/cmd/cli/command/globals_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func Test_configurationPrecedence(t *testing.T) {
HideUpdate: false,
NonInteractive: false, // set to false just for test instead of !term.IsTerminal() for consistency
Verbose: false,
Stack: stacks.Parameters{Provider: client.ProviderAuto, Mode: modes.ModeUnspecified},
Stack: stacks.Parameters{Provider: client.ProviderAuto, Recipe: modes.RecipeUnspecified},
FabricAddr: "",
TenantSelection: "",
}
Expand Down Expand Up @@ -61,7 +61,6 @@ func Test_configurationPrecedence(t *testing.T) {
flags.BoolVarP(&testConfig.Verbose, "verbose", "v", testConfig.Verbose, "verbose logging")
flags.BoolVar(&testConfig.Debug, "debug", testConfig.Debug, "debug logging for troubleshooting the CLI")
flags.BoolVar(&testConfig.NonInteractive, "non-interactive", testConfig.NonInteractive, "disable interactive prompts / no TTY")
flags.VarP(&testConfig.Stack.Mode, "mode", "m", "deployment mode")

// Set flags based on user input (these override env and stack file values)
for flagName, flagValue := range tt.flags {
Expand Down Expand Up @@ -109,8 +108,8 @@ func Test_configurationPrecedence(t *testing.T) {
if testConfig.Stack.Provider != tt.expected.Stack.Provider {
t.Errorf("expected Stack.Provider to be '%s', got '%s'", tt.expected.Stack.Provider, testConfig.Stack.Provider)
}
if testConfig.Stack.Mode != tt.expected.Stack.Mode {
t.Errorf("expected Stack.Mode to be '%s', got '%s'", tt.expected.Stack.Mode, testConfig.Stack.Mode)
if testConfig.Stack.Recipe != tt.expected.Stack.Recipe {
t.Errorf("expected Stack.Mode to be '%s', got '%s'", tt.expected.Stack.Recipe, testConfig.Stack.Recipe)
}
if testConfig.FabricAddr != tt.expected.FabricAddr {
t.Errorf("expected FabricAddr to be '%s', got '%s'", tt.expected.FabricAddr, testConfig.FabricAddr)
Expand Down
93 changes: 93 additions & 0 deletions src/cmd/cli/command/recipe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package command

import (
"errors"

"github.com/DefangLabs/defang/src/pkg/cli"
"github.com/spf13/cobra"
)

func makeRecipeCmd() *cobra.Command {
var recipeCmd = &cobra.Command{
Use: "recipe",
Aliases: []string{"recipes", "modes", "mode"},
Short: "Manage workspace recipes (deployment modes)",
}
recipeListCmd := makeRecipeListCmd()
recipeCmd.AddCommand(recipeListCmd)
recipeShowCmd := makeRecipeShowCmd()
recipeCmd.AddCommand(recipeShowCmd)
recipeDeactivateCmd := makeRecipeDeactivateCmd()
recipeCmd.AddCommand(recipeDeactivateCmd)
recipeActivateCmd := makeRecipeActivateCmd()
recipeCmd.AddCommand(recipeActivateCmd)
return recipeCmd
}

func makeRecipeShowCmd() *cobra.Command {
var recipeShowCmd = &cobra.Command{
Use: "show [RECIPE_NAME]",
Aliases: []string{"get", "describe", "desc"},
Annotations: authNeededAlways,
Args: cobra.ExactArgs(1),
Short: "Show details of a recipe in the current workspace",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
return cli.RecipeShow(ctx, global.Client, args[0])
},
}
return recipeShowCmd
}

func makeRecipeListCmd() *cobra.Command {
var recipeListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Annotations: authNeededAlways,
Args: cobra.NoArgs,
Short: "List recipes in the current workspace",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
return cli.RecipeList(ctx, global.Client)
},
}
return recipeListCmd
}

func makeRecipeDeactivateCmd() *cobra.Command {
var recipeArchiveCmd = &cobra.Command{
Use: "deactivate [RECIPE_NAME...]",
Aliases: []string{"remove", "rm", "delete", "del", "disable", "archive"},
Annotations: authNeededAlways,
Args: cobra.MinimumNArgs(1),
Short: "Deactivates a recipe in the current workspace",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
var errs []error
for _, name := range args {
errs = append(errs, cli.RecipeActivate(ctx, global.Client, name, false))
}
return errors.Join(errs...)
},
}
return recipeArchiveCmd
}

func makeRecipeActivateCmd() *cobra.Command {
var recipeUnarchiveCmd = &cobra.Command{
Use: "activate [RECIPE_NAME...]",
Aliases: []string{"restore", "enable", "undelete", "unarchive"},
Annotations: authNeededAlways,
Args: cobra.MinimumNArgs(1),
Short: "Activates a recipe in the current workspace",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
var errs []error
for _, name := range args {
errs = append(errs, cli.RecipeActivate(ctx, global.Client, name, true))
}
return errors.Join(errs...)
},
}
return recipeUnarchiveCmd
}
4 changes: 2 additions & 2 deletions src/cmd/cli/command/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se
options := session.SessionLoaderOptions{
LoaderOptions: loaderOptionsForCommand(cmd),
GetStackOpts: stacks.GetStackOpts{
Interactive: !global.NonInteractive,
Interactive: global.Interactive(),
Default: global.Stack,
SelectStackOptions: stacks.SelectStackOptions{
AllowStackCreation: opts.AllowStackCreation,
Expand All @@ -57,7 +57,7 @@ func newCommandSessionWithOpts(cmd *cobra.Command, opts commandSessionOpts) (*se
return nil, err
}
if opts.CheckAccountInfo {
if err := session.Provider.Authenticate(ctx, !global.NonInteractive); err != nil {
if err := session.Provider.Authenticate(ctx, global.Interactive()); err != nil {
return nil, fmt.Errorf("failed to authenticate with provider %q: %w", session.Stack.Provider, err)
}
_, err = session.Provider.AccountInfo(ctx)
Expand Down
Loading