Skip to content

Commit

Permalink
v3: user agent as client options (#659)
Browse files Browse the repository at this point in the history
# Description

Currently, the user agent is defined by a global variable, but this is a
problem.

If lego defines the user agent and an app uses exocale and lego, what
will be the user agent at the end?
It's not possible to know because it will depend on when and where the
user agent is set:
- If lego is the last to set the variable then all the calls will have
the lego user agent.
- If lego is the first to set the variable then all the calls will have
the app user agent.
- If the app and/or lego defined the user agent through another global
variable, it will be "random".
- if the user agent is defined inside goroutines, it will be "random".

Another problem: with the global variable, if the user defines the user
agent then information about the API client (versions, etc.) will be
discarded, except if the user appends the previous user agent manually.

Generally speaking, global variables should be avoided for a lib.

---

I added a `ClientOpt` implementation to set the user agent:
(`ClientOptUserAgent()`) and deprecated the global variable.

I created a compatibility layer, so the usage of the global variable
still works:
- for a client, if the global variable `UserAgent` is overridden and
`ClientOptUserAgent()` is not used, the user agent will be `UserAgent`.
- for a client, if `ClientOptUserAgent()` is used, even if `UserAgent`
is overridden, the user agent will be the one defined by
`ClientOptUserAgent()`

2 clients can use either the `UserAgent` or `ClientOptUserAgent()`, and
each client will not be impacted by the other (except if both use the
global variable as before).
  • Loading branch information
ldez authored Sep 13, 2024
1 parent f297a4d commit 3546f6b
Show file tree
Hide file tree
Showing 4 changed files with 732 additions and 383 deletions.
125 changes: 79 additions & 46 deletions v3/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

141 changes: 87 additions & 54 deletions v3/generator/client/client.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (c Client) GetZoneAPIEndpoint(ctx context.Context, zoneName ZoneName) (Endp
type Client struct {
apiKey string
apiSecret string
userAgent string
serverEndpoint string
httpClient *http.Client
pollingInterval time.Duration
Expand All @@ -51,12 +52,8 @@ type Client struct {
// RequestInterceptorFn is the function signature for the RequestInterceptor callback function
type RequestInterceptorFn func(ctx context.Context, req *http.Request) error

// UserAgent is the "User-Agent" HTTP request header added to outgoing HTTP requests.
var UserAgent = fmt.Sprintf("egoscale/%s (%s; %s/%s)",
Version,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH)
// Deprecated: use ClientOptWithUserAgent instead.
var UserAgent = getDefaultUserAgent()

const pollingInterval = 3 * time.Second

Expand All @@ -71,6 +68,14 @@ func ClientOptWithTrace() ClientOpt {
}
}

// ClientOptWithUserAgent returns a ClientOpt setting the user agent header.
func ClientOptWithUserAgent(ua string) ClientOpt {
return func(c *Client) error {
c.userAgent = ua + " " + getDefaultUserAgent()
return nil
}
}

// ClientOptWithValidator returns a ClientOpt with a given validator.
func ClientOptWithValidator(validate *validator.Validate) ClientOpt {
return func(c *Client) error {
Expand Down Expand Up @@ -109,21 +114,31 @@ func ClientOptWithHTTPClient(v *http.Client) ClientOpt {
}
}

// getDefaultUserAgent returns the "User-Agent" HTTP request header added to outgoing HTTP requests.
func getDefaultUserAgent() string {
return fmt.Sprintf("egoscale/%s (%s; %s/%s)",
Version,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH)
}

// NewClient returns a new Exoscale API client.
func NewClient(credentials *credentials.Credentials, opts ...ClientOpt) (*Client, error) {
values, err := credentials.Get()
if err != nil {
return nil, err
}

client := &Client{
apiKey: values.APIKey,
apiSecret: values.APISecret,
serverEndpoint: string({{ .ServerEndpoint }}),
httpClient: http.DefaultClient,
pollingInterval: pollingInterval,
validate: validator.New(),
}
client := &Client{
apiKey: values.APIKey,
apiSecret: values.APISecret,
serverEndpoint: string({{ .ServerEndpoint }}),
httpClient: http.DefaultClient,
pollingInterval: pollingInterval,
validate: validator.New(),
userAgent: getDefaultUserAgent(),
}

for _, opt := range opts {
if err := opt(client); err != nil {
Expand All @@ -134,60 +149,64 @@ func NewClient(credentials *credentials.Credentials, opts ...ClientOpt) (*Client
return client, nil
}

// getUserAgent only for compatibility with UserAgent.
func (c *Client) getUserAgent() string {
defaultUA := getDefaultUserAgent()

if c.userAgent != defaultUA {
return c.userAgent
}

if UserAgent != defaultUA {
return UserAgent
}

return c.userAgent
}

// WithEndpoint returns a copy of Client with new zone Endpoint.
func (c *Client) WithEndpoint(endpoint Endpoint) *Client {
return &Client{
apiKey: c.apiKey,
apiSecret: c.apiSecret,
serverEndpoint: string(endpoint),
httpClient: c.httpClient,
requestInterceptors: c.requestInterceptors,
pollingInterval: c.pollingInterval,
trace: c.trace,
validate: c.validate,
}
clone := cloneClient(c)

clone.serverEndpoint = string(endpoint)

return clone
}

// WithUserAgent returns a copy of Client with new User-Agent.
func (c *Client) WithUserAgent(ua string) *Client {
clone := cloneClient(c)

clone.userAgent = ua + " " + getDefaultUserAgent()

return clone
}

// WithTrace returns a copy of Client with tracing enabled.
func (c *Client) WithTrace() *Client {
return &Client{
apiKey: c.apiKey,
apiSecret: c.apiSecret,
serverEndpoint: c.serverEndpoint,
httpClient: c.httpClient,
requestInterceptors: c.requestInterceptors,
pollingInterval: c.pollingInterval,
trace: true,
validate: c.validate,
}
clone := cloneClient(c)

clone.trace = true

return clone
}

// WithHttpClient returns a copy of Client with new http.Client.
func (c *Client) WithHttpClient(client *http.Client) *Client {
return &Client{
apiKey: c.apiKey,
apiSecret: c.apiSecret,
serverEndpoint: c.serverEndpoint,
httpClient: client,
requestInterceptors: c.requestInterceptors,
pollingInterval: c.pollingInterval,
trace: c.trace,
validate: c.validate,
}
clone := cloneClient(c)

clone.httpClient = client

return clone
}

// WithRequestInterceptor returns a copy of Client with new RequestInterceptors.
func (c *Client) WithRequestInterceptor(f ...RequestInterceptorFn) *Client {
return &Client{
apiKey: c.apiKey,
apiSecret: c.apiSecret,
serverEndpoint: c.serverEndpoint,
httpClient: c.httpClient,
requestInterceptors: append(c.requestInterceptors, f...),
pollingInterval: c.pollingInterval,
trace: c.trace,
validate: c.validate,
}
clone := cloneClient(c)

clone.requestInterceptors = append(clone.requestInterceptors, f...)

return clone
}

func (c *Client) executeRequestInterceptors(ctx context.Context, req *http.Request) error {
Expand All @@ -199,3 +218,17 @@ func (c *Client) executeRequestInterceptors(ctx context.Context, req *http.Reque

return nil
}

func cloneClient(c *Client) *Client {
return &Client{
apiKey: c.apiKey,
apiSecret: c.apiSecret,
userAgent: c.userAgent,
serverEndpoint: c.serverEndpoint,
httpClient: c.httpClient,
requestInterceptors: c.requestInterceptors,
pollingInterval: c.pollingInterval,
trace: c.trace,
validate: c.validate,
}
}
3 changes: 2 additions & 1 deletion v3/generator/operations/request.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ func (c Client) {{ .Name }}({{ .Params }}) {{ .ValueReturn }} {
if err != nil {
return nil, fmt.Errorf("{{ .Name }}: new request: %w", err)
}
request.Header.Add("User-Agent", UserAgent)

request.Header.Add("User-Agent", c.getUserAgent())

{{ if ne .QueryParams nil }}if len(opts) > 0 {
q := request.URL.Query()
Expand Down
Loading

0 comments on commit 3546f6b

Please sign in to comment.