1package hcloud
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"fmt"
8	"net/url"
9	"strconv"
10	"time"
11
12	"github.com/hetznercloud/hcloud-go/hcloud/schema"
13)
14
15// Image represents an Image in the Hetzner Cloud.
16type Image struct {
17	ID          int
18	Name        string
19	Type        ImageType
20	Status      ImageStatus
21	Description string
22	ImageSize   float32
23	DiskSize    float32
24	Created     time.Time
25	CreatedFrom *Server
26	BoundTo     *Server
27	RapidDeploy bool
28
29	OSFlavor  string
30	OSVersion string
31
32	Protection ImageProtection
33	Deprecated time.Time // The zero value denotes the image is not deprecated.
34	Labels     map[string]string
35}
36
37// IsDeprecated returns whether the image is deprecated.
38func (image *Image) IsDeprecated() bool {
39	return !image.Deprecated.IsZero()
40}
41
42// ImageProtection represents the protection level of an image.
43type ImageProtection struct {
44	Delete bool
45}
46
47// ImageType specifies the type of an image.
48type ImageType string
49
50const (
51	// ImageTypeSnapshot represents a snapshot image.
52	ImageTypeSnapshot ImageType = "snapshot"
53	// ImageTypeBackup represents a backup image.
54	ImageTypeBackup ImageType = "backup"
55	// ImageTypeSystem represents a system image.
56	ImageTypeSystem ImageType = "system"
57)
58
59// ImageStatus specifies the status of an image.
60type ImageStatus string
61
62const (
63	// ImageStatusCreating is the status when an image is being created.
64	ImageStatusCreating ImageStatus = "creating"
65	// ImageStatusAvailable is the status when an image is available.
66	ImageStatusAvailable ImageStatus = "available"
67)
68
69// ImageClient is a client for the image API.
70type ImageClient struct {
71	client *Client
72}
73
74// GetByID retrieves an image by its ID. If the image does not exist, nil is returned.
75func (c *ImageClient) GetByID(ctx context.Context, id int) (*Image, *Response, error) {
76	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/images/%d", id), nil)
77	if err != nil {
78		return nil, nil, err
79	}
80
81	var body schema.ImageGetResponse
82	resp, err := c.client.Do(req, &body)
83	if err != nil {
84		if IsError(err, ErrorCodeNotFound) {
85			return nil, resp, nil
86		}
87		return nil, nil, err
88	}
89	return ImageFromSchema(body.Image), resp, nil
90}
91
92// GetByName retrieves an image by its name. If the image does not exist, nil is returned.
93func (c *ImageClient) GetByName(ctx context.Context, name string) (*Image, *Response, error) {
94	if name == "" {
95		return nil, nil, nil
96	}
97	images, response, err := c.List(ctx, ImageListOpts{Name: name})
98	if len(images) == 0 {
99		return nil, response, err
100	}
101	return images[0], response, err
102}
103
104// Get retrieves an image by its ID if the input can be parsed as an integer, otherwise it
105// retrieves an image by its name. If the image does not exist, nil is returned.
106func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) {
107	if id, err := strconv.Atoi(idOrName); err == nil {
108		return c.GetByID(ctx, int(id))
109	}
110	return c.GetByName(ctx, idOrName)
111}
112
113// ImageListOpts specifies options for listing images.
114type ImageListOpts struct {
115	ListOpts
116	Type              []ImageType
117	BoundTo           *Server
118	Name              string
119	Sort              []string
120	Status            []ImageStatus
121	IncludeDeprecated bool
122}
123
124func (l ImageListOpts) values() url.Values {
125	vals := l.ListOpts.values()
126	for _, typ := range l.Type {
127		vals.Add("type", string(typ))
128	}
129	if l.BoundTo != nil {
130		vals.Add("bound_to", strconv.Itoa(l.BoundTo.ID))
131	}
132	if l.Name != "" {
133		vals.Add("name", l.Name)
134	}
135	if l.IncludeDeprecated {
136		vals.Add("include_deprecated", strconv.FormatBool(l.IncludeDeprecated))
137	}
138	for _, sort := range l.Sort {
139		vals.Add("sort", sort)
140	}
141	for _, status := range l.Status {
142		vals.Add("status", string(status))
143	}
144	return vals
145}
146
147// List returns a list of images for a specific page.
148//
149// Please note that filters specified in opts are not taken into account
150// when their value corresponds to their zero value or when they are empty.
151func (c *ImageClient) List(ctx context.Context, opts ImageListOpts) ([]*Image, *Response, error) {
152	path := "/images?" + opts.values().Encode()
153	req, err := c.client.NewRequest(ctx, "GET", path, nil)
154	if err != nil {
155		return nil, nil, err
156	}
157
158	var body schema.ImageListResponse
159	resp, err := c.client.Do(req, &body)
160	if err != nil {
161		return nil, nil, err
162	}
163	images := make([]*Image, 0, len(body.Images))
164	for _, i := range body.Images {
165		images = append(images, ImageFromSchema(i))
166	}
167	return images, resp, nil
168}
169
170// All returns all images.
171func (c *ImageClient) All(ctx context.Context) ([]*Image, error) {
172	return c.AllWithOpts(ctx, ImageListOpts{ListOpts: ListOpts{PerPage: 50}})
173}
174
175// AllWithOpts returns all images for the given options.
176func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) {
177	allImages := []*Image{}
178
179	err := c.client.all(func(page int) (*Response, error) {
180		opts.Page = page
181		images, resp, err := c.List(ctx, opts)
182		if err != nil {
183			return resp, err
184		}
185		allImages = append(allImages, images...)
186		return resp, nil
187	})
188	if err != nil {
189		return nil, err
190	}
191
192	return allImages, nil
193}
194
195// Delete deletes an image.
196func (c *ImageClient) Delete(ctx context.Context, image *Image) (*Response, error) {
197	req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/images/%d", image.ID), nil)
198	if err != nil {
199		return nil, err
200	}
201	return c.client.Do(req, nil)
202}
203
204// ImageUpdateOpts specifies options for updating an image.
205type ImageUpdateOpts struct {
206	Description *string
207	Type        ImageType
208	Labels      map[string]string
209}
210
211// Update updates an image.
212func (c *ImageClient) Update(ctx context.Context, image *Image, opts ImageUpdateOpts) (*Image, *Response, error) {
213	reqBody := schema.ImageUpdateRequest{
214		Description: opts.Description,
215	}
216	if opts.Type != "" {
217		reqBody.Type = String(string(opts.Type))
218	}
219	if opts.Labels != nil {
220		reqBody.Labels = &opts.Labels
221	}
222	reqBodyData, err := json.Marshal(reqBody)
223	if err != nil {
224		return nil, nil, err
225	}
226
227	path := fmt.Sprintf("/images/%d", image.ID)
228	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
229	if err != nil {
230		return nil, nil, err
231	}
232
233	respBody := schema.ImageUpdateResponse{}
234	resp, err := c.client.Do(req, &respBody)
235	if err != nil {
236		return nil, resp, err
237	}
238	return ImageFromSchema(respBody.Image), resp, nil
239}
240
241// ImageChangeProtectionOpts specifies options for changing the resource protection level of an image.
242type ImageChangeProtectionOpts struct {
243	Delete *bool
244}
245
246// ChangeProtection changes the resource protection level of an image.
247func (c *ImageClient) ChangeProtection(ctx context.Context, image *Image, opts ImageChangeProtectionOpts) (*Action, *Response, error) {
248	reqBody := schema.ImageActionChangeProtectionRequest{
249		Delete: opts.Delete,
250	}
251	reqBodyData, err := json.Marshal(reqBody)
252	if err != nil {
253		return nil, nil, err
254	}
255
256	path := fmt.Sprintf("/images/%d/actions/change_protection", image.ID)
257	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
258	if err != nil {
259		return nil, nil, err
260	}
261
262	respBody := schema.ImageActionChangeProtectionResponse{}
263	resp, err := c.client.Do(req, &respBody)
264	if err != nil {
265		return nil, resp, err
266	}
267	return ActionFromSchema(respBody.Action), resp, err
268}
269