-
Notifications
You must be signed in to change notification settings - Fork 2
/
aiven.go
235 lines (217 loc) · 6.26 KB
/
aiven.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
233
234
235
package keys
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
const aivenTokenEndpoint string = "https://api.aiven.io/v1/access_token"
const fullAccountSeparator string = ":"
// AivenKey type
type AivenKey struct{}
// Error type
type Error struct {
Message string `json:"message"`
MoreInfo string `json:"more_info"`
Status int `json:"status"`
}
// Token type
type Token struct {
CreateTime string `json:"create_time"`
CurrentlyActive bool `json:"currently_active"`
Description string `json:"description"`
TokenPrefix string `json:"token_prefix"`
}
// ListTokensResponse type
type ListTokensResponse struct {
Errors []Error `json:"errors"`
Message string `json:"message"`
Tokens []Token `json:"tokens"`
}
// CreateTokenResponse type
type CreateTokenResponse struct {
CreateTime string `json:"create_time"`
CreatedManually bool `json:"created_manually"`
Errors []Error `json:"errors"`
ExtendWhenUsed bool `json:"extend_when_used"`
FullToken string `json:"full_token"`
MaxAgeSeconds int `json:"max_age_seconds"`
Message string `json:"message"`
TokenPrefix string `json:"token_prefix"`
}
// RevokeTokenResponse type
type RevokeTokenResponse struct {
Errors []Error `json:"errors"`
Message string `json:"message"`
}
// Generic functions for sending an HTTP request
func doGenericHTTPReq(method, url, token string, payload io.Reader) (body []byte, err error) {
client := http.Client{}
req, err := http.NewRequest(method, url, payload)
if err != nil {
return
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// Get the listTokensResponse from the Aiven API
func listTokensResponse(token string) (ltr ListTokensResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenList
body, err := doGenericHTTPReq(
http.MethodGet,
aivenTokenEndpoint,
token,
nil,
)
if err != nil {
return
}
err = json.Unmarshal(body, <r)
return
}
// Get the createTokenResponse from the Aiven API
func createTokenResponse(token, description string) (ctr CreateTokenResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenCreate
jsonStr := []byte(fmt.Sprintf("{\"description\":\"%s\"}", description))
body, err := doGenericHTTPReq(
http.MethodPost,
aivenTokenEndpoint,
token,
bytes.NewBuffer(jsonStr),
)
if err != nil {
return
}
err = json.Unmarshal(body, &ctr)
return
}
// Get the revokeTokenResponse from the Aiven API
func revokeTokenResponse(tokenPrefix, token string) (rtr RevokeTokenResponse, err error) {
// https://api.aiven.io/doc/#tag/User/operation/AccessTokenRevoke
body, err := doGenericHTTPReq(
http.MethodDelete,
fmt.Sprintf("%s/%s", aivenTokenEndpoint, tokenPrefix),
token,
nil,
)
if err != nil {
return
}
err = json.Unmarshal(body, &rtr)
if err != nil {
err = fmt.Errorf("Failed unmarshalling response: %s, response from Aiven API: %s", err, string(body[:]))
}
return
}
// Transform a slice of errors (returned in Aiven response) to a single error
func handleAPIErrors(errs []Error) (err error) {
var errorMsgs []string
for _, error := range errs {
msg := fmt.Sprintf("msg: %s, status: %d", error.Message, error.Status)
errorMsgs = append(errorMsgs, msg)
}
return errors.New(strings.Join(errorMsgs, ","))
}
// Return a status string (active|inactive)
func status(currentlyActive bool) string {
status := "Inactive"
if currentlyActive {
status = "Active"
}
return status
}
// Get the description of a key/token from a 'fullAccount' identifier
func tokenPrefixDescriptionFromFullAccount(account string) (tokenPrefix, tokenDescription string, err error) {
tokenPrefix, tokenDescription, found := strings.Cut(account, fullAccountSeparator)
if !found {
err = fmt.Errorf("Separator %s not found in fullAccount: %s", fullAccountSeparator, account)
return
}
return
}
// Keys returns a slice of keys (or tokens in this case) for the user who
// owns the apiToken
func (a AivenKey) Keys(project string, includeInactiveKeys bool, apiToken string) (keys []Key, err error) {
ltr, err := listTokensResponse(apiToken)
if err != nil {
return
}
if len(ltr.Errors) > 0 {
err = handleAPIErrors(ltr.Errors)
return
}
for _, token := range ltr.Tokens {
var createTime time.Time
if createTime, err = time.Parse(aivenTimeFormat, token.CreateTime); err != nil {
return
}
// ignore the token if it has no description (this is the identifier
// we use to track tokens down that are configured for rotation)
tokenDesc := token.Description
tokenPrefix := token.TokenPrefix
if tokenDesc != "" {
key := Key{
Account: tokenDesc,
FullAccount: fmt.Sprintf("%s%s%s", tokenPrefix, fullAccountSeparator, tokenDesc),
Age: time.Since(createTime).Minutes(),
ID: tokenPrefix,
Name: tokenDesc,
Provider: Provider{Provider: aivenProviderString, Token: apiToken},
Status: status(token.CurrentlyActive),
}
keys = append(keys, key)
}
}
return
}
// CreateKey creates a new Aiven API token
func (a AivenKey) CreateKey(project, account, token string) (keyID string, newKey string, err error) {
if account == "" {
err = errors.New("The account string is empty; this is required to explicitly define which keys/tokens to interact with")
return
}
_, description, err := tokenPrefixDescriptionFromFullAccount(account)
if err != nil {
return
}
ctr, err := createTokenResponse(token, description)
if err != nil {
return
}
if len(ctr.Errors) > 0 {
err = handleAPIErrors(ctr.Errors)
return
}
keyID = ctr.TokenPrefix
newKey = ctr.FullToken
return
}
// DeleteKey deletes the specified Aiven API token
func (a AivenKey) DeleteKey(project, account, keyID, token string) (err error) {
tokenPrefix, _, err := tokenPrefixDescriptionFromFullAccount(account)
if err != nil {
return
}
// tokenPrefix is used in the path in the call to Aiven API, some chars
// need escaping otherwise they'll cause a 404
tokenPrefix = url.PathEscape(tokenPrefix)
rtr, err := revokeTokenResponse(tokenPrefix, token)
if err != nil {
return
}
if len(rtr.Errors) > 0 {
err = handleAPIErrors(rtr.Errors)
}
return
}