Skip to content

Commit

Permalink
Unified api and add session invalidation handling
Browse files Browse the repository at this point in the history
* Move to backward compatible 3.x API
    * If you're using Quobyte 2.x refer to 2.x management API definitions, not supported fields by 2.x are ignored by API server.
* Add session cookies to improve API performance
   * Currently, each request is authenticated with username and password. The authentication is expensive and could block requests if there are many concurrent requests. With session cookies solves this problem.
  • Loading branch information
venkatsc authored Jul 29, 2021
1 parent 582c5e6 commit 14fea86
Show file tree
Hide file tree
Showing 20 changed files with 5,469 additions and 9,711 deletions.
63 changes: 58 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
# Quobyte API
# Quobyte API Clients

* User documentation
* [For 2.x API](./v2/README.md)
* [For 3.x API](./v3/README.md)
* Developer documentation (./docs/)
Get the Quobyte api client

```bash
go get github.com/quobyte/api
```

## Usage

`Note:` Create below example in separate project outside of the api repository to avoid circular `go mod` dependencies.

```go
package main

import (
"log"
quobyte_api "github.com/quobyte/api/quobyte"
)

func main() {
url := flag.String("url", "", "URL of Quobyte API")
username := flag.String("username", "", "username")
password := flag.String("password", "", "password")
flag.Parse()

if *url == "" || *username == "" || *password == "" {
flag.PrintDefaults()
os.Exit(1)
}

client := quobyte_api.NewQuobyteClient(*url, *username, *password)
client.SetAPIRetryPolicy(quobyte_api.RetryInfinitely) // Default quobyte_api.RetryInteractive
req := &quobyte_api.CreateVolumeRequest{
Name: "MyVolume",
TenantId: "32edb36d-badc-affe-b44a-4ab749af4d9a",
RootUserId: "root",
RootGroupId: "root",
ConfigurationName: "BASE",
Label: []*quobyte_api.Label{
{Name: "label1", Value: "value1"},
{Name: "label2", Value: "value2"},
},
}

response, err := client.CreateVolume(req)
if err != nil {
log.Fatalf("Error: %v", err)
}

capactiy := int64(1024 * 1024 * 1024)
err = client.SetVolumeQuota(response.VolumeUuid, capactiy)
if err != nil {
log.Fatalf("Error: %v", err)
}

log.Printf("%s", response.VolumeUuid)
}
```
19 changes: 19 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Developer notes

## Releasing new version

* `go.mod` files must be present at the root level of the project
* Each major release beyond V1 (such =v2[+].a.b) must provide unique import path such as `github.com/quobyte/api/vX`
* To get around this issue, we always use v1.x.x (**NEVER** make v2 release)
* Further, each `*.go` file must have a `package XYZ` statement as the first line and must be placed into `XZY`
directory. (It seems go mod ignores .go file that is not placed in declared package directory!!)
* For local testing of edited module,
* Create a standalone project with the `testing or main.go` and `go mod init` inside the project root.
The `go mod init` fetches the depedencies required by code.
* Replace Quobyte API with updated API `go mod edit -replace github.com/quobyte/api=</path/to/local/quobyte/api>`
* Publishing change must always have highest minor version of all the published tags (even if the tag is deleted,
the new version must have the higher version than deleted tag).

Note: go mod updates dependency to the highest minor version available. Even if the version is deleted from the tags,
go-git gets it from the deleted references (Uhhh!!). The only way out is to create a new higher minor
version with the changes.
10 changes: 0 additions & 10 deletions docs/Release-instructions.md

This file was deleted.

3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/quobyte/api

go 1.14
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

35 changes: 21 additions & 14 deletions v3/quobyte.go → quobyte/quobyte.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package quobyte
import (
"log"
"net/http"
"regexp"
"net/http/cookiejar"
"net/url"
"regexp"
)

// retry policy codes
Expand All @@ -17,11 +18,15 @@ var UUIDValidator = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0

type QuobyteClient struct {
client *http.Client
url string
url *url.URL
username string
password string
apiRetryPolicy string
hasCookies bool
// hasCookies bool
}

func (client *QuobyteClient) hasCookies() (bool, error) {
return client.client.Jar != nil && len(client.client.Jar.Cookies(client.url)) > 0, nil
}

func (client *QuobyteClient) SetAPIRetryPolicy(retry string) {
Expand All @@ -37,20 +42,22 @@ func (client *QuobyteClient) SetTransport(t http.RoundTripper) {
}

// NewQuobyteClient creates a new Quobyte API client
func NewQuobyteClient(url string, username string, password string) *QuobyteClient {
func NewQuobyteClient(urlStr string, username string, password string) *QuobyteClient {
url, err := url.Parse(urlStr)
if err != nil {
log.Fatalf("could not parse url due to %s", err.Error())
}
cookieJar, err := cookiejar.New(nil)
if err == nil {
return &QuobyteClient{
client: &http.Client{Jar: cookieJar},
url: url,
username: username,
password: password,
apiRetryPolicy: RetryInteractive,
hasCookies: false,
}
} else {
if err != nil {
log.Fatalf("could not initialize cookie jar due to %s", err.Error())
}
return &QuobyteClient{
client: &http.Client{Jar: cookieJar},
url: url,
username: username,
password: password,
apiRetryPolicy: RetryInteractive,
}
return nil
}

Expand Down
71 changes: 46 additions & 25 deletions v2/rpc_client.go → quobyte/rpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,39 +103,60 @@ func (client QuobyteClient) sendRequest(method string, request interface{}, resp
if err != nil {
return err
}
req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(message))
req, err := http.NewRequest("POST", client.url.String(), bytes.NewBuffer(message))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
// If no cookies, serialize requests such that first successful request sets the cookies
if !client.hasCookies {
for {
mux.Lock()
defer mux.Unlock()
req.SetBasicAuth(client.username, client.password)
}
resp, err := client.client.Do(req)
if err != nil {
return err
}
// log.Printf("response %v", resp)
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
if resp.StatusCode == 401 {
return errors.New("Unable to authenticate with Quobyte API service")
hasCookies, err := client.hasCookies()
if err != nil {
return err
}
if !hasCookies {
req.SetBasicAuth(client.username, client.password)
// no cookies available, must hold lock until request is completed and
// new cookies are created by server
defer mux.Unlock()
} else {
// let every thread/routine send request using the cookie
mux.Unlock()
}
body, err := ioutil.ReadAll(resp.Body)
resp, err := client.client.Do(req)
if err != nil {
return (err)
return err
}
return fmt.Errorf("JsonRPC failed with error (error code: %d) %s",
resp.StatusCode, string(body))
}
if len(resp.Cookies()) > 0 {
client.hasCookies = true
} else {
client.hasCookies = false
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode > 299 {
if resp.StatusCode == 401 {
_, ok := req.Header["Authorization"]
if ok {
return errors.New("Unable to authenticate with Quobyte API service")
}
// Session is not valid anymore (service restart, sesssion invalidated etc)!!
// resend basic auth and get new cookies
// invalidate session cookies
cookieJar := client.client.Jar
if cookieJar != nil {
cookies := cookieJar.Cookies(client.url)
for _, cookie := range cookies {
cookie.MaxAge = -1
}
cookieJar.SetCookies(client.url, cookies)
}
// retry request with authorization header
continue
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return (err)
}
return fmt.Errorf("JsonRPC failed with error (error code: %d) %s",
resp.StatusCode, string(body))
}
return decodeResponse(resp.Body, &response)
}
return decodeResponse(resp.Body, &response)
}
File renamed without changes.
Loading

0 comments on commit 14fea86

Please sign in to comment.