1package godo
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7	"time"
8)
9
10const (
11	storageBasePath  = "v2"
12	storageAllocPath = storageBasePath + "/volumes"
13	storageSnapPath  = storageBasePath + "/snapshots"
14)
15
16// StorageService is an interface for interfacing with the storage
17// endpoints of the Digital Ocean API.
18// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Block-Storage
19type StorageService interface {
20	ListVolumes(context.Context, *ListVolumeParams) ([]Volume, *Response, error)
21	GetVolume(context.Context, string) (*Volume, *Response, error)
22	CreateVolume(context.Context, *VolumeCreateRequest) (*Volume, *Response, error)
23	DeleteVolume(context.Context, string) (*Response, error)
24	ListSnapshots(ctx context.Context, volumeID string, opts *ListOptions) ([]Snapshot, *Response, error)
25	GetSnapshot(context.Context, string) (*Snapshot, *Response, error)
26	CreateSnapshot(context.Context, *SnapshotCreateRequest) (*Snapshot, *Response, error)
27	DeleteSnapshot(context.Context, string) (*Response, error)
28}
29
30// StorageServiceOp handles communication with the storage volumes related methods of the
31// DigitalOcean API.
32type StorageServiceOp struct {
33	client *Client
34}
35
36// ListVolumeParams stores the options you can set for a ListVolumeCall
37type ListVolumeParams struct {
38	Region      string       `json:"region"`
39	Name        string       `json:"name"`
40	ListOptions *ListOptions `json:"list_options,omitempty"`
41}
42
43var _ StorageService = &StorageServiceOp{}
44
45// Volume represents a Digital Ocean block store volume.
46type Volume struct {
47	ID              string    `json:"id"`
48	Region          *Region   `json:"region"`
49	Name            string    `json:"name"`
50	SizeGigaBytes   int64     `json:"size_gigabytes"`
51	Description     string    `json:"description"`
52	DropletIDs      []int     `json:"droplet_ids"`
53	CreatedAt       time.Time `json:"created_at"`
54	FilesystemType  string    `json:"filesystem_type"`
55	FilesystemLabel string    `json:"filesystem_label"`
56	Tags            []string  `json:"tags"`
57}
58
59func (f Volume) String() string {
60	return Stringify(f)
61}
62
63// URN returns the volume ID as a valid DO API URN
64func (f Volume) URN() string {
65	return ToURN("Volume", f.ID)
66}
67
68type storageVolumesRoot struct {
69	Volumes []Volume `json:"volumes"`
70	Links   *Links   `json:"links"`
71	Meta    *Meta    `json:"meta"`
72}
73
74type storageVolumeRoot struct {
75	Volume *Volume `json:"volume"`
76	Links  *Links  `json:"links,omitempty"`
77}
78
79// VolumeCreateRequest represents a request to create a block store
80// volume.
81type VolumeCreateRequest struct {
82	Region          string   `json:"region"`
83	Name            string   `json:"name"`
84	Description     string   `json:"description"`
85	SizeGigaBytes   int64    `json:"size_gigabytes"`
86	SnapshotID      string   `json:"snapshot_id"`
87	FilesystemType  string   `json:"filesystem_type"`
88	FilesystemLabel string   `json:"filesystem_label"`
89	Tags            []string `json:"tags"`
90}
91
92// ListVolumes lists all storage volumes.
93func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolumeParams) ([]Volume, *Response, error) {
94	path := storageAllocPath
95	if params != nil {
96		if params.Region != "" && params.Name != "" {
97			path = fmt.Sprintf("%s?name=%s&region=%s", path, params.Name, params.Region)
98		} else if params.Region != "" {
99			path = fmt.Sprintf("%s?region=%s", path, params.Region)
100		} else if params.Name != "" {
101			path = fmt.Sprintf("%s?name=%s", path, params.Name)
102		}
103
104		if params.ListOptions != nil {
105			var err error
106			path, err = addOptions(path, params.ListOptions)
107			if err != nil {
108				return nil, nil, err
109			}
110		}
111	}
112
113	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
114	if err != nil {
115		return nil, nil, err
116	}
117
118	root := new(storageVolumesRoot)
119	resp, err := svc.client.Do(ctx, req, root)
120	if err != nil {
121		return nil, resp, err
122	}
123
124	if l := root.Links; l != nil {
125		resp.Links = l
126	}
127	if m := root.Meta; m != nil {
128		resp.Meta = m
129	}
130
131	return root.Volumes, resp, nil
132}
133
134// CreateVolume creates a storage volume. The name must be unique.
135func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
136	path := storageAllocPath
137
138	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
139	if err != nil {
140		return nil, nil, err
141	}
142
143	root := new(storageVolumeRoot)
144	resp, err := svc.client.Do(ctx, req, root)
145	if err != nil {
146		return nil, resp, err
147	}
148	return root.Volume, resp, nil
149}
150
151// GetVolume retrieves an individual storage volume.
152func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) {
153	path := fmt.Sprintf("%s/%s", storageAllocPath, id)
154
155	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
156	if err != nil {
157		return nil, nil, err
158	}
159
160	root := new(storageVolumeRoot)
161	resp, err := svc.client.Do(ctx, req, root)
162	if err != nil {
163		return nil, resp, err
164	}
165
166	return root.Volume, resp, nil
167}
168
169// DeleteVolume deletes a storage volume.
170func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) {
171	path := fmt.Sprintf("%s/%s", storageAllocPath, id)
172
173	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
174	if err != nil {
175		return nil, err
176	}
177	return svc.client.Do(ctx, req, nil)
178}
179
180// SnapshotCreateRequest represents a request to create a block store
181// volume.
182type SnapshotCreateRequest struct {
183	VolumeID    string   `json:"volume_id"`
184	Name        string   `json:"name"`
185	Description string   `json:"description"`
186	Tags        []string `json:"tags"`
187}
188
189// ListSnapshots lists all snapshots related to a storage volume.
190func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, opt *ListOptions) ([]Snapshot, *Response, error) {
191	path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, volumeID)
192	path, err := addOptions(path, opt)
193	if err != nil {
194		return nil, nil, err
195	}
196
197	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
198	if err != nil {
199		return nil, nil, err
200	}
201
202	root := new(snapshotsRoot)
203	resp, err := svc.client.Do(ctx, req, root)
204	if err != nil {
205		return nil, resp, err
206	}
207
208	if l := root.Links; l != nil {
209		resp.Links = l
210	}
211	if m := root.Meta; m != nil {
212		resp.Meta = m
213	}
214
215	return root.Snapshots, resp, nil
216}
217
218// CreateSnapshot creates a snapshot of a storage volume.
219func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
220	path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
221
222	req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
223	if err != nil {
224		return nil, nil, err
225	}
226
227	root := new(snapshotRoot)
228	resp, err := svc.client.Do(ctx, req, root)
229	if err != nil {
230		return nil, resp, err
231	}
232	return root.Snapshot, resp, nil
233}
234
235// GetSnapshot retrieves an individual snapshot.
236func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) {
237	path := fmt.Sprintf("%s/%s", storageSnapPath, id)
238
239	req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
240	if err != nil {
241		return nil, nil, err
242	}
243
244	root := new(snapshotRoot)
245	resp, err := svc.client.Do(ctx, req, root)
246	if err != nil {
247		return nil, resp, err
248	}
249
250	return root.Snapshot, resp, nil
251}
252
253// DeleteSnapshot deletes a snapshot.
254func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) {
255	path := fmt.Sprintf("%s/%s", storageSnapPath, id)
256
257	req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
258	if err != nil {
259		return nil, err
260	}
261	return svc.client.Do(ctx, req, nil)
262}
263