Skip to content

Commit

Permalink
Support for Detaching volume in an instance (#38)
Browse files Browse the repository at this point in the history
* Detaching volume from an instance

* Detach volume - work in progress

* Detaching volume-unit test

* Detach Volume Unit test

* Fixed Comments

* fixed format specifier for boolean
  • Loading branch information
nvnyale authored Jun 27, 2022
1 parent 11271cf commit cd1e67c
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 1 deletion.
43 changes: 43 additions & 0 deletions api/handlers_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,46 @@ func (s *server) InstanceUpdateHandler(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusNoContent)
}

func (s *server) VolumeDetachHandler(w http.ResponseWriter, r *http.Request) {
w = LogWriter{w}
vars := mux.Vars(r)
account := s.mapAccountNumber(vars["account"])
instance_id := vars["id"]
volume_id := vars["vid"]
var force bool
if r.URL.Query().Has("force") {
var err error
force, err = strconv.ParseBool(r.URL.Query().Get("force"))
if err != nil {
handleError(w, apierror.New(apierror.ErrBadRequest, "invalid value for force parameter", nil))
return
}
}

policy, err := generatePolicy([]string{"ec2:DetachVolume"})
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.detachVolume(r.Context(), instance_id, volume_id, force)
if err != nil {
handleError(w, err)
return
}
handleResponseOk(w, out)
}
22 changes: 22 additions & 0 deletions api/orchestration_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,28 @@ func (o *ssmOrchestrator) sendInstancesCommand(ctx context.Context, req *SsmComm
}
return aws.StringValue(cmd.CommandId), nil
}

func (o *ec2Orchestrator) detachVolume(ctx context.Context, instanceId, volumeId string, force bool) (string, error) {
if instanceId == "" || volumeId == "" {
return "", apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}

log.Debugf("got request to detach volume: %s from instance %s", volumeId, instanceId)

input := &ec2.DetachVolumeInput{
InstanceId: aws.String(instanceId),
VolumeId: aws.String(volumeId),
Force: aws.Bool(force),
}

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

return aws.StringValue(out), nil
}

func (o *ec2Orchestrator) updateInstanceTags(ctx context.Context, rawTags map[string]string, ids ...string) error {
if len(ids) == 0 || len(rawTags) == 0 {
return apierror.New(apierror.ErrBadRequest, "invalid input", nil)
Expand Down
2 changes: 1 addition & 1 deletion api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/volumes/{id}/tags", s.VolumeUpdateHandler).Methods(http.MethodPut)

api.HandleFunc("/{account}/instances/{id}", s.InstanceDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/instances/{id}/volumes/{vid}", s.ProxyRequestHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/instances/{id}/volumes/{vid}", s.VolumeDetachHandler).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.VolumeDeleteHandler).Methods(http.MethodDelete)
Expand Down
20 changes: 20 additions & 0 deletions ec2/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,23 @@ func (e *Ec2) RebootInstance(ctx context.Context, ids ...string) error {
}
return nil
}

func (e *Ec2) DetachVolume(ctx context.Context, input *ec2.DetachVolumeInput) (*string, error) {
if input == nil {
return nil, apierror.New(apierror.ErrBadRequest, "invalid input", nil)
}
log.Infof("detaching volumes %v, force = %t", input.VolumeId, aws.BoolValue(input.Force))

out, err := e.Service.DetachVolumeWithContext(ctx, input)
if err != nil {
return nil, apierror.New(apierror.ErrInternalError, "failed to detach volume", err)
}

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

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

return out.VolumeId, nil
}
62 changes: 62 additions & 0 deletions ec2/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ func (m mockEC2Client) RebootInstancesWithContext(ctx context.Context, input *ec
return &ec2.RebootInstancesOutput{}, nil
}

func (m mockEC2Client) DetachVolumeWithContext(ctx context.Context, input *ec2.DetachVolumeInput, opts ...request.Option) (*ec2.VolumeAttachment, error) {
if m.err != nil {
return nil, m.err
}

return &ec2.VolumeAttachment{
VolumeId: aws.String("Volume-123")},
nil
}

func TestEc2_CreateInstance(t *testing.T) {
type fields struct {
session *session.Session
Expand Down Expand Up @@ -503,3 +513,55 @@ func TestEc2_RebootInstance(t *testing.T) {
})
}
}

func TestEc2_DetachVolume(t *testing.T) {
type fields struct {
Service ec2iface.EC2API
}
type args struct {
ctx context.Context
input *ec2.DetachVolumeInput
}
tests := []struct {
name string
args args
fields fields
want *string
wantErr bool
}{
{
name: "success case",
args: args{ctx: context.TODO(), input: &ec2.DetachVolumeInput{InstanceId: aws.String("v-123"), Force: aws.Bool(true), VolumeId: aws.String("id-123")}},
fields: fields{Service: newmockEC2Client(t, nil)},
want: aws.String("Volume-123"),
wantErr: false,
},
{
name: "aws error",
args: args{ctx: context.TODO(), input: &ec2.DetachVolumeInput{InstanceId: aws.String("v-123"), Force: aws.Bool(true), VolumeId: aws.String("id-123")}},
fields: fields{Service: newmockEC2Client(t, awserr.New("Bad Request", "boom.", nil))},
wantErr: true,
},
{
name: "nil input",
args: args{ctx: context.TODO(), input: nil},
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,
}
got, err := e.DetachVolume(tt.args.ctx, tt.args.input)
if (err != nil) != tt.wantErr {
t.Errorf("Ec2.DetachVolume() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Ec2.DetachVolume() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit cd1e67c

Please sign in to comment.