Skip to content

Commit

Permalink
Migrate SSM GET Association (#22)
Browse files Browse the repository at this point in the history
* SSMAssociation

* Fixed Comments

* Added to handlers_instances
  • Loading branch information
nvnyale authored Apr 20, 2022
1 parent 48f8482 commit a27aa1c
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 46 deletions.
45 changes: 0 additions & 45 deletions api/handlers_command.go

This file was deleted.

70 changes: 70 additions & 0 deletions api/handlers_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/YaleSpinup/apierror"
"github.com/YaleSpinup/ec2-api/ec2"
"github.com/YaleSpinup/ec2-api/ssm"
"github.com/aws/aws-sdk-go/aws"
"github.com/gorilla/mux"
)
Expand Down Expand Up @@ -278,3 +279,72 @@ func (s *server) InstanceListSnapshotsHandler(w http.ResponseWriter, r *http.Req

handleResponseOk(w, list)
}

func (s *server) InstanceGetCommandHandler(w http.ResponseWriter, r *http.Request) {
w = LogWriter{w}
vars := mux.Vars(r)
account := s.mapAccountNumber(vars["account"])
instance_id := vars["id"]
cmd_id := vars["cid"]

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),
)

out, err := service.GetCommandInvocation(r.Context(), instance_id, cmd_id)
if err != nil {
handleError(w, err)
return
}

handleResponseOk(w, toSSMGetCommandInvocationOutput(out))
}

func (s *server) DescribeAssociationHandler(w http.ResponseWriter, r *http.Request) {
w = LogWriter{w}
vars := mux.Vars(r)
account := s.mapAccountNumber(vars["account"])
instanceId := vars["id"]
doc := vars["doc"]

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),
)

out, err := service.DescribeAssociation(r.Context(), instanceId, doc)
if err != nil {
handleError(w, err)
return
}
handleResponseOk(w, toSSMAssociationDescription(out))
}
2 changes: 1 addition & 1 deletion api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/instances/{id}/snapshots", s.InstanceListSnapshotsHandler).Methods(http.MethodGet)

api.HandleFunc("/{account}/instances/{id}/ssm/command", s.InstanceGetCommandHandler).Methods(http.MethodGet).Queries("command_id", "{cid}")
api.HandleFunc("/{account}/instances/{id}/ssm/association", s.ProxyRequestHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/instances/{id}/ssm/association", s.DescribeAssociationHandler).Methods(http.MethodGet).Queries("document", "{doc}")

api.HandleFunc("/{account}/sgs", s.SecurityGroupListHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupGetHandler).Methods(http.MethodGet)
Expand Down
68 changes: 68 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,71 @@ func toSSMGetCommandInvocationOutput(rawOut *ssm.GetCommandInvocationOutput) *SS
},
}
}

type AssociationDescription struct {
Name string `json:"name"`
InstanceId string `json:"instance_id"`
AssociationVersion string `json:"association_version"`
Date string `json:"date"`
LastUpdateAssociationDate string `json:"last_update_association_date"`
Status AssociationStatus `json:"status"`
Overview AssociationOverview `json:"overview"`
DocumentVersion string `json:"document_version"`
AssociationId string `json:"association_id"`
Targets []AssociationTarget `json:"targets"`
LastExecutionDate string `json:"last_execution_date"`
LastSuccessfulExecutionDate string `json:"last_successful_execution_date"`
ApplyOnlyAtCronInterval bool `json:"apply_only_at_cron_interval"`
}
type AssociationStatus struct {
Date string `json:"date"`
Name string `json:"name"`
Message string `json:"message"`
}
type AssociationOverview struct {
Status string `json:"status"`
DetailedStatus string `json:"detailed_status"`
}

type AssociationTarget struct {
Key string `json:"key"`
Values []string `json:"values"`
}

func toSSMAssociationDescription(rawDesc *ssm.DescribeAssociationOutput) *AssociationDescription {
const dateLayout = "2006-01-02 15:04:05 +0000"

return &AssociationDescription{
Name: aws.StringValue(rawDesc.AssociationDescription.Name),
InstanceId: aws.StringValue(rawDesc.AssociationDescription.InstanceId),
AssociationVersion: aws.StringValue(rawDesc.AssociationDescription.AssociationVersion),
Date: rawDesc.AssociationDescription.Date.Format(dateLayout),
LastUpdateAssociationDate: rawDesc.AssociationDescription.LastUpdateAssociationDate.Format(dateLayout),
Status: AssociationStatus{
Date: rawDesc.AssociationDescription.Status.Date.Format(dateLayout),
Name: aws.StringValue(rawDesc.AssociationDescription.Status.Name),
Message: aws.StringValue(rawDesc.AssociationDescription.Status.Message),
},
Overview: AssociationOverview{
Status: aws.StringValue(rawDesc.AssociationDescription.Overview.Status),
DetailedStatus: aws.StringValue(rawDesc.AssociationDescription.Overview.DetailedStatus),
},
DocumentVersion: aws.StringValue(rawDesc.AssociationDescription.DocumentVersion),
AssociationId: aws.StringValue(rawDesc.AssociationDescription.AssociationId),
Targets: parseAssociationTargets(rawDesc.AssociationDescription.Targets),
LastExecutionDate: rawDesc.AssociationDescription.LastExecutionDate.Format(dateLayout),
LastSuccessfulExecutionDate: rawDesc.AssociationDescription.LastSuccessfulExecutionDate.Format(dateLayout),
ApplyOnlyAtCronInterval: aws.BoolValue(rawDesc.AssociationDescription.ApplyOnlyAtCronInterval),
}
}

func parseAssociationTargets(rawTgts []*ssm.Target) (tgts []AssociationTarget) {
for _, rt := range rawTgts {
t := AssociationTarget{Key: aws.StringValue(rt.Key)}
for _, rv := range rt.Values {
t.Values = append(t.Values, aws.StringValue(rv))
}
tgts = append(tgts, t)
}
return tgts
}
26 changes: 26 additions & 0 deletions ssm/association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ssm

import (
"context"

"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"
)

func (s *SSM) DescribeAssociation(ctx context.Context, instanceId, docName string) (*ssm.DescribeAssociationOutput, error) {
if instanceId == "" || docName == "" {
return nil, apierror.New(apierror.ErrBadRequest, "both instanceId and docName should be present", nil)
}
out, err := s.Service.DescribeAssociationWithContext(ctx, &ssm.DescribeAssociationInput{
Name: aws.String(docName),
InstanceId: aws.String(instanceId),
})
if err != nil {
return nil, common.ErrCode("failed to describe association", err)
}
log.Debugf("got output describing SSM Association: %+v", out)
return out, nil
}
111 changes: 111 additions & 0 deletions ssm/association_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package ssm

import (
"context"
"errors"
"reflect"
"testing"

"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/aws/session"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
)

const (
mockDocName = "doc_341"
)

func (m *mockSSMClient) DescribeAssociationWithContext(ctx context.Context, inp *ssm.DescribeAssociationInput, _ ...request.Option) (*ssm.DescribeAssociationOutput, error) {
if m.err != nil {
return nil, m.err
}
if *inp.InstanceId != mockInstanceId || *inp.Name != mockDocName {
return nil, errors.New("mockssmclient: unknown instance id or doc name")
}
return &ssm.DescribeAssociationOutput{
AssociationDescription: &ssm.AssociationDescription{
Name: inp.Name,
InstanceId: inp.InstanceId,
},
}, nil
}

func TestSSM_DescribeAssociation(t *testing.T) {
type fields struct {
session *session.Session
Service ssmiface.SSMAPI
}
type args struct {
ctx context.Context
instanceId string
docName string
}
tests := []struct {
name string
fields fields
args args
want *ssm.DescribeAssociationOutput
wantErr bool
}{
{
name: "valid input",
fields: fields{Service: newMockSSMClient(t, nil)},
args: args{ctx: context.TODO(), instanceId: mockInstanceId, docName: mockDocName},
want: &ssm.DescribeAssociationOutput{
AssociationDescription: &ssm.AssociationDescription{
Name: aws.String(mockDocName),
InstanceId: aws.String(mockInstanceId),
},
},
},
{
name: "valid input, error from aws",
fields: fields{Service: newMockSSMClient(t, awserr.New("Bad Request", "boom.", nil))},
args: args{ctx: context.TODO(), instanceId: mockInstanceId, docName: mockDocName},
wantErr: true,
},
{
name: "missing instance id",
fields: fields{Service: newMockSSMClient(t, nil)},
args: args{ctx: context.TODO(), docName: mockDocName},
wantErr: true,
},
{
name: "unknown instance id",
fields: fields{Service: newMockSSMClient(t, nil)},
args: args{ctx: context.TODO(), instanceId: "xyz", docName: mockDocName},
wantErr: true,
},
{
name: "missing command id",
fields: fields{Service: newMockSSMClient(t, nil)},
args: args{ctx: context.TODO(), instanceId: mockInstanceId},
wantErr: true,
},
{
name: "unknown command id",
fields: fields{Service: newMockSSMClient(t, nil)},
args: args{ctx: context.TODO(), instanceId: mockInstanceId, docName: "xyz"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &SSM{
session: tt.fields.session,
Service: tt.fields.Service,
}
got, err := s.DescribeAssociation(tt.args.ctx, tt.args.instanceId, tt.args.docName)
if (err != nil) != tt.wantErr {
t.Errorf("SSM.DescribeAssociation() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Ec2.DescribeAssociation() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit a27aa1c

Please sign in to comment.