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  }