Comment se connecter et utiliser l'API de Clever Cloud ?

Lecture 9 min. ‱

Table des matiĂšres

Bonjour 😀

Cela semble bĂȘte dis comme ça mais mĂȘme si je bosse chez Clever Cloud depuis plus d’un an. Je n’ai jamais rĂ©ellement utilisĂ© les produits que j’aide Ă  concevoir.

Sauf cette semaine oĂč je me suis mis en tĂȘte de crĂ©er des rĂŽles ansible et plus particuliĂšre des modules ansible qui nĂ©cessitent de dĂ©velopper en python.

Mais me diriez-vous : “Quel rapport cela a avec Clever-Cloud?”

Bonne question !

La rĂ©ponse est que je veux ĂȘtre capable au moyen d’une API trĂšs simplifiĂ© de faire des trucs du genre:

- name : Order a Postgres Database
  noa.clevercloud.addon_register:
    provider: postgres
    organisation: orga_xxxx
    auth: 
        consumer_key: xxxx
        consumer_secret: xxxx
        ressource_key: xxxx
        ressource_secret: xxxx
    details: 
        plan: xxs_sml
        version: 15
  register: result

- name: Display details
  ansible.builtin.debug:
    msg:
        - "{ result.addon_id }"
        - "{ result.env.POSTGRESQL_ADDON_URI }"

- name: Delete addon
  noa.clevercloud.addon_remove:
    addon: "{ result.addon_id }"

Si vous n’ĂȘtes pas familier de la syntaxe ansible ce n’est pas trĂšs grave. Disons que pour simplifier chaque bloc faits des actions:

Avant que l’on me le dise oui, il existe dĂ©jĂ  une CLI, les clever-tools qui faire tout ce dont j’ai besoin sauf une choses, rĂ©cupĂ©rer les credentials.

Je me connecte. Il vous faut un compte, ce n’est pas long à faire. ^^

clever login

Cela ouvre mon navigateur qui demande de m’authentifier si cela n’est pas dĂ©jĂ  le cas.

Je créé mon addon

clever addon create --plan xxs_sml postgresql-addon deleteme --addon-version 15

Mon addon est bien là 😁

Mais problĂšme, il n’y a rien dans l’API des clever-tools qui me permettent d’accĂ©der aux informations de connexions qui sont dans les variables d’environnement de l’addon.

Cela se comprend, car les addons sont dans la philosophie de Clever censĂ©es fonctionner avec une ou plusieurs applications et de fait si lors de la crĂ©ation de l’addon on le lie Ă  une application en faisant

clever addon create --plan xxs_sml postgresql-addon deleteme --addon-version 15 --link app_xxxx

L’environnement se retrouve alors injectĂ© dans l’application app_xxxx. Ce qui rĂ©sout le problĂšme.

Mais moi, je ne suis pas dans ce cas de figure.

Moi, je veux hacker le systÚme pour créer mon rÎle ansible.

Et donc il me faut trouver un autre moyen. Un moyen me donnant accÚs au précieux environnement tant convoité.

Plan de bataille

Bien maintenant que les présentations sont faites, on commencer à élaborer notre stratégie.

DĂ©jĂ , il nous faut comprendre comment fonctionne l’API de Clever-Cloud.

Authentification

Toute l’API est protĂ©gĂ©e par de l’OAuth 1.0a, ça Ă©voluera bientĂŽt grĂące Ă  merveilleux Biscuit, mais pour l’heure ce n’est pas encore le cas.

Donc, nous allons devoir nous authenfier à l’ancienne.

N’étant pas le premier ni le dernier Ă  m’ĂȘtre fracassĂ© les dents sur le mur de OAuth 1.0. Une personne m’a prĂ©cĂ©dĂ© et Ă  créé un projet oauth-consumer-server, qui a pour but d’aider les Ăąmes en peines comme moi dans le douleureux exercice de la danse de l’authentification.

Une fois la danse réalisée, nous obtiendrons les valeurs pour remplir les xxxx de la section auth.

auth: 
    consumer_key: xxxx
    consumer_secret: xxxx
    ressource_key: xxxx
    ressource_secret: xxxx

Les plans

Ce paramĂštre bien que semblant insignifiant, m’a pris un temps assez considĂ©rable Ă  comprendre. 😅

plan: xxs_sml

Si l’on rĂ©alise l’ingĂ©niĂ©rie inverse de la crĂ©ation d’un addon dans le navigateur depuis la console, on s’apperçoit que ce qui est envoyĂ© Ă  l’api ressemble Ă  ceci:

{
  "name": "deleteme",
  "region": "par",
  "providerId": "postgresql-addon",
  "plan": "plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68",
  "options": {
    "version": "14",
    "encryption": "false"
  }
}

Au travers de

POST https://api.clever-cloud.com/v2/self/addons

Et lĂ , ce fut mon KO technique.

Mais par les Flammes c’est quoi ce plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68 ???

C’est le moment d’explorer l’API !

Me vient alors une idĂ©e, j’ai entendu maintes fois entendre dire Hubert SablonniĂšre que la page de pricing de Clever Ă©tait bĂątis sur des webcomponents. Et dedans il y a les plans. Peut-ĂȘtre aussi la signifiacation de ce hash.

Peut-ĂȘtre que les composants sont hydratĂ© Ă  postĂ©riori du render de la page ?

Et bingo ! En inspectant les requĂȘtes qui passent, on dĂ©couvre celle-ci

GET https://api.clever-cloud.com/v2/products/addonproviders

Qui me renvoit:

[
    {
        "id" : "postgresql-addon",
        "name": "PostgreSQL",
        //...
        "plans" : [
            {
                "id"   : "plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68",
                "name" : "XXS Small Space",
                "slug" : "xxs_sml",
                //...
            },
            {
                "id"   : "plan_b972af97-96cb-4a5d-b4ff-7b3efb6ff44b",
                "name" : "XL Huge Space",
                "slug" : "xl_hug",
                //...
            }
        ]
    },
    {
        "id" : "redis-addon",
        "name": "Redis",
        //...
        "plans" : [
            {
                "id"   : "plan_c62dd71e-15c3-483e-879d-75e4c836e21e",
                "name" : "S",
                "slug" : "s_mono",
                //...
            },
            {
                "id"   : "plan_3901ff93-bb24-411d-9a5c-be3c625e3dd9",
                "name" : "3XL",
                "slug" : "xxxl_mono",
                //...
            }
        ]
    },
    //...
]

Subarashi !! Nous avons tout ce que nous avons besoin. pour chaque addon chaque plan associĂ© ! 😁

Version

Bien maintenant les versions de chaque addon !

Nous en avons besoin, car la requĂȘte de crĂ©ation la spĂ©cifie.

{
  // snip...
  "providerId": "postgresql-addon",
  "options": {
    "version": "14",
    // snip...
  }
}

Malheureusement, la page de pricing ne donne pas cette indication.

On va donc rĂ©cupĂ©rer notre pioche et aller creuser dans les requĂȘtes de la console.

Lorsque l’on est dans le processus de crĂ©ation d’un addon il arrive pour certain addon que la version de celui-ci soit demandĂ©. En mĂȘme temps que la region que l’on verra dans la partie suivante.

Lorsque l’on est authentifiĂ© par la danse et que l’on est en cours de crĂ©ation d’un addon PG la requĂȘte suivant part vers Clever.

GET https://api.clever-cloud.com/v4/addon-providers/postgresql-addon

Celle-ci nous renvoie :

{
    "providerId": "postgresql-addon",
    "clusters": [
        // snip...
    ],
    "dedicated": {
        "12": {
            // snip...
        },
        "15": {
            // snip...
        },
        "11": {
            // snip...
        },
        "13": {
            // snip...
        },
        "10": {
            // snip...
        },
        "14": {
            // snip...
        }
    },
}

Si c’est du redit:

GET https://api.clever-cloud.com/v4/addon-providers/redis-addon

On aura:

{
    "providerId": "redis-addon",
    "clusters": [
        // snip...
    ],
    "dedicated": {
        "7.0.11": {
            // snip...
        },
    },
}

Etc, pour tous les addons versionnés.

Region

Clever n’est pas disponible qu’en France il y a Ă©galement des rĂ©gions partout sur la planĂšte.

Pour déterminer les différentes informations qui sont nécessaire pour remplir la case region

{
  "name": "deleteme",
  "region": "par",
  // snip...
}

Pour cela, il y a encore un autre call API pour nous sauver ^^

GET https://api.clever-cloud.com/v4/products/zones

Plein d’infos encore, c’est super, mais nous c’est le name, city, country qui nous plaüt ^^

[
    {
        "name": "par",
        "country": "France",
        "city": "Paris",
    },
    {
        "name": "mtl",
        "country": "Canada",
        "city": "Montreal",
    },
    {
        "name": "syd",
        "country": "Australia",
        "city": "Sydney",
    },
    // snip ..
]

Bon, on a tout pour crĂ©er l’addon mais on est pas beaucoup plus avancĂ© qu’avec le clever-tools, on l’a juste fait de maniĂšre horriblement inefficace. 😅

RĂ©cupĂ©rer l’environnement de l’addon !

C’est lĂ  qu’en ressortant l’inspecteur d’élĂ©ments en navigant dans les “informations” de l’addon on s’aperçoit qu’un requĂȘte API nous renvoie le beurre et l’argent du beurre. 😁

GET https://api.clever-cloud.com/v2/self/addons/addon_xxxx/env
[
    {
        "name": "POSTGRESQL_ADDON_VERSION",
        "value": "15"
    },
    {
        "name": "POSTGRESQL_ADDON_USER",
        "value": "upkdd3yyifvzo1fzo5nj"
    },
    {
        "name": "POSTGRESQL_ADDON_PASSWORD",
        "value": "xxxx"
    },
    {
        "name": "POSTGRESQL_ADDON_DB",
        "value": "bfi3miifvl16rgqjap1e"
    },
    {
        "name": "POSTGRESQL_ADDON_HOST",
        "value": "bfi3miifvl16rgqjap1e-postgresql.services.clever-cloud.com"
    },
    {
        "name": "POSTGRESQL_ADDON_PORT",
        "value": "6955"
    },
    {
        "name": "POSTGRESQL_ADDON_URI",
        "value": "postgresql://upkdd3yyifvzo1fzo5nj:xxxxx@bfi3miifvl16rgqjap1e-postgresql.services.clever-cloud.com:6955/bfi3miifvl16rgqjap1e"
    }
]

Chaque addon a son propre mode set de variables, je donne ici en exemple pour une PG.

Note

la variable POSTGRESQL_ADDON_PASSWORD sera en clair bien entendu, sinon ça ne sert à rien 😆

Bon lĂ , on est prĂȘt non ?

Oui, je crois que oui.

Le langage

Ah non, il manque un petit truc mineur.

En quoi nous allons coder tout ce bazar ?

Vu que mon but est de crĂ©er un module ansible et ansible jusqu’à ce que jet le remplace, c’est du python, donc go python.

On ne va pas non plus se casser la tĂȘte avec l’enfer sur Terre qu’est oauth car quelqu’un a dĂ©jĂ  fait le sale boulot pour nous.

Cette fois-ci nous sommes prĂȘts !

Pas de quartiers !!

Oauth

Bon ! type la plus complexe, s’authentifier. 😅

Diriger vous vers un dossier de travail.

git clone https://github.com/CleverCloud/oauth-consumer-server.git oauth-server

DĂ©ployez l’app sur Clever

$ cd oauth-server
$ clever create --type maven "Mon app d'OAuth"
Your application has been successfully created!

Fantastique !

En faisant un coup de

clever domain
app-637919bd-94ab-4966-b8f7-b0556ebbc0c6.cleverapps.io

Vous récupérez le domaine sur laquelle tourne votre app, mémorisez le quelque part.

Puis dĂ©finissez une variable d’environnement

clever env set APP_URL https://app-637919bd-94ab-4966-b8f7-b0556ebbc0c6.cleverapps.io/

Attention

Bien mettre le “/” à la fin !!

Maintenant il nous faut quelque chose à consommer et c’est là qu’intervient la seconde brique.

Pour cela, il faut se rendre dans la console de Clever et créer un oAuth consumer

Il va vous demander tout un tas d’information et presque toutes les rĂ©ponses sont le domaine mĂ©morisĂ© plus tĂŽt.

LĂ , libre Ă  vous de lui tomate, salade oignons, moi, je lui mets le minimum syndical de droits.

Vous allez alors accéder à une page avec deux champs. Key et Secret, ce seront pour la suite, notre consumer_key et consumer_secret.

Que le spectacle commence !

clever deploy

AprĂšs quelque instants l’application va dĂ©marrer, vous allez voir des logs de builds passer.

Jusqu’à un mirifique

Deployment successful

Il est maintenant temps de nous rendre sur notre app.

clever open

Elle va faire la gueule

Mais est bien aimable de nous dire pourquoi.

Il manque la consumerKey en query params, et ça on a.

https://app-637919bd-94ab-4966-b8f7-b0556ebbc0c6.cleverapps.io/?consumerKey={{consumer_key}}

Et de mĂȘme pour la consumerSecret

https://app-637919bd-94ab-4966-b8f7-b0556ebbc0c6.cleverapps.io/?consumerKey={{consumer_key}}&consumerSecret={{consumer_secret}}

Cela lance la danse !

Connexion Ă  Clever

Authorisation de la délégation de resource.

Et finalement les tokens !

Your token : xxxxxx Your token secret : xxxxxxx

Nous nommerons respectivement

API

Maintenant que nous avons des tokens, nous pouvons nous connecter Ă  la partie protĂ©gĂ©e de l’API.

Il est temps d’écrire du python ^^

Dans un dossier, on se crée un virtual env.

python -m venv
source ./venv/bin/activate

Puis, on installe les dépendances

pip install requests-oauthlib
pip install python-dotenv

Pour éviter que les credentials soient visibles dans le code nous allons les mettre dans un fichier .env non versionné.

consumer_key=xxxxx
consumer_secret=xxxxxx
resource_key=xxxxx
resource_secret=xxxxx

BiensĂ»r vous remplacez les xxxx 😛

Puis, on charge le tout dans l’environnement dans le fichier main.py

import os
from dotenv import load_dotenv

load_dotenv()

def main():
    consumer_key = os.getenv("consumer_key")
    consumer_secret = os.getenv("consumer_secret")
    resource_key = os.getenv("resource_key")
    resource_secret = os.getenv("resource_secret")

if __name__ == "__main__":
    main()

Cool, on peut maintenant s’authentifier

import os
from dotenv import load_dotenv
+ from requests_oauthlib import OAuth1Session

load_dotenv()

def main():
    consumer_key = os.getenv("consumer_key")
    consumer_secret = os.getenv("consumer_secret")
    resource_key = os.getenv("resource_key")
    resource_secret = os.getenv("resource_secret")

+   clever_api = OAuth1Session(
+      client_key=consumer_key, 
+      client_secret=consumer_secret, 
+      resource_owner_key=resource_key, 
+      resource_owner_secret=resource_secret
+    )

if __name__ == "__main__":
    main()

MĂȘme si ce call n’est pas protĂ©gĂ©, ça fait un bon test ^^

Les plans

def main():
    // snip

    response = clever_api.get(f"{api_endpoint}/v2/products/addonproviders").json()

    print(response)

Et si le prĂ©cĂ©dant ne l’était pas, celui-ci n’est pas accessible publiquement

def main():
    // snip

    response = clever_api.get(f"{api_endpoint}/v2/products/addonproviders/postgresql-addon").json()

    print(response)

Et fonctionne tout aussi bien 😁

Nous sommes connecté et authentifié !

Le deuxiÚme call est une version scopé du premier à un addon en particulier.

Et lĂ  un peu de python magique đŸ§™â€â™‚ïž

def main():
    // snip

    response = clever_api.get(f"{api_endpoint}/v2/products/addonproviders/postgresql-addon").json()

    plans = {x["slug"] : x["id"] for x in response["plan"]} 

Et paf pistache, on a notre mapping 😁

On peut le faire avec tous les addons

import pprint
def main():
    response = clever_api.get(f"{api_endpoint}/v2/products/addonproviders").json()

    providers = {addon["name"]: {
        "id": addon["id"],
        "plans": {x["slug"]: x["id"] for x in addon["plans"]}} for addon in response
    }
    pprint.pprint(providers)

Et pouf, on a tout

Code python
{'Cellar S3 storage': {'id': 'cellar-addon',
                       'plans': {'S': 'plan_84c85ee3-5fdb-4aca-a727-298ddc14b766'}},
 'Configuration provider': {'id': 'config-provider',
                            'plans': {'std': 'plan_5d8e9596-dd73-4b73-84d9-e165372c5324'}},
 'Elastic Stack': {'id': 'es-addon',
                   'plans': {'4xl': 'plan_31ea6328-7df8-4208-a18d-137d2941f16d',
                             '5xl': 'plan_197dbf8a-c30b-49c9-a2ac-126a0b79efba',
                             'l': 'plan_33ad969a-2f37-4d47-8707-93315650fc0f',
                             'm': 'plan_bb93c360-d60c-4441-b18b-cb530a6b7b11',
                             's': 'plan_7675a239-057e-448e-85fb-77b5aa2ef47e',
                             'xl': 'plan_0e494649-d62a-45e7-ba81-61c1a5d8503a',
                             'xs': 'plan_0e0bc5ea-ba21-41e8-865b-1ed48e0163ca',
                             'xxl': 'plan_56265b48-d826-4484-9fd8-d3038c973027',
                             'xxxl': 'plan_a9565b70-7d5b-44b9-892f-de9bc6cade84'}},
 'FS Buckets': {'id': 'fs-bucket',
                'plans': {'s': 'plan_09345cf9-b8ed-4540-b4f7-80ec422fd27b'}},
 'Jenkins': {'id': 'jenkins',
             'plans': {'L': 'plan_f8ee2197-dc72-474e-b396-ee952648bb12',
                       'M': 'plan_57ed26d0-5143-49b1-9232-f6f97f1881f2',
                       'S': 'plan_36ec7fbb-5c1e-4639-b512-1bc864fe52c2',
                       'XL': 'plan_f407e34a-2370-4a67-940f-3ef8d1aec772',
                       'XS': 'plan_9436de3e-b4e6-48e7-8f5c-0bec0dc8b592'}},
 'MailPace - Transactional Email': {'id': 'mailpace',
                                    'plans': {'clever_scaling_10': 'plan_f57a8522-6b62-4928-84db-3ffbff7f9ce3',
                                              'clever_scaling_100': 'plan_c7441676-003b-4c8d-848d-39c666d5e21a',
                                              'clever_scaling_20': 'plan_0bb798b3-74c4-47bb-bc83-81bbae865b23',
                                              'clever_scaling_30': 'plan_a5593c76-e933-4677-989e-e3a1c1af42eb',
                                              'clever_scaling_40': 'plan_5bef85c2-84c1-4c18-8ac9-4aca8a7f9a79',
                                              'clever_scaling_50': 'plan_dc730525-1a5e-487a-b657-4999b5806ad5',
                                              'clever_scaling_70': 'plan_71875a57-74fc-4814-92fc-aa29b98ae1f8',
                                              'clever_solo': 'plan_5a8310e0-0038-4ada-a482-84d761d17b11'}},
 'Matomo Analytics': {'id': 'addon-matomo',
                      'plans': {'beta': 'plan_87283ba6-617c-420d-8e37-3350a2fcdd66'}},
 'MongoDB': {'id': 'mongodb-addon',
             'plans': {'dev': 'plan_847dd55f-0847-4497-9e53-7eef4281e068',
                       'l_big': 'plan_24bbde1f-e8c7-4a45-b8bf-1ab1e320049c',
                       'l_med': 'plan_66f4b19c-621a-4b39-892d-0e6f0bf62fed',
                       'l_sml': 'plan_513675e4-32c0-4b3a-991c-24fcff62b0cf',
                       'm_big': 'plan_e8036bc6-5341-4c6e-b843-f4c60d442198',
                       'm_hug': 'plan_df538656-adf7-4846-8e39-d96d0c362706',
                       'm_med': 'plan_5f1e6e86-a9d7-4698-b6c1-6ef5935d16b7',
                       'm_sml': 'plan_8c14d2c0-05fe-4137-912b-1e99b01b871d',
                       's_big': 'plan_b60a7496-f44d-4f95-b942-c2445958d58a',
                       's_hug': 'plan_5172896b-9f29-48f6-8820-3d07a04b80c9',
                       's_med': 'plan_01fd9ba8-2e8f-4a0a-bbf7-aecb4d982780',
                       's_sml': 'plan_5ad112b9-15ca-4c1d-800d-f1cb2989ee83',
                       'xl_big': 'plan_ffdd05ce-5f30-4750-94c5-424899d1f89d',
                       'xl_med': 'plan_e4745876-6013-48fc-9cbf-c7a49be84cb4',
                       'xl_sml': 'plan_36429877-93c4-4639-83f0-cdc96493caf3',
                       'xs_big': 'plan_8c379b80-6f2d-4b93-8839-8cb14aee4218',
                       'xs_med': 'plan_14ab2ba4-4b61-4e4f-8971-f26e7daa4963',
                       'xs_sml': 'plan_b53983a2-63d3-472d-8c98-f1bdea682912',
                       'xxl_big': 'plan_e6030ef6-005a-4f77-939d-f1395b02f5d6',
                       'xxl_sml': 'plan_98370e12-e54a-4dc1-8ee4-4d4f3007f3eb',
                       'xxx_med': 'plan_8da4539d-198b-48da-9d4e-df4e832085fe'}},
 'MySQL': {'id': 'mysql-addon',
           'plans': {'dev': 'plan_bf78ef5b-aedd-4024-973a-c2ff45541b88',
                     'l_big': 'plan_cecdd927-0c2b-4df5-80bf-7a3956b7da45',
                     'l_med': 'plan_786d19e3-d5ec-43d1-bb99-4bdf51d1708f',
                     'l_sml': 'plan_65af8c37-a9ca-4f37-8490-c727a9d7fe23',
                     'm_big': 'plan_d7f7bcff-df64-4326-a8f7-9d112d75bbff',
                     'm_med': 'plan_ead3f4d7-a920-4b8c-8706-8fa8280d0bcc',
                     'm_sml': 'plan_97afb4d4-cfeb-4fcb-966f-6072c6c327dc',
                     's_big': 'plan_a367b384-2942-4226-884c-5211b79aae81',
                     's_med': 'plan_1940a893-ff6b-4c64-85e7-4955bb3355c0',
                     's_sml': 'plan_445566dd-b996-456f-96e3-186c29f4bd11',
                     'xl_big': 'plan_df6d36d3-07a4-4027-a0bb-333240a6b7c8',
                     'xl_med': 'plan_b6cac148-b1fe-4018-a605-9ecd92b436d1',
                     'xl_sml': 'plan_85292520-3687-488a-aefc-1884349205e4',
                     'xs_big': 'plan_01d5d079-e900-49c7-8273-4fb3473eedea',
                     'xs_med': 'plan_889ddd44-68e1-4473-b152-a1abcaeec5b0',
                     'xs_sml': 'plan_bde0b41f-77a7-4285-a08c-b8f4d9780fc4',
                     'xs_tny': 'plan_0ffb4a1e-ac15-47c6-965f-f87ac990d99c',
                     'xxl_big': 'plan_c26fc00b-abfb-44ec-951d-73ed65892f3e',
                     'xxl_hug': 'plan_9f7b1d05-36e7-493d-a32f-af0507477af0',
                     'xxl_med': 'plan_f6108804-9258-41f3-9920-0b5e0a41cb60',
                     'xxl_sml': 'plan_8c48584e-4d0a-43db-91a1-f2d1a3475fe5',
                     'xxs_big': 'plan_b7ee59a4-4115-41b8-b735-e44378b95c57',
                     'xxs_med': 'plan_53d3c47d-ce81-466b-8acb-d5efaa68d27b',
                     'xxs_sml': 'plan_7ab494e2-c319-4330-8170-35d78738c1ee'}},
 'PostgreSQL': {'id': 'postgresql-addon',
                'plans': {'dev': 'plan_d2ada71a-aa8e-4ead-8cb9-28314664437e',
                          'l_big': 'plan_166c1a3f-3e7d-427c-9a2d-adeaf6c30c1c',
                          'l_gnt': 'plan_957a3cbc-022f-430d-aa6e-3156b8da20c6',
                          'l_med': 'plan_bfc12b2d-37af-4d5c-98b5-f5ce8856965c',
                          'l_sml': 'plan_20cf7db8-2687-495f-978d-785a8ac87814',
                          'm_big': 'plan_55d54396-799b-4e01-92ce-34ad68392e7a',
                          'm_med': 'plan_ef13e023-b519-4130-bfa0-54e2e0893362',
                          'm_sml': 'plan_13def017-26d9-469d-88b1-6591cbf4422f',
                          's_big': 'plan_06303461-b9d1-4418-a352-7be788c17777',
                          's_hug': 'plan_926643d1-3180-4e6a-bbc0-da828b884f77',
                          's_med': 'plan_db834b61-21d3-423f-81ac-e8ec5b94a51c',
                          's_sml': 'plan_f1f39547-be55-4be0-96d0-5140da25c138',
                          'xl_big': 'plan_1067ae12-c433-497d-a20f-d16e50b932d9',
                          'xl_gnt': 'plan_fdbaa2cc-3403-451a-b341-0973e04be1d2',
                          'xl_hug': 'plan_b972af97-96cb-4a5d-b4ff-7b3efb6ff44b',
                          'xl_med': 'plan_b35727fe-2100-4f7c-82c8-31d9471769ab',
                          'xl_sml': 'plan_9b4505e7-ac36-4caa-ac7d-9e11b6a26b15',
                          'xs_big': 'plan_a9cfb2d3-c959-4ab2-8b6f-fcbd07aa4f51',
                          'xs_med': 'plan_cb267d0a-5a2d-4c5b-9709-ee4d321691e7',
                          'xs_sml': 'plan_f14478be-b59a-4f64-870c-6887c561492d',
                          'xs_tny': 'plan_4b988584-adf5-43a5-891b-9ba1d8fe6d5d',
                          'xxl_big': 'plan_d3131cb3-5d92-4b8c-b3db-30e8be4716af',
                          'xxl_hug': 'plan_e89e0986-2664-46b1-aaff-305b9c6ec552',
                          'xxl_med': 'plan_5e33d20a-fe32-4b4f-91f0-b59bf0deae13',
                          'xxl_sml': 'plan_1c8a4179-78ef-4bdd-bfd7-7f389782b8c6',
                          'xxs_big': 'plan_810751de-ab47-4bd8-918b-b857f9011050',
                          'xxs_med': 'plan_9ce0a025-f5bd-4ac4-a5be-e2da37c87583',
                          'xxs_sml': 'plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68',
                          'xxxl_big': 'plan_52b00af0-b49f-477b-a2bb-05e6bdf057af',
                          'xxxl_med': 'plan_9c7779d2-2701-4420-bd8a-3e7b49083847',
                          'xxxl_sml': 'plan_5601b1c6-9850-4a02-9288-8894670f0d7d'}},
 'Pulsar': {'id': 'addon-pulsar',
            'plans': {'beta': 'plan_3ad3c5be-5c1e-4dae-bf9a-87120b88fc13'}},
 'Redis': {'id': 'redis-addon',
           'plans': {'l_mono': 'plan_56579711-5b5c-451c-b274-2662eb528fc1',
                     'm_mono': 'plan_221bbf5a-30d0-49a9-b539-ce09b8a734a9',
                     's_mono': 'plan_c62dd71e-15c3-483e-879d-75e4c836e21e',
                     'xl_mono': 'plan_17f3735a-e1e4-4296-9e4c-e41cf74121c5',
                     'xxl_mono': 'plan_3bb59326-15d2-404b-be6e-97069710e8dd',
                     'xxxl_mono': 'plan_3901ff93-bb24-411d-9a5c-be3c625e3dd9',
                     'xxxxl_mono': 'plan_b43a32be-ada0-49c1-ab3e-db04f9dcfef2'}}}

Not bad đŸ€­

Les versions

On s’attaque aux versions, cette fois-ci, on doit faire plusieurs calls pour chaque addon

def main():
    versions = {}
    for addon in plans.keys():
        addon_response = clever_api.get(f"{api_endpoint}/v4/addon-providers/{addon}")

        if addon_response.ok:
            addon_response_body = addon_response.json()
            if "dedicated" in addon_response_body.keys():
                addon_versions = list(addon_response_body['dedicated'].keys())
                versions[addon] = addon_versions
            else:
                pprint.pprint(addon_response)

    pprint.pprint(versions)

Bon on a semble-t-il tout ce qu’il nous faut.

{
 'addon-pulsar': [],
 'es-addon': ['7', '8'],
 'jenkins': ['LTS'],
 'mongodb-addon': ['4.0.3'],
 'mysql-addon': ['5.7', '8.0'],
 'postgresql-addon': ['12', '15', '11', '13', '10', '14'],
 'redis-addon': ['7.0.11']
 }

Les régions

Au tour des régions maintenant

MĂȘme combat, on appelle et on transforme.

J’ai dĂ©cidĂ© arbitrairent d’une clef composite, mais faites ce que vous voulez ^^

def main():
    zones_response = clever_api.get(f"{api_endpoint}/v4/products/zones")

    zones = {(x["country"], x["city"]): x["name"] for x in zones_response.json()}

    pprint.pprint(zones)

Qui a pour résultat

{('Australia', 'Sydney'): 'syd',
 ('Canada', 'Montreal'): 'mtl',
 ('France', 'North'): 'fr-north-hds',
 ('France', 'Paris'): 'scw',
 ('France', 'Roubaix'): 'rbxhds',
 ('Poland', 'Warsaw'): 'wsw',
 ('Saudi Arabia', 'Jeddah'): 'jed',
 ('Singapore', 'Singapore'): 'sgp'}

Bon, on est pas mal du tout 😎

Attends ! il est oĂč par ??

Ah oups, la zone de “Scaleway” est aussi Ă  ('France', 'Paris') du coup ça s’écrase.

Cela sera un poil moins bien, mais bon, ça sera plus correct.

def main():
   zones_response = clever_api.get(f"{api_endpoint}/v4/products/zones")

   zones = {(x["country"], x["city"]+"-"+x["name"]): x["name"] for x in zones_response.json()}

   pprint.pprint(zones)

{('Australia', 'Sydney-syd'): 'syd',
('Canada', 'Montreal-mtl'): 'mtl',
('France', 'North-fr-north-hds'): 'fr-north-hds',
('France', 'Paris-clevergrid'): 'clevergrid',
('France', 'Paris-par'): 'par',
('France', 'Paris-scw'): 'scw',
('France', 'Roubaix-rbx'): 'rbx',
('France', 'Roubaix-rbxhds'): 'rbxhds',
('Poland', 'Warsaw-wsw'): 'wsw',
('Saudi Arabia', 'Jeddah-jed'): 'jed',
('Singapore', 'Singapore-sgp'): 'sgp'}

Mieux !

Spawn !

On est prĂȘt Ă  spawn notre :

Nous devons donc construire

{
 "name": "toto",
 "region": "par",
 "providerId": "postgresql-addon",
 "plan": "plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68",
 "options": {
   "version": "15",
   "encryption": "false"
 }
}

Ben let’s go !

def main():
    create_data = {
        "name": "toto",
        "region": zones[("France", "Paris-par")],
        "providerId": providers["PostgreSQL"]["id"],
        "plan": providers["PostgreSQL"]["plans"]["xxs_sml"],
        "options" : {
            "version" : "15",
            "encryption": "false"
        }
    }

    pprint.pprint(create_data)

Ce qui donne bien

{'name': 'toto',
 'options': {'encryption': 'false', 'version': '15'},
 'plan': 'plan_c32d00fb-6c06-48a9-a0a3-9d808937ec68',
 'providerId': 'postgresql-addon',
 'region': 'par'}

Let’s go pour la crĂ©ation.

def main():
    response = clever_api.post(f"{api_endpoint}/v2/self/addons", json=create_data)
    addon_id = response.json()["id"]
    pprint.pprint(response.json())

On peut alors récupérer le précieux id

Qui va nous permettre d’enfin atteindre les identifiants de connexions.

def main():
    response = clever_api.get(f"{api_endpoint}/v2/self/addons/{addon_id}/env")
    pprint.pprint(response.json())

Et voilĂ  le travail

[{'name': 'POSTGRESQL_ADDON_VERSION', 'value': '15'},
 {'name': 'POSTGRESQL_ADDON_USER', 'value': 'uzek9l75d2qxpuh9eeel'},
 {'name': 'POSTGRESQL_ADDON_PASSWORD', 'value': 'xxxxxxx'},
 {'name': 'POSTGRESQL_ADDON_DB', 'value': 'bp000eqgpxma5nc0w1lu'},
 {'name': 'POSTGRESQL_ADDON_HOST',
  'value': 'bp000eqgpxma5nc0w1lu-postgresql.services.clever-cloud.com'},
 {'name': 'POSTGRESQL_ADDON_PORT', 'value': '6959'},
 {'name': 'POSTGRESQL_ADDON_URI',
  'value': 'postgresql://uzek9l75d2qxpuh9eeel:xxxxxxxx@bp000eqgpxma5nc0w1lu-postgresql.services.clever-cloud.com:6959/bp000eqgpxma5nc0w1lu'}]

Plus qu’à rendre ça plus sexy

def main():
    pretty_env = {x["name"] : x["value"] for x in response_env}
    pprint.pprint(pretty_env)

Bien mieux ^^

{'POSTGRESQL_ADDON_DB': 'bcxgrphuc6bqapahifm8',
 'POSTGRESQL_ADDON_HOST': 'bcxgrphuc6bqapahifm8-postgresql.services.clever-cloud.com',
 'POSTGRESQL_ADDON_PASSWORD': 'xxxxxx',
 'POSTGRESQL_ADDON_PORT': '6960',
 'POSTGRESQL_ADDON_URI': 'postgresql://u5i6w2q70kb7sdbbthjy:xxxxxx@bcxgrphuc6bqapahifm8-postgresql.services.clever-cloud.com:6960/bcxgrphuc6bqapahifm8',
 'POSTGRESQL_ADDON_USER': 'u5i6w2q70kb7sdbbthjy',
 'POSTGRESQL_ADDON_VERSION': '15'}

Destruction

MĂȘme combat, on fait de l’ingĂ©niĂ©rie inverse et on tombe sur le call pour dĂ©truire l’addon

def main():
    response = clever_api.delete(f"{api_endpoint}/v2/self/addons/{addon_id}")
    pprint.pprint(response.json())

Résultat

{'id': 318,
 'message': 'The server successfully deleted your service',
 'type': 'success'}

Code Complet

Code python
import os
from dotenv import load_dotenv
from requests_oauthlib import OAuth1Session
import pprint

load_dotenv()

def main():
    consumer_key = os.getenv("consumer_key")
    consumer_secret = os.getenv("consumer_secret")
    resource_key = os.getenv("resource_key")
    resource_secret = os.getenv("resource_secret")

    clever_api = OAuth1Session(
        client_key=consumer_key,
        client_secret=consumer_secret,
        resource_owner_key=resource_key,
        resource_owner_secret=resource_secret
    )

    api_endpoint = "https://api.clever-cloud.com"

    response = clever_api.get(f"{api_endpoint}/v2/products/addonproviders").json()

    providers = {addon["name"]: {
        "id": addon["id"],
        "plans": {x["slug"]: x["id"] for x in addon["plans"]}} for addon in response
    }

    versions = {}
    for addon in providers.values():
        addon_response = clever_api.get(f"{api_endpoint}/v4/addon-providers/{addon['id']}")

        if addon_response.ok:
            addon_response_body = addon_response.json()
            if "dedicated" in addon_response_body.keys():
                addon_versions = list(addon_response_body['dedicated'].keys())
                versions[addon['id']] = addon_versions
            else:
                pprint.pprint(addon_response)

    zones_response = clever_api.get(f"{api_endpoint}/v4/products/zones")

    zones = {(x["country"], x["city"]+"-"+x["name"]): x["name"] for x in zones_response.json()}

    create_data = {
        "name": "toto",
        "region": zones[("France", "Paris-par")],
        "providerId": providers["PostgreSQL"]["id"],
        "plan": providers["PostgreSQL"]["plans"]["xxs_sml"],
        "options" : {
            "version" : "15",
            "encryption": "false"
        }
    }

    response = clever_api.post(f"{api_endpoint}/v2/self/addons", json=create_data)
    addon_id = response.json()['id']
    response_env = clever_api.get(f"{api_endpoint}/v2/self/addons/{addon_id}/env").json()

    pretty_env = {x["name"] : x["value"] for x in response_env}

    pprint.pprint(pretty_env)

    response = clever_api.delete(f"{api_endpoint}/v2/self/addons/{addon_id}")
    pprint.pprint(response.json())

if __name__ == "__main__":
    main()

Conclusion

Python c’est trĂšs puissant pour manipuler de la donnĂ©e arbitraire.

Oauth quel enfer.

Je gùre un peu mieux le produit que j’aide à concevoir ^^’

L’open api de clever n’est pas suffisante comme documentation.

Cet article sera un trĂšs bon pense-bĂȘte 😀

A+