diff --git a/lib/srv/discovery/discovery.go b/lib/srv/discovery/discovery.go index d1b490bbe083..6e93a8a8eddc 100644 --- a/lib/srv/discovery/discovery.go +++ b/lib/srv/discovery/discovery.go @@ -990,10 +990,10 @@ func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) err installerScript: req.InstallerScriptName(), }, &usertasksv1.DiscoverEC2Instance{ - // TODO(marco): add instance name DiscoveryConfig: instances.DiscoveryConfig, DiscoveryGroup: s.DiscoveryGroup, InstanceId: instance.InstanceID, + Name: instance.InstanceName, SyncTime: timestamppb.New(s.clock.Now()), }, ) diff --git a/lib/srv/discovery/status.go b/lib/srv/discovery/status.go index 4bb70efaa5b6..7fe0b0f39398 100644 --- a/lib/srv/discovery/status.go +++ b/lib/srv/discovery/status.go @@ -310,12 +310,12 @@ func (s *Server) ReportEC2SSMInstallationResult(ctx context.Context, result *ser installerScript: result.InstallerScript, }, &usertasksv1.DiscoverEC2Instance{ - // TODO(marco): add instance name InvocationUrl: result.SSMRunEvent.InvocationURL, DiscoveryConfig: result.DiscoveryConfig, DiscoveryGroup: s.DiscoveryGroup, SyncTime: timestamppb.New(result.SSMRunEvent.Time), InstanceId: result.SSMRunEvent.InstanceID, + Name: result.InstanceName, }, ) diff --git a/lib/srv/server/ec2_watcher.go b/lib/srv/server/ec2_watcher.go index f8390b032c4d..25f12018e017 100644 --- a/lib/srv/server/ec2_watcher.go +++ b/lib/srv/server/ec2_watcher.go @@ -79,6 +79,7 @@ type EC2Instances struct { // discovered. type EC2Instance struct { InstanceID string + InstanceName string Tags map[string]string OriginalInstance ec2.Instance } @@ -92,6 +93,9 @@ func toEC2Instance(originalInst *ec2.Instance) EC2Instance { for _, tag := range originalInst.Tags { if key := aws.StringValue(tag.Key); key != "" { inst.Tags[key] = aws.StringValue(tag.Value) + if key == "Name" { + inst.InstanceName = aws.StringValue(tag.Value) + } } } return inst diff --git a/lib/srv/server/ec2_watcher_test.go b/lib/srv/server/ec2_watcher_test.go index 2647a7b0f527..8279cbbd868f 100644 --- a/lib/srv/server/ec2_watcher_test.go +++ b/lib/srv/server/ec2_watcher_test.go @@ -176,10 +176,16 @@ func TestEC2Watcher(t *testing.T) { present := ec2.Instance{ InstanceId: aws.String("instance-present"), - Tags: []*ec2.Tag{{ - Key: aws.String("teleport"), - Value: aws.String("yes"), - }}, + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + { + Key: aws.String("Name"), + Value: aws.String("Present"), + }, + }, State: &ec2.InstanceState{ Name: aws.String(ec2.InstanceStateNameRunning), }, @@ -360,3 +366,71 @@ func TestMakeEvents(t *testing.T) { }) } } + +func TestToEC2Instances(t *testing.T) { + sampleInstance := &ec2.Instance{ + InstanceId: aws.String("instance-001"), + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + { + Key: aws.String("Name"), + Value: aws.String("MyInstanceName"), + }, + }, + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + } + + sampleInstanceWithoutName := &ec2.Instance{ + InstanceId: aws.String("instance-001"), + Tags: []*ec2.Tag{ + { + Key: aws.String("teleport"), + Value: aws.String("yes"), + }, + }, + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNameRunning), + }, + } + + for _, tt := range []struct { + name string + input []*ec2.Instance + expected []EC2Instance + }{ + { + name: "with name", + input: []*ec2.Instance{sampleInstance}, + expected: []EC2Instance{{ + InstanceID: "instance-001", + Tags: map[string]string{ + "Name": "MyInstanceName", + "teleport": "yes", + }, + InstanceName: "MyInstanceName", + OriginalInstance: *sampleInstance, + }}, + }, + { + name: "without name", + input: []*ec2.Instance{sampleInstanceWithoutName}, + expected: []EC2Instance{{ + InstanceID: "instance-001", + Tags: map[string]string{ + "teleport": "yes", + }, + OriginalInstance: *sampleInstanceWithoutName, + }}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + got := ToEC2Instances(tt.input) + require.Equal(t, tt.expected, got) + }) + } +} diff --git a/lib/srv/server/ssm_install.go b/lib/srv/server/ssm_install.go index 6c0c2f427708..7d1f28a42252 100644 --- a/lib/srv/server/ssm_install.go +++ b/lib/srv/server/ssm_install.go @@ -67,6 +67,9 @@ type SSMInstallationResult struct { SSMDocumentName string // InstallerScript is the Teleport Installer script name used to install Teleport into the instance. InstallerScript string + // InstanceName is the Instance's name. + // Might be empty. + InstanceName string } // SSMInstaller handles running SSM commands that install Teleport on EC2 instances. @@ -134,9 +137,9 @@ func NewSSMInstaller(cfg SSMInstallerConfig) (*SSMInstaller, error) { // Run executes the SSM document and then blocks until the command has completed. func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { - ids := make([]string, 0, len(req.Instances)) + instances := make(map[string]string, len(req.Instances)) for _, inst := range req.Instances { - ids = append(ids, inst.InstanceID) + instances[inst.InstanceID] = inst.InstanceName } params := make(map[string][]*string) @@ -144,8 +147,8 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { params[k] = []*string{aws.String(v)} } - validInstances := ids - instancesState, err := si.describeSSMAgentState(ctx, req, ids) + validInstances := instances + instancesState, err := si.describeSSMAgentState(ctx, req, instances) switch { case trace.IsAccessDenied(err): // describeSSMAgentState uses `ssm:DescribeInstanceInformation` to gather all the instances information. @@ -169,9 +172,10 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { validInstances = instancesState.valid } + validInstanceIDs := instanceIDsFrom(validInstances) output, err := req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{ DocumentName: aws.String(req.DocumentName), - InstanceIds: aws.StringSlice(validInstances), + InstanceIds: aws.StringSlice(validInstanceIDs), Parameters: params, }) if err != nil { @@ -190,7 +194,7 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { delete(params, ParamSSHDConfigPath) output, err = req.SSM.SendCommandWithContext(ctx, &ssm.SendCommandInput{ DocumentName: aws.String(req.DocumentName), - InstanceIds: aws.StringSlice(validInstances), + InstanceIds: aws.StringSlice(validInstanceIDs), Parameters: params, }) if err != nil { @@ -200,16 +204,17 @@ func (si *SSMInstaller) Run(ctx context.Context, req SSMRunRequest) error { g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) - for _, inst := range validInstances { - inst := inst + for instanceID, instanceName := range validInstances { + instanceID := instanceID + instanceName := instanceName g.Go(func() error { - return trace.Wrap(si.checkCommand(ctx, req, output.Command.CommandId, &inst)) + return trace.Wrap(si.checkCommand(ctx, req, output.Command.CommandId, &instanceID, instanceName)) }) } return trace.Wrap(g.Wait()) } -func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, status, issueType string) *SSMInstallationResult { +func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, instanceName, status, issueType string) *SSMInstallationResult { return &SSMInstallationResult{ SSMRunEvent: &apievents.SSMRun{ Metadata: apievents.Metadata{ @@ -228,13 +233,14 @@ func invalidSSMInstanceInstallationResult(req SSMRunRequest, instanceID, status, IssueType: issueType, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, } } func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRunRequest, instanceIDsState *instanceIDsSSMState) error { var errs []error - for _, instanceID := range instanceIDsState.missing { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.missing { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "EC2 Instance is not registered in SSM. Make sure that the instance has AmazonSSMManagedInstanceCore policy assigned.", usertasks.AutoDiscoverEC2IssueSSMInstanceNotRegistered, ) @@ -243,8 +249,8 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu } } - for _, instanceID := range instanceIDsState.connectionLost { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.connectionLost { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "SSM Agent in EC2 Instance is not connecting to SSM Service. Restart or reinstall the SSM service. See https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html#verify-ssm-agent-status for more details.", usertasks.AutoDiscoverEC2IssueSSMInstanceConnectionLost, ) @@ -253,8 +259,8 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu } } - for _, instanceID := range instanceIDsState.unsupportedOS { - installationResult := invalidSSMInstanceInstallationResult(req, instanceID, + for instanceID, instanceName := range instanceIDsState.unsupportedOS { + installationResult := invalidSSMInstanceInstallationResult(req, instanceID, instanceName, "EC2 instance is running an unsupported Operating System. Only Linux is supported.", usertasks.AutoDiscoverEC2IssueSSMInstanceUnsupportedOS, ) @@ -268,19 +274,33 @@ func (si *SSMInstaller) emitInvalidInstanceEvents(ctx context.Context, req SSMRu // instanceIDsSSMState contains a list of EC2 Instance IDs for a given state. type instanceIDsSSMState struct { - valid []string - missing []string - connectionLost []string - unsupportedOS []string + valid map[string]string + missing map[string]string + connectionLost map[string]string + unsupportedOS map[string]string +} + +func instanceIDsFrom(m map[string]string) []string { + ret := make([]string, 0, len(m)) + for k := range m { + ret = append(ret, k) + } + return ret } // describeSSMAgentState returns the instanceIDsSSMState for all the instances. -func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunRequest, allInstanceIDs []string) (*instanceIDsSSMState, error) { - ret := &instanceIDsSSMState{} +func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunRequest, allInstances map[string]string) (*instanceIDsSSMState, error) { + ret := &instanceIDsSSMState{ + valid: make(map[string]string), + missing: make(map[string]string), + connectionLost: make(map[string]string), + unsupportedOS: make(map[string]string), + } + instanceIDs := instanceIDsFrom(allInstances) ssmInstancesInfo, err := req.SSM.DescribeInstanceInformationWithContext(ctx, &ssm.DescribeInstanceInformationInput{ Filters: []*ssm.InstanceInformationStringFilter{ - {Key: aws.String(ssm.InstanceInformationFilterKeyInstanceIds), Values: aws.StringSlice(allInstanceIDs)}, + {Key: aws.String(ssm.InstanceInformationFilterKeyInstanceIds), Values: aws.StringSlice(instanceIDs)}, }, MaxResults: aws.Int64(awsEC2APIChunkSize), }) @@ -294,24 +314,24 @@ func (si *SSMInstaller) describeSSMAgentState(ctx context.Context, req SSMRunReq instanceStateByInstanceID[aws.StringValue(instanceState.InstanceId)] = instanceState } - for _, instanceID := range allInstanceIDs { + for instanceID, instanceName := range allInstances { instanceState, found := instanceStateByInstanceID[instanceID] if !found { - ret.missing = append(ret.missing, instanceID) + ret.missing[instanceID] = instanceName continue } if aws.StringValue(instanceState.PingStatus) == ssm.PingStatusConnectionLost { - ret.connectionLost = append(ret.connectionLost, instanceID) + ret.connectionLost[instanceID] = instanceName continue } if aws.StringValue(instanceState.PlatformType) != ssm.PlatformTypeLinux { - ret.unsupportedOS = append(ret.unsupportedOS, instanceID) + ret.unsupportedOS[instanceID] = instanceName continue } - ret.valid = append(ret.valid, instanceID) + ret.valid[instanceID] = instanceName } return ret, nil @@ -330,7 +350,7 @@ func skipAWSWaitErr(err error) error { return trace.Wrap(err) } -func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, commandID, instanceID *string) error { +func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, commandID, instanceID *string, instanceName string) error { err := req.SSM.WaitUntilCommandExecutedWithContext(ctx, &ssm.GetCommandInvocationInput{ CommandId: commandID, InstanceId: instanceID, @@ -377,6 +397,7 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com IssueType: usertasks.AutoDiscoverEC2IssueSSMScriptFailure, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, })) } @@ -393,6 +414,7 @@ func (si *SSMInstaller) checkCommand(ctx context.Context, req SSMRunRequest, com IssueType: usertasks.AutoDiscoverEC2IssueSSMScriptFailure, SSMDocumentName: req.DocumentName, InstallerScript: req.InstallerScriptName(), + InstanceName: instanceName, })) } } diff --git a/lib/srv/server/ssm_install_test.go b/lib/srv/server/ssm_install_test.go index 2107e91b0e59..c56b28625852 100644 --- a/lib/srv/server/ssm_install_test.go +++ b/lib/srv/server/ssm_install_test.go @@ -102,7 +102,7 @@ func TestSSMInstaller(t *testing.T) { name: "ssm run was successful", req: SSMRunRequest{ Instances: []EC2Instance{ - {InstanceID: "instance-id-1"}, + {InstanceID: "instance-id-1", InstanceName: "my-instance-name"}, }, DocumentName: document, Params: map[string]string{"token": "abcdefg"}, @@ -146,6 +146,7 @@ func TestSSMInstaller(t *testing.T) { }, IssueType: "ec2-ssm-script-failure", SSMDocumentName: "ssmdocument", + InstanceName: "my-instance-name", }}, }, {