ArgoCD Python Client


A simple Python client to interact with ArgoCD.

import json
import logging
import requests
import time

from collections import namedtuple

class ArgoInteractionError(Exception):
    """Any error in interacting with ArgoCD"""
    pass

AppUrl = namedtuple("AppUrl", ["name", "url"])

class ArgoClient:
    """Simple wrapper class to serve as ArgoCD client"""

    def __init__(self, server, token):
        self.base_url = f"https://{server}/api/v1/"
        self.token = token
        self.server = server

    def request(self, func, endpoint, *args, **kwargs) -> requests.Response:
        headers = kwargs.pop("headers", dict())
        headers["Authorization"] = f"Bearer {self.token}"
        try:
            response = func(self.base_url + endpoint, *args, headers=headers, **kwargs)
            response.raise_for_status()
            return response
        except Exception as e:
            raise ArgoInteractionError(
                f"Could not complete request to ArgoCD, endpoint={endpoint}, method={func}, reason={e.response.text}"
            ) from e

    def post(self, endpoint, *args, **kwargs) -> requests.Response:
        return self.request(requests.post, endpoint, *args, **kwargs)

    def get(self, endpoint, *args, **kwargs) -> requests.Response:
        return self.request(requests.get, endpoint, *args, **kwargs)

    def put(self, endpoint, *args, **kwargs) -> requests.Response:
        return self.request(requests.put, endpoint, *args, **kwargs)

    def patch(self, endpoint, *args, **kwargs) -> requests.Response:
        return self.request(requests.patch, endpoint, *args, **kwargs)

    def delete(self, endpoint, *args, **kwargs) -> requests.Response:
        return self.request(requests.delete, endpoint, *args, **kwargs)

    def get_app(self, name) -> dict:
        """Get an app"""
        try:
            response = self.get(f"applications/{name}")
        except ArgoInteractionError:
            return False
        data = response.json()
        return data

    def get_apps(self, project) -> dict:
        """Get all apps in a given project"""
        params = {
            "project": [
                project,
            ]
        }
        response = self.get("applications", params=params)
        data = response.json()
        return data["items"]

    def delete_app(self, name) -> dict:
        """Delete an app"""
        response = self.delete(f"applications/{name}")
        data = response.json()
        return data

    def upsert_app(
        self,
        environment_id,
        project,
        target_namespace,
        target_cluster,
    ) -> dict:
        """Creates or updates (upsert) an app"""
        app_name = f"{environment_id}-{project.path_slug}"
        logging.info(f"Upserting app {app_name}")

        data = {
            "metadata": {"name": app_name},
            "spec": {
                "destination": {"namespace": target_namespace, "server": target_cluster},
                "project": "envbuilder",
                "source": {
                    "path": ".deploy",
                    "repoURL": project.git_ssh_uri,
                    "targetRevision": project.ref,
                },
                "syncPolicy": {
                    "automated": {
                        "alloEmtpy": True,
                        "prune": True,
                        # For now we can't self heal as we have shared secrets that result in a never ending heal circle
                        "selfHeal": False,
                    },
                    "syncOptions": [
                        "CreateNamespace=true",
                    ],
                },
            },
        }
        logging.debug(f"Upsert payload: {data}")

        params = {"upsert": True}
        response = self.post("applications", params=params, json=data)
        data = response.json()
        logging.debug(f"Upsert response: {data}")
        return data

    def get_app_ressources(self, name, retries=1, wait=0) -> List[dict]:
        """Gets an apps ressources, retrying with the given wait if no ressources are found"""
        remaining_retries = retries
        while remaining_retries > 0:
            logging.info(f"{name}: Trying to get app resources, {remaining_retries}")
            response = self.get(f"applications/{name}/managed-resources")
            data = response.json()
            try:
                items = data["items"]
            except KeyError:
                pass
            else:
                if len(items) > 0:
                    return items
            remaining_retries -= 1
            time.sleep(wait)
        raise ArgoInteractionError(f"Could not get application ressources for {name}")

    def get_urls_of_app(self, name) -> List[AppUrl]:
        """Get URLs of an app based on it's ingress and route objects

        On route objects the label 'url-suffix' can be set to create a suffix for the resulting URL"""
        links = list()
        ressources = self.get_app_ressources(name, retries=2, wait=10)
        for ressource in ressources:
            # When a ressource can't be fully deployed its live state is the string "null"
            # and therefore we can't parse it
            if ressource.get("liveState", "") == "null":
                continue

            if ressource["kind"].lower() == "route":
                ressource_data = json.loads(ressource["liveState"])

                # Try to find a URL suffix label
                labels = ressource_data["metadata"].get("labels", {})
                url_suffix = labels.get("url-suffix", "")

                links.append(
                    AppUrl(
                        name=ressource_data["metadata"]["name"],
                        url=f"https://{ressource_data['spec']['host']}/{url_suffix}",
                    )
                )
            elif ressource["kind"].lower() == "ingress":
                ressource_data = json.loads(ressource["liveState"])
                for rule in ressource_data["spec"]["rules"]:
                    links.append(
                        AppUrl(
                            name=ressource_data["metadata"]["name"],
                            url=f"https://{rule['host']}",
                        )
                    )
        return links

See also