-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add managed instances handler and ssm file for underlying repo logic. * Added updated method for GetManagedInstance that will also look for it by ComputerName --------- Co-authored-by: bt353 <[email protected]>
- Loading branch information
Showing
4 changed files
with
263 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,8 @@ vendor | |
.vscode | ||
debug | ||
.idea/ | ||
*.exe | ||
*.exe | ||
control.json | ||
deco.json | ||
decofile-prod.json | ||
docker-compose.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package api | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"strconv" | ||
|
||
"github.com/YaleSpinup/apierror" | ||
"github.com/YaleSpinup/ec2-api/ssm" | ||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/gorilla/mux" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// ListManagedInstancesHandler lists all hybrid (managed) instances in an account | ||
func (s *server) ListManagedInstancesHandler(w http.ResponseWriter, r *http.Request) { | ||
w = LogWriter{w} | ||
vars := mux.Vars(r) | ||
account := s.mapAccountNumber(vars["account"]) | ||
|
||
log.Infof("listing managed instances in account: %s", account) | ||
|
||
role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName) | ||
session, err := s.assumeRole( | ||
r.Context(), | ||
s.session.ExternalID, | ||
role, | ||
"", | ||
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess", | ||
) | ||
if err != nil { | ||
msg := fmt.Sprintf("failed to assume role in account: %s", account) | ||
handleError(w, apierror.New(apierror.ErrForbidden, msg, err)) | ||
return | ||
} | ||
|
||
service := ssm.New( | ||
ssm.WithSession(session.Session), | ||
) | ||
|
||
perPage := int64(10) // default value | ||
var pageToken *string | ||
for name, values := range r.URL.Query() { | ||
if name == "next" { | ||
pageToken = aws.String(values[0]) | ||
} | ||
|
||
if name == "limit" { | ||
limit, err := strconv.ParseInt(values[0], 10, 64) | ||
if err != nil { | ||
handleError(w, apierror.New(apierror.ErrBadRequest, "invalid value for limit parameter", err)) | ||
return | ||
} | ||
perPage = limit | ||
} | ||
} | ||
|
||
instances, next, err := service.ListManagedInstances(r.Context(), perPage, pageToken) | ||
if err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
|
||
w.Header().Set("X-Items", strconv.Itoa(len(instances))) | ||
if next != nil { | ||
w.Header().Set("X-Per-Page", strconv.FormatInt(perPage, 10)) | ||
w.Header().Set("X-Next-Token", aws.StringValue(next)) | ||
} | ||
|
||
handleResponseOk(w, instances) | ||
} | ||
|
||
// GetManagedInstanceHandler gets details about a specific hybrid (managed) instance | ||
func (s *server) GetManagedInstanceHandler(w http.ResponseWriter, r *http.Request) { | ||
w = LogWriter{w} | ||
vars := mux.Vars(r) | ||
account := s.mapAccountNumber(vars["account"]) | ||
identifier := vars["id"] | ||
|
||
log.Infof("getting managed instance with identifier %s in account: %s", identifier, account) | ||
|
||
if identifier == "" { | ||
handleError(w, apierror.New(apierror.ErrBadRequest, "identifier (instance_id or computer_name) is required", nil)) | ||
return | ||
} | ||
|
||
role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName) | ||
session, err := s.assumeRole( | ||
r.Context(), | ||
s.session.ExternalID, | ||
role, | ||
"", | ||
"arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess", | ||
) | ||
if err != nil { | ||
msg := fmt.Sprintf("failed to assume role in account: %s", account) | ||
handleError(w, apierror.New(apierror.ErrForbidden, msg, err)) | ||
return | ||
} | ||
|
||
service := ssm.New( | ||
ssm.WithSession(session.Session), | ||
) | ||
|
||
instance, err := service.GetManagedInstance(r.Context(), identifier) | ||
if err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
|
||
handleResponseOk(w, instance) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package ssm | ||
|
||
import ( | ||
"context" | ||
"regexp" | ||
|
||
"github.com/YaleSpinup/apierror" | ||
"github.com/YaleSpinup/ec2-api/common" | ||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/service/ssm" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// ManagedInstance represents the response structure for managed instances | ||
type ManagedInstance struct { | ||
InstanceId *string `json:"instanceId"` | ||
Name *string `json:"name"` | ||
PingStatus *string `json:"pingStatus"` | ||
PlatformType *string `json:"platformType"` | ||
ResourceType *string `json:"resourceType"` | ||
ComputerName *string `json:"computerName"` | ||
IPAddress *string `json:"ipAddress"` | ||
AgentVersion *string `json:"agentVersion"` | ||
} | ||
|
||
// managedInstanceFilter creates a StringFilter for managed instances | ||
func managedInstanceFilter() *ssm.InstanceInformationStringFilter { | ||
return &ssm.InstanceInformationStringFilter{ | ||
Key: aws.String("ResourceType"), | ||
Values: []*string{ | ||
aws.String("ManagedInstance"), | ||
}, | ||
} | ||
} | ||
|
||
// ListManagedInstances lists hybrid instances from SSM Fleet Manager | ||
func (s *SSM) ListManagedInstances(ctx context.Context, per int64, next *string) ([]*ManagedInstance, *string, error) { | ||
if per < 1 || per > 50 { | ||
return nil, nil, apierror.New(apierror.ErrBadRequest, "per page must be between 1 and 50", nil) | ||
} | ||
|
||
log.Info("listing managed instances from SSM") | ||
|
||
input := &ssm.DescribeInstanceInformationInput{ | ||
MaxResults: aws.Int64(per), | ||
NextToken: next, | ||
Filters: []*ssm.InstanceInformationStringFilter{ | ||
managedInstanceFilter(), | ||
}, | ||
} | ||
|
||
out, err := s.Service.DescribeInstanceInformationWithContext(ctx, input) | ||
if err != nil { | ||
return nil, nil, common.ErrCode("listing managed instances", err) | ||
} | ||
|
||
log.Debugf("got output from managed instance list: %+v", out) | ||
|
||
instances := make([]*ManagedInstance, 0, len(out.InstanceInformationList)) | ||
for _, info := range out.InstanceInformationList { | ||
instances = append(instances, convertToManagedInstance(info)) | ||
} | ||
|
||
return instances, out.NextToken, nil | ||
} | ||
|
||
// GetManagedInstance gets details about a specific managed instance by ID or computer name | ||
func (s *SSM) GetManagedInstance(ctx context.Context, identifier string) (*ManagedInstance, error) { | ||
if identifier == "" { | ||
return nil, apierror.New(apierror.ErrBadRequest, "identifier (instance id or computer name) is required", nil) | ||
} | ||
|
||
log.Infof("getting managed instance details for identifier: %s", identifier) | ||
|
||
// Check if the identifier matches managed instance ID pattern (mi-xxxxxxxxxxxxxxxxx) | ||
if matched, _ := regexp.MatchString(`^mi-\w{17}$`, identifier); matched { | ||
// If it's an instance ID, use direct filter | ||
filters := []*ssm.InstanceInformationStringFilter{ | ||
managedInstanceFilter(), | ||
{ | ||
Key: aws.String("InstanceId"), | ||
Values: []*string{ | ||
aws.String(identifier), | ||
}, | ||
}, | ||
} | ||
|
||
input := &ssm.DescribeInstanceInformationInput{ | ||
Filters: filters, | ||
} | ||
|
||
out, err := s.Service.DescribeInstanceInformationWithContext(ctx, input) | ||
if err != nil { | ||
return nil, common.ErrCode("getting managed instance", err) | ||
} | ||
|
||
if len(out.InstanceInformationList) == 0 { | ||
return nil, apierror.New(apierror.ErrNotFound, "managed instance not found", nil) | ||
} | ||
|
||
if len(out.InstanceInformationList) > 1 { | ||
return nil, apierror.New(apierror.ErrBadRequest, "multiple instances found", nil) | ||
} | ||
|
||
return convertToManagedInstance(out.InstanceInformationList[0]), nil | ||
} | ||
|
||
// If not an instance ID, assume it's a computer name and list all instances | ||
instances, _, err := s.ListManagedInstances(ctx, 50, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var matches []*ManagedInstance | ||
for _, instance := range instances { | ||
if aws.StringValue(instance.ComputerName) == identifier { | ||
matches = append(matches, instance) | ||
} | ||
} | ||
|
||
if len(matches) == 0 { | ||
return nil, apierror.New(apierror.ErrNotFound, "managed instance not found", nil) | ||
} | ||
|
||
if len(matches) > 1 { | ||
return nil, apierror.New(apierror.ErrBadRequest, "multiple instances found with same computer name", nil) | ||
} | ||
|
||
return matches[0], nil | ||
} | ||
|
||
// Helper function to convert SSM instance info to our ManagedInstance type | ||
func convertToManagedInstance(info *ssm.InstanceInformation) *ManagedInstance { | ||
return &ManagedInstance{ | ||
InstanceId: info.InstanceId, | ||
Name: info.Name, | ||
PingStatus: info.PingStatus, | ||
PlatformType: info.PlatformType, | ||
ResourceType: info.ResourceType, | ||
ComputerName: info.ComputerName, | ||
IPAddress: info.IPAddress, | ||
AgentVersion: info.AgentVersion, | ||
} | ||
} |