@@ -77,6 +77,7 @@ The operations implement the `CloudFormationOperations` interface:
7777
7878type CloudFormationOperations interface {
7979 DeployStack (ctx context.Context , input DeployStackInput ) error
80+ DeployStackWithCallback (ctx context.Context , input DeployStackInput , eventCallback func (StackEvent)) error
8081 UpdateStack (ctx context.Context , input UpdateStackInput ) error
8182 DeleteStack (ctx context.Context , input DeleteStackInput ) error
8283 GetStack (ctx context.Context , stackName string ) (*Stack, error )
@@ -85,14 +86,17 @@ type CloudFormationOperations interface {
8586 StackExists (ctx context.Context , stackName string ) (bool , error )
8687 GetTemplate (ctx context.Context , stackName string ) (string , error )
8788 DescribeStack (ctx context.Context , stackName string ) (*StackInfo, error )
89+ DescribeStackEvents (ctx context.Context , stackName string ) ([]StackEvent, error )
90+ WaitForStackOperation (ctx context.Context , stackName string , eventCallback func (StackEvent)) error
8891 CreateChangeSet (ctx context.Context , params *cloudformation.CreateChangeSetInput , optFns ...func (*cloudformation.Options )) (*cloudformation.CreateChangeSetOutput , error )
8992 DeleteChangeSet (ctx context.Context , params *cloudformation.DeleteChangeSetInput , optFns ...func (*cloudformation.Options )) (*cloudformation.DeleteChangeSetOutput , error )
9093 DescribeChangeSet (ctx context.Context , params *cloudformation.DescribeChangeSetInput , optFns ...func (*cloudformation.Options )) (*cloudformation.DescribeChangeSetOutput , error )
9194}
9295```
9396
9497** Core Operations:**
95- - ` DeployStack() ` - Create new CloudFormation stacks
98+ - ` DeployStack() ` - Create or update CloudFormation stacks (simple version)
99+ - ` DeployStackWithCallback() ` - Create or update stacks with real-time event streaming
96100- ` UpdateStack() ` - Update existing stacks
97101- ` DeleteStack() ` - Delete stacks
98102- ` GetStack() ` - Retrieve stack information
@@ -101,15 +105,19 @@ type CloudFormationOperations interface {
101105- ` StackExists() ` - Check stack existence
102106- ` GetTemplate() ` - Retrieve template content for existing stacks
103107- ` DescribeStack() ` - Get detailed stack information including template
108+ - ` DescribeStackEvents() ` - Retrieve CloudFormation events for a stack
109+ - ` WaitForStackOperation() ` - Wait for stack operation completion with event streaming
104110- ` CreateChangeSet() ` - Create CloudFormation changesets for diff operations
105111- ` DeleteChangeSet() ` - Remove CloudFormation changesets
106112- ` DescribeChangeSet() ` - Get changeset details and proposed changes
107113
108114** Data Types:**
109115- ` Stack ` - Represents CloudFormation stack with cleaned-up fields
110116- ` StackInfo ` - Detailed stack information including template content
117+ - ` StackEvent ` - Represents CloudFormation stack events with timestamp and resource information
111118- ` Parameter ` - Key-value pairs for stack parameters
112119- ` StackStatus ` - Enumerated stack status values
120+ - ` NoChangesError ` - Special error type indicating no changes need to be deployed
113121- Input structs for each operation with required and optional fields
114122
115123** Interface Design:**
@@ -139,8 +147,14 @@ client, err := aws.NewDefaultClient(ctx, aws.Config{
139147// Get CloudFormation operations (returns interface)
140148cfnOps := client.NewCloudFormationOperations ()
141149
142- // Deploy a stack
143- err := cfnOps.DeployStack (ctx, aws.DeployStackInput {
150+ // Deploy a stack with event streaming
151+ eventCallback := func (event aws.StackEvent ) {
152+ timestamp := event.Timestamp .Format (" 2006-01-02 15:04:05" )
153+ fmt.Printf (" [%s ] %-20s %-40s %s \n " ,
154+ timestamp, event.ResourceStatus , event.ResourceType , event.LogicalResourceId )
155+ }
156+
157+ err := cfnOps.DeployStackWithCallback (ctx, aws.DeployStackInput {
144158 StackName : " my-stack" ,
145159 TemplateBody : templateContent,
146160 Parameters : []aws.Parameter {
@@ -149,6 +163,19 @@ err := cfnOps.DeployStack(ctx, aws.DeployStackInput{
149163 Tags : map [string ]string {
150164 " Project" : " stackaroo" ,
151165 },
166+ }, eventCallback)
167+
168+ // Handle no changes scenario
169+ if errors.As (err, &aws.NoChangesError {}) {
170+ fmt.Println (" Stack is already up to date - no changes to deploy" )
171+ } else if err != nil {
172+ return fmt.Errorf (" deployment failed: % w" , err)
173+ }
174+
175+ // Simple deployment without events
176+ err := cfnOps.DeployStack (ctx, aws.DeployStackInput {
177+ StackName : " my-stack" ,
178+ TemplateBody : templateContent,
152179})
153180
154181// Check stack status
@@ -170,6 +197,86 @@ result, err := cfnClient.DescribeStackEvents(ctx, &cloudformation.DescribeStackE
170197})
171198```
172199
200+ ## Advanced Deployment Features
201+
202+ ### Smart Create vs Update Detection
203+
204+ The ` DeployStack() ` and ` DeployStackWithCallback() ` methods automatically detect whether a stack exists and perform the appropriate operation:
205+
206+ - ** New stacks** : Calls CloudFormation ` CreateStack ` operation
207+ - ** Existing stacks** : Calls CloudFormation ` UpdateStack ` operation
208+ - ** No changes needed** : Returns ` NoChangesError ` for graceful handling
209+
210+ ``` go
211+ // This automatically determines create vs update
212+ err := cfnOps.DeployStack (ctx, aws.DeployStackInput {
213+ StackName : " my-stack" ,
214+ TemplateBody : templateContent,
215+ })
216+
217+ // Handle the no changes scenario
218+ var noChangesErr aws.NoChangesError
219+ if errors.As (err, &noChangesErr) {
220+ fmt.Printf (" Stack %s is already up to date\n " , noChangesErr.StackName )
221+ return nil
222+ }
223+ ```
224+
225+ ### Real-time Event Streaming
226+
227+ The ` DeployStackWithCallback() ` method provides real-time CloudFormation event streaming during deployment operations:
228+
229+ ``` go
230+ // Define event callback for real-time feedback
231+ eventCallback := func (event aws.StackEvent ) {
232+ timestamp := event.Timestamp .Format (" 2006-01-02 15:04:05" )
233+ fmt.Printf (" [%s ] %-20s %-40s %s %s \n " ,
234+ timestamp,
235+ event.ResourceStatus ,
236+ event.ResourceType ,
237+ event.LogicalResourceId ,
238+ event.ResourceStatusReason ,
239+ )
240+ }
241+
242+ // Deploy with event streaming
243+ err := cfnOps.DeployStackWithCallback (ctx, deployInput, eventCallback)
244+ ```
245+
246+ ** Event Output Example:**
247+ ```
248+ Starting create operation for stack my-app...
249+ [2025-01-09 15:30:45] CREATE_IN_PROGRESS AWS::CloudFormation::Stack my-app User Initiated
250+ [2025-01-09 15:30:46] CREATE_IN_PROGRESS AWS::S3::Bucket AppBucket
251+ [2025-01-09 15:30:48] CREATE_COMPLETE AWS::S3::Bucket AppBucket
252+ [2025-01-09 15:30:50] CREATE_IN_PROGRESS AWS::Lambda::Function AppFunction
253+ [2025-01-09 15:30:55] CREATE_COMPLETE AWS::Lambda::Function AppFunction
254+ [2025-01-09 15:30:56] CREATE_COMPLETE AWS::CloudFormation::Stack my-app
255+ Stack my-app create completed successfully
256+ ```
257+
258+ ### Operation Waiting and Polling
259+
260+ The deployment operations automatically wait for completion:
261+
262+ - ** Polling interval** : 5 seconds between status checks
263+ - ** Event deduplication** : Only new events are reported via callback
264+ - ** Completion detection** : Monitors stack status for terminal states
265+ - ** Error handling** : Distinguishes between successful and failed operations
266+
267+ ``` go
268+ // This will wait until the operation completes or fails
269+ err := cfnOps.WaitForStackOperation (ctx, " my-stack" , eventCallback)
270+ if err != nil {
271+ // Handle deployment failure
272+ return fmt.Errorf (" stack operation failed: % w" , err)
273+ }
274+ ```
275+
276+ ** Terminal Stack States:**
277+ - ** Success** : ` CREATE_COMPLETE ` , ` UPDATE_COMPLETE ` , ` DELETE_COMPLETE `
278+ - ** Failure** : ` CREATE_FAILED ` , ` UPDATE_FAILED ` , ` ROLLBACK_COMPLETE ` , etc.
279+
173280## Configuration Hierarchy
174281
175282The client respects the standard AWS configuration hierarchy:
@@ -196,6 +303,24 @@ Common error scenarios are identified and handled appropriately:
196303- ** Permission denied** : Clear indication of IAM policy issues
197304- ** Template validation** : Detailed error messages for template problems
198305- ** Rate limiting** : Retryable errors with appropriate backoff
306+ - ** No changes needed** : Special ` NoChangesError ` type for update operations with no changes
307+
308+ ### Special Error Types
309+
310+ #### NoChangesError
311+ When updating a stack that requires no changes, a special ` NoChangesError ` is returned:
312+
313+ ``` go
314+ type NoChangesError struct {
315+ StackName string
316+ }
317+
318+ func (e NoChangesError ) Error () string {
319+ return fmt.Sprintf (" stack %s is already up to date - no changes to deploy" , e.StackName )
320+ }
321+ ```
322+
323+ This allows applications to distinguish between actual deployment failures and successful "no changes" scenarios.
199324
200325### Error Examples
201326
@@ -209,6 +334,17 @@ if err != nil {
209334 // Handle other errors
210335 }
211336}
337+
338+ // Handle deployment with no changes
339+ err := cfnOps.DeployStack (ctx, deployInput)
340+ if err != nil {
341+ var noChangesErr aws.NoChangesError
342+ if errors.As (err, &noChangesErr) {
343+ fmt.Printf (" Stack %s is already up to date\n " , noChangesErr.StackName )
344+ return nil // This is success, not an error
345+ }
346+ return fmt.Errorf (" deployment failed: % w" , err)
347+ }
212348```
213349
214350## Extension Points
@@ -279,6 +415,21 @@ func (m *MockCloudFormationOperations) DeployStack(ctx context.Context, input aw
279415 args := m.Called (ctx, input)
280416 return args.Error (0 )
281417}
418+
419+ func (m *MockCloudFormationOperations ) DeployStackWithCallback (ctx context.Context, input aws.DeployStackInput, eventCallback func(aws.StackEvent)) error {
420+ args := m.Called (ctx, input, eventCallback)
421+ return args.Error (0 )
422+ }
423+
424+ func (m *MockCloudFormationOperations ) DescribeStackEvents (ctx context .Context , stackName string ) ([]aws .StackEvent , error ) {
425+ args := m.Called (ctx, stackName)
426+ return args.Get (0 ).([]aws.StackEvent ), args.Error (1 )
427+ }
428+
429+ func (m *MockCloudFormationOperations ) WaitForStackOperation (ctx context.Context, stackName string, eventCallback func(aws.StackEvent)) error {
430+ args := m.Called (ctx, stackName, eventCallback)
431+ return args.Error (0 )
432+ }
282433```
283434
284435#### Integration with Business Logic
@@ -299,6 +450,40 @@ deployer := deploy.NewAWSDeployer(mockClient)
299450- Test business logic in isolation from AWS SDK
300451- Fast, deterministic tests with no external dependencies
301452
453+ #### Event Callback Testing
454+ When testing operations with event callbacks, use function type matchers:
455+
456+ ``` go
457+ // Test deployment with event streaming
458+ mockCfnOps.On (" DeployStackWithCallback" ,
459+ ctx,
460+ mock.MatchedBy (func (input aws.DeployStackInput ) bool {
461+ return input.StackName == " test-stack"
462+ }),
463+ mock.AnythingOfType (" func(aws.StackEvent)" ), // Event callback matcher
464+ ).Return (nil )
465+
466+ // Test event callback invocation
467+ var capturedEvents []aws.StackEvent
468+ eventCallback := func (event aws.StackEvent ) {
469+ capturedEvents = append (capturedEvents, event)
470+ }
471+
472+ err := cfnOps.DeployStackWithCallback (ctx, input, eventCallback)
473+ assert.NoError (t, err)
474+ assert.Len (t, capturedEvents, expectedEventCount)
475+ ```
476+
477+ #### Testing NoChangesError
478+ ``` go
479+ // Test no changes scenario
480+ mockCfnOps.On (" DeployStackWithCallback" , ctx, input, mock.AnythingOfType (" func(aws.StackEvent)" )).
481+ Return (aws.NoChangesError {StackName: " test-stack" })
482+
483+ err := deployer.DeployStack (ctx, stack)
484+ assert.NoError (t, err) // Should handle NoChangesError gracefully
485+ ```
486+
302487### Integration Testing
303488- Use AWS localstack or moto for local AWS service simulation
304489- Provide test configuration for different AWS environments
0 commit comments