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  }