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
199 changes: 185 additions & 14 deletions cmd/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,80 @@ func NewCmdEnvironment() *cobra.Command {
RunE: runEnvironmentDefault,
}

environmentPreviewCmd := &cobra.Command{
Use: "preview [ID]",
Short: "Converge a preview environment from a YAML config",
Long: helpdocs.MustRender("environment/preview"),
Args: cobra.ExactArgs(1),
RunE: runEnvironmentPreview,
}
environmentPreviewCmd.Flags().StringP("file", "f", "preview.yaml", "Path to the preview config YAML")
environmentPreviewCmd.Flags().StringP("name", "n", "", "Environment name (defaults to ID if not provided)")
environmentPreviewCmd.Flags().StringP("description", "d", "", "Optional environment description")
environmentPreviewCmd.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides `attributes:` in the config file.")
environmentPreviewCmd.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.")

environmentCmd.AddCommand(environmentExportCmd)
environmentCmd.AddCommand(environmentGetCmd)
environmentCmd.AddCommand(environmentListCmd)
environmentCmd.AddCommand(environmentCreateCmd)
environmentCmd.AddCommand(environmentUpdateCmd)
environmentCmd.AddCommand(environmentDefaultCmd)
environmentCmd.AddCommand(environmentPreviewCmd)
environmentCmd.AddCommand(newEnvironmentPreviewCmd())
environmentCmd.AddCommand(newEnvironmentForkCmd())
environmentCmd.AddCommand(newEnvironmentDeployCmd())
environmentCmd.AddCommand(newEnvironmentDecommissionCmd())

return environmentCmd
}

func newEnvironmentPreviewCmd() *cobra.Command {
c := &cobra.Command{
Use: "preview [ID]",
Short: "Converge a preview environment from a YAML config",
Long: helpdocs.MustRender("environment/preview"),
Args: cobra.ExactArgs(1),
RunE: runEnvironmentPreview,
}
c.Flags().StringP("file", "f", "preview.yaml", "Path to the preview config YAML")
c.Flags().StringP("name", "n", "", "Environment name (defaults to ID if not provided)")
c.Flags().StringP("description", "d", "", "Optional environment description")
c.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides `attributes:` in the config file.")
c.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.")
return c
}

func newEnvironmentForkCmd() *cobra.Command {
c := &cobra.Command{
Use: "fork [parent-environment] [new-ID]",
Short: "Fork an existing environment",
Example: `mass environment fork ecomm-production staging`,
Long: helpdocs.MustRender("environment/fork"),
Args: cobra.ExactArgs(2),
RunE: runEnvironmentFork,
}
c.Flags().StringP("name", "n", "", "Environment name (defaults to new-ID if not provided)")
c.Flags().StringP("description", "d", "", "Optional environment description")
c.Flags().StringToStringP("attributes", "a", nil, "Custom attributes for ABAC (e.g. -a region=uswest)")
c.Flags().Bool("copy-environment-defaults", false, "Copy the parent's default resource connections into the fork")
c.Flags().Bool("copy-secrets", false, "Copy every instance's secrets from the parent into the fork")
c.Flags().Bool("copy-remote-references", false, "Copy every instance's remote references from the parent into the fork")
return c
}

func newEnvironmentDeployCmd() *cobra.Command {
c := &cobra.Command{
Use: "deploy [environment]",
Short: "Deploy every instance in an environment, in dependency order",
Example: `mass environment deploy ecomm-staging --follow`,
Long: helpdocs.MustRender("environment/deploy"),
Args: cobra.ExactArgs(1),
RunE: runEnvironmentDeploy,
}
c.Flags().Bool("follow", false, "Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.")
return c
}

func newEnvironmentDecommissionCmd() *cobra.Command {
c := &cobra.Command{
Use: "decommission [environment]",
Short: "Decommission every instance in an environment, in reverse dependency order",
Example: `mass environment decommission ecomm-pr42 --follow`,
Long: helpdocs.MustRender("environment/decommission"),
Args: cobra.ExactArgs(1),
RunE: runEnvironmentDecommission,
}
c.Flags().Bool("follow", false, "Stream every decommission deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.")
return c
}

func runEnvironmentExport(cmd *cobra.Command, args []string) error {
ctx := context.Background()

Expand Down Expand Up @@ -443,3 +493,124 @@ func runEnvironmentPreview(cmd *cobra.Command, args []string) error {
}
return nil
}

func runEnvironmentFork(cmd *cobra.Command, args []string) error {
ctx := context.Background()

parentID := args[0]
newLocalID := args[1]
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
description, err := cmd.Flags().GetString("description")
if err != nil {
return err
}
attrs, err := cmd.Flags().GetStringToString("attributes")
if err != nil {
return err
}
copyDefaults, err := cmd.Flags().GetBool("copy-environment-defaults")
if err != nil {
return err
}
copySecrets, err := cmd.Flags().GetBool("copy-secrets")
if err != nil {
return err
}
copyRefs, err := cmd.Flags().GetBool("copy-remote-references")
if err != nil {
return err
}

if name == "" {
name = newLocalID
}

cmd.SilenceUsage = true

mdClient, mdClientErr := massdriver.NewClient()
if mdClientErr != nil {
return fmt.Errorf("error initializing massdriver client: %w", mdClientErr)
}

input := environments.ForkInput{
ID: newLocalID,
Name: name,
Description: description,
Attributes: cli.AttributesToAnyMap(attrs),
CopyEnvironmentDefaults: copyDefaults,
CopySecrets: copySecrets,
CopyRemoteReferences: copyRefs,
}

env, err := mdClient.Environments.Fork(ctx, parentID, input)
if err != nil {
return err
}

fmt.Printf("✅ Environment `%s` forked from `%s`\n", env.ID, parentID)
fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID))
return nil
}

func runEnvironmentDeploy(cmd *cobra.Command, args []string) error {
ctx := context.Background()

environmentID := args[0]
follow, err := cmd.Flags().GetBool("follow")
if err != nil {
return err
}

cmd.SilenceUsage = true

mdClient, mdClientErr := massdriver.NewClient()
if mdClientErr != nil {
return fmt.Errorf("error initializing massdriver client: %w", mdClientErr)
}

env, err := mdClient.Environments.Deploy(ctx, environmentID)
if err != nil {
return err
}

fmt.Printf("🚀 Deploying environment `%s` — instances roll out in dependency order asynchronously\n", env.ID)
fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID))

if follow {
return environment.FollowEnvironment(ctx, environment.NewFollowAPI(mdClient), env.ID, os.Stdout)
}
return nil
}

func runEnvironmentDecommission(cmd *cobra.Command, args []string) error {
ctx := context.Background()

environmentID := args[0]
follow, err := cmd.Flags().GetBool("follow")
if err != nil {
return err
}

cmd.SilenceUsage = true

mdClient, mdClientErr := massdriver.NewClient()
if mdClientErr != nil {
return fmt.Errorf("error initializing massdriver client: %w", mdClientErr)
}

env, err := mdClient.Environments.Decommission(ctx, environmentID)
if err != nil {
return err
}

fmt.Printf("🔻 Decommissioning environment `%s` — instances tear down in reverse dependency order asynchronously\n", env.ID)
fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID))

if follow {
return environment.FollowEnvironment(ctx, environment.NewFollowAPI(mdClient), env.ID, os.Stdout)
}
return nil
}
71 changes: 71 additions & 0 deletions cmd/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,29 @@ func NewCmdInstance() *cobra.Command {
instanceCmd.AddCommand(instanceVersionCmd)
instanceCmd.AddCommand(instanceDestroyCmd)
instanceCmd.AddCommand(instanceOrphanCmd)
instanceCmd.AddCommand(newInstanceCopyCmd())

return instanceCmd
}

func newInstanceCopyCmd() *cobra.Command {
c := &cobra.Command{
Use: `copy [source] --to [destination]`,
Aliases: []string{"promote"},
Short: "Copy an instance's configuration to another instance of the same component",
Example: `mass instance promote ecomm-staging-db --to ecomm-production-db --copy-secrets`,
Long: helpdocs.MustRender("instance/copy"),
Args: cobra.ExactArgs(1),
RunE: runInstanceCopy,
}
c.Flags().String("to", "", "Destination instance (required). Must be built from the same component as the source.")
c.Flags().StringP("overrides", "o", "", "Path to a JSON or YAML file of param overrides deep-merged onto the source params")
c.Flags().Bool("copy-secrets", false, "Copy secrets from the source instance to the destination")
c.Flags().Bool("copy-remote-references", false, "Copy remote-reference overrides from the source instance to the destination")
_ = c.MarkFlagRequired("to")
return c
}

func runInstanceGet(cmd *cobra.Command, args []string) error {
ctx := context.Background()

Expand Down Expand Up @@ -306,6 +325,58 @@ func readParams(path string) (map[string]any, error) {
return params, nil
}

func runInstanceCopy(cmd *cobra.Command, args []string) error {
ctx := context.Background()

sourceID := args[0]
destinationID, err := cmd.Flags().GetString("to")
if err != nil {
return err
}
overridesPath, err := cmd.Flags().GetString("overrides")
if err != nil {
return err
}
copySecrets, err := cmd.Flags().GetBool("copy-secrets")
if err != nil {
return err
}
copyRefs, err := cmd.Flags().GetBool("copy-remote-references")
if err != nil {
return err
}

cmd.SilenceUsage = true

var overrides map[string]any
if overridesPath != "" {
overrides, err = readParams(overridesPath)
if err != nil {
return err
}
}

mdClient, mdClientErr := massdriver.NewClient()
if mdClientErr != nil {
return fmt.Errorf("error initializing massdriver client: %w", mdClientErr)
}

input := instances.CopyInput{
Overrides: overrides,
CopySecrets: copySecrets,
CopyRemoteReferences: copyRefs,
}

inst, err := mdClient.Instances.Copy(ctx, sourceID, destinationID, input)
if err != nil {
return err
}

fmt.Printf("✅ Instance `%s` configuration copied to `%s`\n", sourceID, destinationID)
fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).InstanceURL(inst.ID))
return nil
}

func runInstanceExport(cmd *cobra.Command, args []string) error {
ctx := context.Background()

Expand Down
3 changes: 3 additions & 0 deletions docs/generated/mass_environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ Environments can be modeled by application stage (production, staging, developme

* [mass](/cli/commands/mass) - Massdriver Cloud CLI
* [mass environment create](/cli/commands/mass_environment_create) - Create an environment
* [mass environment decommission](/cli/commands/mass_environment_decommission) - Decommission every instance in an environment, in reverse dependency order
* [mass environment default](/cli/commands/mass_environment_default) - Set an environment default connection
* [mass environment deploy](/cli/commands/mass_environment_deploy) - Deploy every instance in an environment, in dependency order
* [mass environment export](/cli/commands/mass_environment_export) - Export an environment from Massdriver
* [mass environment fork](/cli/commands/mass_environment_fork) - Fork an existing environment
* [mass environment get](/cli/commands/mass_environment_get) - Get an environment from Massdriver
* [mass environment list](/cli/commands/mass_environment_list) - List environments
* [mass environment preview](/cli/commands/mass_environment_preview) - Converge a preview environment from a YAML config
Expand Down
Loading
Loading