Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

oxide: refactor exported client API #180

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .changelog/v0.1.0-beta3.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,13 @@ title = ""
description = ""

[[breaking]]
title = "Go verrsion"
description = "Minimum required Go version has been updated to 1.21. [#179](https://github.com/oxidecomputer/oxide.go/pull/179)"
title = "Go version"
description = "Minimum required Go version has been updated to 1.21. [#179](https://github.com/oxidecomputer/oxide.go/pull/179)"

sudomateo marked this conversation as resolved.
Show resolved Hide resolved
[[breaking]]
title = "NewClient API change"
description = "The `NewClient` function has been updated to no longer require a user agent parameter. [#180](https://github.com/oxidecomputer/oxide.go/pull/180)"

[[breaking]]
title = "NewClientFromEnv removal"
description = "The `NewClientFromEnv` function has been removed. Users should use `NewClient` instead. [#180](https://github.com/oxidecomputer/oxide.go/pull/180)"
4 changes: 3 additions & 1 deletion .github/ISSUE_TEMPLATE/release_checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ labels: release
After completing each task put an `x` in the corresponding box,
and paste the link to the relevant PR.
-->
- [ ] Make sure version is set to the new version in `VERSION` file.
- [ ] Make sure the following files have the new version you want to release.
- [ ] [`VERSION`](./VERSION)
- [ ] [`oxide/version.go`](./oxide/version.go)
- [ ] Make sure all examples and docs reference the new version.
- [ ] Generate changelog by running `make changelog` and add date of the release to the title.
- [ ] Release the new version by running `make tag`.
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ $ make all

## Releasing a new SDK version

1. Make sure the [`VERSION`](./VERSION) file has the new version you want to release.
1. Make sure the following files have the new version you want to release.
1. [`VERSION`](./VERSION)
1. [`oxide/version.go`](./oxide/version.go)
2. Make sure you have run `make all` and pushed any changes. The release
will fail if running `make all` causes any changes to the generated
code.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import (
)

func main() {
client, err := oxide.NewClient("<auth token>", "<user-agent>", "<host>")
cfg := oxide.Config{
Address: "https://api.oxide.computer",
Token: "oxide-abc123",
}
client, err := oxide.NewClient(&cfg)
if err != nil {
panic(err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/no_resptype_body_method.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
b := params.Body{{end}}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"{{.HTTPMethod}}",
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/no_resptype_method.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
return err
}{{end}}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"{{.HTTPMethod}}",
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/resptype_body_method.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
b := params.Body{{end}}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"{{.HTTPMethod}}",
Expand Down
2 changes: 1 addition & 1 deletion internal/generate/templates/resptype_method.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
return nil, err
}{{end}}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"{{.HTTPMethod}}",
Expand Down
10 changes: 5 additions & 5 deletions internal/generate/test_utils/paths_output
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (c *Client) IpPoolList(ctx context.Context, params IpPoolListParams, ) (*Ip
return nil, err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"GET",
Expand Down Expand Up @@ -95,7 +95,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"POST",
Expand Down Expand Up @@ -141,7 +141,7 @@ func (c *Client) IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*Ip
return nil, err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"GET",
Expand Down Expand Up @@ -194,7 +194,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"PUT",
Expand Down Expand Up @@ -241,7 +241,7 @@ func (c *Client) IpPoolDelete(ctx context.Context, params IpPoolDeleteParams, )
return err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"DELETE",
Expand Down
10 changes: 5 additions & 5 deletions internal/generate/test_utils/paths_output_expected
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (c *Client) IpPoolList(ctx context.Context, params IpPoolListParams, ) (*Ip
return nil, err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"GET",
Expand Down Expand Up @@ -95,7 +95,7 @@ func (c *Client) IpPoolCreate(ctx context.Context, params IpPoolCreateParams, )
}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"POST",
Expand Down Expand Up @@ -141,7 +141,7 @@ func (c *Client) IpPoolView(ctx context.Context, params IpPoolViewParams, ) (*Ip
return nil, err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"GET",
Expand Down Expand Up @@ -194,7 +194,7 @@ func (c *Client) IpPoolUpdate(ctx context.Context, params IpPoolUpdateParams, )
}

// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
b,
"PUT",
Expand Down Expand Up @@ -241,7 +241,7 @@ func (c *Client) IpPoolDelete(ctx context.Context, params IpPoolDeleteParams, )
return err
}
// Create the request
req, err := buildRequest(
req, err := c.buildRequest(
ctx,
nil,
"DELETE",
Expand Down
153 changes: 75 additions & 78 deletions oxide/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,75 +22,98 @@ const TokenEnvVar = "OXIDE_TOKEN"
// HostEnvVar is the environment variable that contains the host.
const HostEnvVar = "OXIDE_HOST"

// Config is the configuration that can be set on a Client.
type Config struct {
// Base URL of the Oxide API including the scheme. For example,
// https://api.oxide.computer.
Address string
sudomateo marked this conversation as resolved.
Show resolved Hide resolved

// Oxide API authentication token.
Token string

// A custom HTTP client to use for the Client instead of the default.
HTTPClient *http.Client

// A custom user agent string to add to every request instead of the
// default.
UserAgent string
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
}

// Client which conforms to the OpenAPI3 specification for this service.
type Client struct {
// The endpoint of the server conforming to this interface, with scheme,
// https://api.oxide.computer for example.
// Base URL of the Oxide API including the scheme. For example,
// https://api.oxide.computer.
server string

// Client is the *http.Client for performing requests.
// Oxide API authentication token.
token string

// HTTP client to make API requests.
client *http.Client

// token is the API token used for authentication.
token string
// The user agent string to add to every API request.
userAgent string
}

// NewClient creates a new client for the Oxide API.
// You need to pass in your API token to create the client.
func NewClient(token, userAgent, host string) (*Client, error) {
if token == "" {
return nil, fmt.Errorf("you need to pass in an API token to create the client")
// NewClient creates a new client for the Oxide API. Pass in a non-nil *Config
// to set the various configuration options on a Client.
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
func NewClient(cfg *Config) (*Client, error) {
token := os.Getenv(TokenEnvVar)
server := os.Getenv(HostEnvVar)
userAgent := defaultUserAgent()
httpClient := &http.Client{
Timeout: 600 * time.Second,
}

baseURL, err := parseBaseURL(host)
if err != nil {
return nil, err
}
// Layer in the user-provided configuration if provided.
if cfg != nil {
if cfg.Address != "" {
server = cfg.Address
}

client := &Client{
server: baseURL,
token: token,
}
if cfg.Token != "" {
token = cfg.Token
}

// Ensure the server URL always has a trailing slash.
if !strings.HasSuffix(client.server, "/") {
client.server += "/"
}
if cfg.UserAgent != "" {
userAgent = cfg.UserAgent
}

uat := userAgentTransport{
base: http.DefaultTransport,
userAgent: userAgent,
client: client,
if cfg.HTTPClient != nil {
httpClient = cfg.HTTPClient
}
}

client.client = &http.Client{
Transport: uat,
// We want a longer timeout since some of the files might take a bit.
Timeout: 600 * time.Second,
server, err := parseBaseURL(server)
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, fmt.Errorf("failed parsing client address: %w", err)
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
}

return client, nil
}

// NewClientFromEnv creates a new client for the Oxide API, using the token
// stored in the environment variable `OXIDE_TOKEN` and the host stored in the
// environment variable `OXIDE_HOST`.
func NewClientFromEnv(userAgent string) (*Client, error) {
token := os.Getenv(TokenEnvVar)
if token == "" {
return nil, fmt.Errorf("the environment variable %s must be set with your API token", TokenEnvVar)
return nil, errors.New("invalid client configuration: token is required")
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
}

host := os.Getenv(HostEnvVar)
if host == "" {
return nil, fmt.Errorf("the environment variable %s must be set with the host of the Oxide API", HostEnvVar)
client := &Client{
token: token,
server: server,
userAgent: userAgent,
client: httpClient,
}

return NewClient(token, userAgent, host)
return client, nil
}

// defaultUserAgent builds and returns the default user agent string.
func defaultUserAgent() string {
return fmt.Sprintf("oxide.go/%s", version)
}

// parseBaseURL parses the base URL from the server URL.
func parseBaseURL(baseURL string) (string, error) {
if baseURL == "" {
return "", errors.New("address is empty")
sudomateo marked this conversation as resolved.
Show resolved Hide resolved
}

if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
// Assume https.
baseURL = "https://" + baseURL
Expand All @@ -111,47 +134,21 @@ func parseBaseURL(baseURL string) (string, error) {
return b, nil
}

// WithToken overrides the token used for authentication.
func (c *Client) WithToken(token string) {
c.token = token
}

type userAgentTransport struct {
userAgent string
base http.RoundTripper
client *Client
}

func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.base == nil {
return nil, errors.New("RoundTrip: no Transport specified")
}

newReq := *req
newReq.Header = make(http.Header)
for k, vv := range req.Header {
newReq.Header[k] = vv
}

// Add the user agent header.
newReq.Header["User-Agent"] = []string{t.userAgent}

// Add the content-type header.
newReq.Header["Content-Type"] = []string{"application/json"}

// Add the authorization header.
newReq.Header["Authorization"] = []string{fmt.Sprintf("Bearer %s", t.client.token)}

return t.base.RoundTrip(&newReq)
}

func buildRequest(ctx context.Context, body io.Reader, method, uri string, params, queries map[string]string) (*http.Request, error) {
// buildRequest creates an HTTP request to interact with the Oxide API.
func (c *Client) buildRequest(ctx context.Context, body io.Reader, method, uri string, params, queries map[string]string) (*http.Request, error) {
// Create the request.
req, err := http.NewRequestWithContext(ctx, method, uri, body)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}

if c.userAgent != "" {
req.Header.Set("User-Agent", c.userAgent)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", c.token)

// Add the parameters to the url.
if err := expandURL(req.URL, params); err != nil {
return nil, fmt.Errorf("expanding URL with parameters failed: %v", err)
Expand Down
Loading