https://lafor.ge/feed.xml

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

2023-10-26

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 de créer des rôle 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:

  • Commande un addon et enregiste le retour dans $result$
  • Affiche le contenu de result (son ID et l'URL de connexion)
  • Détruit l'addon créé

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, c'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à 😁

missing alt

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.

missing alt

Cela se comprends car les addons sont dans la philosophie de Clever sensé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.

missing alt

Peut-être que les composants sont hydraté à postériori du render de la page ?

Et bingo ! En inspectant les requêtes qui passe 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 redis:

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.

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 somme prêts !

Pas de quartiers !!

Oauth

Bon étape 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.

missing alt

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

missing alt

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

missing alt

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

missing alt

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

missing alt

Authorisation de la délégation de resource.

missing alt

Et finalement les tokens !

Your token : xxxxxx Your token secret : xxxxxxx

Nous nommerons respectivement

  • Your token : ressource_key
  • Your token secret : ressource_secret

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 somme 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 au versions, cette fois-ci on doit faire plusieurs call 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 call et on transforme.

J'ai décidez 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 :

  • provider : PG
  • version : 15
  • nom : toto
  • region : (France, Paris)
  • plan : XXS Small space (xxs_sml)

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ées arbitraire.

Oauth quel enfer.

Je gère un peu mieux le produit que j'aide à concevoir ^^'

L'open api de clever est pas suffisante comme documentation.

Cet article sera un très bon pense-bête 😀

A+

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.