Skip to content

Commit

Permalink
support for creating instances
Browse files Browse the repository at this point in the history
  • Loading branch information
Tenyo Grozev committed Mar 31, 2022
1 parent c943798 commit 81a5975
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 112 deletions.
81 changes: 69 additions & 12 deletions api/handlers_instances.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
Expand All @@ -11,17 +12,79 @@ import (
"github.com/gorilla/mux"
)

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

role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName)
req := Ec2InstanceCreateRequest{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
msg := fmt.Sprintf("cannot decode body into create instance input: %s", err)
handleError(w, apierror.New(apierror.ErrBadRequest, msg, err))
return
}

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

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

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

if req.Sgs == nil || len(req.Sgs) < 1 {
handleError(w, apierror.New(apierror.ErrBadRequest, "missing required field: sgs", nil))
return
}

if req.CpuCredits != nil && *req.CpuCredits != "standard" && *req.CpuCredits != "unlimited" {
handleError(w, apierror.New(apierror.ErrBadRequest, "invalid value for cpu_credits: must be standard or unlimited", nil))
return
}

policy, err := instanceCreatePolicy()
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
}

out, err := orch.createInstance(r.Context(), &req)
if err != nil {
handleError(w, err)
return
}

handleResponseOk(w, out)
}

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

session, err := s.assumeRole(
r.Context(),
s.session.ExternalID,
role,
fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName),
"",
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
)
Expand Down Expand Up @@ -76,12 +139,10 @@ func (s *server) InstanceGetHandler(w http.ResponseWriter, r *http.Request) {
account := s.mapAccountNumber(vars["account"])
id := vars["id"]

role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName)

session, err := s.assumeRole(
r.Context(),
s.session.ExternalID,
role,
fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName),
"",
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
)
Expand Down Expand Up @@ -112,12 +173,10 @@ func (s *server) InstanceVolumesHandler(w http.ResponseWriter, r *http.Request)
id := vars["id"]
vid := vars["vid"]

role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName)

session, err := s.assumeRole(
r.Context(),
s.session.ExternalID,
role,
fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName),
"",
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
)
Expand Down Expand Up @@ -156,12 +215,10 @@ func (s *server) InstanceListSnapshotsHandler(w http.ResponseWriter, r *http.Req
account := s.mapAccountNumber(vars["account"])
id := vars["id"]

role := fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName)

session, err := s.assumeRole(
r.Context(),
s.session.ExternalID,
role,
fmt.Sprintf("arn:aws:iam::%s:role/%s", account, s.session.RoleName),
"",
"arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess",
)
Expand Down
77 changes: 77 additions & 0 deletions api/orchestration_instances.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package api

import (
"context"
"strings"

"github.com/YaleSpinup/apierror"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
log "github.com/sirupsen/logrus"
)

func (o *ec2Orchestrator) createInstance(ctx context.Context, req *Ec2InstanceCreateRequest) (string, error) {
if req == nil {
return "", apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

log.Debugf("got request to create instance: %s", awsutil.Prettify(req))

input := &ec2.RunInstancesInput{
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
InstanceType: req.Type,
ImageId: req.Image,
SubnetId: req.Subnet,
SecurityGroupIds: req.Sgs,
KeyName: req.Key,
UserData: req.Userdata64,
}

if req.BlockDevices != nil {
input.BlockDeviceMappings = blockDeviceMappingsFromRequest(req.BlockDevices)
}

if req.InstanceProfile != nil {
input.IamInstanceProfile = &ec2.IamInstanceProfileSpecification{
Name: req.InstanceProfile,
}
}

// set CpuCredits parameter for burstable instances
// default to standard, unless specified
if strings.HasPrefix(aws.StringValue(req.Type), "t") {
cpucredits := aws.String("standard")
if req.CpuCredits != nil {
cpucredits = req.CpuCredits
}
input.CreditSpecification = &ec2.CreditSpecificationRequest{
CpuCredits: cpucredits,
}
}

out, err := o.ec2Client.CreateInstance(ctx, input)
if err != nil {
return "", err
}

return aws.StringValue(out.InstanceId), nil
}

func blockDeviceMappingsFromRequest(r []Ec2BlockDevice) []*ec2.BlockDeviceMapping {
blockDeviceMappings := []*ec2.BlockDeviceMapping{}

for _, bd := range r {
blockDeviceMappings = append(blockDeviceMappings, &ec2.BlockDeviceMapping{
DeviceName: bd.DeviceName,
Ebs: &ec2.EbsBlockDevice{
Encrypted: bd.Ebs.Encrypted,
VolumeSize: bd.Ebs.VolumeSize,
VolumeType: bd.Ebs.VolumeType,
},
})
}

return blockDeviceMappings
}
24 changes: 24 additions & 0 deletions api/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ func orgTagAccessPolicy(org string) (string, error) {
return string(j), nil
}

func instanceCreatePolicy() (string, error) {
log.Debugf("generating instance crete policy document")

policy := iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iam.StatementEntry{
{
Effect: "Allow",
Action: []string{
"ec2:RunInstances",
},
Resource: []string{"*"},
},
},
}

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

return string(j), nil
}

func sgDeletePolicy(id string) (string, error) {
log.Debugf("generating sg delete policy document")

Expand Down
2 changes: 1 addition & 1 deletion api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/vpcs", s.VpcListHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/vpcs/{id}", s.ProxyRequestHandler).Methods(http.MethodGet)

api.HandleFunc("/{account}/instances", s.ProxyRequestHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/instances", s.InstanceCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/instances/{id}/volumes", s.ProxyRequestHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/sgs", s.SecurityGroupCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/volumes", s.ProxyRequestHandler).Methods(http.MethodPost)
Expand Down
23 changes: 23 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ func tzTimeFormat(t *time.Time) string {
return t.UTC().Format("2006-01-02 15:04:05 MST")
}

type Ec2InstanceCreateRequest struct {
Type *string `json:"type"`
Image *string `json:"image"`
Subnet *string `json:"subnet"`
Sgs []*string `json:"sgs"`
CpuCredits *string `json:"cpu_credits"`
InstanceProfile *string `json:"instanceprofile"`
Key *string `json:"key"`
Userdata64 *string `json:"userdata64"`
BlockDevices []Ec2BlockDevice `json:"block_devices"`
}

type Ec2BlockDevice struct {
DeviceName *string `json:"device_name"`
Ebs *Ec2EbsVolume `json:"ebs"`
}

type Ec2EbsVolume struct {
Encrypted *bool `json:"encrypted"`
VolumeSize *int64 `json:"volume_size"`
VolumeType *string `json:"volume_type"`
}

type Volume struct {
AttachTime string `json:"attach_time"`
DeleteOnTermination bool `json:"delete_on_termination"`
Expand Down
22 changes: 22 additions & 0 deletions ec2/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ import (
log "github.com/sirupsen/logrus"
)

// CreateInstance creates a new instance and returns the instance details
func (e *Ec2) CreateInstance(ctx context.Context, input *ec2.RunInstancesInput) (*ec2.Instance, error) {
if input == nil {
return nil, apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

log.Infof("creating instance of type %s", aws.StringValue(input.InstanceType))

out, err := e.Service.RunInstancesWithContext(ctx, input)
if err != nil {
return nil, ErrCode("failed to create instance", err)
}

log.Debugf("got output creating instance: %+v", out)

if out == nil || len(out.Instances) != 1 {
return nil, apierror.New(apierror.ErrBadRequest, "Unexpected instance count", nil)
}

return out.Instances[0], nil
}

// ListInstances lists the instances that are not terminated and not spot
func (e *Ec2) ListInstances(ctx context.Context, org string, per int64, next *string) ([]map[string]*string, *string, error) {
log.Infof("listing ec2 instances")
Expand Down
Loading

0 comments on commit 81a5975

Please sign in to comment.