Skip to content

Commit c09e81e

Browse files
committed
Enhance deploy command to support deploying all stacks in context
- Add optional stack name parameter to deploy command - Change argument validation from ExactArgs(2) to RangeArgs(1, 2) - Implement ListStacks method in ConfigProvider interface and file provider - Deploy all stacks with dependency ordering when no specific stack provided - Add comprehensive test coverage for new functionality - Update documentation with examples for both deployment patterns - Maintain full backward compatibility with existing usage Usage: stackaroo deploy <context> # Deploy all stacks in context stackaroo deploy <context> <stack> # Deploy specific stack (unchanged)
1 parent 106b089 commit c09e81e

File tree

9 files changed

+348
-17
lines changed

9 files changed

+348
-17
lines changed

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,54 @@ go install github.com/orien/stackaroo@latest
5555
```
5656

5757
Or download a binary from the [releases page](https://github.com/orien/stackaroo/releases).
58+
59+
## Quick Start
60+
61+
### Configuration
62+
63+
Create a `stackaroo.yaml` file defining your stacks and contexts:
64+
65+
```yaml
66+
project: my-infrastructure
67+
region: us-east-1
68+
69+
contexts:
70+
development:
71+
account: "123456789012"
72+
region: ap-southeast-4
73+
tags:
74+
Environment: development
75+
production:
76+
account: "987654321098"
77+
region: us-east-1
78+
tags:
79+
Environment: production
80+
81+
stacks:
82+
- name: vpc
83+
template: templates/vpc.yaml
84+
- name: app
85+
template: templates/app.yaml
86+
depends_on:
87+
- vpc
88+
```
89+
90+
### Deployment
91+
92+
Deploy stacks using either pattern:
93+
94+
```bash
95+
# Deploy all stacks in a context (with dependency ordering)
96+
stackaroo deploy development
97+
98+
# Deploy a specific stack (with its dependencies)
99+
stackaroo deploy development vpc
100+
101+
# Preview changes before deployment
102+
stackaroo diff development app
103+
```
104+
105+
### Key Commands
106+
107+
- `deploy <context> [stack]` - Deploy all stacks or a specific stack
108+
- `diff <context> <stack>` - Preview changes before deployment

cmd/deploy.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var (
2222

2323
// deployCmd represents the deploy command
2424
var deployCmd = &cobra.Command{
25-
Use: "deploy <context> <stack-name>",
25+
Use: "deploy <context> [stack-name]",
2626
Short: "Deploy CloudFormation stacks",
2727
Long: `Deploy CloudFormation stacks with integrated change preview.
2828
@@ -37,16 +37,23 @@ ChangeSets to provide accurate previews including:
3737
3838
For new stacks, the command proceeds directly with stack creation.
3939
40+
If no stack name is provided, all stacks in the context will be deployed in
41+
dependency order.
42+
4043
Examples:
41-
stackaroo deploy dev vpc
42-
stackaroo deploy prod app
44+
stackaroo deploy dev # Deploy all stacks in dev context
45+
stackaroo deploy dev vpc # Deploy only the vpc stack in dev context
46+
stackaroo deploy prod app # Deploy only the app stack in prod context
4347
4448
The preview shows the same detailed diff information as 'stackaroo diff' but
4549
automatically proceeds with deployment after displaying the changes.`,
46-
Args: cobra.ExactArgs(2),
50+
Args: cobra.RangeArgs(1, 2),
4751
RunE: func(cmd *cobra.Command, args []string) error {
4852
contextName := args[0]
49-
stackName := args[1]
53+
var stackName string
54+
if len(args) > 1 {
55+
stackName = args[1]
56+
}
5057
ctx := context.Background()
5158

5259
return deployWithConfig(ctx, stackName, contextName)
@@ -82,8 +89,26 @@ func deployWithConfig(ctx context.Context, stackName, contextName string) error
8289
provider := file.NewDefaultProvider()
8390
resolver := resolve.NewStackResolver(provider)
8491

85-
// Resolve stack and all its dependencies
86-
resolved, err := resolver.Resolve(ctx, contextName, []string{stackName})
92+
// Determine which stacks to deploy
93+
var stackNames []string
94+
if stackName != "" {
95+
// Deploy single stack
96+
stackNames = []string{stackName}
97+
} else {
98+
// Deploy all stacks in context
99+
var err error
100+
stackNames, err = provider.ListStacks(contextName)
101+
if err != nil {
102+
return fmt.Errorf("failed to get stacks for context %s: %w", contextName, err)
103+
}
104+
if len(stackNames) == 0 {
105+
fmt.Printf("No stacks found in context %s\n", contextName)
106+
return nil
107+
}
108+
}
109+
110+
// Resolve stack(s) and all their dependencies
111+
resolved, err := resolver.Resolve(ctx, contextName, stackNames)
87112
if err != nil {
88113
return fmt.Errorf("failed to resolve stack dependencies: %w", err)
89114
}

cmd/deploy_test.go

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestDeployCommand_Exists(t *testing.T) {
4141
deployCmd := findCommand(rootCmd, "deploy")
4242

4343
assert.NotNil(t, deployCmd, "deploy command should be registered")
44-
assert.Equal(t, "deploy <context> <stack-name>", deployCmd.Use)
44+
assert.Equal(t, "deploy <context> [stack-name]", deployCmd.Use)
4545
}
4646

4747
func TestDeployCommand_AcceptsStackName(t *testing.T) {
@@ -54,23 +54,23 @@ func TestDeployCommand_AcceptsStackName(t *testing.T) {
5454
}
5555

5656
func TestDeployCommand_AcceptsTwoArgs(t *testing.T) {
57-
// Test that deploy command accepts exactly two arguments (context and stack name)
57+
// Test that deploy command accepts one or two arguments (context and optional stack name)
5858
deployCmd := findCommand(rootCmd, "deploy")
5959
assert.NotNil(t, deployCmd)
6060

61-
// Test that Args validation requires exactly 2 arguments
61+
// Test that Args validation accepts 1-2 arguments
6262
err := deployCmd.Args(deployCmd, []string{"dev", "vpc"})
6363
assert.NoError(t, err, "Two arguments should be valid")
6464

6565
err = deployCmd.Args(deployCmd, []string{"dev"})
66-
assert.Error(t, err, "One argument should be invalid")
66+
assert.NoError(t, err, "One argument should be valid")
6767

6868
err = deployCmd.Args(deployCmd, []string{})
6969
assert.Error(t, err, "No arguments should be invalid")
7070
}
7171

72-
func TestDeployCommand_RequiresTwoArgs(t *testing.T) {
73-
// Test that deploy command requires both context and stack name arguments
72+
func TestDeployCommand_RequiresAtLeastOneArg(t *testing.T) {
73+
// Test that deploy command requires at least a context argument
7474

7575
// Mock deployer that shouldn't be called
7676
mockDeployer := &MockDeployer{}
@@ -79,12 +79,133 @@ func TestDeployCommand_RequiresTwoArgs(t *testing.T) {
7979
SetDeployer(mockDeployer)
8080
defer SetDeployer(oldDeployer)
8181

82-
// Execute with only one argument - should fail
83-
rootCmd.SetArgs([]string{"deploy", "test-stack"})
82+
// Execute with no arguments - should fail
83+
rootCmd.SetArgs([]string{"deploy"})
8484

8585
err := rootCmd.Execute()
86-
assert.Error(t, err, "deploy command should require both context and stack name arguments")
87-
assert.Contains(t, err.Error(), "accepts 2 arg(s), received 1")
86+
assert.Error(t, err, "deploy command should require at least a context argument")
87+
assert.Contains(t, err.Error(), "accepts between 1 and 2 arg(s), received 0")
88+
89+
// Verify no deployer calls were made
90+
mockDeployer.AssertExpectations(t)
91+
}
92+
93+
func TestDeployCommand_DeployAllStacksInContext(t *testing.T) {
94+
// Test that deploy command with single argument deploys all stacks in context
95+
96+
// Create a temporary config file with multiple stacks
97+
configContent := `
98+
project: test-project
99+
100+
contexts:
101+
test-context:
102+
region: us-east-1
103+
104+
stacks:
105+
- name: vpc
106+
template: templates/vpc.yaml
107+
- name: app
108+
template: templates/app.yaml
109+
`
110+
tmpDir := t.TempDir()
111+
configFile := tmpDir + "/stackaroo.yaml"
112+
err := os.WriteFile(configFile, []byte(configContent), 0644)
113+
require.NoError(t, err)
114+
115+
// Create template files
116+
err = os.MkdirAll(tmpDir+"/templates", 0755)
117+
require.NoError(t, err)
118+
119+
vpcTemplate := `AWSTemplateFormatVersion: '2010-09-09'
120+
Resources:
121+
TestVPC:
122+
Type: AWS::EC2::VPC
123+
Properties:
124+
CidrBlock: 10.0.0.0/16`
125+
126+
appTemplate := `AWSTemplateFormatVersion: '2010-09-09'
127+
Resources:
128+
TestApp:
129+
Type: AWS::EC2::Instance
130+
Properties:
131+
ImageId: ami-12345678
132+
InstanceType: t2.micro`
133+
134+
err = os.WriteFile(tmpDir+"/templates/vpc.yaml", []byte(vpcTemplate), 0644)
135+
require.NoError(t, err)
136+
err = os.WriteFile(tmpDir+"/templates/app.yaml", []byte(appTemplate), 0644)
137+
require.NoError(t, err)
138+
139+
// Mock deployer that expects two deployments
140+
mockDeployer := &MockDeployer{}
141+
mockDeployer.On("DeployStack", mock.Anything, mock.MatchedBy(func(stack *model.Stack) bool {
142+
return stack.Name == "vpc"
143+
})).Return(nil).Once()
144+
145+
mockDeployer.On("DeployStack", mock.Anything, mock.MatchedBy(func(stack *model.Stack) bool {
146+
return stack.Name == "app"
147+
})).Return(nil).Once()
148+
149+
oldDeployer := deployer
150+
SetDeployer(mockDeployer)
151+
defer SetDeployer(oldDeployer)
152+
153+
// Change to temp directory and execute
154+
oldDir, _ := os.Getwd()
155+
defer func() {
156+
err := os.Chdir(oldDir)
157+
require.NoError(t, err)
158+
}()
159+
err = os.Chdir(tmpDir)
160+
require.NoError(t, err)
161+
162+
rootCmd.SetArgs([]string{"deploy", "test-context"})
163+
164+
err = rootCmd.Execute()
165+
assert.NoError(t, err, "deploy command should successfully deploy all stacks")
166+
167+
// Verify both stacks were deployed
168+
mockDeployer.AssertExpectations(t)
169+
}
170+
171+
func TestDeployCommand_NoStacksInContext(t *testing.T) {
172+
// Test that deploy command handles context with no stacks gracefully
173+
174+
// Create a temporary config file with no stacks
175+
configContent := `
176+
project: test-project
177+
178+
contexts:
179+
empty-context:
180+
region: us-east-1
181+
182+
stacks: []
183+
`
184+
tmpDir := t.TempDir()
185+
configFile := tmpDir + "/stackaroo.yaml"
186+
err := os.WriteFile(configFile, []byte(configContent), 0644)
187+
require.NoError(t, err)
188+
189+
// Mock deployer that shouldn't be called
190+
mockDeployer := &MockDeployer{}
191+
192+
oldDeployer := deployer
193+
SetDeployer(mockDeployer)
194+
defer SetDeployer(oldDeployer)
195+
196+
// Change to temp directory and execute
197+
oldDir, _ := os.Getwd()
198+
defer func() {
199+
err := os.Chdir(oldDir)
200+
require.NoError(t, err)
201+
}()
202+
err = os.Chdir(tmpDir)
203+
require.NoError(t, err)
204+
205+
rootCmd.SetArgs([]string{"deploy", "empty-context"})
206+
207+
err = rootCmd.Execute()
208+
assert.NoError(t, err, "deploy command should handle empty context without error")
88209

89210
// Verify no deployer calls were made
90211
mockDeployer.AssertExpectations(t)

examples/simple-vpc/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,28 @@ The `stackaroo.yaml` file defines:
5353

5454
3. **Deploy to development** (shows preview before applying changes):
5555
```bash
56+
# Deploy all stacks in the dev context
57+
../../stackaroo deploy dev
58+
59+
# Or deploy a specific stack
5660
../../stackaroo deploy dev vpc
5761
```
5862

5963
4. **Deploy to staging** (shows preview before applying changes):
6064
```bash
65+
# Deploy all stacks in the staging context
66+
../../stackaroo deploy staging
67+
68+
# Or deploy a specific stack
6169
../../stackaroo deploy staging vpc
6270
```
6371

6472
5. **Deploy to production** (requires production account access):
6573
```bash
74+
# Deploy all stacks in the prod context
75+
../../stackaroo deploy prod
76+
77+
# Or deploy a specific stack
6678
../../stackaroo deploy prod vpc
6779
```
6880

@@ -118,13 +130,23 @@ Each deployment creates:
118130

119131
Check the status of your deployments:
120132
```bash
133+
# Check status of all stacks in context
134+
../../stackaroo status dev
135+
136+
# Or check status of specific stack
121137
../../stackaroo status dev vpc
122138
```
123139

124140
## Cleanup
125141

126142
To remove the infrastructure:
127143
```bash
144+
# Delete all stacks in each context
145+
../../stackaroo delete dev
146+
../../stackaroo delete staging
147+
../../stackaroo delete prod
148+
149+
# Or delete specific stacks
128150
../../stackaroo delete dev vpc
129151
../../stackaroo delete staging vpc
130152
../../stackaroo delete prod vpc

internal/config/file/provider.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ func (fp *Provider) GetStack(stackName, context string) (*config.StackConfig, er
109109
return stack, nil
110110
}
111111

112+
// ListStacks returns all available stack names for a specific context
113+
func (fp *Provider) ListStacks(context string) ([]string, error) {
114+
if err := fp.ensureLoaded(); err != nil {
115+
return nil, err
116+
}
117+
118+
// Check if the context exists
119+
if _, exists := fp.rawConfig.Contexts[context]; !exists {
120+
return nil, fmt.Errorf("context '%s' not found in configuration", context)
121+
}
122+
123+
// Extract stack names
124+
stackNames := make([]string, 0, len(fp.rawConfig.Stacks))
125+
for _, stack := range fp.rawConfig.Stacks {
126+
stackNames = append(stackNames, stack.Name)
127+
}
128+
129+
return stackNames, nil
130+
}
131+
112132
// Validate checks the configuration for consistency and errors
113133
func (fp *Provider) Validate() error {
114134
if err := fp.ensureLoaded(); err != nil {

0 commit comments

Comments
 (0)