@@ -5,11 +5,15 @@ package ssm_document
55
66import (
77 "encoding/json"
8+ "errors"
89 "fmt"
910 "log"
11+ "os"
12+ "strings"
1013
1114 "github.com/aws/aws-sdk-go-v2/service/ssm"
1215 "github.com/aws/aws-sdk-go-v2/service/ssm/types"
16+ "github.com/aws/smithy-go"
1317
1418 "github.com/aws/amazon-cloudwatch-agent-test/util/awsservice"
1519)
@@ -24,23 +28,23 @@ func cleanupSSMParameter(name string) {
2428 }
2529}
2630
27- func RunAndVerifySSMAction (documentName string , instanceIds []string , tc testCase ) error {
31+ func runAndVerifySSMAction (documentName string , instanceIds []string , tc testCase ) error {
2832 log .Printf ("Testing %s action" , tc .actionName )
2933
3034 out , err := awsservice .RunSSMDocument (documentName , instanceIds , tc .parameters )
3135 if err != nil {
3236 return fmt .Errorf ("%s action failed: %v" , tc .actionName , err )
3337 }
3438
35- if err := VerifyAgentAction (out , instanceIds [0 ], documentName , tc ); err != nil {
39+ if err := verifyAgentAction (out , instanceIds [0 ], documentName , tc ); err != nil {
3640 return fmt .Errorf ("%s verification failed: %v" , tc .actionName , err )
3741 }
3842
3943 log .Printf ("%s action completed successfully" , tc .actionName )
4044 return nil
4145}
4246
43- func VerifyAgentAction (out * ssm.SendCommandOutput , instanceId , documentName string , tc testCase ) error {
47+ func verifyAgentAction (out * ssm.SendCommandOutput , instanceId , documentName string , tc testCase ) error {
4448 var status agentStatus
4549
4650 //Wait for command completion
@@ -63,6 +67,10 @@ func VerifyAgentAction(out *ssm.SendCommandOutput, instanceId, documentName stri
6367 return fmt .Errorf ("failed to get status result: %v" , err )
6468 }
6569
70+ if len (statusResult .CommandInvocations ) == 0 {
71+ return fmt .Errorf ("no command invocations returned for status check" )
72+ }
73+
6674 for _ , plugin := range statusResult .CommandInvocations [0 ].CommandPlugins {
6775 if plugin .Status == types .CommandPluginStatusFailed {
6876 return fmt .Errorf ("command plugin failed: %s" , * plugin .Name )
@@ -88,3 +96,127 @@ func VerifyAgentAction(out *ssm.SendCommandOutput, instanceId, documentName stri
8896
8997 return nil
9098}
99+
100+ // runAndVerifySSMActionWithOutput behaves like runAndVerifySSMAction and additionally
101+ // asserts that expectedOutput appears in the command's output.
102+ func runAndVerifySSMActionWithOutput (documentName string , instanceIds []string , tc testCase , expectedOutput string ) error {
103+ log .Printf ("Testing %s action" , tc .actionName )
104+
105+ out , err := awsservice .RunSSMDocument (documentName , instanceIds , tc .parameters )
106+ if err != nil {
107+ return fmt .Errorf ("%s action failed: %v" , tc .actionName , err )
108+ }
109+
110+ result , err := awsservice .WaitForCommandCompletion (* out .Command .CommandId , instanceIds [0 ])
111+ if err != nil {
112+ commandOutput := awsservice .GetCommandInvocationDetails (* out .Command .CommandId , instanceIds [0 ])
113+ return fmt .Errorf ("%s action failed to complete: %v\n Command output:\n %s" , tc .actionName , err , commandOutput )
114+ }
115+
116+ if ! commandOutputContains (result , expectedOutput ) {
117+ commandOutput := awsservice .GetCommandInvocationDetails (* out .Command .CommandId , instanceIds [0 ])
118+ return fmt .Errorf ("%s output verification failed: expected output %q not found\n Command output:\n %s" , tc .actionName , expectedOutput , commandOutput )
119+ }
120+
121+ if err := verifyAgentAction (out , instanceIds [0 ], documentName , tc ); err != nil {
122+ return fmt .Errorf ("%s verification failed: %v" , tc .actionName , err )
123+ }
124+
125+ log .Printf ("%s action completed successfully" , tc .actionName )
126+ return nil
127+ }
128+
129+ // runAndVerifySSMActionFailure runs the document action and expects the command invocation
130+ // to reach the terminal Failed state (e.g. document-level parameter validation errors).
131+ // If expectedOutput is non-empty, it must appear in the failed command's output.
132+ func runAndVerifySSMActionFailure (documentName string , instanceIds []string , tc testCase , expectedOutput string ) error {
133+ log .Printf ("Testing %s action (expecting failure)" , tc .actionName )
134+
135+ out , err := awsservice .RunSSMDocument (documentName , instanceIds , tc .parameters )
136+ if err != nil {
137+ return fmt .Errorf ("%s action failed to send: %v" , tc .actionName , err )
138+ }
139+
140+ commandId := * out .Command .CommandId
141+ _ , err = awsservice .WaitForCommandCompletion (commandId , instanceIds [0 ])
142+ commandOutput := awsservice .GetCommandInvocationDetails (commandId , instanceIds [0 ])
143+ if err == nil {
144+ return fmt .Errorf ("%s action was expected to fail but succeeded\n Command output:\n %s" , tc .actionName , commandOutput )
145+ }
146+ // WaitForCommandCompletion returns a *CommandTerminalError for Failed/Cancelled/TimedOut;
147+ // require specifically the Failed status.
148+ var termErr * awsservice.CommandTerminalError
149+ if ! errors .As (err , & termErr ) || termErr .Status != types .CommandInvocationStatusFailed {
150+ return fmt .Errorf ("%s action reached an unexpected terminal state: %v\n Command output:\n %s" , tc .actionName , err , commandOutput )
151+ }
152+ if expectedOutput != "" && ! strings .Contains (commandOutput , expectedOutput ) {
153+ return fmt .Errorf ("%s failure output verification failed: expected output %q not found\n Command output:\n %s" , tc .actionName , expectedOutput , commandOutput )
154+ }
155+
156+ log .Printf ("%s action failed as expected" , tc .actionName )
157+ return nil
158+ }
159+
160+ // verifyEnvConfigContent reads the agent's env-config.json directly from the local
161+ // filesystem (the test runs on the instance) and asserts that the file contains every
162+ // expected key/value pair. Additional keys in the file are ignored.
163+ func verifyEnvConfigContent (expected map [string ]string ) error {
164+ log .Printf ("Verifying env-config.json content via direct file read: %s" , envConfigPath )
165+
166+ data , err := os .ReadFile (envConfigPath )
167+ if err != nil {
168+ return fmt .Errorf ("failed to read env-config.json at %s: %v" , envConfigPath , err )
169+ }
170+
171+ var envConfig map [string ]string
172+ if err := json .Unmarshal (data , & envConfig ); err != nil {
173+ return fmt .Errorf ("failed to unmarshal env-config.json: %v\n Content:\n %s" , err , string (data ))
174+ }
175+
176+ for key , want := range expected {
177+ got , ok := envConfig [key ]
178+ if ! ok {
179+ return fmt .Errorf ("env-config.json is missing expected key %q. Content: %v" , key , envConfig )
180+ }
181+ if got != want {
182+ return fmt .Errorf ("env-config.json key %q verification failed. Expected: %q, Got: %q" , key , want , got )
183+ }
184+ }
185+
186+ log .Println ("env-config.json content verified successfully" )
187+ return nil
188+ }
189+
190+ // commandOutputContains reports whether any command plugin's output contains expected.
191+ func commandOutputContains (result * ssm.ListCommandInvocationsOutput , expected string ) bool {
192+ if len (result .CommandInvocations ) == 0 {
193+ return false
194+ }
195+ for _ , plugin := range result .CommandInvocations [0 ].CommandPlugins {
196+ if plugin .Output != nil && strings .Contains (* plugin .Output , expected ) {
197+ return true
198+ }
199+ }
200+ return false
201+ }
202+
203+ // verifySSMSendCommandRejection runs the document action and expects SendCommand itself
204+ // to reject the request (e.g. because a parameter value violates an allowedPattern).
205+ // The AWS SDK returns an InvalidParameters error from SendCommand in this case.
206+ func verifySSMSendCommandRejection (documentName string , instanceIds []string , tc testCase ) error {
207+ log .Printf ("Testing %s action (expecting SendCommand rejection)" , tc .actionName )
208+
209+ _ , err := awsservice .RunSSMDocument (documentName , instanceIds , tc .parameters )
210+ if err == nil {
211+ return fmt .Errorf ("%s action was expected to be rejected at SendCommand but succeeded" , tc .actionName )
212+ }
213+
214+ // SSM returns an InvalidParameters API error when allowedPattern validation fails.
215+ var apiErr smithy.APIError
216+ if ! errors .As (err , & apiErr ) || apiErr .ErrorCode () != "InvalidParameters" {
217+ return fmt .Errorf ("%s action failed with unexpected error (expected InvalidParameters API error): %v" , tc .actionName , err )
218+ }
219+
220+ log .Printf ("%s action rejected at SendCommand as expected: %v" , tc .actionName , err )
221+ return nil
222+ }
0 commit comments