github.com/aiven/aiven-go-client@v1.36.0/client.go (about) 1 package aiven 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/tls" 7 "crypto/x509" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io" 12 "log" 13 "net/http" 14 "os" 15 "regexp" 16 "time" 17 18 "github.com/hashicorp/go-cleanhttp" 19 "github.com/hashicorp/go-retryablehttp" 20 ) 21 22 // apiUrl and apiUrlV2 are the URLs we'll use to speak to Aiven. This can be overwritten. 23 var ( 24 apiUrl = "https://api.aiven.io/v1" 25 apiUrlV2 = "https://api.aiven.io/v2" 26 ) 27 28 var ( 29 // errUnableToCreateAivenClient is a template error for when the client cannot be created. 30 errUnableToCreateAivenClient = func(err error) error { 31 return fmt.Errorf("unable to create Aiven client: %w", err) 32 } 33 ) 34 35 func init() { 36 value, isSet := os.LookupEnv("AIVEN_WEB_URL") 37 if isSet { 38 apiUrl = value + "/v1" 39 apiUrlV2 = value + "/v2" 40 } 41 } 42 43 // Client represents the instance that does all the calls to the Aiven API. 44 type Client struct { 45 ctx context.Context 46 APIKey string 47 Client *http.Client 48 UserAgent string 49 50 Projects *ProjectsHandler 51 ProjectUsers *ProjectUsersHandler 52 CA *CAHandler 53 CardsHandler *CardsHandler 54 ServiceIntegrationEndpoints *ServiceIntegrationEndpointsHandler 55 ServiceIntegrations *ServiceIntegrationsHandler 56 ServiceTypes *ServiceTypesHandler 57 ServiceTask *ServiceTaskHandler 58 Services *ServicesHandler 59 ConnectionPools *ConnectionPoolsHandler 60 Databases *DatabasesHandler 61 ServiceUsers *ServiceUsersHandler 62 KafkaACLs *KafkaACLHandler 63 KafkaSchemaRegistryACLs *KafkaSchemaRegistryACLHandler 64 KafkaSubjectSchemas *KafkaSubjectSchemasHandler 65 KafkaGlobalSchemaConfig *KafkaGlobalSchemaConfigHandler 66 KafkaConnectors *KafkaConnectorsHandler 67 KafkaMirrorMakerReplicationFlow *MirrorMakerReplicationFlowHandler 68 ElasticsearchACLs *ElasticSearchACLsHandler 69 KafkaTopics *KafkaTopicsHandler 70 VPCs *VPCsHandler 71 VPCPeeringConnections *VPCPeeringConnectionsHandler 72 Accounts *AccountsHandler 73 AccountTeams *AccountTeamsHandler 74 AccountTeamMembers *AccountTeamMembersHandler 75 AccountTeamProjects *AccountTeamProjectsHandler 76 AccountAuthentications *AccountAuthenticationsHandler 77 AccountTeamInvites *AccountTeamInvitesHandler 78 TransitGatewayVPCAttachment *TransitGatewayVPCAttachmentHandler 79 BillingGroup *BillingGroupHandler 80 AWSPrivatelink *AWSPrivatelinkHandler 81 AzurePrivatelink *AzurePrivatelinkHandler 82 GCPPrivatelink *GCPPrivatelinkHandler 83 FlinkJobs *FlinkJobHandler 84 FlinkApplications *FlinkApplicationHandler 85 FlinkApplicationDeployments *FlinkApplicationDeploymentHandler 86 FlinkApplicationVersions *FlinkApplicationVersionHandler 87 FlinkApplicationQueries *FlinkApplicationQueryHandler 88 StaticIPs *StaticIPsHandler 89 ClickhouseDatabase *ClickhouseDatabaseHandler 90 ClickhouseUser *ClickhouseUserHandler 91 ClickHouseQuery *ClickhouseQueryHandler 92 ServiceTags *ServiceTagsHandler 93 Organization *OrganizationHandler 94 OrganizationUser *OrganizationUserHandler 95 OrganizationUserInvitations *OrganizationUserInvitationsHandler 96 OrganizationUserGroups *OrganizationUserGroupHandler 97 OrganizationUserGroupMembers *OrganizationUserGroupMembersHandler 98 OpenSearchSecurityPluginHandler *OpenSearchSecurityPluginHandler 99 } 100 101 // GetUserAgentOrDefault configures a default userAgent value, if one has not been provided. 102 func GetUserAgentOrDefault(userAgent string) string { 103 if userAgent != "" { 104 return userAgent 105 } 106 return "aiven-go-client/" + Version() 107 } 108 109 // NewMFAUserClient creates a new client based on email, one-time password and password. 110 func NewMFAUserClient(email, otp, password string, userAgent string) (*Client, error) { 111 c := &Client{ 112 Client: buildHttpClient(), 113 UserAgent: GetUserAgentOrDefault(userAgent), 114 } 115 116 bts, err := c.doPostRequest("/userauth", authRequest{email, otp, password}) 117 if err != nil { 118 return nil, err 119 } 120 121 var r authResponse 122 if err := checkAPIResponse(bts, &r); err != nil { 123 return nil, err 124 } 125 126 return NewTokenClient(r.Token, userAgent) 127 } 128 129 // NewUserClient creates a new client based on email and password. 130 func NewUserClient(email, password string, userAgent string) (*Client, error) { 131 return NewMFAUserClient(email, "", password, userAgent) 132 } 133 134 // NewTokenClient creates a new client based on a given token. 135 func NewTokenClient(key string, userAgent string) (*Client, error) { 136 c := &Client{ 137 APIKey: key, 138 Client: buildHttpClient(), 139 UserAgent: GetUserAgentOrDefault(userAgent), 140 } 141 c.Init() 142 143 return c, nil 144 } 145 146 // SetupEnvClient creates a new client using the provided web URL and token in the environment. 147 // This should only be used for testing and development purposes, or if you know what you're doing. 148 func SetupEnvClient(userAgent string) (*Client, error) { 149 webUrl := os.Getenv("AIVEN_WEB_URL") 150 if webUrl != "" { 151 apiUrl = webUrl + "/v1" 152 apiUrlV2 = webUrl + "/v2" 153 } 154 155 token := os.Getenv("AIVEN_TOKEN") 156 if token == "" { 157 return nil, errUnableToCreateAivenClient(errors.New("AIVEN_TOKEN environment variable is required")) 158 } 159 160 client, err := NewTokenClient(token, userAgent) 161 if err != nil { 162 return nil, errUnableToCreateAivenClient(err) 163 } 164 165 return client, nil 166 } 167 168 // buildHttpClient it builds http.Client, if environment variable AIVEN_CA_CERT 169 // contains a path to a valid CA certificate HTTPS client will be configured to use it 170 func buildHttpClient() *http.Client { 171 retryClient := newRetryableClient() 172 caFilename := os.Getenv("AIVEN_CA_CERT") 173 if caFilename == "" { 174 return retryClient.StandardClient() 175 } 176 177 // Load CA cert 178 caCert, err := os.ReadFile(caFilename) 179 if err != nil { 180 log.Fatal("cannot load ca cert: %w", err) 181 } 182 183 // Append CA cert to the system pool 184 caCertPool, _ := x509.SystemCertPool() 185 if caCertPool == nil { 186 caCertPool = x509.NewCertPool() 187 } 188 189 if ok := caCertPool.AppendCertsFromPEM(caCert); !ok { 190 log.Println("[WARNING] No certs appended, using system certs only") 191 } 192 193 // Setups root custom transport with certs 194 transport := cleanhttp.DefaultPooledTransport() 195 transport.TLSClientConfig = &tls.Config{ 196 RootCAs: caCertPool, 197 } 198 retryClient.HTTPClient.Transport = transport 199 return retryClient.StandardClient() 200 } 201 202 // newRetryableClient 203 // Setups retryable http client 204 // retryablehttp performs automatic retries under certain conditions. 205 // Mainly, if an error is returned by the client (connection errors, etc.), 206 // or if a 500-range response code is received (except 501), then a retry is invoked after a wait period. 207 // Otherwise, the response is returned and left to the caller to interpret. 208 func newRetryableClient() *retryablehttp.Client { 209 retryClient := retryablehttp.NewClient() 210 retryClient.Logger = nil 211 retryClient.CheckRetry = checkRetry 212 213 // With given RetryMax, RetryWaitMin, RetryWaitMax: 214 // Default backoff is wait max of: RetryWaitMin, 2^attemptNum * RetryWaitMin, 215 // That makes 1, 4, 8, 16, 30, 30, ... 216 retryClient.RetryMax = 10 217 retryClient.RetryWaitMin = 1 * time.Second 218 retryClient.RetryWaitMax = 30 * time.Second 219 return retryClient 220 } 221 222 // Init initializes the client and sets up all the handlers. 223 func (c *Client) Init() { 224 c.Projects = &ProjectsHandler{c} 225 c.ProjectUsers = &ProjectUsersHandler{c} 226 c.CA = &CAHandler{c} 227 c.CardsHandler = &CardsHandler{c} 228 c.ServiceIntegrationEndpoints = &ServiceIntegrationEndpointsHandler{c} 229 c.ServiceIntegrations = &ServiceIntegrationsHandler{c} 230 c.ServiceTypes = &ServiceTypesHandler{c} 231 c.ServiceTask = &ServiceTaskHandler{c} 232 c.Services = &ServicesHandler{c} 233 c.ConnectionPools = &ConnectionPoolsHandler{c} 234 c.Databases = &DatabasesHandler{c} 235 c.ServiceUsers = &ServiceUsersHandler{c} 236 c.KafkaACLs = &KafkaACLHandler{c} 237 c.KafkaSchemaRegistryACLs = &KafkaSchemaRegistryACLHandler{c} 238 c.KafkaSubjectSchemas = &KafkaSubjectSchemasHandler{c} 239 c.KafkaGlobalSchemaConfig = &KafkaGlobalSchemaConfigHandler{c} 240 c.KafkaConnectors = &KafkaConnectorsHandler{c} 241 c.KafkaMirrorMakerReplicationFlow = &MirrorMakerReplicationFlowHandler{c} 242 c.ElasticsearchACLs = &ElasticSearchACLsHandler{c} 243 c.KafkaTopics = &KafkaTopicsHandler{c} 244 c.VPCs = &VPCsHandler{c} 245 c.VPCPeeringConnections = &VPCPeeringConnectionsHandler{c} 246 c.Accounts = &AccountsHandler{c} 247 c.AccountTeams = &AccountTeamsHandler{c} 248 c.AccountTeamMembers = &AccountTeamMembersHandler{c} 249 c.AccountTeamProjects = &AccountTeamProjectsHandler{c} 250 c.AccountAuthentications = &AccountAuthenticationsHandler{c} 251 c.AccountTeamInvites = &AccountTeamInvitesHandler{c} 252 c.TransitGatewayVPCAttachment = &TransitGatewayVPCAttachmentHandler{c} 253 c.BillingGroup = &BillingGroupHandler{c} 254 c.AWSPrivatelink = &AWSPrivatelinkHandler{c} 255 c.AzurePrivatelink = &AzurePrivatelinkHandler{c} 256 c.GCPPrivatelink = &GCPPrivatelinkHandler{c} 257 c.FlinkJobs = &FlinkJobHandler{c} 258 c.FlinkApplications = &FlinkApplicationHandler{c} 259 c.FlinkApplicationDeployments = &FlinkApplicationDeploymentHandler{c} 260 c.FlinkApplicationQueries = &FlinkApplicationQueryHandler{c} 261 c.FlinkApplicationVersions = &FlinkApplicationVersionHandler{c} 262 c.StaticIPs = &StaticIPsHandler{c} 263 c.ClickhouseDatabase = &ClickhouseDatabaseHandler{c} 264 c.ClickhouseUser = &ClickhouseUserHandler{c} 265 c.ClickHouseQuery = &ClickhouseQueryHandler{c} 266 c.ServiceTags = &ServiceTagsHandler{c} 267 c.Organization = &OrganizationHandler{c} 268 c.OrganizationUser = &OrganizationUserHandler{c} 269 c.OrganizationUserInvitations = &OrganizationUserInvitationsHandler{c} 270 c.OrganizationUserGroups = &OrganizationUserGroupHandler{c} 271 c.OrganizationUserGroupMembers = &OrganizationUserGroupMembersHandler{c} 272 c.OpenSearchSecurityPluginHandler = &OpenSearchSecurityPluginHandler{c} 273 } 274 275 // WithContext create a copy of Client where all request would be using the provided context 276 func (c *Client) WithContext(ctx context.Context) *Client { 277 o := &Client{ctx: ctx, APIKey: c.APIKey, UserAgent: c.UserAgent, Client: c.Client} 278 o.Init() 279 return o 280 } 281 282 func (c *Client) doGetRequest(endpoint string, req interface{}) ([]byte, error) { 283 return c.doRequest("GET", endpoint, req, 1) 284 } 285 286 func (c *Client) doPutRequest(endpoint string, req interface{}) ([]byte, error) { 287 return c.doRequest("PUT", endpoint, req, 1) 288 } 289 290 func (c *Client) doPostRequest(endpoint string, req interface{}) ([]byte, error) { 291 return c.doRequest("POST", endpoint, req, 1) 292 } 293 294 func (c *Client) doPatchRequest(endpoint string, req interface{}) ([]byte, error) { 295 return c.doRequest("PATCH", endpoint, req, 1) 296 } 297 298 func (c *Client) doDeleteRequest(endpoint string, req interface{}) ([]byte, error) { 299 return c.doRequest("DELETE", endpoint, req, 1) 300 } 301 302 //nolint:unused 303 func (c *Client) doV2GetRequest(endpoint string, req interface{}) ([]byte, error) { 304 return c.doRequest("GET", endpoint, req, 2) 305 } 306 307 //nolint:unused 308 func (c *Client) doV2PutRequest(endpoint string, req interface{}) ([]byte, error) { 309 return c.doRequest("PUT", endpoint, req, 2) 310 } 311 312 func (c *Client) doV2PostRequest(endpoint string, req interface{}) ([]byte, error) { 313 return c.doRequest("POST", endpoint, req, 2) 314 } 315 316 //nolint:unused 317 func (c *Client) doV2DeleteRequest(endpoint string, req interface{}) ([]byte, error) { 318 return c.doRequest("DELETE", endpoint, req, 2) 319 } 320 321 func (c *Client) doRequest(method, uri string, body interface{}, apiVersion int) ([]byte, error) { 322 var bts []byte 323 if body != nil { 324 var err error 325 bts, err = json.Marshal(body) 326 if err != nil { 327 return nil, err 328 } 329 } 330 331 var url string 332 switch apiVersion { 333 case 1: 334 url = endpoint(uri) 335 case 2: 336 url = endpointV2(uri) 337 default: 338 return nil, fmt.Errorf("aiven API apiVersion `%d` is not supported", apiVersion) 339 } 340 341 ctx := c.ctx 342 if ctx == nil { 343 ctx = context.Background() 344 } 345 req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(bts)) 346 if err != nil { 347 return nil, err 348 } 349 350 req.Header.Set("Content-Type", "application/json") 351 req.Header.Set("User-Agent", c.UserAgent) 352 req.Header.Set("Authorization", "aivenv1 "+c.APIKey) 353 354 // TODO: BAD hack to get around pagination in most cases 355 // we should implement this properly at some point but for now 356 // that should be its own issue 357 query := req.URL.Query() 358 query.Add("limit", "999") 359 req.URL.RawQuery = query.Encode() 360 361 rsp, err := c.Client.Do(req) 362 if err != nil { 363 return nil, err 364 } 365 366 defer func() { 367 err := rsp.Body.Close() 368 if err != nil { 369 log.Printf("[WARNING] cannot close response body: %s \n", err) 370 } 371 }() 372 373 responseBody, err := io.ReadAll(rsp.Body) 374 if err != nil || rsp.StatusCode < 200 || rsp.StatusCode >= 300 { 375 return nil, Error{Message: string(responseBody), Status: rsp.StatusCode} 376 } 377 return responseBody, nil 378 } 379 380 func endpoint(uri string) string { 381 return apiUrl + uri 382 } 383 384 func endpointV2(uri string) string { 385 return apiUrlV2 + uri 386 } 387 388 // ToStringPointer converts string to a string pointer 389 func ToStringPointer(s string) *string { 390 return &s 391 } 392 393 func PointerToString(s *string) string { 394 if s == nil { 395 return "" 396 } 397 return *s 398 } 399 400 // checkRetry does basic retries (>=500 && != 501, timeouts, connection errors) 401 // Plus custom retries, see isRetryable 402 // If ErrorPropagatedRetryPolicy returns error it is either >=500 403 // or things you can't retry like an invalid protocol scheme 404 // Suspends errors, cause that's what retryablehttp.DefaultRetryPolicy does 405 func checkRetry(ctx context.Context, rsp *http.Response, err error) (bool, error) { 406 shouldRetry, err := retryablehttp.ErrorPropagatedRetryPolicy(ctx, rsp, err) 407 return shouldRetry || err == nil && isRetryable(rsp), nil 408 } 409 410 // isRetryable 411 // 501 — some resources like kafka topic and kafka connector may return 501. Which means retry later 412 // 417 — has dependent resource pending: Application deletion forbidden because it has 1 deployment(s). 413 // 408 — dependent server time out 414 // 404 — see retryableChecks 415 func isRetryable(rsp *http.Response) bool { 416 // This might happen in tests only 417 if rsp.Request == nil { 418 return false 419 } 420 421 switch rsp.StatusCode { 422 case http.StatusRequestTimeout, http.StatusNotImplemented: 423 return true 424 case http.StatusExpectationFailed: 425 return rsp.Request.Method == "DELETE" 426 case http.StatusNotFound: 427 // We need to restore the body 428 body := rsp.Body 429 defer body.Close() 430 431 // Shouldn't be there much of data, ReadAll is ok 432 b, err := io.ReadAll(body) 433 if err != nil { 434 return false 435 } 436 437 // Restores the body 438 rsp.Body = io.NopCloser(bytes.NewReader(b)) 439 440 // Error checks 441 s := string(b) 442 for _, c := range retryableChecks { 443 if c(rsp.Request.Method, s) { 444 return true 445 } 446 } 447 } 448 return false 449 } 450 451 var retryableChecks = []func(meth, body string) bool{ 452 isServiceLagError, 453 isUserError, 454 } 455 456 var ( 457 reServiceNotFound = regexp.MustCompile(`Service \S+ does not exist`) 458 reUserNotFound = regexp.MustCompile(`User (avnadmin|root) with component main not found`) 459 ) 460 461 // isServiceLagError service is might be ready, but there is a lag that need to wait for ending 462 func isServiceLagError(meth, body string) bool { 463 return meth == "POST" && reServiceNotFound.MatchString(body) 464 } 465 466 // isUserError an internal unknown error 467 func isUserError(_, body string) bool { 468 return reUserNotFound.MatchString(body) 469 }