-
Notifications
You must be signed in to change notification settings - Fork 111
/
habapi.go
232 lines (200 loc) · 6.67 KB
/
habapi.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// Package habapi provides access to the Habitat supervisor's HTTP API
//
// The Habitat supervisor's API contains information about
// the running services.
//
// This package is currently incomplete and contains only those API
// functions used in other parts of automate-deployment.
package habapi
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"time"
"github.com/sirupsen/logrus"
)
// A Client is a Habitat API client.
type Client struct {
// BaseURI is the Habitat API endpoint that this client should
// talk to. For example: "http://127.0.0.1:9631"
BaseURI *url.URL
// HTTPClient is the underlying http.Client used to
// communicate with the Habitat API.
//
// NOTE(ssd) 2018-01-24: We use a concrete type here since
// there is no standard http client interface. The standard
// advice is that if you need to swap something out, you can
// swap out the Transport field in the http.Client. We don't
// currently use this for testing since we start a test http
// server instead.
HTTPClient http.Client
}
// HabServiceInfoAPIClient interface describes things that can get the ServiceInfo
type HabServiceInfoAPIClient interface {
ServiceInfo(ctx context.Context, serviceName string, serviceGroup string) (ServiceInfo, error)
}
// ServiceInfo contains information about a service according to the
// Habitat supervisor. Note that this structure currently only
// contains a small portion of the data Habitat actually knows about.
type ServiceInfo struct {
Binds []string `json:"binds"`
Pkg ServicePkg `json:"pkg"`
StartStyle string `json:"start_style"`
UpdateStrategy string `json:"update_strategy"`
Process ServiceProcess `json:"process"`
Sys SysInfo `json:"sys"`
DesiredState string `json:"desired_state"`
Cfg map[string]interface{} `json:"cfg"`
}
type SysInfo struct {
GossipIP string `json:"gossip_ip"`
GossipPort uint32 `json:"gossip_port"`
Hostname string `json:"hostname"`
HTTPGatewayIP string `json:"http_gateway_ip"`
HTTPGatewayPort uint32 `json:"http_gateway_port"`
IP string `json:"ip"`
MemberID string `json:"member_id"`
Permanent bool `json:"permanent"`
Version string `json:"version"`
}
type ServiceProcess struct {
PID uint64 `json:"pid"`
State string `json:"state"`
TimeStateEntered uint64 `json:"state_entered"`
}
// ServicePkg represents package information for a service. Note
// that this structure is currently incomplete when compared to the
// API return.
type ServicePkg struct {
Origin string `json:"origin"`
Name string `json:"name"`
Version string `json:"version"`
Release string `json:"release"`
Channel string `json:"channel"`
}
// ServiceStatus represents the current health of a service according
// to the Habitat supervisor. This is returned by ServiceHealth.
type ServiceStatus int
const (
// StatusOk indicates that the health endpoint has
// returned 200 OK.
StatusOk ServiceStatus = iota
// StatusWarning is included for completeness but is not
// currently returned because the health API does not
// distinguish between OK and WARNING (both return 200 OK) and
// the other API that may provide insight appears to have a
// bug:
//
// https://github.com/habitat-sh/core-plans/issues/1474
StatusWarning
// StatusCritical indicates that the health endpoint returned
// 503
StatusCritical
// StatusUnknown indicates that the health endpoint returned
// 500 or another unrecognized status code.
StatusUnknown
// StatusDown indicates that the habitat supervisor doesn't
// know about this service and it is likely not started.
StatusDown
)
var (
ErrServiceNotFound = errors.New("service not found")
)
// ServiceHealth returns the health of a service according to the
// Habitat supervisor.
//
// Uses the /service/SERVICE_NAME/SERVICE_GROUP/health endpoint.
func (c *Client) ServiceHealth(ctx context.Context, serviceName string, serviceGroup string) (ServiceStatus, error) {
path := fmt.Sprintf("/services/%s/%s/health", serviceName, serviceGroup)
response, err := c.apiGet(ctx, path)
if err != nil {
return StatusUnknown, err
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
return StatusOk, nil
case http.StatusServiceUnavailable:
return StatusCritical, nil
case http.StatusInternalServerError:
return StatusUnknown, nil
case http.StatusNotFound:
return StatusDown, nil
default:
return StatusUnknown, nil
}
}
// ServiceInfo returns information about a service running under the Habitat supervisor.
//
// Uses the /service/SERVICE_NAME/SERVICE_GROUP endpoint
func (c *Client) ServiceInfo(ctx context.Context, serviceName string, serviceGroup string) (ServiceInfo, error) {
var info ServiceInfo
path := fmt.Sprintf("/services/%s/%s", serviceName, serviceGroup)
response, err := c.apiGet(ctx, path)
if err != nil {
return info, err
}
defer response.Body.Close()
switch response.StatusCode {
case 404:
return info, ErrServiceNotFound
case 200:
// Don't log in the success case
default:
logrus.WithField("status_code", response.StatusCode).Debug("Unexpected response from Habitat ServiceInfo API call")
}
err = json.NewDecoder(response.Body).Decode(&info)
return info, err
}
// ListServices returns a list of services currently running under the Habitat supervisor
func (c *Client) ListServices(ctx context.Context) ([]ServiceInfo, error) {
services := []ServiceInfo{}
path := "/services"
response, err := c.apiGet(ctx, path)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
err = json.NewDecoder(response.Body).Decode(&services)
return services, err
}
func mustParse(uri string) *url.URL {
u, err := url.Parse(uri)
if err != nil {
logrus.WithError(err).Fatal("failed to initialize url")
}
return u
}
// New returns a Client with all passed options applied to it.
func New(baseURI string, options ...func(*Client)) *Client {
c := &Client{}
c.BaseURI = mustParse(baseURI)
c.HTTPClient = http.Client{}
for _, option := range options {
option(c)
}
return c
}
// Timeout returns an "option func" suitable for passing to New which
// will set to Client's timeout to the specified duration.
func Timeout(timeout time.Duration) func(*Client) {
return func(c *Client) {
c.HTTPClient.Timeout = timeout
}
}
func (c *Client) apiGet(ctx context.Context, p string) (*http.Response, error) {
u := *c.BaseURI
u.Path = path.Join(u.Path, p)
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
return c.HTTPClient.Do(req)
}