Skip to content

Commit

Permalink
Support for creating/deleting volumes (#24)
Browse files Browse the repository at this point in the history
* add support for creating volumes

* add support for deleting volumes

* fixup

* update modules
  • Loading branch information
tenyo authored Apr 26, 2022
1 parent 45bc9d9 commit 072fec8
Show file tree
Hide file tree
Showing 11 changed files with 466 additions and 27 deletions.
8 changes: 7 additions & 1 deletion api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,14 @@ func NotImplemented(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Not Implemented"))
}

// handleResponseOk handles the standard response
// handleResponseOk handles the standard success response
// returns 200 or 204 (if no response content)
func handleResponseOk(w http.ResponseWriter, response interface{}) {
if response == nil {
w.WriteHeader(http.StatusNoContent)
return
}

j, err := json.Marshal(response)
if err != nil {
log.Errorf("cannot marshal response (%v) into JSON: %s", response, err)
Expand Down
2 changes: 1 addition & 1 deletion api/handlers_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (s *server) InstanceDeleteHandler(w http.ResponseWriter, r *http.Request) {
return
}

handleResponseOk(w, "OK")
handleResponseOk(w, nil)
}

func (s *server) InstanceListHandler(w http.ResponseWriter, r *http.Request) {
Expand Down
95 changes: 95 additions & 0 deletions api/handlers_volumes.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,6 +12,100 @@ import (
"github.com/gorilla/mux"
)

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

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

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

if req.Size == nil && req.SnapshotId == nil {
handleError(w, apierror.New(apierror.ErrBadRequest, "at least one of these must be specified: size, snapshot_id", nil))
return
}

if aws.Int64Value(req.Size) < 1 || aws.Int64Value(req.Size) > 16384 {
handleError(w, apierror.New(apierror.ErrBadRequest, "volume size must be between 1 and 16384", nil))
return
}

if req.Type == nil {
req.Type = aws.String("gp3")
}

if req.Encrypted == nil {
req.Encrypted = aws.Bool(false)
}

policy, err := volumeCreatePolicy()
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.createVolume(r.Context(), &req)
if err != nil {
handleError(w, err)
return
}

handleResponseOk(w, out)
}

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

policy, err := volumeDeletePolicy(id)
if err != nil {
handleError(w, apierror.New(apierror.ErrInternalError, "failed to generate policy", 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.deleteVolume(r.Context(), id); err != nil {
handleError(w, err)
return
}

handleResponseOk(w, nil)
}

func (s *server) VolumeListHandler(w http.ResponseWriter, r *http.Request) {
w = LogWriter{w}
vars := mux.Vars(r)
Expand Down
50 changes: 50 additions & 0 deletions api/orchestration_volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package api

import (
"context"

"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) createVolume(ctx context.Context, req *Ec2VolumeCreateRequest) (string, error) {
if req == nil {
return "", apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

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

input := &ec2.CreateVolumeInput{
AvailabilityZone: req.AZ,
Encrypted: req.Encrypted,
Iops: req.Iops,
KmsKeyId: req.KmsKeyId,
Size: req.Size,
SnapshotId: req.SnapshotId,
VolumeType: req.Type,
}

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

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

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

log.Debugf("got request to delete volume %s", id)

if err := o.ec2Client.DeleteVolume(ctx, id); err != nil {
return err
}

return nil
}
50 changes: 50 additions & 0 deletions api/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ func sgUpdatePolicy(id string) (string, error) {
}

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

policy := iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iam.StatementEntry{
Expand All @@ -192,3 +194,51 @@ func tagCreatePolicy() (string, error) {

return string(j), nil
}

func volumeCreatePolicy() (string, error) {
log.Debugf("generating volume crete policy document")

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

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

return string(j), nil
}

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

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

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

return string(j), nil
}
4 changes: 2 additions & 2 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (s *server) routes() {
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)
api.HandleFunc("/{account}/volumes", s.VolumeCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/snapshots", s.ProxyRequestHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/images", s.ProxyRequestHandler).Methods(http.MethodPost)

Expand All @@ -80,7 +80,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/instances/{id}/volumes/{vid}", s.ProxyRequestHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/instanceprofiles/{name}", s.ProxyRequestHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/volumes/{id}", s.ProxyRequestHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/volumes/{id}", s.VolumeDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/snapshots/{id}", s.ProxyRequestHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/images/{id}", s.ProxyRequestHandler).Methods(http.MethodDelete)
}
10 changes: 10 additions & 0 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ type Ec2BlockDevice struct {
Ebs *Ec2EbsVolume `json:"ebs"`
}

type Ec2VolumeCreateRequest struct {
Type *string `json:"type"`
Size *int64 `json:"size"`
Iops *int64 `json:"iops"`
AZ *string `json:"az"`
SnapshotId *string `json:"snapshot_id"`
KmsKeyId *string `json:"kms_key_id"`
Encrypted *bool `json:"encrypted"`
}

type Ec2EbsVolume struct {
Encrypted *bool `json:"encrypted"`
VolumeSize *int64 `json:"volume_size"`
Expand Down
41 changes: 41 additions & 0 deletions ec2/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,47 @@ import (
log "github.com/sirupsen/logrus"
)

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

log.Infof("creating volume of type %s, size %d", aws.StringValue(input.VolumeType), aws.Int64Value(input.Size))

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

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

if out == nil {
return nil, apierror.New(apierror.ErrInternalError, "Unexpected volume output", nil)
}

return out, nil
}

func (e *Ec2) DeleteVolume(ctx context.Context, id string) error {
if id == "" {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

log.Infof("deleting volume %s", id)

out, err := e.Service.DeleteVolumeWithContext(ctx, &ec2.DeleteVolumeInput{
VolumeId: aws.String(id),
})
if err != nil {
return common.ErrCode("failed to delete volume", err)
}

log.Debugf("got output deleting volume: %+v", out)

return nil
}

func (e *Ec2) ListVolumes(ctx context.Context, org string, per int64, next *string) ([]map[string]*string, *string, error) {
log.Infof("listing volumes")

Expand Down
Loading

0 comments on commit 072fec8

Please sign in to comment.