Skip to content

Commit

Permalink
Merge pull request #41 from Scalingo/feat/db-api-compat
Browse files Browse the repository at this point in the history
feat(db-api) Add database and backup APIs
  • Loading branch information
aurelien-reeves-scalingo authored Dec 22, 2022
2 parents 8a036b4 + 191b0bb commit bcb006c
Show file tree
Hide file tree
Showing 27 changed files with 700 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## Unreleased

* Removal: `Scalingo::Client#agora_fr1` had been removed since the region no longer exists.
* New: Add `addons#authenticate!` endpoint
* New API: database API
* New API: backup API

## 3.1.0

Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A ruby wrapper for the Scalingo API
### Migration from v2

This gem is changing its name from `scalingo-ruby-api` to `scalingo`,
and the versionning does **not** reset; the first major version of `scalingo`
and the versioning does **not** reset; the first major version of `scalingo`
will therefore be `3.x.x`.

You can check the version 2 at [the v2 branch of this repository](https://github.com/Scalingo/scalingo-ruby-api/tree/v2)
Expand Down Expand Up @@ -145,6 +145,37 @@ scalingo.osc_fr1.apps.all # OR scalingo.region(:osc_fr1).apps.all
scalingo.apps.create(name: "my-new-app", dry_run: true)
```

### Interacting with databases

Requests to the [database API](https://developers.scalingo.com/databases/) requires
extra authentication for each addon you want to interact with. [Addon authentication
tokens are valid for one hour](https://developers.scalingo.com/addons#get-addon-token).

Supported regions for database API are `db_api_osc_fr1` and `db_api_osc_secnum_fr1`.

```ruby
require "scalingo"

scalingo = Scalingo::Client.new
scalingo.authenticate_with(access_token: "my_access_token")

# First, authenticate using the `addons` API
scalingo.osc_fr1.addons.authenticate!(app_id, addon_id)

# Once authenticated for that specific addon, you can interact with
# database and backup APIs.
# IDs of databases are the IDs of the corresponding addons

# get all information for a given database
scalingo.db_api_osc_fr1.databases.find(addon_id)

# get all backups for a given database
scalingo.db_api_osc_fr1.backups.for(addon_id)

# get URL to download backup archive
scalingo.db_api_osc_fr1.backups.archive(addon_id, backup_id)
```

## Development

### Install
Expand Down
32 changes: 23 additions & 9 deletions lib/scalingo/api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,11 @@ def connection_options
# this method may return the unauthenticated connection
# even with `fallback_to_guest: false`
def connection(fallback_to_guest: false)
if fallback_to_guest
begin
authenticated_connection
rescue Error::Unauthenticated
unauthenticated_connection
end
else
authenticated_connection
end
authenticated_connection
rescue Error::Unauthenticated
raise unless fallback_to_guest

unauthenticated_connection
end

def unauthenticated_connection
Expand Down Expand Up @@ -119,6 +115,24 @@ def authenticated_connection
conn.adapter(config.faraday_adapter) if config.faraday_adapter
}
end

def database_connection(database_id)
raise Error::Unauthenticated unless token_holder.authenticated_for_database?(database_id)

@database_connections ||= {}
@database_connections[database_id] ||= Faraday.new(connection_options) { |conn|
conn.response :json, content_type: /\bjson$/, parser_options: {symbolize_names: true}
conn.request :json

bearer_token = token_holder.database_tokens[database_id]&.value
if bearer_token
auth_header = Faraday::Request::Authorization.header "Bearer", bearer_token
conn.headers[Faraday::Request::Authorization::KEY] = auth_header
end

conn.adapter(config.faraday_adapter) if config.faraday_adapter
}
end
end
end
end
1 change: 1 addition & 0 deletions lib/scalingo/api/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def initialize(client)
end

def_delegator :client, :connection
def_delegator :client, :database_connection

def inspect
str = %(<#{self.class}:0x#{object_id.to_s(16)} base_url:"#{@client.url}" endpoints:)
Expand Down
17 changes: 17 additions & 0 deletions lib/scalingo/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "scalingo/auth"
require "scalingo/billing"
require "scalingo/regional"
require "scalingo/regional_database"

module Scalingo
class Client < CoreClient
Expand All @@ -26,12 +27,28 @@ def osc_fr1
scalingo: self,
)
end
alias_method :apps_api_osc_fr1, :osc_fr1

def osc_secnum_fr1
@osc_secnum_fr1 ||= Regional.new(
"https://api.osc-secnum-fr1.scalingo.com/v1",
scalingo: self,
)
end
alias_method :apps_api_osc_secnum_fr1, :osc_secnum_fr1

def db_api_osc_fr1
@db_api_osc_fr1 ||= RegionalDatabase.new(
"https://db-api.osc-fr1.scalingo.com/api",
scalingo: self,
)
end

def db_api_osc_secnum_fr1
@db_api_osc_secnum_fr1 ||= RegionalDatabase.new(
"https://db-api.osc-secnum-fr1.scalingo.com/api",
scalingo: self,
)
end
end
end
15 changes: 15 additions & 0 deletions lib/scalingo/regional/addons.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ def sso(app_id, addon_id, headers = nil, &block)
unpack(:addon) { response }
end

def authenticate!(app_id, addon_id, headers = nil, &block)
response = token(app_id, addon_id, headers, &block)
return response unless response.status == 200

token = response.data[:token]
client.token_holder.authenticate_database_with_bearer_token(
addon_id,
token,
expires_at: Time.now + 1.hour,
raise_on_expired_token: client.config.raise_on_expired_token,
)

response
end

def token(app_id, addon_id, headers = nil, &block)
data = nil

Expand Down
13 changes: 13 additions & 0 deletions lib/scalingo/regional_database.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require "scalingo/api/client"

module Scalingo
class RegionalDatabase < API::Client
require "scalingo/regional_database/databases"
require "scalingo/regional_database/backups"

register_handlers!(
databases: Databases,
backups: Backups,
)
end
end
44 changes: 44 additions & 0 deletions lib/scalingo/regional_database/backups.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require "scalingo/api/endpoint"

module Scalingo
class RegionalDatabase::Backups < API::Endpoint
def create(addon_id, headers = nil, &block)
data = nil

response = database_connection(addon_id).post(
"databases/#{addon_id}/backups",
data,
headers,
&block
)

unpack { response }
end

def for(addon_id, headers = nil, &block)
data = nil

response = database_connection(addon_id).get(
"databases/#{addon_id}/backups",
data,
headers,
&block
)

unpack(:database_backups) { response }
end

def archive(addon_id, backup_id, headers = nil, &block)
data = nil

response = database_connection(addon_id).get(
"databases/#{addon_id}/backups/#{backup_id}/archive",
data,
headers,
&block
)

unpack { response }
end
end
end
18 changes: 18 additions & 0 deletions lib/scalingo/regional_database/databases.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require "scalingo/api/endpoint"

module Scalingo
class RegionalDatabase::Databases < API::Endpoint
def find(id, headers = nil, &block)
data = nil

response = database_connection(id).get(
"databases/#{id}",
data,
headers,
&block
)

unpack(:database) { response }
end
end
end
57 changes: 44 additions & 13 deletions lib/scalingo/token_holder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,59 @@ module Scalingo
module TokenHolder
def self.included(base)
base.attr_reader :token
base.attr_reader :database_tokens
end

def token=(input)
@token = input.is_a?(BearerToken) ? input : BearerToken.new(input.to_s, raise_on_expired: config.raise_on_expired_token)
@token = bearer_token(input)
end

def add_database_token(database_id, token)
@database_tokens ||= {}
@database_tokens[database_id] = bearer_token(token)
end

def authenticated?
token.present? && !token.expired?
valid?(token)
end

def authenticated_for_database?(database_id)
return false if database_tokens.nil?
return false unless database_tokens.has_key?(database_id)

valid?(database_tokens[database_id])
end

def authenticate_with_bearer_token(bearer_token, expires_at:, raise_on_expired_token:)
self.token = if expires_at
token = bearer_token.is_a?(BearerToken) ? bearer_token.value : bearer_token.to_s

BearerToken.new(
token,
expires_at: expires_at,
raise_on_expired: raise_on_expired_token,
)
else
bearer_token
end
self.token = build_bearer_token(bearer_token, expires_at: expires_at, raise_on_expired_token: raise_on_expired_token)
end

def authenticate_database_with_bearer_token(database_id, bearer_token, expires_at:, raise_on_expired_token:)
bearer_token = build_bearer_token(bearer_token, expires_at: expires_at, raise_on_expired_token: raise_on_expired_token)

add_database_token(database_id, bearer_token)
end

private

def valid?(token)
token.present? && !token.expired?
end

def bearer_token(token)
token.is_a?(BearerToken) ? token : BearerToken.new(token.to_s, raise_on_expired: config.raise_on_expired_token)
end

def build_bearer_token(bearer_token, expires_at:, raise_on_expired_token:)
return bearer_token unless expires_at

token = bearer_token.is_a?(BearerToken) ? bearer_token.value : bearer_token.to_s

BearerToken.new(
token,
expires_at: expires_at,
raise_on_expired: raise_on_expired_token,
)
end
end
end
4 changes: 4 additions & 0 deletions samples/regional_database/backups/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"addon_id": "ad-5ed10967884fef000f5e4fff",
"backup_id": "5bb95a904ffb096e9a2831b8"
}
24 changes: 24 additions & 0 deletions samples/regional_database/backups/archive-200.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"path": "/databases/ad-5ed10967884fef000f5e4fff/backups/5bb95a904ffb096e9a2831b8/archive",
"method": "get",
"request": {
"headers": {
"Authorization": "Bearer the-bearer-token"
}
},
"response": {
"status": 200,
"headers": {
"Date": "Fri, 29 May 2020 13:08:59 GMT",
"Etag": "W/\"a9504bb2f6f87c65ff68074ae787831e\"",
"Content-Type": "application/json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Cache-Control": "max-age=0, private, must-revalidate",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"json_body": {
"download_url": "https://regional-database.scalingo.test/databases/ad-5ed10967884fef000f5e4fff/backups/5bb95a904ffb096e9a2831b8/download?token=token1234"
}
}
}
24 changes: 24 additions & 0 deletions samples/regional_database/backups/archive-400.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"path": "/databases/ad-5ed10967884fef000f5e4fff/backups/5bb95a904ffb096e9a2831b8/archive",
"method": "get",
"request": {
"headers": {
"Authorization": "Bearer the-bearer-token"
}
},
"response": {
"status": 400,
"headers": {
"Date": "Fri, 29 May 2020 13:08:59 GMT",
"Etag": "W/\"a9504bb2f6f87c65ff68074ae787831e\"",
"Content-Type": "application/json; charset=utf-8",
"Transfer-Encoding": "chunked",
"Connection": "keep-alive",
"Cache-Control": "max-age=0, private, must-revalidate",
"Referrer-Policy": "strict-origin-when-cross-origin"
},
"json_body": {
"error": "unauthorized"
}
}
}
Loading

0 comments on commit bcb006c

Please sign in to comment.