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
75 changes: 75 additions & 0 deletions cmd/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"embed"
"encoding/json"
"fmt"
"os"
"strings"
"text/template"

Expand Down Expand Up @@ -86,12 +87,26 @@ 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)

return environmentCmd
}
Expand Down Expand Up @@ -368,3 +383,63 @@ func runEnvironmentDefault(cmd *cobra.Command, args []string) error {

return nil
}

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

id := args[0]
configPath, err := cmd.Flags().GetString("file")
if err != nil {
return err
}
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
}
follow, err := cmd.Flags().GetBool("follow")
if err != nil {
return err
}

cmd.SilenceUsage = true

config, configErr := environment.LoadPreviewConfig(configPath)
if configErr != nil {
return configErr
}

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

previewOpts := environment.PreviewOptions{
ID: id,
Name: name,
Description: description,
}
if cmd.Flags().Changed("attributes") {
previewOpts.Attributes = attrs
}

env, runErr := environment.RunPreview(ctx, environment.NewPreviewAPI(mdClient), config, previewOpts)
if runErr != nil {
return runErr
}

fmt.Printf("✅ Preview environment `%s` converged\n", env.ID)
fmt.Printf("🔗 %s\n", mdClient.URLs.Helper(ctx).EnvironmentURL(env.ID))
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include an option to watch for deployments and tail prefixing each line with the instance.id for grepability since some may run in parallel

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0c78eb8 — added --follow to both environment preview (here) and environment deploy (in #238, since that's where the deploy command lives). Output looks like:

[ecomm-pr42-db]  applying db schema
[ecomm-pr42-app] starting container
[ecomm-pr42-db]  migrations done

Each tail goroutine line-buffers and serializes through a mutex'd sink, so parallel deployments don't tear lines.

One implementation note worth flagging: I went with polling rather than subscribing to environmentEvents / instanceEvents as you suggested. The SDK's Absinthe/Phoenix-channels machinery is in internal/absinthe, so doing real subscriptions from the CLI would mean re-porting that layer just for this flag. Polling per-instance Deployments.List every ~2s has a few seconds of discovery latency per new deployment, which seemed acceptable next to per-instance deploy times measured in minutes. Happy to switch to the subscription path the moment the SDK exposes event subscriptions publicly — at that point it's a one-file swap inside internal/commands/environment/follow.go.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed the SDK ask: massdriver-cloud/massdriver-sdk-go#21 — request to expose environmentEvents / instanceEvents as typed Subscribe methods on the existing services. Once that ships, the swap inside follow.go is one file.


if follow {
return environment.FollowEnvironment(ctx, environment.NewFollowAPI(mdClient), env.ID, os.Stdout)
}
return nil
}
1 change: 1 addition & 0 deletions docs/generated/mass_environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ Environments can be modeled by application stage (production, staging, developme
* [mass environment export](/cli/commands/mass_environment_export) - Export an environment from Massdriver
* [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
* [mass environment update](/cli/commands/mass_environment_update) - Update an environment's name, description, or attributes
143 changes: 143 additions & 0 deletions docs/generated/mass_environment_preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
id: mass_environment_preview.md
slug: /cli/commands/mass_environment_preview
title: Mass Environment Preview
sidebar_label: Mass Environment Preview
---
## mass environment preview

Converge a preview environment from a YAML config

### Synopsis

# Preview Environment

Converges a preview environment from a YAML config: forks a base environment,
pins environment defaults, applies per-instance overrides, and triggers a deploy.

Re-running the command against the same config is safe — every step is
idempotent. Use it to ramp up a per-PR environment on every git push, and again
to reset the env back to the declared state.

## Usage

```bash
mass environment preview <ID> [flags]
```

## Arguments

- `ID`: the local segment of the preview environment's identifier (e.g.
`pr123`). Must match `^[a-z0-9]{1,20}$` — lowercase alphanumeric only, no
dashes. The full stored identifier becomes `<project>-<ID>`, where
`project` comes from the config file.

## Flags

- `--file, -f`: path to the preview YAML config (default `preview.yaml`).
- `--name, -n`: human-readable environment name (defaults to `ID`).
- `--description, -d`: optional environment description.
- `--attributes, -a`: custom attributes for ABAC, e.g. `-a environment=preview,region=uswest`. Overrides `attributes:` in the config file.
- `--follow`: stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id so the interleaved output stays grep-friendly when multiple deployments run in parallel.

## Environment-variable expansion

`${VAR}` / `$VAR` references in the config file are expanded from the
process environment before parsing. Use this for CI-injected values like PR
numbers:

```yaml
instances:
chatsvc:
params:
host: chatty-pr-${GITHUB_PR}.example.com
attributes:
pr: "${GITHUB_PR}"
```

Undefined variables expand to empty strings.

## Config schema

```yaml
# Required: the project the preview env lives in.
project: demo

# Required: the local segment of the env to fork from. The full parent
# identifier is `<project>-<baseEnvironment>`.
baseEnvironment: production

# Optional fork-level macros. Defaults to false.
copyEnvironmentDefaults: true # carry the parent's default resources over
copySecrets: false # fan copyInstance(copySecrets: true) to every instance
copyRemoteReferences: false # fan copyInstance(copyRemoteReferences: true) to every instance

# Optional. Required when the organization declares attributes at the
# environment scope. Both keys and values must be strings.
attributes:
region: us-east-1
pr: "${GITHUB_PR}"

# Optional: pin specific resources as defaults for this env. `resourceType` is
# documentation for readers; the CLI only needs `resourceId`.
environmentDefaults:
- resourceType: aws-iam-role
resourceId: 161aeb95-e1c5-4f8d-803e-ef82087d7ad4

# Optional: per-instance overrides. Listed instances with no fields just
# inherit from the fork's seed.
instances:
chatdb:
version: "~2.0" # stable channel

chatsvc:
version: "latest+dev" # `+dev` pulls from the development channel
params:
ingress:
enabled: true
host: chatty-pr-${GITHUB_PR}.mdawssbx.com
path: /
secrets:
- name: STRIPE_KEY
value: FOO

# listed without overrides — inherit from the fork
imported:
sessions:
sessionsapi:
sessionsfn:
sharedvpc:
```

## Examples

```bash
# Converge a preview env for PR 123 from the default `preview.yaml`
mass environment preview pr123

# Same, with a friendly name
mass environment preview pr123 -n "Chat PR #123"

# Point at a config in another path
mass environment preview pr123 -f .github/preview.yml
```


```
mass environment preview [ID] [flags]
```

### Options

```
-a, --attributes attributes: Custom attributes for ABAC (e.g. -a environment=preview,region=uswest). Overrides attributes: in the config file. (default [])
-d, --description string Optional environment description
-f, --file string Path to the preview config YAML (default "preview.yaml")
--follow Stream every deployment's logs to stdout until the rollout completes. Each line is prefixed with the instance id.
-h, --help help for preview
-n, --name string Environment name (defaults to ID if not provided)
```

### SEE ALSO

* [mass environment](/cli/commands/mass_environment) - Environment management
56 changes: 56 additions & 0 deletions docs/helpdocs/environment/preview.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Example preview environment config. Drive a converge with:
#
# mass environment preview pr123 -f preview.yaml
#
# Every step is idempotent — re-running the command resets the preview env
# back to the declared state.

# The project the preview env lives in.
project: demo

# The base env to fork from. The full parent identifier is
# `<project>-<baseEnvironment>` — `demo-production` here.
baseEnvironment: production

# Carry the parent env's default resource connections into the fork.
copyEnvironmentDefaults: true

# Optional. Set environment-scope attributes for ABAC. Required when the org
# declares attributes at the environment scope. `${VAR}` references are
# expanded from the process environment before parsing — handy for piping
# CI metadata in without rewriting the file.
attributes:
region: us-east-1
pr: "${GITHUB_PR}"

# Override env-level defaults for specific resource types. `resourceType` is
# documentation for the human reader; the CLI only needs `resourceId`.
environmentDefaults:
- resourceType: aws-iam-role
resourceId: 161aeb95-e1c5-4f8d-803e-ef82087d7ad4
- resourceType: aws-vpc
resourceId: 1e9fc8a3-f011-433f-b937-b5e525fd753c

# Per-instance overrides. Listed instances with no fields just inherit from
# the fork's seed.
instances:
chatsvc:
version: "latest+dev" # `+dev` pulls from the development channel
params:
ingress:
enabled: true
host: chatty-pr-${GITHUB_PR}.mdawssbx.com
path: /
secrets:
- name: STRIPE_KEY
value: FOO

chatdb:
version: "~2.0" # stable channel

# listed without overrides — inherit from the fork
imported:
sessions:
sessionsapi:
sessionsfn:
sharedvpc:
Loading
Loading