1package godo
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7)
8
9const loadBalancersBasePath = "/v2/load_balancers"
10const forwardingRulesPath = "forwarding_rules"
11
12const dropletsPath = "droplets"
13
14// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
15// See: https://developers.digitalocean.com/documentation/v2#load-balancers
16type LoadBalancersService interface {
17	Get(context.Context, string) (*LoadBalancer, *Response, error)
18	List(context.Context, *ListOptions) ([]LoadBalancer, *Response, error)
19	Create(context.Context, *LoadBalancerRequest) (*LoadBalancer, *Response, error)
20	Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error)
21	Delete(ctx context.Context, lbID string) (*Response, error)
22	AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
23	RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
24	AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
25	RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
26}
27
28// LoadBalancer represents a DigitalOcean load balancer configuration.
29// Tags can only be provided upon the creation of a Load Balancer.
30type LoadBalancer struct {
31	ID                  string           `json:"id,omitempty"`
32	Name                string           `json:"name,omitempty"`
33	IP                  string           `json:"ip,omitempty"`
34	Algorithm           string           `json:"algorithm,omitempty"`
35	Status              string           `json:"status,omitempty"`
36	Created             string           `json:"created_at,omitempty"`
37	ForwardingRules     []ForwardingRule `json:"forwarding_rules,omitempty"`
38	HealthCheck         *HealthCheck     `json:"health_check,omitempty"`
39	StickySessions      *StickySessions  `json:"sticky_sessions,omitempty"`
40	Region              *Region          `json:"region,omitempty"`
41	DropletIDs          []int            `json:"droplet_ids,omitempty"`
42	Tag                 string           `json:"tag,omitempty"`
43	Tags                []string         `json:"tags,omitempty"`
44	RedirectHttpToHttps bool             `json:"redirect_http_to_https,omitempty"`
45}
46
47// String creates a human-readable description of a LoadBalancer.
48func (l LoadBalancer) String() string {
49	return Stringify(l)
50}
51
52func (l LoadBalancer) URN() string {
53	return ToURN("LoadBalancer", l.ID)
54}
55
56// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer.
57// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer.
58func (l LoadBalancer) AsRequest() *LoadBalancerRequest {
59	r := LoadBalancerRequest{
60		Name:                l.Name,
61		Algorithm:           l.Algorithm,
62		ForwardingRules:     append([]ForwardingRule(nil), l.ForwardingRules...),
63		DropletIDs:          append([]int(nil), l.DropletIDs...),
64		Tag:                 l.Tag,
65		RedirectHttpToHttps: l.RedirectHttpToHttps,
66		HealthCheck:         l.HealthCheck,
67	}
68
69	if l.HealthCheck != nil {
70		r.HealthCheck = &HealthCheck{}
71		*r.HealthCheck = *l.HealthCheck
72	}
73	if l.StickySessions != nil {
74		r.StickySessions = &StickySessions{}
75		*r.StickySessions = *l.StickySessions
76	}
77	if l.Region != nil {
78		r.Region = l.Region.Slug
79	}
80	return &r
81}
82
83// ForwardingRule represents load balancer forwarding rules.
84type ForwardingRule struct {
85	EntryProtocol  string `json:"entry_protocol,omitempty"`
86	EntryPort      int    `json:"entry_port,omitempty"`
87	TargetProtocol string `json:"target_protocol,omitempty"`
88	TargetPort     int    `json:"target_port,omitempty"`
89	CertificateID  string `json:"certificate_id,omitempty"`
90	TlsPassthrough bool   `json:"tls_passthrough,omitempty"`
91}
92
93// String creates a human-readable description of a ForwardingRule.
94func (f ForwardingRule) String() string {
95	return Stringify(f)
96}
97
98// HealthCheck represents optional load balancer health check rules.
99type HealthCheck struct {
100	Protocol               string `json:"protocol,omitempty"`
101	Port                   int    `json:"port,omitempty"`
102	Path                   string `json:"path,omitempty"`
103	CheckIntervalSeconds   int    `json:"check_interval_seconds,omitempty"`
104	ResponseTimeoutSeconds int    `json:"response_timeout_seconds,omitempty"`
105	HealthyThreshold       int    `json:"healthy_threshold,omitempty"`
106	UnhealthyThreshold     int    `json:"unhealthy_threshold,omitempty"`
107}
108
109// String creates a human-readable description of a HealthCheck.
110func (h HealthCheck) String() string {
111	return Stringify(h)
112}
113
114// StickySessions represents optional load balancer session affinity rules.
115type StickySessions struct {
116	Type             string `json:"type,omitempty"`
117	CookieName       string `json:"cookie_name,omitempty"`
118	CookieTtlSeconds int    `json:"cookie_ttl_seconds,omitempty"`
119}
120
121// String creates a human-readable description of a StickySessions instance.
122func (s StickySessions) String() string {
123	return Stringify(s)
124}
125
126// LoadBalancerRequest represents the configuration to be applied to an existing or a new load balancer.
127type LoadBalancerRequest struct {
128	Name                string           `json:"name,omitempty"`
129	Algorithm           string           `json:"algorithm,omitempty"`
130	Region              string           `json:"region,omitempty"`
131	ForwardingRules     []ForwardingRule `json:"forwarding_rules,omitempty"`
132	HealthCheck         *HealthCheck     `json:"health_check,omitempty"`
133	StickySessions      *StickySessions  `json:"sticky_sessions,omitempty"`
134	DropletIDs          []int            `json:"droplet_ids,omitempty"`
135	Tag                 string           `json:"tag,omitempty"`
136	Tags                []string         `json:"tags,omitempty"`
137	RedirectHttpToHttps bool             `json:"redirect_http_to_https,omitempty"`
138}
139
140// String creates a human-readable description of a LoadBalancerRequest.
141func (l LoadBalancerRequest) String() string {
142	return Stringify(l)
143}
144
145type forwardingRulesRequest struct {
146	Rules []ForwardingRule `json:"forwarding_rules,omitempty"`
147}
148
149func (l forwardingRulesRequest) String() string {
150	return Stringify(l)
151}
152
153type dropletIDsRequest struct {
154	IDs []int `json:"droplet_ids,omitempty"`
155}
156
157func (l dropletIDsRequest) String() string {
158	return Stringify(l)
159}
160
161type loadBalancersRoot struct {
162	LoadBalancers []LoadBalancer `json:"load_balancers"`
163	Links         *Links         `json:"links"`
164}
165
166type loadBalancerRoot struct {
167	LoadBalancer *LoadBalancer `json:"load_balancer"`
168}
169
170// LoadBalancersServiceOp handles communication with load balancer-related methods of the DigitalOcean API.
171type LoadBalancersServiceOp struct {
172	client *Client
173}
174
175var _ LoadBalancersService = &LoadBalancersServiceOp{}
176
177// Get an existing load balancer by its identifier.
178func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
179	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
180
181	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
182	if err != nil {
183		return nil, nil, err
184	}
185
186	root := new(loadBalancerRoot)
187	resp, err := l.client.Do(ctx, req, root)
188	if err != nil {
189		return nil, resp, err
190	}
191
192	return root.LoadBalancer, resp, err
193}
194
195// List load balancers, with optional pagination.
196func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]LoadBalancer, *Response, error) {
197	path, err := addOptions(loadBalancersBasePath, opt)
198	if err != nil {
199		return nil, nil, err
200	}
201
202	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
203	if err != nil {
204		return nil, nil, err
205	}
206
207	root := new(loadBalancersRoot)
208	resp, err := l.client.Do(ctx, req, root)
209	if err != nil {
210		return nil, resp, err
211	}
212	if l := root.Links; l != nil {
213		resp.Links = l
214	}
215
216	return root.LoadBalancers, resp, err
217}
218
219// Create a new load balancer with a given configuration.
220func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
221	req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr)
222	if err != nil {
223		return nil, nil, err
224	}
225
226	root := new(loadBalancerRoot)
227	resp, err := l.client.Do(ctx, req, root)
228	if err != nil {
229		return nil, resp, err
230	}
231
232	return root.LoadBalancer, resp, err
233}
234
235// Update an existing load balancer with new configuration.
236func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
237	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
238
239	req, err := l.client.NewRequest(ctx, "PUT", path, lbr)
240	if err != nil {
241		return nil, nil, err
242	}
243
244	root := new(loadBalancerRoot)
245	resp, err := l.client.Do(ctx, req, root)
246	if err != nil {
247		return nil, resp, err
248	}
249
250	return root.LoadBalancer, resp, err
251}
252
253// Delete a load balancer by its identifier.
254func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
255	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)
256
257	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
258	if err != nil {
259		return nil, err
260	}
261
262	return l.client.Do(ctx, req, nil)
263}
264
265// AddDroplets adds droplets to a load balancer.
266func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
267	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
268
269	req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
270	if err != nil {
271		return nil, err
272	}
273
274	return l.client.Do(ctx, req, nil)
275}
276
277// RemoveDroplets removes droplets from a load balancer.
278func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
279	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
280
281	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
282	if err != nil {
283		return nil, err
284	}
285
286	return l.client.Do(ctx, req, nil)
287}
288
289// AddForwardingRules adds forwarding rules to a load balancer.
290func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
291	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
292
293	req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
294	if err != nil {
295		return nil, err
296	}
297
298	return l.client.Do(ctx, req, nil)
299}
300
301// RemoveForwardingRules removes forwarding rules from a load balancer.
302func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
303	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
304
305	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
306	if err != nil {
307		return nil, err
308	}
309
310	return l.client.Do(ctx, req, nil)
311}
312