1package hcloud
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"net/url"
10	"strconv"
11	"time"
12
13	"github.com/hetznercloud/hcloud-go/hcloud/schema"
14)
15
16// SSHKey represents a SSH key in the Hetzner Cloud.
17type SSHKey struct {
18	ID          int
19	Name        string
20	Fingerprint string
21	PublicKey   string
22	Labels      map[string]string
23	Created     time.Time
24}
25
26// SSHKeyClient is a client for the SSH keys API.
27type SSHKeyClient struct {
28	client *Client
29}
30
31// GetByID retrieves a SSH key by its ID. If the SSH key does not exist, nil is returned.
32func (c *SSHKeyClient) GetByID(ctx context.Context, id int) (*SSHKey, *Response, error) {
33	req, err := c.client.NewRequest(ctx, "GET", fmt.Sprintf("/ssh_keys/%d", id), nil)
34	if err != nil {
35		return nil, nil, err
36	}
37
38	var body schema.SSHKeyGetResponse
39	resp, err := c.client.Do(req, &body)
40	if err != nil {
41		if IsError(err, ErrorCodeNotFound) {
42			return nil, resp, nil
43		}
44		return nil, nil, err
45	}
46	return SSHKeyFromSchema(body.SSHKey), resp, nil
47}
48
49// GetByName retrieves a SSH key by its name. If the SSH key does not exist, nil is returned.
50func (c *SSHKeyClient) GetByName(ctx context.Context, name string) (*SSHKey, *Response, error) {
51	if name == "" {
52		return nil, nil, nil
53	}
54	sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Name: name})
55	if len(sshKeys) == 0 {
56		return nil, response, err
57	}
58	return sshKeys[0], response, err
59}
60
61// GetByFingerprint retreives a SSH key by its fingerprint. If the SSH key does not exist, nil is returned.
62func (c *SSHKeyClient) GetByFingerprint(ctx context.Context, fingerprint string) (*SSHKey, *Response, error) {
63	sshKeys, response, err := c.List(ctx, SSHKeyListOpts{Fingerprint: fingerprint})
64	if len(sshKeys) == 0 {
65		return nil, response, err
66	}
67	return sshKeys[0], response, err
68}
69
70// Get retrieves a SSH key by its ID if the input can be parsed as an integer, otherwise it
71// retrieves a SSH key by its name. If the SSH key does not exist, nil is returned.
72func (c *SSHKeyClient) Get(ctx context.Context, idOrName string) (*SSHKey, *Response, error) {
73	if id, err := strconv.Atoi(idOrName); err == nil {
74		return c.GetByID(ctx, int(id))
75	}
76	return c.GetByName(ctx, idOrName)
77}
78
79// SSHKeyListOpts specifies options for listing SSH keys.
80type SSHKeyListOpts struct {
81	ListOpts
82	Name        string
83	Fingerprint string
84}
85
86func (l SSHKeyListOpts) values() url.Values {
87	vals := l.ListOpts.values()
88	if l.Name != "" {
89		vals.Add("name", l.Name)
90	}
91	if l.Fingerprint != "" {
92		vals.Add("fingerprint", l.Fingerprint)
93	}
94	return vals
95}
96
97// List returns a list of SSH keys for a specific page.
98//
99// Please note that filters specified in opts are not taken into account
100// when their value corresponds to their zero value or when they are empty.
101func (c *SSHKeyClient) List(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, *Response, error) {
102	path := "/ssh_keys?" + opts.values().Encode()
103	req, err := c.client.NewRequest(ctx, "GET", path, nil)
104	if err != nil {
105		return nil, nil, err
106	}
107
108	var body schema.SSHKeyListResponse
109	resp, err := c.client.Do(req, &body)
110	if err != nil {
111		return nil, nil, err
112	}
113	sshKeys := make([]*SSHKey, 0, len(body.SSHKeys))
114	for _, s := range body.SSHKeys {
115		sshKeys = append(sshKeys, SSHKeyFromSchema(s))
116	}
117	return sshKeys, resp, nil
118}
119
120// All returns all SSH keys.
121func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) {
122	return c.AllWithOpts(ctx, SSHKeyListOpts{ListOpts: ListOpts{PerPage: 50}})
123}
124
125// AllWithOpts returns all SSH keys with the given options.
126func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) {
127	allSSHKeys := []*SSHKey{}
128
129	err := c.client.all(func(page int) (*Response, error) {
130		opts.Page = page
131		sshKeys, resp, err := c.List(ctx, opts)
132		if err != nil {
133			return resp, err
134		}
135		allSSHKeys = append(allSSHKeys, sshKeys...)
136		return resp, nil
137	})
138	if err != nil {
139		return nil, err
140	}
141
142	return allSSHKeys, nil
143}
144
145// SSHKeyCreateOpts specifies parameters for creating a SSH key.
146type SSHKeyCreateOpts struct {
147	Name      string
148	PublicKey string
149	Labels    map[string]string
150}
151
152// Validate checks if options are valid.
153func (o SSHKeyCreateOpts) Validate() error {
154	if o.Name == "" {
155		return errors.New("missing name")
156	}
157	if o.PublicKey == "" {
158		return errors.New("missing public key")
159	}
160	return nil
161}
162
163// Create creates a new SSH key with the given options.
164func (c *SSHKeyClient) Create(ctx context.Context, opts SSHKeyCreateOpts) (*SSHKey, *Response, error) {
165	if err := opts.Validate(); err != nil {
166		return nil, nil, err
167	}
168	reqBody := schema.SSHKeyCreateRequest{
169		Name:      opts.Name,
170		PublicKey: opts.PublicKey,
171	}
172	if opts.Labels != nil {
173		reqBody.Labels = &opts.Labels
174	}
175	reqBodyData, err := json.Marshal(reqBody)
176	if err != nil {
177		return nil, nil, err
178	}
179
180	req, err := c.client.NewRequest(ctx, "POST", "/ssh_keys", bytes.NewReader(reqBodyData))
181	if err != nil {
182		return nil, nil, err
183	}
184
185	var respBody schema.SSHKeyCreateResponse
186	resp, err := c.client.Do(req, &respBody)
187	if err != nil {
188		return nil, resp, err
189	}
190	return SSHKeyFromSchema(respBody.SSHKey), resp, nil
191}
192
193// Delete deletes a SSH key.
194func (c *SSHKeyClient) Delete(ctx context.Context, sshKey *SSHKey) (*Response, error) {
195	req, err := c.client.NewRequest(ctx, "DELETE", fmt.Sprintf("/ssh_keys/%d", sshKey.ID), nil)
196	if err != nil {
197		return nil, err
198	}
199	return c.client.Do(req, nil)
200}
201
202// SSHKeyUpdateOpts specifies options for updating a SSH key.
203type SSHKeyUpdateOpts struct {
204	Name   string
205	Labels map[string]string
206}
207
208// Update updates a SSH key.
209func (c *SSHKeyClient) Update(ctx context.Context, sshKey *SSHKey, opts SSHKeyUpdateOpts) (*SSHKey, *Response, error) {
210	reqBody := schema.SSHKeyUpdateRequest{
211		Name: opts.Name,
212	}
213	if opts.Labels != nil {
214		reqBody.Labels = &opts.Labels
215	}
216	reqBodyData, err := json.Marshal(reqBody)
217	if err != nil {
218		return nil, nil, err
219	}
220
221	path := fmt.Sprintf("/ssh_keys/%d", sshKey.ID)
222	req, err := c.client.NewRequest(ctx, "PUT", path, bytes.NewReader(reqBodyData))
223	if err != nil {
224		return nil, nil, err
225	}
226
227	respBody := schema.SSHKeyUpdateResponse{}
228	resp, err := c.client.Do(req, &respBody)
229	if err != nil {
230		return nil, resp, err
231	}
232	return SSHKeyFromSchema(respBody.SSHKey), resp, nil
233}
234