diff --git a/README.md b/README.md index 4293db8..115346d 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. @@ -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 @@ -100,7 +100,7 @@ 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. @@ -108,7 +108,7 @@ Hence, to use the API, you must first register your application and then ask you 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. @@ -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 @@ -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 @@ -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 @@ -444,7 +467,7 @@ go vet ./... ## Supported APIs -### OVH Europe +### OVHcloud Europe - **Documentation**: https://eu.api.ovh.com/ - **Community support**: api-subscribe@ml.ovh.net @@ -452,14 +475,14 @@ go vet ./... - **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 diff --git a/ovh/configuration.go b/ovh/configuration.go index c0bf795..b648c0f 100644 --- a/ovh/configuration.go +++ b/ovh/configuration.go @@ -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() diff --git a/ovh/configuration_test.go b/ovh/configuration_test.go index 392d3cf..a08b0e5 100644 --- a/ovh/configuration_test.go +++ b/ovh/configuration_test.go @@ -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) diff --git a/ovh/ovh.go b/ovh/ovh.go index 46a468a..a6ee2db 100644 --- a/ovh/ovh.go +++ b/ovh/ovh.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "net/http" "strconv" + "strings" "sync/atomic" "time" ) @@ -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 @@ -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 } diff --git a/ovh/ovh_test.go b/ovh/ovh_test.go index 9a70fe0..e52c657 100644 --- a/ovh/ovh_test.go +++ b/ovh/ovh_test.go @@ -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) +}