github.com/mponton/terratest@v0.44.0/modules/aws/ssm.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "time" 6 7 "github.com/aws/aws-sdk-go/aws" 8 "github.com/aws/aws-sdk-go/service/ssm" 9 "github.com/mponton/terratest/modules/logger" 10 "github.com/mponton/terratest/modules/retry" 11 "github.com/mponton/terratest/modules/testing" 12 "github.com/stretchr/testify/require" 13 ) 14 15 // GetParameter retrieves the latest version of SSM Parameter at keyName with decryption. 16 func GetParameter(t testing.TestingT, awsRegion string, keyName string) string { 17 keyValue, err := GetParameterE(t, awsRegion, keyName) 18 require.NoError(t, err) 19 return keyValue 20 } 21 22 // GetParameterE retrieves the latest version of SSM Parameter at keyName with decryption. 23 func GetParameterE(t testing.TestingT, awsRegion string, keyName string) (string, error) { 24 ssmClient, err := NewSsmClientE(t, awsRegion) 25 if err != nil { 26 return "", err 27 } 28 29 return GetParameterWithClientE(t, ssmClient, keyName) 30 } 31 32 // GetParameterE retrieves the latest version of SSM Parameter at keyName with decryption with the ability to provide the SSM client. 33 func GetParameterWithClientE(t testing.TestingT, client *ssm.SSM, keyName string) (string, error) { 34 resp, err := client.GetParameter(&ssm.GetParameterInput{Name: aws.String(keyName), WithDecryption: aws.Bool(true)}) 35 if err != nil { 36 return "", err 37 } 38 39 parameter := *resp.Parameter 40 return *parameter.Value, nil 41 } 42 43 // PutParameter creates new version of SSM Parameter at keyName with keyValue as SecureString. 44 func PutParameter(t testing.TestingT, awsRegion string, keyName string, keyDescription string, keyValue string) int64 { 45 version, err := PutParameterE(t, awsRegion, keyName, keyDescription, keyValue) 46 require.NoError(t, err) 47 return version 48 } 49 50 // PutParameterE creates new version of SSM Parameter at keyName with keyValue as SecureString. 51 func PutParameterE(t testing.TestingT, awsRegion string, keyName string, keyDescription string, keyValue string) (int64, error) { 52 ssmClient, err := NewSsmClientE(t, awsRegion) 53 if err != nil { 54 return 0, err 55 } 56 return PutParameterWithClientE(t, ssmClient, keyName, keyDescription, keyValue) 57 } 58 59 // PutParameterE creates new version of SSM Parameter at keyName with keyValue as SecureString with the ability to provide the SSM client. 60 func PutParameterWithClientE(t testing.TestingT, client *ssm.SSM, keyName string, keyDescription string, keyValue string) (int64, error) { 61 resp, err := client.PutParameter(&ssm.PutParameterInput{Name: aws.String(keyName), Description: aws.String(keyDescription), Value: aws.String(keyValue), Type: aws.String("SecureString")}) 62 if err != nil { 63 return 0, err 64 } 65 66 return *resp.Version, nil 67 } 68 69 // DeleteParameter deletes all versions of SSM Parameter at keyName. 70 func DeleteParameter(t testing.TestingT, awsRegion string, keyName string) { 71 err := DeleteParameterE(t, awsRegion, keyName) 72 require.NoError(t, err) 73 } 74 75 // DeleteParameterE deletes all versions of SSM Parameter at keyName. 76 func DeleteParameterE(t testing.TestingT, awsRegion string, keyName string) error { 77 ssmClient, err := NewSsmClientE(t, awsRegion) 78 if err != nil { 79 return err 80 } 81 return DeleteParameterWithClientE(t, ssmClient, keyName) 82 } 83 84 // DeleteParameterE deletes all versions of SSM Parameter at keyName with the ability to provide the SSM client. 85 func DeleteParameterWithClientE(t testing.TestingT, client *ssm.SSM, keyName string) error { 86 _, err := client.DeleteParameter(&ssm.DeleteParameterInput{Name: aws.String(keyName)}) 87 if err != nil { 88 return err 89 } 90 91 return nil 92 } 93 94 // NewSsmClient creates a SSM client. 95 func NewSsmClient(t testing.TestingT, region string) *ssm.SSM { 96 client, err := NewSsmClientE(t, region) 97 require.NoError(t, err) 98 return client 99 } 100 101 // NewSsmClientE creates an SSM client. 102 func NewSsmClientE(t testing.TestingT, region string) (*ssm.SSM, error) { 103 sess, err := NewAuthenticatedSession(region) 104 if err != nil { 105 return nil, err 106 } 107 108 return ssm.New(sess), nil 109 } 110 111 // WaitForSsmInstanceE waits until the instance get registered to the SSM inventory. 112 func WaitForSsmInstanceE(t testing.TestingT, awsRegion, instanceID string, timeout time.Duration) error { 113 client, err := NewSsmClientE(t, awsRegion) 114 if err != nil { 115 return err 116 } 117 return WaitForSsmInstanceWithClientE(t, client, instanceID, timeout) 118 } 119 120 // WaitForSsmInstanceE waits until the instance get registered to the SSM inventory with the ability to provide the SSM client. 121 func WaitForSsmInstanceWithClientE(t testing.TestingT, client *ssm.SSM, instanceID string, timeout time.Duration) error { 122 timeBetweenRetries := 2 * time.Second 123 maxRetries := int(timeout.Seconds() / timeBetweenRetries.Seconds()) 124 description := fmt.Sprintf("Waiting for %s to appear in the SSM inventory", instanceID) 125 126 input := &ssm.GetInventoryInput{ 127 Filters: []*ssm.InventoryFilter{ 128 { 129 Key: aws.String("AWS:InstanceInformation.InstanceId"), 130 Type: aws.String("Equal"), 131 Values: aws.StringSlice([]string{instanceID}), 132 }, 133 }, 134 } 135 _, err := retry.DoWithRetryE(t, description, maxRetries, timeBetweenRetries, func() (string, error) { 136 resp, err := client.GetInventory(input) 137 138 if err != nil { 139 return "", err 140 } 141 142 if len(resp.Entities) != 1 { 143 return "", fmt.Errorf("%s is not in the SSM inventory", instanceID) 144 } 145 146 return "", nil 147 }) 148 149 return err 150 } 151 152 // WaitForSsmInstance waits until the instance get registered to the SSM inventory. 153 func WaitForSsmInstance(t testing.TestingT, awsRegion, instanceID string, timeout time.Duration) { 154 err := WaitForSsmInstanceE(t, awsRegion, instanceID, timeout) 155 require.NoError(t, err) 156 } 157 158 // CheckSsmCommand checks that you can run the given command on the given instance through AWS SSM. 159 func CheckSsmCommand(t testing.TestingT, awsRegion, instanceID, command string, timeout time.Duration) *CommandOutput { 160 return CheckSsmCommandWithDocument(t, awsRegion, instanceID, command, "AWS-RunShellScript", timeout) 161 } 162 163 // CommandOutput contains the result of the SSM command. 164 type CommandOutput struct { 165 Stdout string 166 Stderr string 167 ExitCode int64 168 } 169 170 // CheckSsmCommandE checks that you can run the given command on the given instance through AWS SSM. Returns the result and an error if one occurs. 171 func CheckSsmCommandE(t testing.TestingT, awsRegion, instanceID, command string, timeout time.Duration) (*CommandOutput, error) { 172 return CheckSsmCommandWithDocumentE(t, awsRegion, instanceID, command, "AWS-RunShellScript", timeout) 173 } 174 175 // CheckSSMCommandWithClientE checks that you can run the given command on the given instance through AWS SSM with the ability to provide the SSM client. Returns the result and an error if one occurs. 176 func CheckSSMCommandWithClientE(t testing.TestingT, client *ssm.SSM, instanceID, command string, timeout time.Duration) (*CommandOutput, error) { 177 return CheckSSMCommandWithClientWithDocumentE(t, client, instanceID, command, "AWS-RunShellScript", timeout) 178 } 179 180 // CheckSsmCommandWithDocument checks that you can run the given command on the given instance through AWS SSM with specified Command Doc type. 181 func CheckSsmCommandWithDocument(t testing.TestingT, awsRegion, instanceID, command string, commandDocName string, timeout time.Duration) *CommandOutput { 182 result, err := CheckSsmCommandWithDocumentE(t, awsRegion, instanceID, command, commandDocName, timeout) 183 require.NoErrorf(t, err, "failed to execute '%s' on %s (%v):]\n stdout: %#v\n stderr: %#v", command, instanceID, err, result.Stdout, result.Stderr) 184 return result 185 } 186 187 // CheckSsmCommandWithDocumentE checks that you can run the given command on the given instance through AWS SSM with specified Command Doc type. Returns the result and an error if one occurs. 188 func CheckSsmCommandWithDocumentE(t testing.TestingT, awsRegion, instanceID, command string, commandDocName string, timeout time.Duration) (*CommandOutput, error) { 189 logger.Logf(t, "Running command '%s' on EC2 instance with ID '%s'", command, instanceID) 190 191 // Now that we know the instance in the SSM inventory, we can send the command 192 client, err := NewSsmClientE(t, awsRegion) 193 if err != nil { 194 return nil, err 195 } 196 return CheckSSMCommandWithClientWithDocumentE(t, client, instanceID, command, commandDocName, timeout) 197 } 198 199 // CheckSSMCommandWithClientWithDocumentE checks that you can run the given command on the given instance through AWS SSM with the ability to provide the SSM client with specified Command Doc type. Returns the result and an error if one occurs. 200 func CheckSSMCommandWithClientWithDocumentE(t testing.TestingT, client *ssm.SSM, instanceID, command string, commandDocName string, timeout time.Duration) (*CommandOutput, error) { 201 202 timeBetweenRetries := 2 * time.Second 203 maxRetries := int(timeout.Seconds() / timeBetweenRetries.Seconds()) 204 205 resp, err := client.SendCommand(&ssm.SendCommandInput{ 206 Comment: aws.String("Terratest SSM"), 207 DocumentName: aws.String(commandDocName), 208 InstanceIds: aws.StringSlice([]string{instanceID}), 209 Parameters: map[string][]*string{ 210 "commands": aws.StringSlice([]string{command}), 211 }, 212 }) 213 if err != nil { 214 return nil, err 215 } 216 217 // Wait for the result 218 description := "Waiting for the result of the command" 219 retryableErrors := map[string]string{ 220 "InvocationDoesNotExist": "InvocationDoesNotExist", 221 "bad status: Pending": "bad status: Pending", 222 "bad status: InProgress": "bad status: InProgress", 223 "bad status: Delayed": "bad status: Delayed", 224 } 225 226 result := &CommandOutput{} 227 _, err = retry.DoWithRetryableErrorsE(t, description, retryableErrors, maxRetries, timeBetweenRetries, func() (string, error) { 228 resp, err := client.GetCommandInvocation(&ssm.GetCommandInvocationInput{ 229 CommandId: resp.Command.CommandId, 230 InstanceId: &instanceID, 231 }) 232 233 if err != nil { 234 return "", err 235 } 236 237 result.Stderr = aws.StringValue(resp.StandardErrorContent) 238 result.Stdout = aws.StringValue(resp.StandardOutputContent) 239 result.ExitCode = aws.Int64Value(resp.ResponseCode) 240 241 status := aws.StringValue(resp.Status) 242 243 if status == ssm.CommandInvocationStatusSuccess { 244 return "", nil 245 } 246 247 if status == ssm.CommandInvocationStatusFailed { 248 return "", fmt.Errorf(aws.StringValue(resp.StatusDetails)) 249 } 250 251 return "", fmt.Errorf("bad status: %s", status) 252 }) 253 254 if err != nil { 255 if actualErr, ok := err.(retry.FatalError); ok { 256 return result, actualErr.Underlying 257 } 258 return result, fmt.Errorf("unexpected error: %v", err) 259 } 260 261 return result, nil 262 }