Port ssm client to v2 sdk (#49733)

This commit is contained in:
Andrew Burke
2024-12-04 18:57:15 +00:00
committed by GitHub
parent db9505d169
commit 3b41106834
5 changed files with 319 additions and 311 deletions
-23
View File
@@ -60,8 +60,6 @@ import (
"github.com/aws/aws-sdk-go/service/s3/s3iface"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/gravitational/trace"
@@ -133,8 +131,6 @@ type AWSClients interface {
GetAWSIAMClient(ctx context.Context, region string, opts ...AWSOptionsFn) (iamiface.IAMAPI, error)
// GetAWSSTSClient returns AWS STS client for the specified region.
GetAWSSTSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (stsiface.STSAPI, error)
// GetAWSSSMClient returns AWS SSM client for the specified region.
GetAWSSSMClient(ctx context.Context, region string, opts ...AWSOptionsFn) (ssmiface.SSMAPI, error)
// GetAWSEKSClient returns AWS EKS client for the specified region.
GetAWSEKSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (eksiface.EKSAPI, error)
// GetAWSKMSClient returns AWS KMS client for the specified region.
@@ -592,15 +588,6 @@ func (c *cloudClients) GetAWSSTSClient(ctx context.Context, region string, opts
return sts.New(session), nil
}
// GetAWSSSMClient returns AWS SSM client for the specified region.
func (c *cloudClients) GetAWSSSMClient(ctx context.Context, region string, opts ...AWSOptionsFn) (ssmiface.SSMAPI, error) {
session, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return ssm.New(session), nil
}
// GetAWSEKSClient returns AWS EKS client for the specified region.
func (c *cloudClients) GetAWSEKSClient(ctx context.Context, region string, opts ...AWSOptionsFn) (eksiface.EKSAPI, error) {
session, err := c.GetAWSSession(ctx, region, opts...)
@@ -1021,7 +1008,6 @@ type TestCloudClients struct {
GCPGKE gcp.GKEClient
GCPProjects gcp.ProjectsClient
GCPInstances gcp.InstancesClient
SSM ssmiface.SSMAPI
InstanceMetadata imds.Client
EKS eksiface.EKSAPI
KMS kmsiface.KMSAPI
@@ -1191,15 +1177,6 @@ func (c *TestCloudClients) GetAWSKMSClient(ctx context.Context, region string, o
return c.KMS, nil
}
// GetAWSSSMClient returns an AWS SSM client
func (c *TestCloudClients) GetAWSSSMClient(ctx context.Context, region string, opts ...AWSOptionsFn) (ssmiface.SSMAPI, error) {
_, err := c.GetAWSSession(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return c.SSM, nil
}
// GetGCPIAMClient returns GCP IAM client.
func (c *TestCloudClients) GetGCPIAMClient(ctx context.Context) (*gcpcredentials.IamCredentialsClient, error) {
return gcpcredentials.NewIamCredentialsClient(ctx,
+46 -31
View File
@@ -30,12 +30,11 @@ import (
"time"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v3"
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go-v2/service/ssm"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
@@ -116,6 +115,8 @@ type Config struct {
CloudClients cloud.Clients
// GetEC2Client gets an AWS EC2 client for the given region.
GetEC2Client server.EC2ClientGetter
// GetSSMClient gets an AWS SSM client for the given region.
GetSSMClient func(ctx context.Context, region string, opts ...config.AWSOptionsFn) (server.SSMClient, error)
// IntegrationOnlyCredentials discards any Matcher that don't have an Integration.
// When true, ambient credentials (used by the Cloud SDKs) are not used.
IntegrationOnlyCredentials bool
@@ -221,32 +222,22 @@ kubernetes matchers are present.`)
}
if c.GetEC2Client == nil {
c.GetEC2Client = func(ctx context.Context, region string, opts ...config.AWSOptionsFn) (ec2.DescribeInstancesAPIClient, error) {
opts = append(opts, config.WithAWSIntegrationCredentialProvider(func(ctx context.Context, region, integrationName string) (awsv2.CredentialsProvider, error) {
integration, err := c.AccessPoint.GetIntegration(ctx, integrationName)
if err != nil {
return nil, trace.Wrap(err)
}
if integration.GetAWSOIDCIntegrationSpec() == nil {
return nil, trace.BadParameter("integration does not have aws oidc spec fields %q", integrationName)
}
token, err := c.AccessPoint.GenerateAWSOIDCToken(ctx, integrationName)
if err != nil {
return nil, trace.Wrap(err)
}
cred, err := awsoidc.NewAWSCredentialsProvider(ctx, &awsoidc.AWSClientRequest{
Token: token,
RoleARN: integration.GetAWSOIDCIntegrationSpec().RoleARN,
Region: region,
})
return cred, trace.Wrap(err)
}))
cfg, err := config.GetAWSConfig(ctx, region, opts...)
cfg, err := c.getAWSConfig(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return ec2.NewFromConfig(cfg), nil
}
}
if c.GetSSMClient == nil {
c.GetSSMClient = func(ctx context.Context, region string, opts ...config.AWSOptionsFn) (server.SSMClient, error) {
cfg, err := c.getAWSConfig(ctx, region, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return ssm.NewFromConfig(cfg), nil
}
}
if c.KubernetesClient == nil && len(c.Matchers.Kubernetes) > 0 {
cfg, err := rest.InClusterConfig()
if err != nil {
@@ -302,6 +293,30 @@ kubernetes matchers are present.`)
return nil
}
func (c *Config) getAWSConfig(ctx context.Context, region string, opts ...config.AWSOptionsFn) (aws.Config, error) {
opts = append(opts, config.WithAWSIntegrationCredentialProvider(func(ctx context.Context, region, integrationName string) (aws.CredentialsProvider, error) {
integration, err := c.AccessPoint.GetIntegration(ctx, integrationName)
if err != nil {
return nil, trace.Wrap(err)
}
if integration.GetAWSOIDCIntegrationSpec() == nil {
return nil, trace.BadParameter("integration does not have aws oidc spec fields %q", integrationName)
}
token, err := c.AccessPoint.GenerateAWSOIDCToken(ctx, integrationName)
if err != nil {
return nil, trace.Wrap(err)
}
cred, err := awsoidc.NewAWSCredentialsProvider(ctx, &awsoidc.AWSClientRequest{
Token: token,
RoleARN: integration.GetAWSOIDCIntegrationSpec().RoleARN,
Region: region,
})
return cred, trace.Wrap(err)
}))
cfg, err := config.GetAWSConfig(ctx, region, opts...)
return cfg, trace.Wrap(err)
}
// Server is a discovery server, used to discover cloud resources for
// inclusion in Teleport
type Server struct {
@@ -863,7 +878,7 @@ func genEC2InstancesLogStr(instances []server.EC2Instance) string {
func genAzureInstancesLogStr(instances []*armcompute.VirtualMachine) string {
return genInstancesLogStr(instances, func(i *armcompute.VirtualMachine) string {
return aws.StringValue(i.Name)
return aws.ToString(i.Name)
})
}
@@ -1019,9 +1034,9 @@ func (s *Server) heartbeatEICEInstance(instances *server.EC2Instances) {
func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) error {
// TODO(gavin): support assume_role_arn for ec2.
ec2Client, err := s.CloudClients.GetAWSSSMClient(s.ctx,
ssmClient, err := s.GetSSMClient(s.ctx,
instances.Region,
cloud.WithCredentialsMaybeIntegration(instances.Integration),
config.WithCredentialsMaybeIntegration(instances.Integration),
)
if err != nil {
return trace.Wrap(err)
@@ -1031,7 +1046,7 @@ func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) err
req := server.SSMRunRequest{
DocumentName: instances.DocumentName,
SSM: ec2Client,
SSM: ssmClient,
Instances: instances.Instances,
Params: instances.Parameters,
Region: instances.Region,
@@ -1070,8 +1085,8 @@ func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) err
}
func (s *Server) logHandleInstancesErr(err error) {
var aErr awserr.Error
if errors.As(err, &aErr) && aErr.Code() == ssm.ErrCodeInvalidInstanceId {
var instanceIDErr *ssmtypes.InvalidInstanceId
if errors.As(err, &instanceIDErr) {
const errorMessage = "SSM SendCommand failed with ErrCodeInvalidInstanceId. " +
"Make sure that the instances have AmazonSSMManagedInstanceCore policy assigned. " +
"Also check that SSM agent is running and registered with the SSM endpoint on that instance and try restarting or reinstalling it in case of issues. " +
@@ -1210,7 +1225,7 @@ outer:
for _, node := range nodes {
var vmID string
if inst.Properties != nil {
vmID = aws.StringValue(inst.Properties.VMID)
vmID = aws.ToString(inst.Properties.VMID)
}
match := types.MatchLabels(node, map[string]string{
types.SubscriptionIDLabel: instances.SubscriptionID,
+40 -49
View File
@@ -39,15 +39,14 @@ import (
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/ssm"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/eks"
"github.com/aws/aws-sdk-go/service/eks/eksiface"
"github.com/aws/aws-sdk-go/service/rds"
"github.com/aws/aws-sdk-go/service/redshift"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
@@ -98,26 +97,19 @@ func TestMain(m *testing.M) {
}
type mockSSMClient struct {
ssmiface.SSMAPI
server.SSMClient
commandOutput *ssm.SendCommandOutput
invokeOutput *ssm.GetCommandInvocationOutput
}
func (sm *mockSSMClient) SendCommandWithContext(_ context.Context, input *ssm.SendCommandInput, _ ...request.Option) (*ssm.SendCommandOutput, error) {
func (sm *mockSSMClient) SendCommand(_ context.Context, input *ssm.SendCommandInput, _ ...func(*ssm.Options)) (*ssm.SendCommandOutput, error) {
return sm.commandOutput, nil
}
func (sm *mockSSMClient) GetCommandInvocationWithContext(_ context.Context, input *ssm.GetCommandInvocationInput, _ ...request.Option) (*ssm.GetCommandInvocationOutput, error) {
func (sm *mockSSMClient) GetCommandInvocation(_ context.Context, input *ssm.GetCommandInvocationInput, _ ...func(*ssm.Options)) (*ssm.GetCommandInvocationOutput, error) {
return sm.invokeOutput, nil
}
func (sm *mockSSMClient) WaitUntilCommandExecutedWithContext(aws.Context, *ssm.GetCommandInvocationInput, ...request.WaiterOption) error {
if aws.StringValue(sm.commandOutput.Command.Status) == ssm.CommandStatusFailed {
return awserr.New(request.WaiterResourceNotReadyErrorCode, "err", nil)
}
return nil
}
type mockEmitter struct {
eventHandler func(*testing.T, events.AuditEvent, *Server)
server *Server
@@ -325,13 +317,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
emitter: &mockEmitter{
@@ -347,7 +339,7 @@ func TestDiscoveryServer(t *testing.T) {
InstanceID: "instance-id-1",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
}, ae)
},
},
@@ -383,13 +375,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
staticMatchers: defaultStaticMatcher,
@@ -424,13 +416,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
emitter: &mockEmitter{},
@@ -443,13 +435,13 @@ func TestDiscoveryServer(t *testing.T) {
foundEC2Instances: genEC2Instances(58),
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
emitter: &mockEmitter{},
@@ -473,13 +465,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
emitter: &mockEmitter{
@@ -495,7 +487,7 @@ func TestDiscoveryServer(t *testing.T) {
InstanceID: "instance-id-1",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
}, ae)
},
},
@@ -520,13 +512,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
emitter: &mockEmitter{
@@ -542,7 +534,7 @@ func TestDiscoveryServer(t *testing.T) {
InstanceID: "instance-id-1",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
}, ae)
},
},
@@ -582,13 +574,13 @@ func TestDiscoveryServer(t *testing.T) {
},
ssm: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
Command: &ssmtypes.Command{
CommandId: awsv2.String("command-id-1"),
},
},
invokeOutput: &ssm.GetCommandInvocationOutput{
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
ssmRunError: trace.BadParameter("ssm run failed"),
@@ -605,7 +597,7 @@ func TestDiscoveryServer(t *testing.T) {
InstanceID: "instance-id-1",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
}, ae)
},
},
@@ -640,9 +632,6 @@ func TestDiscoveryServer(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
testCloudClients := &cloud.TestCloudClients{
SSM: tc.ssm,
}
ec2Client := &mockEC2Client{output: &ec2.DescribeInstancesOutput{
Reservations: []ec2types.Reservation{
{
@@ -691,10 +680,12 @@ func TestDiscoveryServer(t *testing.T) {
}
server, err := New(authz.ContextWithUser(context.Background(), identity.I), &Config{
CloudClients: testCloudClients,
GetEC2Client: func(ctx context.Context, region string, opts ...config.AWSOptionsFn) (ec2.DescribeInstancesAPIClient, error) {
return ec2Client, nil
},
GetSSMClient: func(ctx context.Context, region string, opts ...config.AWSOptionsFn) (server.SSMClient, error) {
return tc.ssm, nil
},
ClusterFeatures: func() proto.Features { return proto.Features{} },
KubernetesClient: fake.NewSimpleClientset(),
AccessPoint: getDiscoveryAccessPoint(tlsServer.Auth(), authClient),
+66 -42
View File
@@ -26,12 +26,11 @@ import (
"maps"
"slices"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/gravitational/trace"
"golang.org/x/sync/errgroup"
@@ -42,6 +41,23 @@ import (
libevents "github.com/gravitational/teleport/lib/events"
)
// waiterTimedOutErrorMessage is the error message returned by the AWS SDK command
// executed waiter when it times out.
const waiterTimedOutErrorMessage = "exceeded max wait time for CommandExecuted waiter"
// SSMClient is the subset of the AWS SSM API required for EC2 discovery.
type SSMClient interface {
ssm.DescribeInstanceInformationAPIClient
ssm.GetCommandInvocationAPIClient
ssm.ListCommandInvocationsAPIClient
// SendCommand runs commands on one or more managed nodes.
SendCommand(ctx context.Context, params *ssm.SendCommandInput, optFns ...func(*ssm.Options)) (*ssm.SendCommandOutput, error)
}
type commandWaiter interface {
Wait(ctx context.Context, params *ssm.GetCommandInvocationInput, maxWaitDur time.Duration, optFns ...func(*ssm.CommandExecutedWaiterOptions)) error
}
// SSMInstallerConfig represents configuration for an SSM install
// script executor.
type SSMInstallerConfig struct {
@@ -50,6 +66,9 @@ type SSMInstallerConfig struct {
// Logger is used to log messages.
// Optional. A logger is created if one not supplied.
Logger *slog.Logger
// getWaiter replaces the default command waiter for a given SSM client.
// Used in tests.
getWaiter func(SSMClient) commandWaiter
}
// SSMInstallationResult contains the result of trying to install teleport
@@ -84,7 +103,7 @@ type SSMRunRequest struct {
// DocumentName is the name of the SSM document to run.
DocumentName string
// SSM is an SSM API client.
SSM ssmiface.SSMAPI
SSM SSMClient
// Instances is the list of instances that will have the SSM
// document executed on them.
Instances []EC2Instance
@@ -124,6 +143,12 @@ func (c *SSMInstallerConfig) checkAndSetDefaults() error {
c.Logger = slog.Default().With(teleport.ComponentKey, "ssminstaller")
}
if c.getWaiter == nil {
c.getWaiter = func(s SSMClient) commandWaiter {
return ssm.NewCommandExecutedWaiter(s)
}
}
return nil
}
@@ -144,9 +169,9 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error {
instances[inst.InstanceID] = inst.InstanceName
}
params := make(map[string][]*string)
params := make(map[string][]string)
for k, v := range req.Params {
params[k] = []*string{aws.String(v)}
params[k] = []string{v}
}
validInstances := instances
@@ -175,9 +200,9 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error {
}
validInstanceIDs := instanceIDsFrom(validInstances)
output, err := req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{
output, err := req.SSM.SendCommand(ctx, &ssm.SendCommandInput{
DocumentName: aws.String(req.DocumentName),
InstanceIds: aws.StringSlice(validInstanceIDs),
InstanceIds: validInstanceIDs,
Parameters: params,
})
if err != nil {
@@ -194,9 +219,9 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error {
// As a best effort, we try to call ssm.SendCommand again but this time without the "sshdConfigPath" param
// We must not remove the Param "sshdConfigPath" beforehand because customers might be using custom SSM Documents for ec2 auto discovery.
delete(params, ParamSSHDConfigPath)
output, err = req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{
output, err = req.SSM.SendCommand(ctx, &ssm.SendCommandInput{
DocumentName: aws.String(req.DocumentName),
InstanceIds: aws.StringSlice(validInstanceIDs),
InstanceIds: validInstanceIDs,
Parameters: params,
})
if err != nil {
@@ -296,20 +321,20 @@ func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunReq
}
instanceIDs := instanceIDsFrom(allInstances)
ssmInstancesInfo, err := req.SSM.DescribeInstanceInformationWithContext(ctx, &ssm.DescribeInstanceInformationInput{
Filters: []*ssm.InstanceInformationStringFilter{
{Key: aws.String(ssm.InstanceInformationFilterKeyInstanceIds), Values: aws.StringSlice(instanceIDs)},
ssmInstancesInfo, err := req.SSM.DescribeInstanceInformation(ctx, &ssm.DescribeInstanceInformationInput{
Filters: []ssmtypes.InstanceInformationStringFilter{
{Key: aws.String(string(ssmtypes.InstanceInformationFilterKeyInstanceIds)), Values: instanceIDs},
},
MaxResults: aws.Int64(awsEC2APIChunkSize),
MaxResults: aws.Int32(awsEC2APIChunkSize),
})
if err != nil {
return nil, trace.Wrap(awslib.ConvertRequestFailureError(err))
}
instanceStateByInstanceID := make(map[string]*ssm.InstanceInformation, len(ssmInstancesInfo.InstanceInformationList))
instanceStateByInstanceID := make(map[string]ssmtypes.InstanceInformation, len(ssmInstancesInfo.InstanceInformationList))
for _, instanceState := range ssmInstancesInfo.InstanceInformationList {
// instanceState.InstanceId always has the InstanceID value according to AWS Docs.
instanceStateByInstanceID[aws.StringValue(instanceState.InstanceId)] = instanceState
instanceStateByInstanceID[aws.ToString(instanceState.InstanceId)] = instanceState
}
for instanceID, instanceName := range allInstances {
@@ -319,12 +344,12 @@ func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunReq
continue
}
if aws.StringValue(instanceState.PingStatus) == ssm.PingStatusConnectionLost {
if instanceState.PingStatus == ssmtypes.PingStatusConnectionLost {
ret.connectionLost[instanceID] = instanceName
continue
}
if aws.StringValue(instanceState.PlatformType) != ssm.PlatformTypeLinux {
if instanceState.PlatformType != ssmtypes.PlatformTypeLinux {
ret.unsupportedOS[instanceID] = instanceName
continue
}
@@ -336,23 +361,22 @@ func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunReq
}
// skipAWSWaitErr is used to ignore the error returned from
// WaitUntilCommandExecutedWithContext if it is a resource not ready
// code as this can represent one of several different errors which
// Wait if it times out, as this can represent one of several different errors which
// are handled by checking the command invocation after calling this
// to get more information about the error.
func skipAWSWaitErr(err error) error {
var aErr awserr.Error
if errors.As(err, &aErr) && aErr.Code() == request.WaiterResourceNotReadyErrorCode {
if err != nil && err.Error() == waiterTimedOutErrorMessage {
return nil
}
return trace.Wrap(err)
}
func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, commandID, instanceID *string, instanceName string) error {
err := req.SSM.WaitUntilCommandExecutedWithContext(ctx, &ssm.GetCommandInvocationInput{
err := si.getWaiter(req.SSM).Wait(ctx, &ssm.GetCommandInvocationInput{
CommandId: commandID,
InstanceId: instanceID,
})
// 100 seconds to match v1 sdk waiter default.
}, 100*time.Second)
if err := skipAWSWaitErr(err); err != nil {
return trace.Wrap(err)
@@ -378,7 +402,7 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com
for i, step := range invocationSteps {
stepResultEvent, err := si.getCommandStepStatusEvent(ctx, step, req, commandID, instanceID)
if err != nil {
var invalidPluginNameErr *ssm.InvalidPluginName
var invalidPluginNameErr *ssmtypes.InvalidPluginName
if errors.As(err, &invalidPluginNameErr) {
// If using a custom SSM Document and the client does not have access to ssm:ListCommandInvocations
// the list of invocationSteps (ie plugin name) might be wrong.
@@ -422,10 +446,10 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com
func (si *SSMInstaller) getInvocationSteps(ctx context.Context, req SSMRunRequest, commandID, instanceID *string) ([]string, error) {
// ssm:ListCommandInvocations is used to list the actual steps because users might be using a custom SSM Document.
listCommandInvocationResp, err := req.SSM.ListCommandInvocationsWithContext(ctx, &ssm.ListCommandInvocationsInput{
listCommandInvocationResp, err := req.SSM.ListCommandInvocations(ctx, &ssm.ListCommandInvocationsInput{
CommandId: commandID,
InstanceId: instanceID,
Details: aws.Bool(true),
Details: true,
})
if err != nil {
return nil, trace.Wrap(awslib.ConvertRequestFailureError(err))
@@ -436,8 +460,8 @@ func (si *SSMInstaller) getInvocationSteps(ctx context.Context, req SSMRunReques
if len(listCommandInvocationResp.CommandInvocations) == 0 {
si.Logger.WarnContext(ctx,
"No command invocation was found.",
"command_id", aws.StringValue(commandID),
"instance_id", aws.StringValue(instanceID),
"command_id", aws.ToString(commandID),
"instance_id", aws.ToString(instanceID),
)
return nil, trace.BadParameter("no command invocation was found")
}
@@ -445,7 +469,7 @@ func (si *SSMInstaller) getInvocationSteps(ctx context.Context, req SSMRunReques
documentSteps := make([]string, 0, len(commandInvocation.CommandPlugins))
for _, step := range commandInvocation.CommandPlugins {
documentSteps = append(documentSteps, aws.StringValue(step.Name))
documentSteps = append(documentSteps, aws.ToString(step.Name))
}
return documentSteps, nil
}
@@ -458,16 +482,16 @@ func (si *SSMInstaller) getCommandStepStatusEvent(ctx context.Context, step stri
if step != "" {
getCommandInvocationReq.PluginName = aws.String(step)
}
stepResult, err := req.SSM.GetCommandInvocationWithContext(ctx, getCommandInvocationReq)
stepResult, err := req.SSM.GetCommandInvocation(ctx, getCommandInvocationReq)
if err != nil {
return nil, trace.Wrap(err)
}
status := aws.StringValue(stepResult.Status)
exitCode := aws.Int64Value(stepResult.ResponseCode)
status := stepResult.Status
exitCode := int64(stepResult.ResponseCode)
eventCode := libevents.SSMRunSuccessCode
if status != ssm.CommandStatusSuccess {
if status != ssmtypes.CommandInvocationStatusSuccess {
eventCode = libevents.SSMRunFailCode
if exitCode == 0 {
exitCode = -1
@@ -479,7 +503,7 @@ func (si *SSMInstaller) getCommandStepStatusEvent(ctx context.Context, step stri
// Example:
// https://eu-west-2.console.aws.amazon.com/systems-manager/run-command/3cb11aaa-11aa-1111-aaaa-2188108225de/i-0775091aa11111111
invocationURL := fmt.Sprintf("https://%s.console.aws.amazon.com/systems-manager/run-command/%s/%s",
req.Region, aws.StringValue(commandID), aws.StringValue(instanceID),
req.Region, aws.ToString(commandID), aws.ToString(instanceID),
)
return &apievents.SSMRun{
@@ -487,14 +511,14 @@ func (si *SSMInstaller) getCommandStepStatusEvent(ctx context.Context, step stri
Type: libevents.SSMRunEvent,
Code: eventCode,
},
CommandID: aws.StringValue(commandID),
InstanceID: aws.StringValue(instanceID),
CommandID: aws.ToString(commandID),
InstanceID: aws.ToString(instanceID),
AccountID: req.AccountID,
Region: req.Region,
ExitCode: exitCode,
Status: status,
StandardOutput: aws.StringValue(stepResult.StandardOutputContent),
StandardError: aws.StringValue(stepResult.StandardErrorContent),
Status: string(status),
StandardOutput: aws.ToString(stepResult.StandardOutputContent),
StandardError: aws.ToString(stepResult.StandardErrorContent),
InvocationURL: invocationURL,
}, nil
}
+167 -166
View File
@@ -21,15 +21,13 @@ package server
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/google/uuid"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types/events"
@@ -37,7 +35,7 @@ import (
)
type mockSSMClient struct {
ssmiface.SSMAPI
SSMClient
commandOutput *ssm.SendCommandOutput
commandInvokeOutput map[string]*ssm.GetCommandInvocationOutput
describeOutput *ssm.DescribeInstanceInformationOutput
@@ -46,37 +44,37 @@ type mockSSMClient struct {
const docWithoutSSHDConfigPathParam = "ssmdocument-without-sshdConfigPath-param"
func (sm *mockSSMClient) SendCommandWithContext(_ context.Context, input *ssm.SendCommandInput, _ ...request.Option) (*ssm.SendCommandOutput, error) {
if _, hasExtraParam := input.Parameters["sshdConfigPath"]; hasExtraParam && aws.StringValue(input.DocumentName) == docWithoutSSHDConfigPathParam {
func (sm *mockSSMClient) SendCommand(_ context.Context, input *ssm.SendCommandInput, _ ...func(*ssm.Options)) (*ssm.SendCommandOutput, error) {
if _, hasExtraParam := input.Parameters["sshdConfigPath"]; hasExtraParam && aws.ToString(input.DocumentName) == docWithoutSSHDConfigPathParam {
return nil, fmt.Errorf("InvalidParameters: document %s does not support parameters", docWithoutSSHDConfigPathParam)
}
return sm.commandOutput, nil
}
func (sm *mockSSMClient) GetCommandInvocationWithContext(_ context.Context, input *ssm.GetCommandInvocationInput, _ ...request.Option) (*ssm.GetCommandInvocationOutput, error) {
if stepResult, found := sm.commandInvokeOutput[aws.StringValue(input.PluginName)]; found {
func (sm *mockSSMClient) GetCommandInvocation(_ context.Context, input *ssm.GetCommandInvocationInput, _ ...func(*ssm.Options)) (*ssm.GetCommandInvocationOutput, error) {
if stepResult, found := sm.commandInvokeOutput[aws.ToString(input.PluginName)]; found {
return stepResult, nil
}
return nil, &ssm.InvalidPluginName{}
return nil, &ssmtypes.InvalidPluginName{}
}
func (sm *mockSSMClient) DescribeInstanceInformationWithContext(_ context.Context, input *ssm.DescribeInstanceInformationInput, _ ...request.Option) (*ssm.DescribeInstanceInformationOutput, error) {
func (sm *mockSSMClient) DescribeInstanceInformation(_ context.Context, input *ssm.DescribeInstanceInformationInput, _ ...func(*ssm.Options)) (*ssm.DescribeInstanceInformationOutput, error) {
if sm.describeOutput == nil {
return nil, awserr.NewRequestFailure(awserr.New("AccessDeniedException", "message", nil), http.StatusBadRequest, uuid.NewString())
return nil, trace.AccessDenied("")
}
return sm.describeOutput, nil
}
func (sm *mockSSMClient) ListCommandInvocationsWithContext(aws.Context, *ssm.ListCommandInvocationsInput, ...request.Option) (*ssm.ListCommandInvocationsOutput, error) {
func (sm *mockSSMClient) ListCommandInvocations(_ context.Context, input *ssm.ListCommandInvocationsInput, _ ...func(*ssm.Options)) (*ssm.ListCommandInvocationsOutput, error) {
if sm.listCommandInvocations == nil {
return nil, awserr.NewRequestFailure(awserr.New("AccessDeniedException", "message", nil), http.StatusBadRequest, uuid.NewString())
return nil, trace.AccessDenied("")
}
return sm.listCommandInvocations, nil
}
func (sm *mockSSMClient) WaitUntilCommandExecutedWithContext(aws.Context, *ssm.GetCommandInvocationInput, ...request.WaiterOption) error {
if aws.StringValue(sm.commandOutput.Command.Status) == ssm.CommandStatusFailed {
return awserr.New(request.WaiterResourceNotReadyErrorCode, "err", nil)
func (sm *mockSSMClient) Wait(ctx context.Context, params *ssm.GetCommandInvocationInput, maxWaitDur time.Duration, optFns ...func(*ssm.CommandExecutedWaiterOptions)) error {
if sm.commandOutput.Command.Status == ssmtypes.CommandStatusFailed {
return trace.Errorf(waiterTimedOutErrorMessage)
}
return nil
}
@@ -94,6 +92,7 @@ func TestSSMInstaller(t *testing.T) {
document := "ssmdocument"
for _, tc := range []struct {
client *mockSSMClient
req SSMRunRequest
expectedInstallations []*SSMInstallationResult
name string
@@ -108,25 +107,25 @@ func TestSSMInstaller(t *testing.T) {
Params: map[string]string{"token": "abcdefg"},
IntegrationName: "aws-integration",
DiscoveryConfig: "dc001",
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
"runShellScript": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
"runShellScript": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
Region: "eu-central-1",
AccountID: "account-id",
},
expectedInstallations: []*SSMInstallationResult{{
IntegrationName: "aws-integration",
@@ -141,7 +140,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
},
IssueType: "ec2-ssm-script-failure",
@@ -157,25 +156,25 @@ func TestSSMInstaller(t *testing.T) {
},
DocumentName: docWithoutSSHDConfigPathParam,
Params: map[string]string{"sshdConfigPath": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
"runShellScript": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
"runShellScript": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
Region: "eu-central-1",
AccountID: "account-id",
},
expectedInstallations: []*SSMInstallationResult{{
SSMRunEvent: &events.SSMRun{
@@ -188,7 +187,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
},
IssueType: "ec2-ssm-script-failure",
@@ -204,23 +203,23 @@ func TestSSMInstaller(t *testing.T) {
},
IntegrationName: "aws-1",
Params: map[string]string{"token": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: aws.String(ssm.CommandStatusFailed),
ResponseCode: aws.Int64(1),
StandardErrorContent: aws.String("timeout error"),
StandardOutputContent: aws.String(""),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: ssmtypes.CommandInvocationStatusFailed,
ResponseCode: 1,
StandardErrorContent: aws.String("timeout error"),
StandardOutputContent: aws.String(""),
},
},
Region: "eu-central-1",
AccountID: "account-id",
},
expectedInstallations: []*SSMInstallationResult{{
IntegrationName: "aws-1",
@@ -234,7 +233,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 1,
Status: ssm.CommandStatusFailed,
Status: string(ssmtypes.CommandInvocationStatusFailed),
StandardOutput: "",
StandardError: "timeout error",
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
@@ -250,31 +249,31 @@ func TestSSMInstaller(t *testing.T) {
Instances: []EC2Instance{
{InstanceID: "instance-id-1"},
},
Params: map[string]string{"token": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
StandardErrorContent: aws.String("no error"),
StandardOutputContent: aws.String(""),
},
"runShellScript": {
Status: aws.String(ssm.CommandStatusFailed),
ResponseCode: aws.Int64(1),
StandardErrorContent: aws.String("timeout error"),
StandardOutputContent: aws.String(""),
},
},
},
Params: map[string]string{"token": "abcdefg"},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
StandardErrorContent: aws.String("no error"),
StandardOutputContent: aws.String(""),
},
"runShellScript": {
Status: ssmtypes.CommandInvocationStatusFailed,
ResponseCode: 1,
StandardErrorContent: aws.String("timeout error"),
StandardOutputContent: aws.String(""),
},
},
},
expectedInstallations: []*SSMInstallationResult{{
SSMRunEvent: &events.SSMRun{
Metadata: events.Metadata{
@@ -286,7 +285,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 1,
Status: ssm.CommandStatusFailed,
Status: string(ssmtypes.CommandInvocationStatusFailed),
StandardOutput: "",
StandardError: "timeout error",
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
@@ -306,44 +305,44 @@ func TestSSMInstaller(t *testing.T) {
},
DocumentName: document,
Params: map[string]string{"token": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
"runShellScript": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContent": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
describeOutput: &ssm.DescribeInstanceInformationOutput{
InstanceInformationList: []*ssm.InstanceInformation{
{
InstanceId: aws.String("instance-id-1"),
PingStatus: aws.String("Online"),
PlatformType: aws.String("Linux"),
},
{
InstanceId: aws.String("instance-id-2"),
PingStatus: aws.String("ConnectionLost"),
PlatformType: aws.String("Linux"),
},
{
InstanceId: aws.String("instance-id-3"),
PingStatus: aws.String("Online"),
PlatformType: aws.String("Windows"),
},
"runShellScript": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
describeOutput: &ssm.DescribeInstanceInformationOutput{
InstanceInformationList: []ssmtypes.InstanceInformation{
{
InstanceId: aws.String("instance-id-1"),
PingStatus: ssmtypes.PingStatusOnline,
PlatformType: ssmtypes.PlatformTypeLinux,
},
{
InstanceId: aws.String("instance-id-2"),
PingStatus: ssmtypes.PingStatusConnectionLost,
PlatformType: ssmtypes.PlatformTypeLinux,
},
{
InstanceId: aws.String("instance-id-3"),
PingStatus: ssmtypes.PingStatusOnline,
PlatformType: ssmtypes.PlatformTypeWindows,
},
},
},
Region: "eu-central-1",
AccountID: "account-id",
},
expectedInstallations: []*SSMInstallationResult{
{
@@ -357,7 +356,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
},
IssueType: "ec2-ssm-script-failure",
@@ -421,34 +420,34 @@ func TestSSMInstaller(t *testing.T) {
},
DocumentName: document,
Params: map[string]string{"token": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContentCustom": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
"runShellScriptCustom": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
StandardOutputContent: aws.String("custom output"),
},
},
listCommandInvocations: &ssm.ListCommandInvocationsOutput{
CommandInvocations: []*ssm.CommandInvocation{{
CommandPlugins: []*ssm.CommandPlugin{
{Name: aws.String("downloadContentCustom")},
{Name: aws.String("runShellScriptCustom")},
},
}},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
},
Region: "eu-central-1",
AccountID: "account-id",
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"downloadContentCustom": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
"runShellScriptCustom": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
StandardOutputContent: aws.String("custom output"),
},
},
listCommandInvocations: &ssm.ListCommandInvocationsOutput{
CommandInvocations: []ssmtypes.CommandInvocation{{
CommandPlugins: []ssmtypes.CommandPlugin{
{Name: aws.String("downloadContentCustom")},
{Name: aws.String("runShellScriptCustom")},
},
}},
},
},
expectedInstallations: []*SSMInstallationResult{{
SSMRunEvent: &events.SSMRun{
@@ -461,7 +460,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
StandardOutput: "custom output",
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
},
@@ -477,21 +476,21 @@ func TestSSMInstaller(t *testing.T) {
},
DocumentName: document,
Params: map[string]string{"token": "abcdefg"},
SSM: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssm.Command{
CommandId: aws.String("command-id-1"),
},
Region: "eu-central-1",
AccountID: "account-id",
},
client: &mockSSMClient{
commandOutput: &ssm.SendCommandOutput{
Command: &ssmtypes.Command{
CommandId: aws.String("command-id-1"),
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"": {
Status: aws.String(ssm.CommandStatusSuccess),
ResponseCode: aws.Int64(0),
},
},
commandInvokeOutput: map[string]*ssm.GetCommandInvocationOutput{
"": {
Status: ssmtypes.CommandInvocationStatusSuccess,
ResponseCode: 0,
},
},
Region: "eu-central-1",
AccountID: "account-id",
},
expectedInstallations: []*SSMInstallationResult{{
SSMRunEvent: &events.SSMRun{
@@ -504,7 +503,7 @@ func TestSSMInstaller(t *testing.T) {
AccountID: "account-id",
Region: "eu-central-1",
ExitCode: 0,
Status: ssm.CommandStatusSuccess,
Status: string(ssmtypes.CommandInvocationStatusSuccess),
InvocationURL: "https://eu-central-1.console.aws.amazon.com/systems-manager/run-command/command-id-1/instance-id-1",
},
IssueType: "ec2-ssm-script-failure",
@@ -516,9 +515,11 @@ func TestSSMInstaller(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
tc.req.SSM = tc.client
installationResultsCollector := &mockInstallationResults{}
inst, err := NewSSMInstaller(SSMInstallerConfig{
ReportSSMInstallationResultFunc: installationResultsCollector.ReportInstallationResult,
getWaiter: func(s SSMClient) commandWaiter { return tc.client },
})
require.NoError(t, err)