-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for updating instances #27
Changes from 4 commits
f223778
2409e78
a17b0ef
c48bb62
11782be
3427772
a683732
f8c75ee
f67007e
9cfb7a9
e049389
d944c1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -443,3 +443,112 @@ func (s *server) InstanceSendCommandHandler(w http.ResponseWriter, r *http.Reque | |
handleResponseOk(w, out) | ||
|
||
} | ||
|
||
func (s *server) InstanceIDHandler(w http.ResponseWriter, r *http.Request) { | ||
w = LogWriter{w} | ||
w.WriteHeader(http.StatusNotImplemented) | ||
} | ||
|
||
func (s *server) InstanceSSMAssociationHandler(w http.ResponseWriter, r *http.Request) { | ||
w = LogWriter{w} | ||
vars := mux.Vars(r) | ||
account := s.mapAccountNumber(vars["account"]) | ||
instanceId := vars["id"] | ||
|
||
req := &SSMAssociationRequest{} | ||
if err := json.NewDecoder(r.Body).Decode(req); err != nil { | ||
msg := fmt.Sprintf("cannot decode body into ssm create input: %s", err) | ||
handleError(w, apierror.New(apierror.ErrBadRequest, msg, err)) | ||
return | ||
} | ||
|
||
if req.Document == "" { | ||
handleError(w, apierror.New(apierror.ErrBadRequest, "Document is mandatory", 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), | ||
) | ||
|
||
out, err := service.CreateAssociation(r.Context(), instanceId, req.Document) | ||
if err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
|
||
handleResponseOk(w, struct{ AssociationId string }{AssociationId: *out.AssociationDescription.AssociationId}) | ||
} | ||
|
||
func (s *server) InstanceUpdateHandler(w http.ResponseWriter, r *http.Request) { | ||
w = LogWriter{w} | ||
vars := mux.Vars(r) | ||
account := s.mapAccountNumber(vars["account"]) | ||
instanceId := vars["id"] | ||
|
||
req := &Ec2InstanceUpdateRequest{} | ||
if err := json.NewDecoder(r.Body).Decode(req); err != nil { | ||
msg := fmt.Sprintf("cannot decode body into update image input: %s", err) | ||
handleError(w, apierror.New(apierror.ErrBadRequest, msg, err)) | ||
return | ||
} | ||
|
||
if len(req.Tags) == 0 && len(req.InstanceType) == 0 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should also check that both are not > 0 and return an error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
handleError(w, apierror.New(apierror.ErrBadRequest, "missing required fields", nil)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should create and use a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. updated code. |
||
role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName) | ||
policy, err := tagCreatePolicy() | ||
if err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
|
||
session, err := s.assumeRole( | ||
r.Context(), | ||
s.session.ExternalID, | ||
role, | ||
policy, | ||
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess", | ||
) | ||
if err != nil { | ||
msg := fmt.Sprintf("failed to assume role in account: %s", account) | ||
handleError(w, apierror.New(apierror.ErrForbidden, msg, err)) | ||
return | ||
} | ||
|
||
service := ec2.New( | ||
ec2.WithSession(session.Session), | ||
ec2.WithOrg(s.org), | ||
) | ||
|
||
if len(req.Tags) > 0 { | ||
if err := service.UpdateTags(r.Context(), req.Tags, instanceId); err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
} else if len(req.InstanceType) > 0 { | ||
if err := service.UpdateAttributes(r.Context(), req.InstanceType["value"], instanceId); err != nil { | ||
handleError(w, err) | ||
return | ||
} | ||
} | ||
|
||
w.WriteHeader(http.StatusNoContent) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,12 +65,12 @@ func (s *server) routes() { | |
api.HandleFunc("/{account}/images", s.ProxyRequestHandler).Methods(http.MethodPost) | ||
|
||
api.HandleFunc("/{account}/images/{id}/tags", s.ImageUpdateHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}", s.InstanceIDHandler).Methods(http.MethodPut) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For ID's route, handler is not implemented, so I didn't generalize this handler. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, then let's just call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed. |
||
api.HandleFunc("/{account}/instances/{id}/power", s.InstanceStateHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/ssm/command", s.InstanceSendCommandHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/ssm/association", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/tags", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/attribute", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/ssm/association", s.InstanceSSMAssociationHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/tags", s.InstanceUpdateHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/instances/{id}/attribute", s.InstanceUpdateHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupUpdateHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/sgs/{id}/tags", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
api.HandleFunc("/{account}/volumes/{id}", s.ProxyRequestHandler).Methods(http.MethodPut) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package ec2 | ||
|
||
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/ec2" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
func (e *Ec2) UpdateAttributes(ctx context.Context, instanceType, instanceId string) error { | ||
if len(instanceId) == 0 || len(instanceType) == 0 { | ||
return apierror.New(apierror.ErrBadRequest, "invalid input", nil) | ||
} | ||
|
||
log.Infof("updating attributes: %v with instance type %+v", instanceId, instanceType) | ||
|
||
input := ec2.ModifyInstanceAttributeInput{ | ||
InstanceType: &ec2.AttributeValue{Value: aws.String(instanceType)}, | ||
InstanceId: aws.String(instanceId), | ||
} | ||
|
||
if _, err := e.Service.ModifyInstanceAttributeWithContext(ctx, &input); err != nil { | ||
return common.ErrCode("updating attributes", err) | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package ec2 | ||
|
||
import ( | ||
"context" | ||
"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/service/ec2" | ||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface" | ||
) | ||
|
||
func (m *mockEC2Client) ModifyInstanceAttributeWithContext(ctx aws.Context, inp *ec2.ModifyInstanceAttributeInput, opt ...request.Option) (*ec2.ModifyInstanceAttributeOutput, error) { | ||
if m.err != nil { | ||
return nil, m.err | ||
} | ||
return &ec2.ModifyInstanceAttributeOutput{}, nil | ||
|
||
} | ||
func TestEc2_UpdateAttributes(t *testing.T) { | ||
type fields struct { | ||
Service ec2iface.EC2API | ||
} | ||
type args struct { | ||
ctx context.Context | ||
instanceType string | ||
instanceId string | ||
} | ||
tests := []struct { | ||
name string | ||
fields fields | ||
e *Ec2 | ||
args args | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "success case", | ||
args: args{ctx: context.TODO(), instanceType: "Type1", instanceId: "i-123"}, | ||
fields: fields{Service: newmockEC2Client(t, nil)}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "aws error", | ||
args: args{ctx: context.TODO(), instanceType: "Type1", instanceId: "i-123"}, | ||
fields: fields{Service: newmockEC2Client(t, awserr.New("Bad Request", "boom.", nil))}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "invalid input, instance id is empty", | ||
args: args{ctx: context.TODO(), instanceType: "Type1", instanceId: ""}, | ||
fields: fields{Service: newmockEC2Client(t, nil)}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "invalid input, instance type is empty", | ||
args: args{ctx: context.TODO(), instanceType: "", instanceId: "i-123"}, | ||
fields: fields{Service: newmockEC2Client(t, nil)}, | ||
wantErr: true, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
e := &Ec2{ | ||
Service: tt.fields.Service, | ||
} | ||
if err := e.UpdateAttributes(tt.args.ctx, tt.args.instanceType, tt.args.instanceId); (err != nil) != tt.wantErr { | ||
t.Errorf("Ec2.UpdateAttributes() error = %v, wantErr %v", err, tt.wantErr) | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, does this work? you're not passing any IAM policy besides the read-only access
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point, I missed it. I am not familiar with the AWS policies and can you please help me to choose the correct policy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case you're working with SSM (Systems Manager) so you can take a look here first: https://docs.aws.amazon.com/systems-manager/latest/userguide/security_iam_service-with-iam.html
Try to find which IAM permissions are required to do the work (in this case CreateAssociation).
For example, here you can see all the different IAM permissions: https://aws.permissions.cloud/iam/ssm
In some cases it may be a bit of trial-and-error until you get all the permissions right, but in this case should be pretty straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.