Skip to content

Commit

Permalink
feat: allow /v1 or /v2 prefixes in path
Browse files Browse the repository at this point in the history
Signed-off-by: Adrien Barreau <adrien.barreau@ovhcloud.com>
  • Loading branch information
deathiop committed Mar 22, 2023
1 parent 39b6ccf commit 3f38947
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 14 deletions.
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
go-ovh
======

Lightweight Go wrapper around OVH's APIs. Handles all the hard work including credential creation and requests signing.
Lightweight Go wrapper around OVHcloud's APIs. Handles all the hard work including credential creation and requests signing.

[![GoDoc](https://godoc.org/github.com/ovh/go-ovh/go-ovh?status.svg)](http://godoc.org/github.com/ovh/go-ovh/ovh)
[![Build Status](https://github.com/ovh/go-ovh/actions/workflows/golang-build.yaml/badge.svg?branch=master)](https://github.com/ovh/go-ovh/actions?query=workflow:golang-build)
Expand Down Expand Up @@ -53,7 +53,7 @@ import (

## Configuration

The straightforward way to use OVH's API keys is to embed them directly in the
The straightforward way to use OVHcloud's API keys is to embed them directly in the
application code. While this is very convenient, it lacks of elegance and
flexibility.

Expand All @@ -80,9 +80,9 @@ consumer_key=my_consumer_key

Depending on the API you want to use, you may set the ``endpoint`` to:

* ``ovh-eu`` for OVH Europe API
* ``ovh-us`` for OVH US API
* ``ovh-ca`` for OVH Canada API
* ``ovh-eu`` for OVHcloud Europe API
* ``ovh-us`` for OVHcloud US API
* ``ovh-ca`` for OVHcloud Canada API
* ``soyoustart-eu`` for So you Start Europe API
* ``soyoustart-ca`` for So you Start Canada API
* ``kimsufi-eu`` for Kimsufi Europe API
Expand All @@ -100,15 +100,15 @@ project or user.

## Register your app

OVH's API, like most modern APIs is designed to authenticate both an application and
OVHcloud's API, like most modern APIs is designed to authenticate both an application and
a user, without requiring the user to provide a password. Your application will be
identified by its "application secret" and "application key" tokens.

Hence, to use the API, you must first register your application and then ask your
user to authenticate on a specific URL. Once authenticated, you'll have a valid
"consumer key" which will grant your application on specific APIs.

The user may choose the validity period of its authorization. The default period is
The user may choose the validity period of his authorization. The default period is
24h. He may also revoke an authorization at any time. Hence, your application should
be prepared to receive 403 HTTP errors and prompt the user to re-authenticated.

Expand All @@ -126,7 +126,6 @@ The consumer key has two types of restriction:
* path: eg. only the ```GET``` method on ```/me```
* time: eg. expire in 1 day


Then, get a consumer key. Here's an example on how to generate one.

First, create a 'ovh.conf' file in the current directory with the application key and
Expand Down Expand Up @@ -282,6 +281,30 @@ func main() {
}
```

### Use v1 and v2 API versions

When using OVHcloud APIs (not So you Start or Kimsufi ones), you are given the
opportunity to aim for two API versions. For the European API, for example:

- the v1 is reachable through https://eu.api.ovh.com/v1
- the v2 is reachable through https://eu.api.ovh.com/v2
- the legacy URL is https://eu.api.ovh.com/1.0

Calling `client.Get`, you can target the API version you want:

```go
client, _ := ovh.NewEndpointClient("ovh-eu")

// Call to https://eu.api.ovh.com/v1/xdsl/xdsl-yourservice
client.Get("/v1/xdsl/xdsl-yourservice", nil)

// Call to https://eu.api.ovh.com/v2/xdsl/xdsl-yourservice
client.Get("/v2/xdsl/xdsl-yourservice", nil)

// Legacy call to https://eu.api.ovh.com/1.0/xdsl/xdsl-yourservice
client.Get("/xdsl/xdsl-yourservice", nil)
```

## API Documentation

### Create a client
Expand Down Expand Up @@ -310,7 +333,7 @@ Alternatively, you may directly use the low level ``CallAPI`` method.
- Use ``client.Put()`` for PUT requests
- Use ``client.Delete()`` for DELETE requests

Or, for unautenticated requests:
Or, for unauthenticated requests:

- Use ``client.GetUnAuth()`` for GET requests
- Use ``client.PostUnAuth()`` for POST requests
Expand Down Expand Up @@ -444,22 +467,22 @@ go vet ./...

## Supported APIs

### OVH Europe
### OVHcloud Europe

- **Documentation**: https://eu.api.ovh.com/
- **Community support**: api-subscribe@ml.ovh.net
- **Console**: https://eu.api.ovh.com/console
- **Create application credentials**: https://eu.api.ovh.com/createApp/
- **Create script credentials** (all keys at once): https://eu.api.ovh.com/createToken/

### OVH US
### OVHcloud US

- **Documentation**: https://api.us.ovhcloud.com/
- **Console**: https://api.us.ovhcloud.com/console/
- **Create application credentials**: https://api.us.ovhcloud.com/createApp/
- **Create script credentials** (all keys at once): https://api.us.ovhcloud.com/createToken/

### OVH Canada
### OVHcloud Canada

- **Documentation**: https://ca.api.ovh.com/
- **Community support**: api-subscribe@ml.ovh.net
Expand Down
4 changes: 4 additions & 0 deletions ovh/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ func loadINI() (*ini.File, error) {
// - $HOME/.ovh.conf
// - /etc/ovh.conf
func (c *Client) loadConfig(endpointName string) error {
if strings.HasSuffix(endpointName, "/") {
return fmt.Errorf("endpoint name cannot have a tailing slash")
}

// Load configuration files by order of increasing priority. All configuration
// files are optional. Only load file from user home if home could be resolve
cfg, err := loadINI()
Expand Down
6 changes: 6 additions & 0 deletions ovh/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ func setConfigPaths(t testing.TB, paths ...string) {
t.Cleanup(func() { configPaths = old })
}

func TestConfigForbidsTrailingSlash(t *testing.T) {
client := Client{}
err := client.loadConfig("https://example.org/")
td.Require(t).String(err, "endpoint name cannot have a tailing slash")
}

func TestConfigFromFiles(t *testing.T) {
setConfigPaths(t, systemConf, userPartialConf, localPartialConf)

Expand Down
15 changes: 13 additions & 2 deletions ovh/ovh.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"strconv"
"strings"
"sync/atomic"
"time"
)
Expand Down Expand Up @@ -250,6 +251,17 @@ func (c *Client) getTime() (*time.Time, error) {
return &serverTime, nil
}

// getTarget returns the URL to target given and endpoint and a path.
// If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint.
func getTarget(endpoint, path string) string {
// /1.0 + /v1/ or /1.0 + /v2/
if strings.HasSuffix(endpoint, "/1.0") && (strings.HasPrefix(path, "/v1/") || strings.HasPrefix(path, "/v2/")) {
return endpoint[:len(endpoint)-4] + path
}

return endpoint + path
}

// NewRequest returns a new HTTP request
func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) {
var body []byte
Expand All @@ -262,8 +274,7 @@ func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth b
}
}

target := fmt.Sprintf("%s%s", c.endpoint, path)
req, err := http.NewRequest(method, target, bytes.NewReader(body))
req, err := http.NewRequest(method, getTarget(c.endpoint, path), bytes.NewReader(body))
if err != nil {
return nil, err
}
Expand Down
26 changes: 26 additions & 0 deletions ovh/ovh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,29 @@ func TestConstructors(t *testing.T) {
require.CmpNoError(err)
assert.Cmp(client, expected)
}

func (ms *MockSuite) TestVersionInURL(assert, require *td.T) {
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/call", httpmock.NewStringResponder(200, "{}"))
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/v1/call", httpmock.NewStringResponder(200, "{}"))
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/v2/call", httpmock.NewStringResponder(200, "{}"))

assertCallCount := func(assert *td.T, ccNoVersion, ccV1, ccV2 int) {
assert.Helper()
assert.Cmp(httpmock.GetCallCountInfo(), map[string]int{
"GET https://eu.api.ovh.com/1.0/call": ccNoVersion,
"GET https://eu.api.ovh.com/v1/call": ccV1,
"GET https://eu.api.ovh.com/v2/call": ccV2,
})
}

require.Cmp(ms.client.endpoint, "https://eu.api.ovh.com/1.0")

require.CmpNoError(ms.client.GetUnAuth("/call", nil))
assertCallCount(assert, 1, 0, 0)

require.CmpNoError(ms.client.GetUnAuth("/v1/call", nil))
assertCallCount(assert, 1, 1, 0)

require.CmpNoError(ms.client.GetUnAuth("/v2/call", nil))
assertCallCount(assert, 1, 1, 1)
}

0 comments on commit 3f38947

Please sign in to comment.