Skip to content

Commit

Permalink
Support for Modifying instances state (#25)
Browse files Browse the repository at this point in the history
* Migrate Modify Instances State

* Added comments

* InstanceState Unit test
  • Loading branch information
nvnyale authored Apr 28, 2022
1 parent 072fec8 commit 718f11f
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 3 deletions.
44 changes: 44 additions & 0 deletions api/handlers_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,3 +348,47 @@ func (s *server) DescribeAssociationHandler(w http.ResponseWriter, r *http.Reque
}
handleResponseOk(w, toSSMAssociationDescription(out))
}

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

req := &Ec2InstanceStateChangeRequest{}
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
msg := fmt.Sprintf("cannot decode body into change power input: %s", err)
handleError(w, apierror.New(apierror.ErrBadRequest, msg, err))
return
}

if req.State == "" {
handleError(w, apierror.New(apierror.ErrBadRequest, "missing required field: state", nil))
return
}

policy, err := changeInstanceStatePolicy()
if err != nil {
handleError(w, err)
return
}

orch, err := s.newEc2Orchestrator(r.Context(), &sessionParams{
role: fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName),
inlinePolicy: policy,
policyArns: []string{
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
},
})
if err != nil {
handleError(w, err)
return
}

if err := orch.instancesState(r.Context(), req.State, id); err != nil {
handleError(w, err)
return
}

w.WriteHeader(http.StatusNoContent)
}
22 changes: 22 additions & 0 deletions api/orchestration_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"fmt"
"strings"

"github.com/YaleSpinup/apierror"
Expand Down Expand Up @@ -89,3 +90,24 @@ func blockDeviceMappingsFromRequest(r []Ec2BlockDevice) []*ec2.BlockDeviceMappin

return blockDeviceMappings
}

// instancesState is used to start, stop and reboot a given instance
func (o *ec2Orchestrator) instancesState(ctx context.Context, state string, ids ...string) error {
if len(ids) == 0 || state == "" {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

state = strings.ToLower(state)
switch state {
case "start":
return o.ec2Client.StartInstance(ctx, ids...)
case "stop", "poweroff":
isForce := state == "poweroff"
return o.ec2Client.StopInstance(ctx, isForce, ids...)
case "reboot":
return o.ec2Client.RebootInstance(ctx, ids...)
default:
msg := fmt.Sprintf("unknown power state %q", state)
return apierror.New(apierror.ErrBadRequest, msg, nil)
}
}
28 changes: 26 additions & 2 deletions api/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,7 @@ func sgUpdatePolicy(id string) (string, error) {
}

func tagCreatePolicy() (string, error) {
log.Debugf("generating tag crete policy document")

log.Debugf("generating tag create policy document")
policy := iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iam.StatementEntry{
Expand Down Expand Up @@ -242,3 +241,28 @@ func volumeDeletePolicy(id string) (string, error) {

return string(j), nil
}

func changeInstanceStatePolicy() (string, error) {
log.Debugf("generating power update policy document")
policy := iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iam.StatementEntry{
{
Effect: "Allow",
Action: []string{
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances",
},
Resource: []string{"*"},
},
},
}

j, err := json.Marshal(policy)
if err != nil {
return "", err
}

return string(j), nil
}
2 changes: 1 addition & 1 deletion api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (s *server) routes() {

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}/power", s.ProxyRequestHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/instances/{id}/power", s.InstanceStateHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/instances/{id}/ssm/command", s.ProxyRequestHandler).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)
Expand Down
4 changes: 4 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,7 @@ func parseAssociationTargets(rawTgts []*ssm.Target) (tgts []AssociationTarget) {
}
return tgts
}

type Ec2InstanceStateChangeRequest struct {
State string
}
43 changes: 43 additions & 0 deletions ec2/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,46 @@ func (e *Ec2) GetInstanceVolume(ctx context.Context, id, volid string) (*ec2.Vol

return out.Volumes[0], nil
}

func (e *Ec2) StartInstance(ctx context.Context, ids ...string) error {
if len(ids) == 0 {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}
log.Infof("starting instance %s/%v", e.org, ids)
inp := &ec2.StartInstancesInput{
InstanceIds: aws.StringSlice(ids),
}
if _, err := e.Service.StartInstancesWithContext(ctx, inp); err != nil {
return common.ErrCode("starting instance", err)
}
return nil
}

func (e *Ec2) StopInstance(ctx context.Context, force bool, ids ...string) error {
if len(ids) == 0 {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}
log.Infof("stopping instance %s/%v", e.org, ids)
inp := &ec2.StopInstancesInput{
Force: aws.Bool(force),
InstanceIds: aws.StringSlice(ids),
}
if _, err := e.Service.StopInstancesWithContext(ctx, inp); err != nil {
return common.ErrCode("stopping instance", err)
}
return nil
}

func (e *Ec2) RebootInstance(ctx context.Context, ids ...string) error {
if len(ids) == 0 {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}
log.Infof("rebooting instance %s/%v", e.org, ids)
inp := &ec2.StartInstancesInput{
InstanceIds: aws.StringSlice(ids),
}
if _, err := e.Service.StartInstancesWithContext(ctx, inp); err != nil {
return common.ErrCode("rebooting instance", err)
}
return nil
}
Loading

0 comments on commit 718f11f

Please sign in to comment.