1//
2// Copyright (c) 2018, Joyent, Inc. All rights reserved.
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at http://mozilla.org/MPL/2.0/.
7//
8
9package compute
10
11import (
12	"context"
13	"encoding/json"
14	"net/http"
15	"net/url"
16	"path"
17	"time"
18
19	"github.com/joyent/triton-go/client"
20	"github.com/pkg/errors"
21)
22
23type ImagesClient struct {
24	client *client.Client
25}
26
27type ImageFile struct {
28	Compression string `json:"compression"`
29	SHA1        string `json:"sha1"`
30	Size        int64  `json:"size"`
31}
32
33type Image struct {
34	ID           string                 `json:"id"`
35	Name         string                 `json:"name"`
36	OS           string                 `json:"os"`
37	Description  string                 `json:"description"`
38	Version      string                 `json:"version"`
39	Type         string                 `json:"type"`
40	Requirements map[string]interface{} `json:"requirements"`
41	Homepage     string                 `json:"homepage"`
42	Files        []*ImageFile           `json:"files"`
43	PublishedAt  time.Time              `json:"published_at"`
44	Owner        string                 `json:"owner"`
45	Public       bool                   `json:"public"`
46	State        string                 `json:"state"`
47	Tags         map[string]string      `json:"tags"`
48	EULA         string                 `json:"eula"`
49	ACL          []string               `json:"acl"`
50}
51
52type ListImagesInput struct {
53	Name    string
54	OS      string
55	Version string
56	Public  bool
57	State   string
58	Owner   string
59	Type    string
60}
61
62func (c *ImagesClient) List(ctx context.Context, input *ListImagesInput) ([]*Image, error) {
63	fullPath := path.Join("/", c.client.AccountName, "images")
64
65	query := &url.Values{}
66	if input.Name != "" {
67		query.Set("name", input.Name)
68	}
69	if input.OS != "" {
70		query.Set("os", input.OS)
71	}
72	if input.Version != "" {
73		query.Set("version", input.Version)
74	}
75	if input.Public {
76		query.Set("public", "true")
77	}
78	if input.State != "" {
79		query.Set("state", input.State)
80	}
81	if input.Owner != "" {
82		query.Set("owner", input.Owner)
83	}
84	if input.Type != "" {
85		query.Set("type", input.Type)
86	}
87
88	reqInputs := client.RequestInput{
89		Method: http.MethodGet,
90		Path:   fullPath,
91		Query:  query,
92	}
93	respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
94	if respReader != nil {
95		defer respReader.Close()
96	}
97	if err != nil {
98		return nil, errors.Wrap(err, "unable to list images")
99	}
100
101	var result []*Image
102	decoder := json.NewDecoder(respReader)
103	if err = decoder.Decode(&result); err != nil {
104		return nil, errors.Wrap(err, "unable to decode list images response")
105	}
106
107	return result, nil
108}
109
110type GetImageInput struct {
111	ImageID string
112}
113
114func (c *ImagesClient) Get(ctx context.Context, input *GetImageInput) (*Image, error) {
115	fullPath := path.Join("/", c.client.AccountName, "images", input.ImageID)
116	reqInputs := client.RequestInput{
117		Method: http.MethodGet,
118		Path:   fullPath,
119	}
120	respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
121	if respReader != nil {
122		defer respReader.Close()
123	}
124	if err != nil {
125		return nil, errors.Wrap(err, "unable to get image")
126	}
127
128	var result *Image
129	decoder := json.NewDecoder(respReader)
130	if err = decoder.Decode(&result); err != nil {
131		return nil, errors.Wrap(err, "unable to decode get image response")
132	}
133
134	return result, nil
135}
136
137type DeleteImageInput struct {
138	ImageID string
139}
140
141func (c *ImagesClient) Delete(ctx context.Context, input *DeleteImageInput) error {
142	fullPath := path.Join("/", c.client.AccountName, "images", input.ImageID)
143	reqInputs := client.RequestInput{
144		Method: http.MethodDelete,
145		Path:   fullPath,
146	}
147	respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
148	if respReader != nil {
149		defer respReader.Close()
150	}
151	if err != nil {
152		return errors.Wrap(err, "unable to delete image")
153	}
154
155	return nil
156}
157
158type ExportImageInput struct {
159	ImageID   string
160	MantaPath string
161}
162
163type MantaLocation struct {
164	MantaURL     string `json:"manta_url"`
165	ImagePath    string `json:"image_path"`
166	ManifestPath string `json:"manifest_path"`
167}
168
169func (c *ImagesClient) Export(ctx context.Context, input *ExportImageInput) (*MantaLocation, error) {
170	fullPath := path.Join("/", c.client.AccountName, "images", input.ImageID)
171	query := &url.Values{}
172	query.Set("action", "export")
173
174	reqInputs := client.RequestInput{
175		Method: http.MethodPost,
176		Path:   fullPath,
177		Query:  query,
178	}
179	respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
180	if respReader != nil {
181		defer respReader.Close()
182	}
183	if err != nil {
184		return nil, errors.Wrap(err, "unable to export image")
185	}
186
187	var result *MantaLocation
188	decoder := json.NewDecoder(respReader)
189	if err = decoder.Decode(&result); err != nil {
190		return nil, errors.Wrap(err, "unable to decode export image response")
191	}
192
193	return result, nil
194}
195
196type CreateImageFromMachineInput struct {
197	MachineID   string            `json:"machine"`
198	Name        string            `json:"name"`
199	Version     string            `json:"version,omitempty"`
200	Description string            `json:"description,omitempty"`
201	HomePage    string            `json:"homepage,omitempty"`
202	EULA        string            `json:"eula,omitempty"`
203	ACL         []string          `json:"acl,omitempty"`
204	Tags        map[string]string `json:"tags,omitempty"`
205}
206
207func (c *ImagesClient) CreateFromMachine(ctx context.Context, input *CreateImageFromMachineInput) (*Image, error) {
208	fullPath := path.Join("/", c.client.AccountName, "images")
209	reqInputs := client.RequestInput{
210		Method: http.MethodPost,
211		Path:   fullPath,
212		Body:   input,
213	}
214	respReader, err := c.client.ExecuteRequest(ctx, reqInputs)
215	if respReader != nil {
216		defer respReader.Close()
217	}
218	if err != nil {
219		return nil, errors.Wrap(err, "unable to create machine from image")
220	}
221
222	var result *Image
223	decoder := json.NewDecoder(respReader)
224	if err = decoder.Decode(&result); err != nil {
225		return nil, errors.Wrap(err, "unable to decode create machine from image response")
226	}
227
228	return result, nil
229}
230
231type UpdateImageInput struct {
232	ImageID     string            `json:"-"`
233	Name        string            `json:"name,omitempty"`
234	Version     string            `json:"version,omitempty"`
235	Description string            `json:"description,omitempty"`
236	HomePage    string            `json:"homepage,omitempty"`
237	EULA        string            `json:"eula,omitempty"`
238	ACL         []string          `json:"acl,omitempty"`
239	Tags        map[string]string `json:"tags,omitempty"`
240}
241
242func (c *ImagesClient) Update(ctx context.Context, input *UpdateImageInput) (*Image, error) {
243	fullPath := path.Join("/", c.client.AccountName, "images", input.ImageID)
244	query := &url.Values{}
245	query.Set("action", "update")
246
247	reqInputs := client.RequestInput{
248		Method: http.MethodPost,
249		Path:   fullPath,
250		Query:  query,
251		Body:   input,
252	}
253	respReader, err := c.client.ExecuteRequestURIParams(ctx, reqInputs)
254	if respReader != nil {
255		defer respReader.Close()
256	}
257	if err != nil {
258		return nil, errors.Wrap(err, "unable to update image")
259	}
260
261	var result *Image
262	decoder := json.NewDecoder(respReader)
263	if err = decoder.Decode(&result); err != nil {
264		return nil, errors.Wrap(err, "unable to decode update image response")
265	}
266
267	return result, nil
268}
269