diff --git a/.editorconfig b/.editorconfig index 68fff41..1cf3736 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,15 +4,12 @@ root = true insert_final_newline = true end_of_line = lf charset = utf-8 +indent_size = 2 [*.py] indent_style = space indent_size = 4 -[*.{yaml,yml,ini}] -indent_style = space -indent_size = 2 - [Makefile] indent_size = tab diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b4dd335..0000000 --- a/.flake8 +++ /dev/null @@ -1,18 +0,0 @@ -[flake8] -exclude = venv - __pycache__ - *.pyc - __init__.py - setup.py - examples -ignore = E501 # line too long - D100 # missing docstring in public module - D101 # missing docstring in public class - D102 # missing docstring in public method - D105 # missing docstring in magic method -verbose = 2 -doctests = True -format = [%(code)s] %(path)s [%(row)d,%(col)d] %(text)s -show_source = True -statistics = True -count = True diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d2ee1da --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.pxd text diff=python +*.py text diff=python +*.py3 text diff=python +*.pyw text diff=python +*.pyx text diff=python +*.pyz text diff=python +*.pyi text diff=python diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c298020 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: ci + +on: [ push, pull_request ] + +jobs: + tests: + strategy: + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11'] + os: [ubuntu] + fail-fast: true + runs-on: ${{ matrix.os }}-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@master + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - uses: actions/cache@v2 + with: + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} + path: ~/.cache/pip + restore-keys: | + ${{ runner.os }}-pip- + - name: Install Python dependencies + uses: py-actions/py-dependency-install@v2 + with: + path: "./requirements-dev.txt" + - name: Run lint & tests + run: | + make ci SKIP_STYLE=true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + fail_ci_if_error: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..cc8a683 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,37 @@ +fail_fast: false +repos: +- repo: local + hooks: + - id: black + name: black + entry: black + language: system + types: [python] + - id: isort + name: isort + entry: isort + language: system + types: [python] + args: ["--profile", "black"] + - id: flake8 + name: flake8 + entry: flake8 + language: system + types: [ python ] + - id: mypy + name: mypy + entry: mypy + language: system + types: [ python ] +- repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.3.0 + hooks: + - id: forbid-crlf +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3327821..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -dist: xenial -language: python -python: - - 3.6 - - 3.7 - - 3.8 -install: - - make install-deps -script: - - make ci -notifications: - webhooks: - urls: - - https://zeus.ci/hooks/41f59acc-412b-11e9-baf0-0a580a281a0a/public/provider/travis/webhook - on_success: always - on_failure: always - on_start: always - on_cancel: always - on_error: always -after_script: - - npm install -g @zeus-ci/cli - - zeus upload -t "mime/type" path/to/artifact diff --git a/Makefile b/Makefile index d376c02..7219d38 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,15 @@ .DEFAULT_GOAL := about -VERSION := $(shell cat discovery/__version__.py | cut -d'"' -f2) -DTYPE=server +VERSION := $(shell cat discovery/__init__.py | grep version | cut -d'"' -f2) lint: +ifeq ($(SKIP_STYLE), ) @echo "> running isort..." - isort -rc discovery - isort -rc tests + isort discovery --profile black + isort tests --profile black @echo "> running black..." black discovery black tests +endif @echo "> running flake8..." flake8 discovery flake8 tests @@ -17,43 +18,45 @@ lint: tests: @echo "> running tests" - python -m pytest -v --cov-report xml --cov-report term --cov=discovery tests + python -m pytest -vv --no-cov-on-fail --color=yes --cov-report xml --cov-report term --cov=discovery tests + +ci: lint tests +ifeq ($(GITHUB_HEAD_REF), false) + @echo "--- codecov report ---" + codecov --file coverage.xml -t $$CODECOV_TOKEN + @echo "--- codeclimate report ---" + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + ./cc-test-reporter format-coverage -t coverage.py -o codeclimate.json + ./cc-test-reporter upload-coverage -i codeclimate.json -r $$CC_TEST_REPORTER_ID +endif docs: @echo "> generate project documentation..." - portray $(DTYPE) + @cp README.md docs/index.md + mkdocs serve + +tox: + @echo "> running tox..." + tox -r -p all install-deps: - @echo "> installing development dependencies..." + @echo "> installing dependencies..." pip install -r requirements-dev.txt + pre-commit install about: - @echo "> discovery-client v$(VERSION)" + @echo "> discovery-client $(VERSION)" @echo "" @echo "make lint - Runs: [isort > black > flake8 > mypy]" @echo "make tests - Execute tests" - @echo "make tox - Runs tox" - @echo "make docs - Generate project documentation [DTYPE=server]" @echo "make ci - Runs: [make lint > make tests]" - @echo "make install-deps - Install development dependencies" + @echo "make tox - Runs tox" + @echo "make docs - Generate project documentation" + @echo "make install-deps - Install development dependencies." @echo "" @echo "mailto: alexandre.fmenezes@gmail.com" -ci: lint tests -ifeq ($(CI), true) - @echo "--- download CI dependencies ---" - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - @echo "--- report upload ---" - codecov --file coverage.xml -t $$CODECOV_TOKEN - ./cc-test-reporter format-coverage -t coverage.py -o codeclimate.json - ./cc-test-reporter upload-coverage -i codeclimate.json -r $$CC_TEST_REPORTER_ID -endif - -tox: - @echo "> running tox..." - tox -r -p all - -all: install-deps ci docs +all: ci tox -.PHONY: lint tests docs about ci all +.PHONY: lint tests ci tox docs all diff --git a/README.md b/README.md index 19fc722..a77f630 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://travis-ci.org/amenezes/discovery-client.svg?branch=master)](https://travis-ci.org/amenezes/discovery-client) -[![Maintainability](https://api.codeclimate.com/v1/badges/fc7916aab464c8b7d742/maintainability)](https://codeclimate.com/github/amenezes/discovery-client/maintainability) +[![ci](https://github.com/amenezes/discovery-client/workflows/ci/badge.svg)](https://github.com/amenezes/discovery-client/actions) [![codecov](https://codecov.io/gh/amenezes/discovery-client/branch/master/graph/badge.svg)](https://codecov.io/gh/amenezes/discovery-client) [![PyPI version](https://badge.fury.io/py/discovery-client.svg)](https://badge.fury.io/py/discovery-client) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/discovery-client) @@ -7,19 +6,32 @@ # discovery-client -async client [consul](https://consul.io). +Async Python client for [consul](https://consul.io). + +HTTP engine options available: + +- aiohttp `default`; +- httpx. ## Installing Install and update using pip: +### default client + ````bash pip install -U discovery-client ```` +### httpx client + +````bash +pip install -U 'discovery-client[httpx]' +```` + ## Links - License: [Apache License](https://choosealicense.com/licenses/apache-2.0/) -- Code: https://github.com/amenezes/discovery-client -- Issue tracker: https://github.com/amenezes/discovery-client/issues -- Docs: https://discovery-client.amenezes.net +- Code: [https://github.com/amenezes/discovery-client](https://github.com/amenezes/discovery-client) +- Issue tracker: [https://github.com/amenezes/discovery-client/issues](https://github.com/amenezes/discovery-client/issues) +- Docs: [https://discovery-client.amenezes.net](https://discovery-client.amenezes.net) diff --git a/discovery/__init__.py b/discovery/__init__.py index ca99a6d..86b35b9 100644 --- a/discovery/__init__.py +++ b/discovery/__init__.py @@ -1,6 +1,31 @@ -import logging +from . import checks, utils +from ._logger import log +from .api import ( + Behavior, + CheckStatus, + HealthState, + IntentionFilter, + IntentionsAction, + LogLevel, + TokenLocality, + TokenType, + kind, +) +from .client import Consul -from discovery.__version__ import __version__ - -log = logging.getLogger("discovery-client") -log.addHandler(logging.NullHandler()) +__version__ = "1.0.0" +__all__ = [ + "Consul", + "HealthState", + "LogLevel", + "TokenType", + "checks", + "utils", + "Kind", + "TokenLocality", + "IntentionsAction", + "IntentionFilter", + "IntentionBy", + "CheckStatus", + "Behavior", +] diff --git a/discovery/__main__.py b/discovery/__main__.py index 02ff490..1c1e4e6 100644 --- a/discovery/__main__.py +++ b/discovery/__main__.py @@ -1,14 +1,4 @@ -from cleo import Application -from dotenv import load_dotenv - -from discovery import __version__ -from discovery.cli.commands import CatalogCommand - -load_dotenv() - -application = Application("discovery-client", f"{__version__}") -application.add(CatalogCommand()) - +from discovery.cli import cli if __name__ == "__main__": - application.run() + cli() diff --git a/discovery/__version__.py b/discovery/__version__.py deleted file mode 100644 index 5313823..0000000 --- a/discovery/__version__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.0.0b1" diff --git a/discovery/_logger.py b/discovery/_logger.py new file mode 100644 index 0000000..866c710 --- /dev/null +++ b/discovery/_logger.py @@ -0,0 +1,4 @@ +import logging + +log = logging.getLogger("discovery-client") +log.addHandler(logging.NullHandler()) diff --git a/discovery/abc.py b/discovery/abc.py deleted file mode 100644 index 7c95d11..0000000 --- a/discovery/abc.py +++ /dev/null @@ -1,35 +0,0 @@ -import abc -import os - -from discovery import api - - -class BaseClient(abc.ABC): - def __init__(self, client, timeout=30, **kwargs): - self.client = client - self._timeout = int(os.getenv("DEFAULT_TIMEOUT", timeout)) - - # base api - self.catalog = kwargs.get("catalog") or api.Catalog(client=self.client) - self.config = kwargs.get("config") or api.Config(client=self.client) - self.coordinate = kwargs.get("coordinate") or api.Coordinate(client=self.client) - self.events = kwargs.get("events") or api.Events(client=self.client) - self.health = kwargs.get("health") or api.Health(client=self.client) - self.kv = kwargs.get("kv") or api.Kv(client=self.client) - self.query = kwargs.get("query") or api.Query(client=self.client) - self.session = kwargs.get("session") or api.Session(client=self.client) - self.snapshot = kwargs.get("snapshot") or api.Snapshot(client=self.client) - self.status = kwargs.get("status") or api.Status(client=self.client) - self.txn = kwargs.get("txn") or api.Txn(client=self.client) - # agent - self.agent = kwargs.get("agent") or api.Agent(client=self.client) - # connect - self.connect = kwargs.get("connect") or api.Connect(client=self.client) - # acl - self.acl = kwargs.get("acl") or api.Acl(client=self.client) - # operator - self.operator = kwargs.get("operator") or api.Operator(client=self.client) - - @property - def timeout(self): - return self._timeout diff --git a/discovery/api/__init__.py b/discovery/api/__init__.py index 364c8e2..e4d3bfe 100644 --- a/discovery/api/__init__.py +++ b/discovery/api/__init__.py @@ -1,32 +1,40 @@ -# ACL -from discovery.api.acl import Acl -from discovery.api.agent import Agent -from discovery.api.area import Area -from discovery.api.auth_method import AuthMethod -from discovery.api.autopilot import AutoPilot -from discovery.api.binding_rule import BindingRule -from discovery.api.ca import CA -from discovery.api.catalog import Catalog -from discovery.api.checks import Checks -from discovery.api.config import Config -from discovery.api.connect import Connect -from discovery.api.coordinate import Coordinate -from discovery.api.events import Events -from discovery.api.health import Health -from discovery.api.intention import Intentions -from discovery.api.keyring import Keyring -from discovery.api.kv import Kv -from discovery.api.license import License -from discovery.api.namespace import Namespace -from discovery.api.operator import Operator -from discovery.api.policy import Policy -from discovery.api.query import Query -from discovery.api.raft import Raft -from discovery.api.role import Role -from discovery.api.segment import Segment -from discovery.api.service import Service -from discovery.api.session import Session -from discovery.api.snapshot import Snapshot -from discovery.api.status import Status -from discovery.api.token import Token -from discovery.api.txn import Txn +from .acl import Acl +from .agent import Agent +from .area import Area +from .auth_method import AuthMethod +from .autopilot import AutoPilot +from .behavior import Behavior +from .binding_rule import BindingRule +from .ca import CA +from .catalog import Catalog +from .check_status import CheckStatus +from .checks import Checks +from .config import Config +from .connect import Connect +from .coordinate import Coordinate +from .events import Events +from .health import Health +from .health_state import HealthState +from .intention import Intentions +from .intention_by import IntentionBy +from .intention_filter import IntentionFilter +from .intentions_action import IntentionsAction +from .keyring import Keyring +from .kv import Kv +from .license import License +from .loglevel import LogLevel +from .namespace import Namespace +from .operator import Operator +from .policy import Policy +from .query import Query +from .raft import Raft +from .role import Role +from .segment import Segment +from .service import Service +from .session import Session +from .snapshot import Snapshot +from .status import Status +from .token import Token +from .token_locality import TokenLocality +from .token_type import TokenType +from .txn import Txn diff --git a/discovery/api/abc.py b/discovery/api/abc.py index 133f979..9f6ffe3 100644 --- a/discovery/api/abc.py +++ b/discovery/api/abc.py @@ -1,9 +1,9 @@ -import abc +from abc import ABC from discovery.engine.abc import Engine -class Api(abc.ABC): +class Api(ABC): def __init__( self, client: Engine, endpoint: str = "/", version: str = "v1" ) -> None: @@ -12,8 +12,8 @@ def __init__( self.version = version def __repr__(self) -> str: - *_, name = str(self.__module__).split(".") - return f"{name.title()}(endpoint={self.url})" + *_, name = str(self.__class__).split(".") + return f"{name[:-2]}(endpoint={self.url})" @property def client(self) -> Engine: @@ -22,3 +22,13 @@ def client(self) -> Engine: @property def url(self) -> str: return f"{self._client.url}/{self.version}{self.endpoint}" + + def _prepare_request_url(self, base_url: str, **kwargs) -> str: + params = [ + f"{key.replace('_', '-')}={value}" + for key, value in kwargs.items() + if value is not None + ] + if len(params) > 0: + return f"{base_url}?{'&'.join(params)}" + return base_url diff --git a/discovery/api/acl.py b/discovery/api/acl.py index 8e2339e..71163b8 100644 --- a/discovery/api/acl.py +++ b/discovery/api/acl.py @@ -1,4 +1,6 @@ -from discovery import api, logging +from typing import Optional + +from discovery import api, log from discovery.api.abc import Api @@ -12,7 +14,7 @@ def __init__( token=None, endpoint: str = "/acl", **kwargs, - ): + ) -> None: super().__init__(endpoint=endpoint, **kwargs) self.auth_method = auth_method or api.AuthMethod(client=self.client) self.binding_rule = binding_rule or api.BindingRule(client=self.client) @@ -20,22 +22,23 @@ def __init__( self.role = role or api.Role(client=self.client) self.token = token or api.Token(client=self.client) - async def bootstrap(self): - response = await self.client.put(f"{self.url}/bootstrap") - return response + async def bootstrap(self) -> dict: + async with self.client.put(f"{self.url}/bootstrap") as resp: + return await resp.json() # type: ignore - async def replication(self, **kwargs): - response = await self.client.get(f"{self.url}/replication", **kwargs) - return response + async def replication(self, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/replication", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def translate(self, data, **kwargs): - logging.warning( + async def translate(self, data, **kwargs) -> dict: + log.warning( "Deprecated - This endpoint was introduced in Consul 1.4.0 " "for migration from the previous ACL system. " "It will be removed in a future major Consul " "version when support for legacy ACLs is removed." ) - response = await self.client.post( - f"{self.url}/rules/translate", data=data, **kwargs - ) - return response + async with self.client.post( + f"{self.url}/rules/translate", json=data, **kwargs + ) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/acl_link.py b/discovery/api/acl_link.py new file mode 100644 index 0000000..ef386ae --- /dev/null +++ b/discovery/api/acl_link.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass +class ACLLink: + uuid: str + name: str + + +@dataclass +class PolicyDefaults(ACLLink): + pass + + +@dataclass +class RoleDefaults(ACLLink): + pass diff --git a/discovery/api/agent.py b/discovery/api/agent.py index 80fb45a..dde34c9 100644 --- a/discovery/api/agent.py +++ b/discovery/api/agent.py @@ -1,7 +1,11 @@ +from contextlib import asynccontextmanager +from typing import AsyncIterator, Optional from urllib.parse import quote_plus -from discovery import api +from discovery import api, log from discovery.api.abc import Api +from discovery.api.loglevel import LogLevel +from discovery.api.token_type import TokenType class Agent(Api): @@ -12,7 +16,7 @@ def __init__( service=None, endpoint: str = "/agent", **kwargs, - ): + ) -> None: super().__init__(endpoint=endpoint, **kwargs) self.checks = checks or api.Checks(client=self.client) self.connect = connect or api.Connect( @@ -22,67 +26,87 @@ def __init__( ) self.service = service or api.Service(client=self.client) - async def members(self, **kwargs): - response = await self.client.get(f"{self.url}/members", **kwargs) - return response + async def host_information(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/host", **kwargs) as resp: + return await resp.json() # type: ignore - async def read_configuration(self, **kwargs): - response = await self.client.get(f"{self.url}/self", **kwargs) - return response + async def members( + self, wan: Optional[bool] = None, segment: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/members", wan=wan, segment=segment) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def reload(self, **kwargs): - response = await self.client.put(f"{self.url}/reload", **kwargs) - return response + async def read_configuration(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/self", **kwargs) as resp: + return await resp.json() # type: ignore - async def maintenance(self, enable=True, reason=None, **kwargs): - reason = reason or "" - response = await self.client.put( - f"{self.url}/maintenance?enable={enable}&reason={quote_plus(reason)}", - **kwargs, + async def reload(self, **kwargs) -> None: + async with self.client.put(f"{self.url}/reload", **kwargs): + pass + + async def maintenance( + self, enable: bool = True, reason: Optional[str] = None, **kwargs + ) -> None: + if reason: + reason = quote_plus(reason) + url = self._prepare_request_url( + f"{self.url}/maintenance", enable=enable, reason=reason ) - return response + async with self.client.put(url, **kwargs): + pass - async def metrics(self, **kwargs): - response = await self.client.get(f"{self.url}/metrics", **kwargs) - return response + async def metrics(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/metrics", **kwargs) as resp: + return await resp.json() # type: ignore - async def stream_logs(self, chunk_size=20, **kwargs): - async with self.client.session.get(f"{self.url}/monitor", **kwargs) as resp: - with open("/tmp/teste", "wb") as fd: - while True: - chunk = await resp.content.read(chunk_size) - if not chunk: - break - fd.write(chunk) - # response = await self.client.get(f"{self.url}/monitor", **kwargs) - # return response + @asynccontextmanager + async def stream_logs( + self, + loglevel: LogLevel = LogLevel.INFO, + logjson: bool = False, + chunk_size: int = 1000, + **kwargs, + ) -> AsyncIterator: + url = self._prepare_request_url( + f"{self.url}/monitor", loglevel=loglevel, logjson=logjson + ) + async with self.client.get(url, **kwargs) as resp: + yield await resp.content(chunk_size) - async def join(self, address, **kwargs): - response = await self.client.put(f"{self.url}/join/{address}", **kwargs) - return response + async def join(self, address: str, wan: Optional[bool] = None, **kwargs) -> None: + url = self._prepare_request_url(f"{self.url}/join/{address}", wan=wan) + async with self.client.put(url, **kwargs): + pass - async def leave(self, **kwargs): - response = await self.client.put(f"{self.url}/leave", **kwargs) - return response + async def leave(self, **kwargs) -> None: + async with self.client.put(f"{self.url}/leave", **kwargs): + pass - async def force_leave(self, node, **kwargs): - response = await self.client.put(f"{self.url}/force-leave/{node}", **kwargs) - return response + async def force_leave( + self, + node_name: str, + prune: Optional[bool] = None, + wan: Optional[bool] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url( + f"{self.url}/force-leave/{node_name}", prune=prune, wan=wan + ) + async with self.client.put(url, **kwargs): + pass - async def update_acl_token(self, token_type: str): - if token_type not in [ - "default", - "agent", - "agent_master", - "replication", - "acl_token", # legacy - "acl_agent_token", - "acl_agent_master_token", - "acl_replication_token", + async def update_acl_token(self, token: str, token_type: TokenType) -> dict: + if token_type == TokenType.AGENT_MASTER: + log.warning("Deprecated in version 1.11") + elif token_type in [ + TokenType.ACL_TOKEN, + TokenType.ACL_AGENT_TOKEN, + TokenType.ACL_AGENT_MASTER_TOKEN, + TokenType.ACL_REPLICATION_TOKEN, ]: - raise ValueError( - "token_type invalid. See the valid values in: " - "https://www.consul.io/api/agent.html#update-acl-tokens" - ) - response = await self.client.put(f"{self.url}/token/{token_type}") - return response + log.warning("Deprecated in version 1.4.3") + async with self.client.put( + f"{self.url}/token/{token_type}", json={"Token": token} + ) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/area.py b/discovery/api/area.py index f69f1b1..8553e49 100644 --- a/discovery/api/area.py +++ b/discovery/api/area.py @@ -1,34 +1,69 @@ +from typing import List, Optional + from discovery.api.abc import Api class Area(Api): - def __init__(self, endpoint: str = "/operator/area", **kwargs): + def __init__(self, endpoint: str = "/operator/area", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.post(f"{self.url}", data=data, **kwargs) - return response - - async def list(self, uuid=None, **kwargs): - if uuid: - uri = f"{self.url}/{uuid}" - else: - uri = f"{self.url}" - response = await self.client.get(uri, **kwargs) - return response - - async def update(self, uuid, data, **kwargs): - response = await self.client.put(f"{self.url}/{uuid}", data=data, **kwargs) - return response - - async def delete(self, uuid, **kwargs): - response = await self.client.delete(f"{self.url}/{uuid}", **kwargs) - return response - - async def join(self, uuid, data, **kwargs): - response = await self.client.put(f"{self.url}/{uuid}/join", data=data, **kwargs) - return response - - async def members(self, uuid, **kwargs): - response = await self.client.get(f"{self.url}/{uuid}/members", **kwargs) - return response + async def create_network( + self, + peer_datacenter: str, + retry_join: Optional[List[str]] = None, + use_tls: bool = False, + dc: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + payload = dict(PeerDatacenter=peer_datacenter, UseTLS=use_tls) + if retry_join: + payload.update({"RetryJoin": retry_join}) + async with self.client.post(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def list_network(self, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update_network( + self, + uuid: str, + dc: Optional[str] = None, + use_tls: bool = True, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + async with self.client.put(url, json=dict(UseTLS=use_tls), **kwargs): + pass + + async def list_specific_network(self, uuid: str, dc: Optional[str] = None) -> dict: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + async with self.client.get(url) as resp: + return await resp.json() # type: ignore + + async def delete_network( + self, uuid: str, dc: Optional[str] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + async with self.client.delete(url, **kwargs): + pass + + async def join_network( + self, + uuid: str, + data: List[str], + dc: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{uuid}/join", dc=dc) + async with self.client.put(url, json=data, **kwargs) as resp: + return await resp.json() # type: ignore + + async def list_network_members( + self, uuid, dc: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{uuid}/members", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/auth_method.py b/discovery/api/auth_method.py index 07d6c21..ad9a794 100644 --- a/discovery/api/auth_method.py +++ b/discovery/api/auth_method.py @@ -1,26 +1,94 @@ +from typing import Dict, List, Optional + from discovery.api.abc import Api +from discovery.api.token_locality import TokenLocality class AuthMethod(Api): - def __init__(self, endpoint: str = "/acl/auth-method", **kwargs): + def __init__(self, endpoint: str = "/acl/auth-method", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + name: str, + type: str, + description: str, + config: Dict[str, str], + display_name: Optional[str] = None, + max_token_ttl: Optional[str] = None, + token_locality: Optional[TokenLocality] = None, + namespace: Optional[str] = None, + namespace_rules="", + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + payload = dict(Name=name, Type=type, Description=description, Config=config) + + if display_name: + payload.update({"DisplayName": display_name}) + + if max_token_ttl: + payload.update({"MaxTokenTTL": max_token_ttl}) + + if token_locality: + payload.update({"TokenLocality": token_locality}) + + if namespace: + payload.update({"Namespace": namespace}) + + if namespace_rules: + payload.update({"NamespaceRules": namespace_rules}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read(self, name: str, ns: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/{name}", ns=ns) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + name: str, + type: str, + description: str, + config: Dict[str, str], + display_name: Optional[str] = None, + max_token_ttl: Optional[str] = None, + token_locality: Optional[TokenLocality] = None, + namespace: Optional[str] = None, + namespace_rules="", + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{name}", ns=ns) + payload = dict(Name=name, Type=type, Description=description, Config=config) + + if display_name: + payload.update({"DisplayName": display_name}) + + if max_token_ttl: + payload.update({"MaxTokenTTL": max_token_ttl}) + + if token_locality: + payload.update({"TokenLocality": token_locality}) + + if namespace: + payload.update({"Namespace": namespace}) - async def read(self, name, **kwargs): - response = await self.client.put(f"{self.url}/{name}", **kwargs) - return response + if namespace_rules: + payload.update({"NamespaceRules": namespace_rules}) - async def update(self, name, data, **kwargs): - response = await self.client.put(f"{self.url}/{name}", data=data, **kwargs) - return response + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, name, **kwargs): - response = await self.client.delete(f"{self.url}/{name}", **kwargs) - return response + async def delete(self, name: str, ns: Optional[str] = None, **kwargs) -> bool: + url = self._prepare_request_url(f"{self.url}/{name}", ns=ns) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.put(f"{self.url}s", **kwargs) - return response + async def list(self, ns: Optional[str] = None, **kwargs) -> List[dict]: + url = self._prepare_request_url(f"{self.url}s", ns=ns) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/autopilot.py b/discovery/api/autopilot.py index 9ce4c13..75c7f70 100644 --- a/discovery/api/autopilot.py +++ b/discovery/api/autopilot.py @@ -1,20 +1,53 @@ +from typing import Optional + from discovery.api.abc import Api class AutoPilot(Api): - def __init__(self, endpoint: str = "/operator/autopilot", **kwargs): + def __init__(self, endpoint: str = "/operator/autopilot", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def read_configuration(self, **kwargs): - response = await self.client.get(f"{self.url}/configuration", **kwargs) - return response + async def read_configuration( + self, dc: Optional[str] = None, stale: Optional[bool] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/configuration", dc=dc, stale=stale) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def update_configuration(self, data, **kwargs): - response = await self.client.put( - f"{self.url}/configuration", data=data, **kwargs + async def update_configuration( + self, + cleanup_dead_servers: bool = True, + last_contact_threshold: str = "200ms", + max_trailing_logs: int = 250, + min_quorum: int = 0, + server_stabilization_time: str = "10s", + redundancy_zone_tag: str = "", + disable_upgrade_migration: bool = False, + upgrade_version_tag: str = "", + dc: Optional[str] = None, + cas: Optional[int] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/configuration", dc=dc, cas=cas) + data = dict( + CleanupDeadServers=cleanup_dead_servers, + LastContactThreshold=last_contact_threshold, + MaxTrailingLogs=max_trailing_logs, + MinQuorum=min_quorum, + ServerStabilizationTime=server_stabilization_time, + RedundancyZoneTag=redundancy_zone_tag, + DisableUpgradeMigration=disable_upgrade_migration, + UpgradeVersionTag=upgrade_version_tag, ) - return response + async with self.client.put(url, json=data, **kwargs): + pass + + async def read_health(self, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/health", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def read_health(self, **kwargs): - response = await self.client.get(f"{self.url}/health", **kwargs) - return response + async def read_state(self, dc: Optional[str] = None, **kwargs): + url = self._prepare_request_url(f"{self.url}/state", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() diff --git a/discovery/api/behavior.py b/discovery/api/behavior.py new file mode 100644 index 0000000..19db9a1 --- /dev/null +++ b/discovery/api/behavior.py @@ -0,0 +1,10 @@ +from enum import Enum, unique + + +@unique +class Behavior(str, Enum): + RELEASE: str = "release" + DELETE: str = "delete" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/binding_rule.py b/discovery/api/binding_rule.py index 61798e7..6a23b58 100644 --- a/discovery/api/binding_rule.py +++ b/discovery/api/binding_rule.py @@ -1,26 +1,78 @@ +from typing import Optional + from discovery.api.abc import Api class BindingRule(Api): - def __init__(self, endpoint: str = "/acl/binding-rule", **kwargs): + def __init__(self, endpoint: str = "/acl/binding-rule", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + auth_method: str, + bind_type: str, + bind_name: str, + description: str = "", + selector: str = "", + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + payload = dict( + Description=description, + AuthMethod=auth_method, + Selector=selector, + BindType=bind_type, + BindName=bind_name, + ) + + if namespace: + payload.update({"Namespace": namespace}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read(self, role_id: str, ns: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + role_id: str, + auth_method: str, + bind_type: str, + bind_name: str, + description: str = "", + selector: str = "", + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + payload = dict( + Description=description, + Selector=selector, + BindType=bind_type, + BindName=bind_name, + AuthMethod=auth_method, + ) - async def read(self, role_id, **kwargs): - response = await self.client.get(f"{self.url}/{role_id}", **kwargs) - return response + if namespace: + payload.update({"Namespace": namespace}) - async def update(self, data, role_id, **kwargs): - response = await self.client.put(f"{self.url}/{role_id}", data=data, **kwargs) - return response + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, role_id, **kwargs): - response = await self.client.delete(f"{self.url}/{role_id}", **kwargs) - return response + async def delete(self, role_id: str, ns: Optional[str] = None, **kwargs) -> bool: + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}", **kwargs) - return response + async def list( + self, auth_method: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/ca.py b/discovery/api/ca.py index e8ff27b..b480487 100644 --- a/discovery/api/ca.py +++ b/discovery/api/ca.py @@ -1,20 +1,34 @@ +from typing import Dict, Optional + from discovery.api.abc import Api class CA(Api): - def __init__(self, endpoint: str = "/connect/ca", **kwargs): + def __init__(self, endpoint: str = "/connect/ca", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def roots(self, **kwargs): - response = await self.client.get(f"{self.url}/roots", **kwargs) - return response + async def list_root_certificates( + self, pem: Optional[bool] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/roots", pem=pem) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def configuration(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/configuration", **kwargs) as resp: + return await resp.json() # type: ignore + + async def update_configuration( + self, + provider: str, + config: Dict[str, str], + force_without_cross_signing: bool = False, + **kwargs, + ) -> None: + payload = dict(Provider=provider, Config=config) - async def configuration(self, **kwargs): - response = await self.client.get(f"{self.url}/configuration", **kwargs) - return response + if force_without_cross_signing: + payload.update({"ForceWithoutCrossSigning": force_without_cross_signing}) # type: ignore - async def update(self, data, **kwargs): - response = await self.client.put( - f"{self.url}/configuration", data=data, **kwargs - ) - return response + async with self.client.put(f"{self.url}/configuration", json=payload, **kwargs): + pass diff --git a/discovery/api/catalog.py b/discovery/api/catalog.py index 4a06d90..cf1b0d5 100644 --- a/discovery/api/catalog.py +++ b/discovery/api/catalog.py @@ -1,38 +1,184 @@ +from typing import Dict, Optional + from discovery.api.abc import Api class Catalog(Api): - def __init__(self, endpoint: str = "/catalog", **kwargs): + def __init__(self, endpoint: str = "/catalog", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def register(self, data: dict, **kwargs): - response = await self.client.put(f"{self.url}/register", data=data, **kwargs) - return response + async def register_entity( + self, + address: str, + datacenter: str, + node: str, + node_id: Optional[str] = None, + tagged_addresses: Optional[Dict[str, str]] = None, + node_meta: Optional[dict] = None, + service: Optional[dict] = None, + check: Optional[dict] = None, + skip_node_update: bool = False, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/register", ns=ns) + payload = dict(Datacenter=datacenter, Node=node, Address=address) + + if tagged_addresses: + payload.update({"TaggedAddresses": tagged_addresses}) # type: ignore + + if node_meta: + payload.update({"NodeMeta": node_meta}) # type: ignore + + if service: + payload.update({"Service": service}) # type: ignore + + if check: + payload.update({"Check": check}) # type: ignore + + if skip_node_update: + payload.update({"SkipNodeUpdate": skip_node_update}) # type: ignore + + if namespace: + payload.update({"Namespace": namespace}) + + if node_id: + payload.update({"ID": node_id}) + + async with self.client.put(url, json=payload, **kwargs): + pass + + async def deregister_entity( + self, + node: str, + datacenter: str, + check_id: Optional[str] = None, + service_id: Optional[str] = None, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/deregister", ns=ns) + payload = dict(Node=node, Datacenter=datacenter) + + if check_id: + payload.update({"CheckID": check_id}) + + if service_id: + payload.update({"ServiceID": service_id}) + + if namespace: + payload.update({"Namespace": namespace}) + + async with self.client.put(url, **kwargs, json=payload): + pass + + async def list_datacenters(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/datacenters", **kwargs) as resp: + return await resp.json() # type: ignore - async def deregister(self, data, **kwargs): - response = await self.client.put(f"{self.url}/deregister", **kwargs, data=data) - return response + async def list_nodes( + self, + dc: Optional[str] = None, + near: Optional[str] = None, + filter_options: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/nodes", dc=dc, near=near, filter=filter_options + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def datacenters(self, **kwargs): - response = await self.client.get(f"{self.url}/datacenters", **kwargs) - return response + async def list_services( + self, + dc: Optional[str] = None, + node_meta: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/services", dc=dc, node_meta=node_meta, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def nodes(self, **kwargs): - response = await self.client.get(f"{self.url}/nodes", **kwargs) - return response + async def list_nodes_for_service( + self, + service_name: str, + dc: Optional[str] = None, + near: Optional[str] = None, + filter_options: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/service/{service_name}", + dc=dc, + near=near, + filter=filter_options, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def services(self, **kwargs): - response = await self.client.get(f"{self.url}/services", **kwargs) - return response + async def list_nodes_for_connect( + self, + service: str, + dc: Optional[str] = None, + near: Optional[str] = None, + filter_options: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/connect/{service}", + dc=dc, + near=near, + filter=filter_options, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def service(self, name, **kwargs): - response = await self.client.get(f"{self.url}/service/{name}", **kwargs) - return response + async def services_for_node( + self, + node_name: str, + dc: Optional[str] = None, + filter_options: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/node/{node_name}", dc=dc, filter=filter_options, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def connect(self, service, **kwargs): - response = await self.client.get(f"{self.url}/connect/{service}", **kwargs) - return response + async def list_services_for_node( + self, + node_name: str, + dc: Optional[str] = None, + filter_options: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/node-services/{node_name}", dc=dc, filter=filter_options, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def node(self, node, **kwargs): - response = await self.client.get(f"{self.url}/node/{node}", **kwargs) - return response + async def list_services_for_gateway( + self, + gateway: str, + dc: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/gateway-services/{gateway}", dc=dc, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/check_status.py b/discovery/api/check_status.py new file mode 100644 index 0000000..725eb71 --- /dev/null +++ b/discovery/api/check_status.py @@ -0,0 +1,11 @@ +from enum import Enum, unique + + +@unique +class CheckStatus(str, Enum): + PASSING: str = "passing" + WARNING: str = "warning" + CRITICAL: str = "critical" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/checks.py b/discovery/api/checks.py index 0e60f5d..9a5c3d5 100644 --- a/discovery/api/checks.py +++ b/discovery/api/checks.py @@ -1,48 +1,79 @@ -import json +from typing import Optional from discovery.api.abc import Api +from discovery.api.check_status import CheckStatus class Checks(Api): - def __init__(self, endpoint: str = "/agent/check", **kwargs): + def __init__(self, endpoint: str = "/agent/check", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def checks(self, **kwargs): - response = await self.client.get(f"{self.url}s", **kwargs) - return response - - async def register(self, data, **kwargs): - response = await self.client.put(f"{self.url}/register", data=data, **kwargs) - return response - - async def deregister(self, check_id, **kwargs): - response = await self.client.put(f"{self.url}/deregister/{check_id}", **kwargs) - return response - - async def check_pass(self, check_id, notes="", **kwargs): - response = await self.client.put( - f"{self.url}/pass/{check_id}", data=notes, **kwargs - ) - return response - - async def check_warn(self, check_id, notes="", **kwargs): - response = await self.client.put( - f"{self.url}/warn/{check_id}", data=notes, **kwargs - ) - return response - - async def check_fail(self, check_id, notes="", **kwargs): - response = await self.client.put( - f"{self.url}/fail/{check_id}", data=notes, **kwargs - ) - return response - - async def check_update(self, check_id, status, output="", **kwargs): - status = str(status).lower() - if status not in ["passing", "warning", "critical"]: - raise ValueError('Valid values are "passing", "warning", and "critical"') - data = dict(status=status, output=output) - response = await self.client.put( - f"{self.url}/update/{check_id}", data=json.dumps(data), **kwargs - ) - return response + async def list( + self, filter: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}s", filter=filter, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def register(self, data: dict, ns: Optional[str] = None, **kwargs) -> None: + url = self._prepare_request_url(f"{self.url}/register", ns=ns) + async with self.client.put(url, json=data, **kwargs): + pass + + async def deregister( + self, check_id: str, ns: Optional[str] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}/deregister/{check_id}", ns=ns) + async with self.client.put(url, **kwargs): + pass + + async def check_pass( + self, + check_id: str, + note: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/pass/{check_id}", note=note, ns=ns) + async with self.client.put(url, **kwargs): + pass + + async def check_warn( + self, + check_id: str, + note: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/warn/{check_id}", note=note, ns=ns) + async with self.client.put(url, **kwargs): + pass + + async def check_fail( + self, + check_id: str, + note: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/fail/{check_id}", note=note, ns=ns) + async with self.client.put(url, **kwargs): + pass + + async def check_update( + self, + check_id: str, + status: CheckStatus = CheckStatus.PASSING, + output: str = "", + ns: Optional[str] = None, + **kwargs, + ) -> None: + if len([st for st in CheckStatus if status == st]) != 1: + raise ValueError( + f"status must be: ['passing', 'warning' or 'critical'] got '{status}'" + ) + url = self._prepare_request_url(f"{self.url}/update/{check_id}", ns=ns) + async with self.client.put( + url, json=dict(status=status, output=output), **kwargs + ): + pass diff --git a/discovery/api/config.py b/discovery/api/config.py index adce669..6f446be 100644 --- a/discovery/api/config.py +++ b/discovery/api/config.py @@ -1,33 +1,55 @@ +from typing import Optional + from discovery.api.abc import Api +from discovery.api.kind import Kind class Config(Api): - def __init__(self, endpoint: str = "/config", **kwargs): + def __init__(self, endpoint: str = "/config", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - def _config_entry_kind_is_valid(self, kind): - kind = str(kind).lower() - if kind not in ["service-defaults", "proxy-defaults"]: - raise ValueError( - 'Valid values are "service-defaults" and "proxy-defaults".' - ) - return True - - async def apply(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def apply( + self, + data: dict, + dc: Optional[str] = None, + cas: Optional[int] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}", dc=dc, cas=cas, ns=ns) + async with self.client.put(url, json=data, **kwargs): + pass - async def get(self, kind, name, **kwargs): - if self._config_entry_kind_is_valid(kind): - response = await self.client.get(f"{self.url}/{kind}/{name}", **kwargs) - return response + async def get( + self, + kind: Kind, + name: str, + dc: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{kind}/{name}", dc=dc, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, kind, **kwargs): - if self._config_entry_kind_is_valid(kind): - response = await self.client.get(f"{self.url}/{kind}", **kwargs) - return response + async def list( + self, kind: Kind, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{kind}", dc=dc, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, kind, name, **kwargs): - if self._config_entry_kind_is_valid(kind): - response = await self.client.delete(f"{self.url}/{kind}/{name}", **kwargs) - return response + async def delete( + self, + kind: Kind, + name: str, + dc: Optional[str] = None, + cas: Optional[int] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url( + f"{self.url}/{kind}/{name}", dc=dc, cas=cas, ns=ns + ) + async with self.client.delete(url, **kwargs): + pass diff --git a/discovery/api/connect.py b/discovery/api/connect.py index bb216fa..d9ce2e2 100644 --- a/discovery/api/connect.py +++ b/discovery/api/connect.py @@ -1,4 +1,4 @@ -import json +from typing import Optional from discovery import api from discovery.api.abc import Api @@ -7,30 +7,36 @@ class Connect(Api): def __init__( self, ca=None, intentions=None, endpoint: str = "/agent/connect", **kwargs - ): + ) -> None: super().__init__(endpoint=endpoint, **kwargs) self.ca = ca or api.CA(client=self.client) self.intentions = intentions or api.Intentions(client=self.client) async def authorize( - self, target, client_cert_uri, client_cert_serial, namespace=None - ): - data = dict( + self, + target: str, + client_cert_uri: str, + client_cert_serial: str, + ns: Optional[str] = None, + namespace: Optional[str] = None, + **kwargs, + ) -> dict: + payload = dict( Target=target, ClientCertURI=client_cert_uri, ClientCertSerial=client_cert_serial, ) if namespace: - data.update({"Namespace": namespace}) - response = await self.client.post( - f"{self.url}/authorize", data=json.dumps(data) - ) - return response + payload.update({"Namespace": namespace}) + url = self._prepare_request_url(f"{self.url}/authorize", ns=ns) + async with self.client.post(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def ca_roots(self): - response = await self.client.get(f"{self.url}/ca/roots") - return response + async def ca_roots(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/ca/roots", **kwargs) as resp: + return await resp.json() # type: ignore - async def leaf_certificate(self, service: str): - response = await self.client.get(f"{self.url}/ca/leaf/{service}") - return response + async def leaf_certificate(self, service: str, ns: Optional[str] = None) -> dict: + url = self._prepare_request_url(f"{self.url}/ca/leaf/{service}", ns=ns) + async with self.client.get(url) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/coordinate.py b/discovery/api/coordinate.py index a27bf87..1360366 100644 --- a/discovery/api/coordinate.py +++ b/discovery/api/coordinate.py @@ -1,22 +1,39 @@ +from typing import Optional + from discovery.api.abc import Api class Coordinate(Api): - def __init__(self, endpoint: str = "/coordinate", **kwargs): + def __init__(self, endpoint: str = "/coordinate", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def read_wan(self, **kwargs): - response = await self.client.get(f"{self.url}/datacenters", **kwargs) - return response + async def read_wan(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}/datacenters", **kwargs) as resp: + return await resp.json() # type: ignore - async def read_lan(self, **kwargs): - response = await self.client.get(f"{self.url}/nodes", **kwargs) - return response + async def read_lan_for_all_nodes( + self, dc: Optional[str] = None, segment: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/nodes", dc=dc, segment=segment) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def read_lan_node(self, node, **kwargs): - response = await self.client.get(f"{self.url}/node/{node}", **kwargs) - return response + async def read_lan_for_node( + self, + node_name: str, + dc: Optional[str] = None, + segment: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/node/{node_name}", dc=dc, segment=segment + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def update_lan_node(self, data, **kwargs): - response = await self.client.put(f"{self.url}/update", data=data, **kwargs) - return response + async def update_lan_for_node( + self, data: dict, dc: Optional[str] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}/update", dc=dc) + async with self.client.put(url, json=data, **kwargs): + pass diff --git a/discovery/api/events.py b/discovery/api/events.py index 210753a..aa9424e 100644 --- a/discovery/api/events.py +++ b/discovery/api/events.py @@ -1,14 +1,38 @@ +from typing import Optional + from discovery.api.abc import Api class Events(Api): - def __init__(self, endpoint: str = "/event", **kwargs): + def __init__(self, endpoint: str = "/event", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def fire(self, name, data, **kwargs): - response = await self.client.put(f"{self.url}/fire/{name}", data=data, **kwargs) - return response + async def fire_event( + self, + name: str, + data: dict, + dc: Optional[str] = None, + node: Optional[str] = None, + service: Optional[str] = None, + tag: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/fire/{name}", dc=dc, node=node, service=service, tag=tag + ) + async with self.client.put(url, json=data, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, key, **kwargs): - response = await self.client.get(f"{self.url}/list", **kwargs) - return response + async def list( + self, + name: Optional[str] = None, + node: Optional[str] = None, + service: Optional[str] = None, + tag: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/list", name=name, node=node, service=service, tag=tag + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/health.py b/discovery/api/health.py index 61f0849..5228a81 100644 --- a/discovery/api/health.py +++ b/discovery/api/health.py @@ -1,29 +1,134 @@ +from typing import List, Optional + from discovery.api.abc import Api +from discovery.api.health_state import HealthState class Health(Api): - def __init__(self, endpoint: str = "/health", **kwargs): + def __init__(self, endpoint: str = "/health", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def node(self, node, **kwargs): - response = await self.client.get(f"{self.url}/node/{node}", **kwargs) - return response + async def checks_for_node( + self, + node: str, + dc: Optional[str] = None, + filter: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/node/{node}", dc=dc, filter=filter, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def checks_for_service( + self, + service: str, + dc: Optional[str] = None, + near: Optional[str] = None, + node_meta: Optional[str] = None, + filter: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/checks/{service}", + dc=dc, + near=near, + node_meta=node_meta, + filter=filter, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def checks(self, service, **kwargs): - response = await self.client.get(f"{self.url}/checks/{service}", **kwargs) - return response + async def service_instances( + self, + service: str, + dc: Optional[str] = None, + near: Optional[str] = None, + passing: Optional[bool] = None, + filter: Optional[str] = None, + peer: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/service/{service}", + dc=dc, + near=near, + passing=passing, + filter=filter, + peer=peer, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def service(self, service, **kwargs): - response = await self.client.get(f"{self.url}/service/{service}", **kwargs) - return response + async def service_instances_for_connect( + self, + service: str, + dc: Optional[str] = None, + near: Optional[str] = None, + passing: Optional[bool] = None, + filter: Optional[str] = None, + peer: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/connect/{service}", + dc=dc, + near=near, + passing=passing, + filter=filter, + peer=peer, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def connect(self, service, **kwargs): - response = await self.client.get(f"{self.url}/connect/{service}", **kwargs) - return response + async def service_instances_for_ingress( + self, + service: str, + dc: Optional[str] = None, + near: Optional[str] = None, + passing: Optional[bool] = None, + filter: Optional[str] = None, + peer: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/ingress/{service}", + dc=dc, + near=near, + passing=passing, + filter=filter, + peer=peer, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def state(self, state, **kwargs): - state = str(state).lower() - if state not in ["passing", "warning", "critical"]: - raise ValueError('Valid values are "passing", "warning", and "critical"') - response = await self.client.get(f"{self.url}/state/{str(state)}", **kwargs) - return response + async def checks_in_state( + self, + state: HealthState = HealthState.ANY, + dc: Optional[str] = None, + near: Optional[str] = None, + node_meta: Optional[str] = None, + filter: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[str]: + url = self._prepare_request_url( + f"{self.url}/state/{state}", + dc=dc, + near=near, + node_meta=node_meta, + filter=filter, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/health_state.py b/discovery/api/health_state.py new file mode 100644 index 0000000..832d1c6 --- /dev/null +++ b/discovery/api/health_state.py @@ -0,0 +1,12 @@ +from enum import Enum, unique + + +@unique +class HealthState(str, Enum): + ANY: str = "any" + PASSING: str = "passing" + WARNING: str = "warning" + CRITICAL: str = "critical" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/intention.py b/discovery/api/intention.py index 1e77f06..7ebf7d1 100644 --- a/discovery/api/intention.py +++ b/discovery/api/intention.py @@ -1,44 +1,81 @@ +from typing import List, Optional + from discovery.api.abc import Api +from discovery.api.intention_by import IntentionBy +from discovery.api.intention_filter import IntentionFilter +from discovery.api.intentions_action import IntentionsAction class Intentions(Api): - def __init__(self, endpoint: str = "/connect/intentions", **kwargs): + def __init__(self, endpoint: str = "/connect/intentions", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - def by_is_valid(self, by): - if by.lower() not in ["source", "destination"]: - raise ValueError('by must be: "source" or "destination"') - return True + async def upsert_by_name( + self, + source: str, + destination: str, + ns: Optional[str] = None, + source_type: str = "consul", + action: IntentionsAction = IntentionsAction.ALLOW, + permissions: Optional[List[str]] = None, + description: str = "", + **kwargs, + ) -> bool: + payload = dict(SourceType=source_type, Action=action, Description=description) - async def create(self, data, **kwargs): - response = await self.client.post(f"{self.url}", data=data, **kwargs) - return response + if permissions: + payload.update({"Permissions": permissions}) # type: ignore - async def read(self, uuid, **kwargs): - response = await self.client.get(f"{self.url}/{uuid}", **kwargs) - return response + url = self._prepare_request_url( + f"{self.url}/exact", source=source, destination=destination, ns=ns + ) + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}", **kwargs) - return response + async def read_by_name( + self, source: str, destination: str, ns: Optional[str] = None, **kwargs + ): + url = self._prepare_request_url( + f"{self.url}/exact", source=source, destination=destination, ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() - async def update(self, uuid, data, **kwargs): - response = await self.client.put(f"{self.url}/{uuid}", data=data, **kwargs) - return response + async def list( + self, + filter: IntentionFilter = IntentionFilter.SOURCE_NAME, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", filter=filter, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, uuid, **kwargs): - response = await self.client.delete(f"{self.url}/{uuid}", **kwargs) - return response + async def delete_by_name( + self, source: str, destination: str, ns: Optional[str] = None, **kwargs + ) -> None: + url = self._prepare_request_url( + f"{self.url}/exact", source=source, destination=destination, ns=ns + ) + async with self.client.delete(url, **kwargs): + pass - async def check(self, source, destination, **kwargs): - response = await self.client.get( - f"{self.url}/check?source={source}&destination={destination}", **kwargs + async def check( + self, source: str, destination: str, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/check", source=source, destination=destination, ns=ns ) - return response - - async def match(self, by, name, **kwargs): - if self.by_is_valid(by): - response = await self.client.get( - f"{self.url}/match?by={by}&name={name}", **kwargs - ) - return response + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def list_match( + self, + name: str, + by: IntentionBy = IntentionBy.NAME, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/match", by=by, name=name, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/intention_by.py b/discovery/api/intention_by.py new file mode 100644 index 0000000..67543ce --- /dev/null +++ b/discovery/api/intention_by.py @@ -0,0 +1,11 @@ +from enum import Enum, unique + + +@unique +class IntentionBy(str, Enum): + NAME: str = "name" + SOURCE: str = "source" + DESTINATION: str = "destination" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/intention_filter.py b/discovery/api/intention_filter.py new file mode 100644 index 0000000..c259a2e --- /dev/null +++ b/discovery/api/intention_filter.py @@ -0,0 +1,19 @@ +from enum import Enum, unique + + +@unique +class IntentionFilter(str, Enum): + ACTION: str = "Action" + DESCRIPTION: str = "Description" + DESTINATION_NS: str = "DestinationNS" + DESTINATION_NAME: str = "DestinationName" + ID: str = "ID" + META: str = "Meta" + META_ANY: str = "Meta." + PRECEDENCE: str = "Precedence" + SOURCE_NS: str = "SourceNS" + SOURCE_NAME: str = "SourceName" + SOURCE_TYPE: str = "SourceType" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/intentions_action.py b/discovery/api/intentions_action.py new file mode 100644 index 0000000..40db662 --- /dev/null +++ b/discovery/api/intentions_action.py @@ -0,0 +1,10 @@ +from enum import Enum, unique + + +@unique +class IntentionsAction(str, Enum): + ALLOW: str = "allow" + DENY: str = "deny" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/keyring.py b/discovery/api/keyring.py index 1b84683..47e56f9 100644 --- a/discovery/api/keyring.py +++ b/discovery/api/keyring.py @@ -1,22 +1,41 @@ +from typing import Optional + from discovery.api.abc import Api class Keyring(Api): - def __init__(self, endpoint: str = "/operator/keyring", **kwargs): + def __init__(self, endpoint: str = "/operator/keyring", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}", **kwargs) - return response + async def list_keys( + self, + relay_factor: Optional[int] = None, + local_only: Optional[bool] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}", relay_factor=relay_factor, local_only=local_only + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def add(self, data, **kwargs): - response = await self.client.post(f"{self.url}", data=data, **kwargs) - return response + async def add_encryption_key( + self, key: str, relay_factor: Optional[int] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}", relay_factor=relay_factor) + async with self.client.post(url, json=dict(Key=key), **kwargs): + pass - async def change(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def change_encryption_key( + self, key: str, relay_factor: Optional[int] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}", relay_factor=relay_factor) + async with self.client.put(url, json=dict(Key=key), **kwargs): + pass - async def delete(self, data, **kwargs): - response = await self.client.delete(f"{self.url}", data=data, **kwargs) - return response + async def delete_encryption_key( + self, key: str, relay_factor: Optional[int] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}", relay_factor=relay_factor) + async with self.client.delete(url, json=dict(Key=key), **kwargs): + pass diff --git a/discovery/api/kind.py b/discovery/api/kind.py new file mode 100644 index 0000000..40ed778 --- /dev/null +++ b/discovery/api/kind.py @@ -0,0 +1,16 @@ +from enum import Enum, unique + + +@unique +class Kind(str, Enum): + INGRESS_GATEWAY: str = "ingress-gateway" + PROXY_DEFAULTS: str = "proxy-defaults" + SERVICE_DEFAULTS: str = "service-defaults" + SERVICE_INTENTIONS: str = "service-intentions" + SERVICE_RESOLVER: str = "service-resolver" + SERVICE_ROUTER: str = "service-router" + SERVICE_SPLITTER: str = "service-splitter" + TERMINATING_GATEWAY: str = "terminating-gateway" + + def __str__(self) -> str: + return str.__str__(self) diff --git a/discovery/api/kv.py b/discovery/api/kv.py index 16c01fc..e8a0816 100644 --- a/discovery/api/kv.py +++ b/discovery/api/kv.py @@ -1,22 +1,106 @@ +import base64 +from typing import Any, List, Optional + +from aiohttp import ContentTypeError + from discovery.api.abc import Api class Kv(Api): - def __init__(self, endpoint: str = "/kv", **kwargs): + def __init__(self, endpoint: str = "/kv", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, key, data, **kwargs): - response = await self.update(key, data, **kwargs) - return response + async def create( + self, + key: str, + data: Any, + dc: Optional[str] = None, + flags: Optional[int] = None, + cas: Optional[int] = None, + acquire: Optional[str] = None, + release: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> bool: + url = self._prepare_request_url( + f"{self.url}/{key}", + dc=dc, + flags=flags, + cas=cas, + acquire=acquire, + release=release, + ns=ns, + ) + async with self.client.put(url, data=data, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + key: str, + data: Any, + dc: Optional[str] = None, + flags: Optional[int] = None, + cas: Optional[int] = None, + acquire: Optional[str] = None, + release: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> bool: + return await self.create( + key, data, dc, flags, cas, acquire, release, ns, **kwargs + ) - async def read(self, key, **kwargs): - response = await self.client.get(f"{self.url}/{key}", **kwargs) - return response + async def read( + self, + key: str, + dc: Optional[str] = None, + recurse: Optional[bool] = None, + raw: Optional[bool] = None, + keys: Optional[bool] = None, + separator: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[dict]: + url = self._prepare_request_url( + f"{self.url}/{key}", + dc=dc, + recurse=recurse, + raw=raw, + keys=keys, + separator=separator, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + try: + return await resp.json() # type: ignore + except ContentTypeError: + return [] - async def update(self, key, data, **kwargs): - response = await self.client.put(f"{self.url}/{key}", data=data, **kwargs) - return response + async def read_value( + self, + key: str, + dc: Optional[str] = None, + recurse: Optional[bool] = None, + separator: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> List[bytes]: + resp = await self.read( + key, dc=dc, recurse=recurse, separator=separator, ns=ns, **kwargs + ) + return [base64.b64decode(data["Value"]) for data in resp] - async def delete(self, key, **kwargs): - response = await self.client.delete(f"{self.url}/{key}", **kwargs) - return response + async def delete( + self, + key: str, + dc: Optional[bool] = None, + recurse: Optional[str] = None, + cas: Optional[int] = None, + ns: Optional[str] = None, + **kwargs, + ) -> bool: + url = self._prepare_request_url( + f"{self.url}/{key}", dc=dc, recurse=recurse, cas=cas, ns=ns + ) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/license.py b/discovery/api/license.py index 620b48f..c21f73c 100644 --- a/discovery/api/license.py +++ b/discovery/api/license.py @@ -1,18 +1,23 @@ +from typing import Any, Optional + from discovery.api.abc import Api class License(Api): - def __init__(self, endpoint: str = "/operator/license", **kwargs): + def __init__(self, endpoint: str = "/operator/license", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def current(self, **kwargs): - resp = await self.client.get(f"{self.url}", **kwargs) - return resp + async def current(self, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def update(self, data, **kwargs): - resp = await self.client.put(f"{self.url}", data=data, **kwargs) - return resp + async def update(self, data: Any, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.put(url, data=data, **kwargs) as resp: + return await resp.json() # type: ignore - async def reset(self, **kwargs): - response = await self.client.delete(f"{self.url}", **kwargs) - return response + async def reset(self, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/loglevel.py b/discovery/api/loglevel.py new file mode 100644 index 0000000..0fcf01c --- /dev/null +++ b/discovery/api/loglevel.py @@ -0,0 +1,13 @@ +from enum import Enum, unique + + +@unique +class LogLevel(str, Enum): + INFO: str = "info" + WARNING: str = "warning" + ERROR: str = "error" + DEBUG: str = "debug" + CRITICAL: str = "critical" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/namespace.py b/discovery/api/namespace.py index 0b8e8e6..b211ab0 100644 --- a/discovery/api/namespace.py +++ b/discovery/api/namespace.py @@ -1,26 +1,59 @@ +from typing import List, Optional + from discovery.api.abc import Api +from discovery.api.acl_link import ACLLink class Namespace(Api): - def __init__(self, endpoint: str = "/namespace", **kwargs): + def __init__(self, endpoint: str = "/namespace", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + name: str, + description: str = "", + acls: Optional[List[ACLLink]] = None, + meta: Optional[dict] = None, + **kwargs, + ) -> dict: + payload = dict(Name=name, Description=description) + if acls: + payload.update({"ACLs": acls}) # type: ignore + + if meta: + payload.update({"Meta": meta}) # type: ignore + + async with self.client.put(f"{self.url}", json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read(self, name: str, **kwargs) -> dict: + async with self.client.get(f"{self.url}/{name}", **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + name: str, + description: str = "", + acls: Optional[List[ACLLink]] = None, + meta: Optional[dict] = None, + **kwargs, + ) -> dict: + payload = dict(Description=description) + if acls: + payload.update({"ACLs": acls}) # type: ignore - async def read(self, name, **kwargs): - response = await self.client.get(f"{self.url}/{name}", **kwargs) - return response + if meta: + payload.update({"Meta": meta}) # type: ignore - async def update(self, name, data, **kwargs): - response = await self.client.put(f"{self.url}/{name}", data=data, **kwargs) - return response + async with self.client.put( + f"{self.url}/{name}", json=payload, **kwargs + ) as resp: + return await resp.json() # type: ignore - async def delete(self, name, **kwargs): - response = await self.client.delete(f"{self.url}/{name}", **kwargs) - return response + async def delete(self, name: str, **kwargs) -> dict: + async with self.client.delete(f"{self.url}/{name}", **kwargs) as resp: + return await resp.json() # type: ignore - async def list_all(self, **kwargs): - response = await self.client.get(f"{self.url}s", **kwargs) - return response + async def list_all(self, **kwargs) -> dict: + async with self.client.get(f"{self.url}s", **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/operator.py b/discovery/api/operator.py index 6825b9b..89ac830 100644 --- a/discovery/api/operator.py +++ b/discovery/api/operator.py @@ -13,7 +13,7 @@ def __init__( segment=None, endpoint: str = "/operator", **kwargs - ): + ) -> None: super().__init__(endpoint=endpoint, **kwargs) self.area = area or api.Area(client=self.client) self.autopilot = autopilot or api.AutoPilot(client=self.client) diff --git a/discovery/api/policy.py b/discovery/api/policy.py index 583a253..de97d51 100644 --- a/discovery/api/policy.py +++ b/discovery/api/policy.py @@ -1,27 +1,73 @@ +from typing import List, Optional + from discovery.api.abc import Api class Policy(Api): - def __init__(self, endpoint: str = "/acl/policy", **kwargs): + def __init__(self, endpoint: str = "/acl/policy", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + name: str, + description: str, + rules: str, + datacenters: List[str], + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + payload = dict( + Name=name, Description=description, Rules=rules, Datacenters=datacenters + ) + + if namespace: + payload.update({"Namespace": namespace}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read(self, policy_id: str, ns: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/{policy_id}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read_by_name(self, name: str, ns: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/name/{name}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + policy_id: str, + name: str, + description: str, + rules: str, + datacenters: List[str], + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + payload = dict( + Name=name, Description=description, Rules=rules, Datacenters=datacenters + ) - async def read(self, policy_id, **kwargs): - response = await self.client.get(f"{self.url}/{policy_id}", **kwargs) - return response + if namespace: + payload.update({"Namespace": namespace}) - async def update(self, policy_id, data, **kwargs): - response = await self.client.put(f"{self.url}/{policy_id}", data=data, **kwargs) - return response + url = self._prepare_request_url(f"{self.url}/{policy_id}", ns=ns) + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, policy_id, **kwargs): - response = await self.client.delete(f"{self.url}/{policy_id}", **kwargs) - return response + async def delete(self, policy_id: str, ns: Optional[str] = None, **kwargs) -> bool: + url = self._prepare_request_url(f"{self.url}/{policy_id}", ns=ns) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - url = self.url.replace("policy", "policies") - response = await self.client.get(f"{url}", **kwargs) - return response + async def list(self, ns: Optional[str] = None, **kwargs) -> List[dict]: + url = self._prepare_request_url( + f"{self.url.replace('policy', 'policies')}", ns=ns + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/query.py b/discovery/api/query.py index a9e7a37..312ebf0 100644 --- a/discovery/api/query.py +++ b/discovery/api/query.py @@ -1,34 +1,120 @@ +from typing import Dict, List, Optional + from discovery.api.abc import Api class Query(Api): - def __init__(self, endpoint: str = "/query", **kwargs): + def __init__(self, endpoint: str = "/query", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.post(f"{self.url}", data=data, **kwargs) - return response - - async def read(self, uuid=None, **kwargs): - if uuid: - uri = f"{self.url}/{uuid}" - else: - uri = f"{self.url}" - response = await self.client.get(uri, **kwargs) - return response - - async def delete(self, uuid, **kwargs): - response = await self.client.delete(f"{self.url}/{uuid}", **kwargs) - return response - - async def update(self, uuid, data, **kwargs): - response = await self.client.put(f"{self.url}/{uuid}", **kwargs) - return response - - async def execute(self, uuid, **kwargs): - response = await self.client.get(f"{self.url}/{uuid}/execute", **kwargs) - return response - - async def explain(self, uuid, **kwargs): - response = await self.client.get(f"{self.url}/{uuid}/explain", **kwargs) - return response + async def create( + self, + name: str, + service: dict, + session: Optional[str] = None, + token: Optional[str] = None, + tags: Optional[List[str]] = None, + node_meta: Optional[Dict[str, str]] = None, + service_meta: Optional[Dict[str, str]] = None, + connect: bool = False, + dns: Optional[dict] = None, + dc: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + payload = dict(Name=name, Service=service, Connect=connect) + + if session: + payload.update({"Session": session}) + + if token: + payload.update({"Token": token}) + + if tags: + payload.update({"Tags": tags}) + + if node_meta: + payload.update({"NodeMeta": node_meta}) + + if service_meta: + payload.update({"ServiceMeta": service_meta}) + + if dns: + payload.update({"DNS": dns}) + + async with self.client.post(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read(self, uuid: str, dc: Optional[str] = None, **kwargs) -> List[dict]: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def delete(self, uuid: str, dc: Optional[str] = None, **kwargs) -> None: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + async with self.client.delete(url, **kwargs): + pass + + async def update( + self, + uuid: str, + name: str, + service: dict, + session: Optional[str] = None, + token: Optional[str] = None, + tags: Optional[List[str]] = None, + node_meta: Optional[Dict[str, str]] = None, + service_meta: Optional[Dict[str, str]] = None, + connect: bool = False, + dns: Optional[dict] = None, + dc: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url(f"{self.url}/{uuid}", dc=dc) + payload = dict(Name=name, Service=service, Connect=connect) + + if session: + payload.update({"Session": session}) + + if token: + payload.update({"Token": token}) + + if tags: + payload.update({"Tags": tags}) + + if node_meta: + payload.update({"NodeMeta": node_meta}) + + if service_meta: + payload.update({"ServiceMeta": service_meta}) + + if dns: + payload.update({"DNS": dns}) + + async with self.client.put(url, json=payload, **kwargs): + pass + + async def execute( + self, + uuid: str, + dc: Optional[str] = None, + near: Optional[str] = None, + limit: Optional[int] = None, + connect: Optional[bool] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/{uuid}/execute", dc=dc, near=near, limit=limit, connect=connect + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def list(self, dc: Optional[str] = None, **kwargs) -> List[dict]: + url = self._prepare_request_url(self.url, dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def explain(self, uuid: str, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/{uuid}/explain", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/raft.py b/discovery/api/raft.py index 490af3a..8186174 100644 --- a/discovery/api/raft.py +++ b/discovery/api/raft.py @@ -1,14 +1,35 @@ +from typing import Optional + from discovery.api.abc import Api class Raft(Api): - def __init__(self, endpoint: str = "/operator/raft", **kwargs): + def __init__(self, endpoint: str = "/operator/raft", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def read_configuration(self, **kwargs): - response = await self.client.get(f"{self.url}/configuration", **kwargs) - return response + async def read_configuration( + self, dc: Optional[str] = None, stale: Optional[bool] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/configuration", dc=dc, stale=stale) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def delete_peer( + self, + peer_id: Optional[str] = None, + address: Optional[str] = None, + dc: Optional[str] = None, + **kwargs, + ) -> None: + if peer_id and address: + raise ValueError("specify only peer_id or address field") - async def delete_peer(self, **kwargs): - response = await self.client.delete(f"{self.url}/peer", **kwargs) - return response + if peer_id: + query_param = {"id": peer_id} + elif address: + query_param = {"address": address} + else: + raise ValueError("peer_id or address are required field") + url = self._prepare_request_url(f"{self.url}/peer", **query_param, dc=dc) + async with self.client.delete(url, **kwargs): + pass diff --git a/discovery/api/role.py b/discovery/api/role.py index fe66913..f7dd377 100644 --- a/discovery/api/role.py +++ b/discovery/api/role.py @@ -1,30 +1,84 @@ +from typing import List, Optional + from discovery.api.abc import Api class Role(Api): - def __init__(self, endpoint: str = "/acl/role", **kwargs): + def __init__(self, endpoint: str = "/acl/role", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + name: str, + description: str, + policies: List[dict], + service_identities: Optional[List[dict]] = None, + node_identities: Optional[List[dict]] = None, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + payload = dict(Name=name, Description=description, Policies=policies) + + if service_identities: + payload.update({"ServiceIdentities": service_identities}) + + if node_identities: + payload.update({"NodeIdentities": node_identities}) + + if namespace: + payload.update({"Namespace": namespace}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read_by_id( + self, role_id: str, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read_by_name(self, name: str, ns: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}/name/{name}", ns=ns) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def update( + self, + role_id: str, + name: str, + description: str, + policies: List[dict], + service_identities: Optional[List[dict]] = None, + node_identities: Optional[List[dict]] = None, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + payload = dict(Name=name, Description=description, Policies=policies) - async def read_by_id(self, role_id, **kwargs): - response = await self.client.put(f"{self.url}/{role_id}", **kwargs) - return response + if service_identities: + payload.update({"ServiceIdentities": service_identities}) - async def read_by_name(self, name, **kwargs): - response = await self.client.put(f"{self.url}/name/{name}", **kwargs) - return response + if node_identities: + payload.update({"NodeIdentities": node_identities}) - async def update(self, role_id, data, **kwargs): - response = await self.client.put(f"{self.url}/{role_id}", data=data, **kwargs) - return response + if namespace: + payload.update({"Namespace": namespace}) + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, role_id, **kwargs): - response = await self.client.delete(f"{self.url}/{role_id}", **kwargs) - return response + async def delete(self, role_id: str, ns: Optional[str] = None, **kwargs) -> bool: + url = self._prepare_request_url(f"{self.url}/{role_id}", ns=ns) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}s", **kwargs) - return response + async def list( + self, policy: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> List[dict]: + url = self._prepare_request_url(f"{self.url}s", policy=policy, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/segment.py b/discovery/api/segment.py index 8f7ae4e..92c8e31 100644 --- a/discovery/api/segment.py +++ b/discovery/api/segment.py @@ -1,10 +1,13 @@ +from typing import List, Optional + from discovery.api.abc import Api class Segment(Api): - def __init__(self, endpoint: str = "/operator/segment", **kwargs): + def __init__(self, endpoint: str = "/operator/segment", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}", **kwargs) - return response + async def list(self, dc: Optional[str] = None, **kwargs) -> List[str]: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/service.py b/discovery/api/service.py index ab57fdf..9767359 100644 --- a/discovery/api/service.py +++ b/discovery/api/service.py @@ -1,51 +1,75 @@ +from typing import Optional from urllib.parse import quote_plus from discovery.api.abc import Api class Service(Api): - def __init__(self, endpoint: str = "/agent/service", **kwargs): + def __init__(self, endpoint: str = "/agent/service", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def services(self, **kwargs): - response = await self.client.get(f"{self.url}s", **kwargs) - return response + async def list( + self, filter: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}s", filter=filter, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def service(self, service_id, **kwargs): - response = await self.client.get(f"{self.url}/{service_id}", **kwargs) - return response + async def configuration( + self, service_id: str, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{service_id}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def configuration(self, service_id, **kwargs): - response = await self.client.get(f"{self.url}/{service_id}", **kwargs) - return response + async def health_by_name( + self, service_name: str, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/name/{service_name}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def register(self, data, **kwargs): - response = await self.client.put(f"{self.url}/register", data=data, **kwargs) - return response + async def health_by_id( + self, service_id: str, ns: Optional[str] = None, **kwargs + ) -> dict: + url = self._prepare_request_url(f"{self.url}/id/{service_id}", ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def deregister(self, service_id, **kwargs): - response = await self.client.put( - f"{self.url}/deregister/{service_id}", **kwargs + async def register( + self, + data, + replace_existing_checks: Optional[bool] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + url = self._prepare_request_url( + f"{self.url}/register", + replace_existing_checks=replace_existing_checks, + ns=ns, ) - return response - - async def maintenance(self, service_id, enable, reason="", **kwargs): - reason = quote_plus(reason) - enable = str(enable).lower() - response = await self.client.put( - f"{self.url}/maintenance/{service_id}?enable={enable}&reason={reason}", - **kwargs, - ) - return response + async with self.client.put(url, json=data, **kwargs): + pass - async def service_health_by_name(self, name, **kwargs): - response = await self.client.get( - f"{self.url}/health/service/name/{name}", **kwargs - ) - return response + async def deregister( + self, service_id: str, ns: Optional[str] = None, **kwargs + ) -> None: + url = self._prepare_request_url(f"{self.url}/deregister/{service_id}", ns=ns) + async with self.client.put(url, **kwargs): + pass - async def service_health_by_id(self, name, **kwargs): - response = await self.client.get( - f"{self.url}/health/service/id/{name}", **kwargs + async def enable_maintenance( + self, + service_id: str, + enable: Optional[bool] = None, + reason: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> None: + if reason: + reason = quote_plus(reason) + url = self._prepare_request_url( + f"{self.url}/maintenance/{service_id}", enable=enable, reason=reason, ns=ns ) - return response + async with self.client.put(url, **kwargs): + pass diff --git a/discovery/api/session.py b/discovery/api/session.py index 3aa8e4a..678d90b 100644 --- a/discovery/api/session.py +++ b/discovery/api/session.py @@ -1,30 +1,82 @@ +from typing import Dict, List, Optional + from discovery.api.abc import Api +from discovery.api.behavior import Behavior class Session(Api): - def __init__(self, endpoint: str = "/session", **kwargs): + def __init__(self, endpoint: str = "/session", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}/create", data=data, **kwargs) - return response + async def create( + self, + name: str, + node: Optional[str] = None, + lock_delay: str = "15s", + checks: Optional[List[str]] = None, + node_checks: Optional[List[str]] = None, + service_checks: Optional[Dict[str, str]] = None, + behavior: Behavior = Behavior.RELEASE, + ttl: Optional[str] = None, + ns: Optional[str] = None, + dc: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/create", ns=ns, dc=dc) + payload = dict(Name=name, LockDelay=lock_delay) + + if node: + payload.update({"Node": node}) + + if checks: + payload.update({"Checks": checks}) # type: ignore + + if node_checks: + payload.update({"NodeChecks": node_checks}) # type: ignore + + if service_checks: + payload.update({"ServiceChecks": service_checks}) # type: ignore + + if behavior: + payload.update({"Behavior": behavior}) + + if ttl: + payload.update({"TTL": ttl}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, uuid, **kwargs): - response = await self.client.put(f"{self.url}/destroy/{uuid}", **kwargs) - return response + async def delete( + self, uuid: str, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> bool: + url = self._prepare_request_url(f"{self.url}/destroy/{uuid}", dc=dc, ns=ns) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def read(self, uuid, **kwargs): - response = await self.client.get(f"{self.url}/info/{uuid}", **kwargs) - return response + async def read( + self, uuid: str, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> List[dict]: + url = self._prepare_request_url(f"{self.url}/info/{uuid}", dc=dc, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list_node_session(self, node, **kwargs): - response = await self.client.get(f"{self.url}/node/{node}", **kwargs) - return response + async def list_sessions_for_node( + self, node: str, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> List[dict]: + url = self._prepare_request_url(f"{self.url}/node/{node}", dc=dc, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}/list", **kwargs) - return response + async def list( + self, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> List[dict]: + url = self._prepare_request_url(f"{self.url}/list", dc=dc, ns=ns) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def renew(self, uuid, **kwargs): - response = await self.client.put(f"{self.url}/renew/{uuid}", **kwargs) - return response + async def renew( + self, uuid: str, dc: Optional[str] = None, ns: Optional[str] = None, **kwargs + ) -> List[dict]: + url = self._prepare_request_url(f"{self.url}/renew/{uuid}", ns=ns, dc=dc) + async with self.client.put(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/snapshot.py b/discovery/api/snapshot.py index 01d1f00..1dca9d1 100644 --- a/discovery/api/snapshot.py +++ b/discovery/api/snapshot.py @@ -1,3 +1,5 @@ +from typing import Optional + from discovery.api.abc import Api @@ -5,10 +7,14 @@ class Snapshot(Api): def __init__(self, endpoint: str = "/snapshot", **kwargs): super().__init__(endpoint=endpoint, **kwargs) - async def generate(self, **kwargs): - response = await self.client.get(f"{self.url}", **kwargs) - return response + async def generate( + self, dc: Optional[str] = None, stale: Optional[bool] = None, **kwargs + ) -> bytes: + url = self._prepare_request_url(f"{self.url}", dc=dc, stale=stale) + async with self.client.get(url, **kwargs) as resp: + return await resp.content() # type: ignore - async def restore(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def restore(self, data: bytes, dc: Optional[str] = None, **kwargs) -> None: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.put(url, data=data, **kwargs): + pass diff --git a/discovery/api/status.py b/discovery/api/status.py index 3a0d7e3..e1c0911 100644 --- a/discovery/api/status.py +++ b/discovery/api/status.py @@ -1,14 +1,18 @@ +from typing import List, Optional + from discovery.api.abc import Api class Status(Api): - def __init__(self, endpoint: str = "/status", **kwargs): + def __init__(self, endpoint: str = "/status", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def leader(self, **kwargs): - response = await self.client.get(f"{self.url}/leader", **kwargs) - return response + async def leader(self, dc: Optional[str] = None, **kwargs) -> str: + url = self._prepare_request_url(f"{self.url}/leader", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def peers(self, **kwargs): - response = await self.client.get(f"{self.url}/peers", **kwargs) - return response + async def peers(self, dc: Optional[str] = None, **kwargs) -> List[str]: + url = self._prepare_request_url(f"{self.url}/peers", dc=dc) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/token.py b/discovery/api/token.py index 1de053a..6a3d0a8 100644 --- a/discovery/api/token.py +++ b/discovery/api/token.py @@ -1,38 +1,156 @@ +from typing import Any, List, Optional + from discovery.api.abc import Api class Token(Api): - def __init__(self, endpoint: str = "/acl/token", **kwargs): + def __init__(self, endpoint: str = "/acl/token", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return response + async def create( + self, + description: str, + policies: Optional[List[dict]] = None, + roles: Optional[List[dict]] = None, + service_identities: Optional[List[dict]] = None, + node_identities: Optional[List[dict]] = None, + local: bool = False, + expiration_time: Optional[Any] = None, + expiration_ttl: Optional[Any] = None, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}", ns=ns) + payload = dict(Description=description, local=local) + + if policies: + payload.update({"Policies": policies}) + + if roles: + payload.update({"Roles": roles}) + + if service_identities: + payload.update({"ServiceIdentities": service_identities}) + + if node_identities: + payload.update({"NodeIdentities": node_identities}) + + if expiration_time: + payload.update({"ExpirationTime": expiration_time}) + + if expiration_ttl: + payload.update({"ExpirationTTL": expiration_ttl}) + + if namespace: + payload.update({"Namespace": namespace}) + + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore + + async def read( + self, + accessor_id: str, + ns: Optional[str] = None, + expanded: Optional[bool] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}/{accessor_id}", ns=ns, expanded=expanded + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore + + async def details(self, headers: dict = {}) -> dict: + async with self.client.get(f"{self.url}/self", headers=headers) as resp: + return await resp.json() # type: ignore + + async def update( + self, + accessor_id: str, + description: str, + secret_id: Optional[str] = None, + policies: Optional[List[dict]] = None, + roles: Optional[List[dict]] = None, + service_identities: Optional[List[dict]] = None, + node_identities: Optional[List[dict]] = None, + local: bool = False, + auth_method: Optional[str] = None, + expiration_time: Optional = None, # type: ignore + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{accessor_id}", ns=ns) + payload = dict(SecretID=secret_id, Description=description, Local=local) + + if secret_id: + payload.update({"SecretID": secret_id}) + + if policies: + payload.update({"Policies": policies}) # type: ignore + + if roles: + payload.update({"Roles": roles}) # type: ignore + + if service_identities: + payload.update({"ServiceIdentities": service_identities}) # type: ignore + + if node_identities: + payload.update({"NodeIdentities": node_identities}) # type: ignore + + if auth_method: + payload.update({"AuthMethod": auth_method}) + + if expiration_time: + payload.update({"ExpirationTime": expiration_time}) - async def read_by_id(self, role_id, **kwargs): - response = await self.client.get(f"{self.url}/{role_id}", **kwargs) - return response + if namespace: + payload.update({"Namespace": namespace}) - async def read_by_name(self, name, **kwargs): - response = await self.client.get(f"{self.url}/name/{name}", **kwargs) - return response + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def details(self, headers={}): - response = await self.client.get(f"{self.url}/self", headers=headers) - return response + async def clone( + self, + accessor_id: str, + description: str, + namespace: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url(f"{self.url}/{accessor_id}/clone", ns=ns) + payload = dict(Description=description) - async def clone(self, accessor_id: str, **kwargs): - response = await self.client.put(f"{self.url}/{accessor_id}/clone", **kwargs) - return response + if namespace: + payload.update({"Namespace": namespace}) - async def update(self, role_id, data, **kwargs): - response = await self.client.put(f"{self.url}/{role_id}", data=data, **kwargs) - return response + async with self.client.put(url, json=payload, **kwargs) as resp: + return await resp.json() # type: ignore - async def delete(self, role_id, **kwargs): - response = await self.client.delete(f"{self.url}/{role_id}", **kwargs) - return response + async def delete( + self, accessor_id: str, ns: Optional[str] = None, **kwargs + ) -> bool: + url = self._prepare_request_url(f"{self.url}/{accessor_id}", ns=ns) + async with self.client.delete(url, **kwargs) as resp: + return await resp.json() # type: ignore - async def list(self, **kwargs): - response = await self.client.get(f"{self.url}s", **kwargs) - return response + async def list( + self, + policy: Optional[str] = None, + role: Optional[str] = None, + auth_method: Optional[str] = None, + auth_method_ns: Optional[str] = None, + ns: Optional[str] = None, + **kwargs, + ) -> dict: + url = self._prepare_request_url( + f"{self.url}s", + policy=policy, + role=role, + authmethod=auth_method, + authmethod_ns=auth_method_ns, + ns=ns, + ) + async with self.client.get(url, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/api/token_locality.py b/discovery/api/token_locality.py new file mode 100644 index 0000000..47d4771 --- /dev/null +++ b/discovery/api/token_locality.py @@ -0,0 +1,10 @@ +from enum import Enum, unique + + +@unique +class TokenLocality(str, Enum): + LOCAL: str = "local" + GLOBAL: str = "global" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/token_type.py b/discovery/api/token_type.py new file mode 100644 index 0000000..96d074a --- /dev/null +++ b/discovery/api/token_type.py @@ -0,0 +1,17 @@ +from enum import Enum, unique + + +@unique +class TokenType(str, Enum): + DEFAULT: str = "default" + AGENT: str = "agent" + AGENT_RECOVERY: str = "agent_recovery" + REPLICATION: str = "replication" + AGENT_MASTER: str = "agent_master" + ACL_TOKEN: str = "acl_token" + ACL_AGENT_TOKEN: str = "acl_agent_token" + ACL_AGENT_MASTER_TOKEN: str = "acl_agent_master_token" + ACL_REPLICATION_TOKEN: str = "acl_replication_token" + + def __str__(self): + return str.__str__(self) diff --git a/discovery/api/txn.py b/discovery/api/txn.py index 71afda2..2771922 100644 --- a/discovery/api/txn.py +++ b/discovery/api/txn.py @@ -1,11 +1,13 @@ +from typing import Optional + from discovery.api.abc import Api -from discovery.engine.response import HttpResponse class Txn(Api): def __init__(self, endpoint: str = "/txn", **kwargs) -> None: super().__init__(endpoint=endpoint, **kwargs) - async def create(self, data, **kwargs): - response = await self.client.put(f"{self.url}", data=data, **kwargs) - return HttpResponse(response) + async def create(self, data: dict, dc: Optional[str] = None, **kwargs) -> dict: + url = self._prepare_request_url(f"{self.url}", dc=dc) + async with self.client.put(url, json=data, **kwargs) as resp: + return await resp.json() # type: ignore diff --git a/discovery/checks.py b/discovery/checks.py new file mode 100644 index 0000000..8eb1994 --- /dev/null +++ b/discovery/checks.py @@ -0,0 +1,121 @@ +from typing import List, Optional +from uuid import uuid4 + + +def script( + args: List[str], + name: Optional[str] = None, + interval: str = "10s", + timeout: str = "5s", +) -> dict: + name = name or f"script-{uuid4().hex}" + return dict(name=name, args=args, interval=interval, timeout=timeout) + + +def http( + url: str, + name: Optional[str] = None, + tls_skip_verify: bool = True, + tls_server_name: str = "", + method: str = "GET", + header: Optional[dict] = None, + body: str = "", + interval: str = "10s", + timeout: str = "5s", + deregister_after: str = "1m", + disable_redirects: bool = True, +) -> dict: + name = name or f"http-{uuid4().hex}" + header = header or {} + return dict( + name=name, + http=url, + tls_server_name=tls_server_name, + tls_skip_verify=tls_skip_verify, + method=method, + header=header, + body=body, + disable_redirects=disable_redirects, + interval=interval, + timeout=timeout, + deregister_critical_service_after=deregister_after, + ) + + +def tcp( + tcp_check: str, + name: Optional[str] = None, + interval: str = "10s", + timeout: str = "5s", +) -> dict: + name = name or f"tcp-{uuid4().hex}" + return dict(name=name, tcp=tcp_check, interval=interval, timeout=timeout) + + +def ttl(notes: str, name: Optional[str] = None, ttl_check: str = "30s") -> dict: + name = name or f"ttl-{uuid4().hex}" + return dict(name=name, notes=notes, ttl=ttl_check) + + +def docker( + container_id: str, + args: str, + shell: Optional[str] = None, + name: Optional[str] = None, + interval: str = "10s", +) -> dict: + name = name or f"docker-{uuid4().hex}" + return dict( + name=name, + docker_container_id=container_id, + shell=shell, + args=args, + interval=interval, + ) + + +def grpc( + grpc_check: str, name: Optional[str] = None, tls: bool = True, interval: str = "10s" +) -> dict: + name = name or f"grpc-{uuid4().hex}" + return dict(name=name, grpc=grpc_check, grpc_use_tls=tls, interval=interval) + + +def h2ping( + h2ping_check: str, + name: Optional[str] = None, + interval: str = "10s", + tls: bool = False, +) -> dict: + check_id = f"h2ping-{uuid4().hex}" + name = name or check_id + return dict( + id=check_id, + name=name, + h2ping=h2ping_check, + interval=interval, + h2ping_use_tls=tls, + ) + + +def alias( + service_id: str, + alias_service: str, + alias_node: Optional[str] = None, + name: Optional[str] = None, +) -> dict: + """Consul's alias check. + + Usage: + checks.alias('webapp1', 'webapp1-c5a5a6f40d7243ef84c8439b093de962') + + :param service_id: service name as registered in the Consul's catalog. + :param alias_service: ServiceID of service registered in the Consul's catalog. + :param alias_node: if the service is not registered with the same agent, "alias_node": "" must also be specified. + :param name: health check name. + """ + name = name or f"alias-{uuid4().hex}" + resp = dict(name=name, service_id=service_id, alias_service=alias_service) + if alias_node: + resp.update({"alias_node": alias_node}) + return resp diff --git a/discovery/cli/__init__.py b/discovery/cli/__init__.py index e69de29..4a44bab 100644 --- a/discovery/cli/__init__.py +++ b/discovery/cli/__init__.py @@ -0,0 +1,119 @@ +import asyncio + +import click +from rich.console import Console +from rich.panel import Panel +from rich.table import Table + +from discovery import Consul, __version__ + +CONTEXT_SETTINGS = dict( + help_option_names=["-h", "--help"], +) +console = Console() +loop = asyncio.get_event_loop() +consul = Consul() + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.version_option(version=__version__) +def cli(): + pass + + +@cli.command() +@click.option("-l", "--leader", is_flag=True, help="Get Raft Leader.") +@click.option("-p", "--peers", is_flag=True, help="List Raft Peers.") +def status(leader, peers): + """Status API.""" + try: + if leader: + resp = loop.run_until_complete(consul.status.leader()) + elif peers: + resp = loop.run_until_complete(consul.status.leader()) + except Exception: + click.echo("[!] Falha ao realizar a operação.") + click.echo(resp) + + +@cli.command() +@click.option("-s", "--services", is_flag=True, help="List services catalog.") +@click.option("-d", "--datacenters", is_flag=True, help="List datacenters.") +@click.option("-n", "--nodes", is_flag=True, help="List nodes.") +def catalog(services, datacenters, nodes): + """Catalog API.""" + table = Table.grid(padding=(0, 1)) + table.add_column(style="cyan", justify="right") + table.add_column(style="magenta") + + try: + if services: + resp = loop.run_until_complete(consul.catalog.list_services()) + for i, svc in enumerate(resp, start=1): + table.add_row(f"{i}[yellow]:[/yellow]", svc) + elif datacenters: + resp = loop.run_until_complete(consul.catalog.list_datacenters()) + for i, dc in enumerate(resp, start=1): + table.add_row(f"{i}[yellow]:[/yellow]", dc) + elif nodes: + resp = loop.run_until_complete(consul.catalog.list_nodes()) + leader_id = loop.run_until_complete(consul.leader_id()) + for i, node in enumerate(resp, start=1): + if node["ID"] == leader_id: + table.add_row( + f"{i}[yellow]:[/yellow]", + node["Node"], + node["Address"], + "[yellow][bold]leader[/bold][/yellow]", + ) + else: + table.add_row( + f"{i}[yellow]:[/yellow]", node["Node"], node["Address"] + ) + except Exception: + click.echo("[!] Falha ao realizar a operação.") + console.print( + Panel( + table, + border_style="yellow", + expand=True, + ) + ) + + +@cli.command() +@click.option("-n", "--node", help="Node name.") +@click.option("-s", "--service", help="Service name.") +@click.option("--state", help="State name.") +def health(node, service, state): + """Health API.""" + try: + if node: + resp = loop.run_until_complete(consul.health.checks_for_node(node)) + elif service: + resp = loop.run_until_complete(consul.health.checks_for_service(service)) + elif state: + resp = loop.run_until_complete(consul.health.checks_in_state(state)) + except Exception: + click.echo("[!] Falha ao realizar a operação.") + raise SystemExit(1) + click.echo( + resp + # f"{highlight(json.dumps(resp, indent=4, sort_keys=True), JsonLexer(), TerminalFormatter())}" + ) + + +@cli.command() +@click.option("-r", "--read", is_flag=True, help="Read configuration.") +@click.option("-d", "--delete", help="Delete raft peer.") +def raft(read, delete): + """Raft API.""" + try: + if read: + resp = loop.run_until_complete(consul.operator.raft.read_configuration()) + click.echo(resp) + elif delete: + resp = loop.run_until_complete(consul.operator.raft.delete_peer()) + click.echo(resp) + except Exception: + click.echo("[!] Falha ao realizar a operação.") diff --git a/discovery/cli/commands/__init__.py b/discovery/cli/commands/__init__.py deleted file mode 100644 index 205b9d5..0000000 --- a/discovery/cli/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from discovery.cli.commands.catalog import CatalogCommand diff --git a/discovery/cli/commands/catalog.py b/discovery/cli/commands/catalog.py deleted file mode 100644 index fdb603b..0000000 --- a/discovery/cli/commands/catalog.py +++ /dev/null @@ -1,61 +0,0 @@ -import asyncio -import logging -import os -import pickle -import sys -from pathlib import Path - -from cleo import Command -from dotenv import load_dotenv - -from discovery.client import Consul -from discovery.engine import AioEngine, aiohttp_session - -logging.getLogger().addHandler(logging.NullHandler()) - - -load_dotenv() -loop = asyncio.get_event_loop() -session = loop.run_until_complete(aiohttp_session()) -client = Consul(AioEngine(session=session)) - - -class CatalogCommand(Command): - """ - Interact with Consul's catalog. - - catalog - {--s|services : List services catalog.} - {--d|deregister= : Deregister services from .} - """ - - def handle(self): - if self.option("services"): - self.list_services() - elif self.option("deregister"): - self.deregister_service() - - def list_services(self): - try: - resp = loop.run_until_complete(client.catalog.services()) - except Exception: - self.line( - f"[!] falha ao conectar no Consul({os.getenv('CONSUL_HOST', 'localhost')}:{os.getenv('CONSUL_PORT', 8500)})" - ) - sys.exit(1) - resp = loop.run_until_complete(resp.json()) - for svc in resp.keys(): - self.line(f"{svc}") - sys.exit(0) - - def deregister_service(self): - try: - with open(self.option("deregister"), "rb") as f: - services = pickle.loads(f.read()) - except FileNotFoundError: - self.line("[!] arquivo não localizado") - sys.exit(1) - for service_id in services.keys(): - loop.run_until_complete(client.agent.service.deregister(service_id)) - Path(f"{self.option('deregister')}").unlink() - sys.exit(0) diff --git a/discovery/client.py b/discovery/client.py index 5e3074e..77dc19b 100644 --- a/discovery/client.py +++ b/discovery/client.py @@ -1,114 +1,124 @@ import asyncio -import json -import pickle +import os +from typing import Callable, List, Optional, Union -from discovery import log -from discovery.abc import BaseClient -from discovery.exceptions import NoConsulLeaderException, ServiceNotFoundException -from discovery.model.agent.service import service -from discovery.utils import select_one_rr +from discovery import api, log, utils +from .engine.aiohttp import AIOHTTPEngine +from .engine.httpx import HTTPXEngine +from .exceptions import NoConsulLeaderException +from .utils import Service -class Consul(BaseClient): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.managed_services = {} - self._leader_id = None - self.consul_current_leader_id = None - async def find_service(self, name, fn=select_one_rr): - response = await self.find_services(name) +class Consul: + def __init__( + self, + client: Optional[Union[AIOHTTPEngine, HTTPXEngine]] = None, + *args, + **kwargs, + ): + self.client = client or AIOHTTPEngine(*args, **kwargs) + + self.catalog = api.Catalog(client=self.client) + self.config = api.Config(client=self.client) + self.coordinate = api.Coordinate(client=self.client) + self.events = api.Events(client=self.client) + self.health = api.Health(client=self.client) + self.kv = api.Kv(client=self.client) + self.namespace = api.Namespace(client=self.client) + self.query = api.Query(client=self.client) + self.session = api.Session(client=self.client) + self.snapshot = api.Snapshot(client=self.client) + self.status = api.Status(client=self.client) + self.txn = api.Txn(client=self.client) + self.agent = api.Agent(client=self.client) + self.connect = api.Connect(client=self.client) + self.acl = api.Acl(client=self.client) + self.operator = api.Operator(client=self.client) + self.binding_rule = api.BindingRule(client=self.client) + self.policy = api.Policy(client=self.client) + self.role = api.Role(client=self.client) + self.token = api.Token(client=self.client) + self.check = api.Checks(client=self.client) + self.services = api.Service(client=self.client) + self.ca = api.CA(client=self.client) + self.intentions = api.Intentions(client=self.client) + self.event = api.Events(client=self.client) + + self.reconnect_timeout = float( + os.getenv("DISCOVERY_CLIENT_DEFAULT_TIMEOUT", 30) + ) + self._leader_id: Optional[str] = None + + async def leader_ip(self, *args, **kwargs) -> str: try: - return fn(response) + current_leader = await self.status.leader(*args, **kwargs) + leader_ip, _ = current_leader.split(":") except Exception: - raise ServiceNotFoundException( - f"service {name} not found in the Consul's catalog" - ) + raise NoConsulLeaderException + return leader_ip - async def find_services(self, name): - resp = await self.catalog.service(name) - response = await self._get_response(resp) - return response + async def leader_id(self, **kwargs) -> str: + leader_ip = await self.leader_ip(**kwargs.get("leader_options", {})) + instances = await self.health.service_instances( + "consul", **kwargs.get("instance_options", {}) + ) + current_id = [ + instance["Node"]["ID"] + for instance in instances + if instance["Node"]["Address"] == leader_ip + ][0] + try: + return str(current_id) + except Exception: + raise NoConsulLeaderException + + async def find_services(self, name: str) -> List[dict]: + return await self.catalog.list_nodes_for_service(name) # type: ignore + + async def find_service( + self, name: str, fn: Callable = utils.select_one_rr, *args, **kwargs + ) -> Optional[dict]: + response = await self.find_services(name, *args, **kwargs) + return fn(response) # type: ignore async def register( - self, service_name: str, service_port: int, check=None, dump_service=True + self, + service: Service, + enable_watch: bool = False, + **kwargs, ) -> None: - svc = service(service_name, service_port, check=check) + self._leader_id = await self.leader_id(**kwargs) try: - await self.agent.service.register(svc) - self.managed_services[json.loads(svc).get("id")] = { - "service_name": service_name, - "port": service_port, - "check": check, - } - self.consul_current_leader_id = await self.leader_current_id() - if dump_service: - self._dump_registered_service + await self.agent.service.register(service.dict(), **kwargs) except Exception as err: raise err - def _dump_registered_service(self): - with open(".service", "wb") as f: - f.write(pickle.dumps(self.managed_services)) - - async def check_consul_health(self): - while True: - try: - await asyncio.sleep(self.timeout) - current_id = await self.leader_current_id() - if current_id != self.consul_current_leader_id: - await self.reconnect() - except Exception: - await asyncio.sleep(self.timeout) - await self.check_consul_health() - - async def reconnect(self): - old_service = self.managed_services.copy() - await self.deregister() - for key, value in old_service.items(): - await self.register( - service_name=value.get("service_name"), - service_port=value.get("port"), - check=value.get("check"), + if enable_watch: + loop = asyncio.get_running_loop() + loop.create_task( + self._watch_connection(service, enable_watch, **kwargs), + name="discovery-client-watch-connection", ) - log.info("Service successfully re-registered") - - async def leader_current_id(self): - consul_leader = await self.leader_ip() - consul_instances = await self.consul_healthy_instances() - current_id = [ - instance.get("Node").get("ID") - for instance in consul_instances - if instance.get("Node").get("Address") == consul_leader - ] - if current_id is not None: - current_id = current_id[0] - return current_id - - async def leader_ip(self): - leader_response = await self.status.leader() - leader_response = await self._get_response(leader_response) - try: - consul_leader, _ = leader_response.split(":") - except ValueError: - raise NoConsulLeaderException("Error to identify Consul's leader.") - return consul_leader + async def deregister(self, service_id: str, ns: Optional[str] = None) -> None: + await self.agent.service.deregister(service_id, ns) - async def consul_healthy_instances(self): - health_response = await self.health.service("consul") - consul_instances = await self._get_response(health_response) - return consul_instances + async def reconnect(self, service: Service, *args, **kwargs) -> None: + await self.deregister(service.id) # type: ignore + await self.register(service, *args, **kwargs) - async def _get_response(self, resp): - try: - response = await resp.json() - return response - except Exception: - response = await resp.text() - return response + async def _watch_connection(self, service: Service, *args, **kwargs) -> None: + while True: + try: + await asyncio.sleep(self.reconnect_timeout) + current_id = await self.leader_id() + if current_id != self._leader_id: + await self.reconnect(service, *args, **kwargs) + except Exception: + log.error( + f"Failed to connect to Consul, trying again at {self.reconnect_timeout}/s" + ) - async def deregister(self) -> None: - for service_id in self.managed_services.keys(): - await self.agent.service.deregister(service_id) - self.managed_services.clear() + def __repr__(self) -> str: + return f"Consul(engine={self.client})" diff --git a/discovery/engine/__init__.py b/discovery/engine/__init__.py index 6e748ad..8c7ec7c 100644 --- a/discovery/engine/__init__.py +++ b/discovery/engine/__init__.py @@ -1,2 +1,11 @@ from discovery.engine.abc import Engine -from discovery.engine.aio import AioEngine, aiohttp_session, httpx_client + +try: + from discovery.engine.aiohttp import AIOHTTPEngine +except ImportError: + aiohttp = None + +try: + from discovery.engine.httpx import HTTPXEngine +except ImportError: + httpx = None diff --git a/discovery/engine/abc.py b/discovery/engine/abc.py index 79f223c..29c06c3 100644 --- a/discovery/engine/abc.py +++ b/discovery/engine/abc.py @@ -1,37 +1,49 @@ -import abc import os +from abc import ABC +from contextlib import asynccontextmanager +from functools import cached_property -class Engine(abc.ABC): +class Engine(ABC): def __init__(self, host: str = "localhost", port: int = 8500, scheme: str = "http"): - self._host = str(os.getenv("CONSUL_HOST", host)) + self._host = os.getenv("CONSUL_HOST", host) self._port = int(os.getenv("CONSUL_PORT", port)) - self._scheme = str(os.getenv("CONSUL_SCHEMA", scheme)) + self._scheme = os.getenv("CONSUL_SCHEMA", scheme) @property - def host(self): + def host(self) -> str: return self._host @property - def port(self): + def port(self) -> int: return self._port @property - def scheme(self): + def scheme(self) -> str: return self._scheme - @property - def url(self): + @cached_property + def url(self) -> str: return f"{self.scheme}://{self.host}:{self.port}" + @asynccontextmanager async def get(self, *args, **kwargs): raise NotImplementedError + @asynccontextmanager async def put(self, *args, **kwargs): raise NotImplementedError + @asynccontextmanager async def delete(self, *args, **kwargs): raise NotImplementedError + @asynccontextmanager async def post(self, *args, **kwargs): raise NotImplementedError + + def __repr__(self) -> str: + *_, name = str(self.__class__).split(".") + return ( + f"{name[:-2]}(host='{self.host}', port={self.port}, scheme='{self.scheme}')" + ) diff --git a/discovery/engine/aio.py b/discovery/engine/aio.py deleted file mode 100644 index 6ec7d02..0000000 --- a/discovery/engine/aio.py +++ /dev/null @@ -1,52 +0,0 @@ -from contextlib import suppress - -from discovery.engine.abc import Engine -from discovery.engine.response import HttpResponse - -has_httpx = False -has_aiohttp = False - -with suppress(ImportError): - import aiohttp - - has_aiohttp = True - import httpx - - has_httpx = True - - -class AioEngine(Engine): - def __init__(self, session=None, **kwargs): - super().__init__(**kwargs) - self._session = session - - async def get(self, *args, **kwargs): - response = await self._session.get(*args, **kwargs) - return HttpResponse(response) - - async def put(self, *args, **kwargs): - response = await self._session.put(*args, **kwargs) - return HttpResponse(response) - - async def delete(self, *args, **kwargs): - response = await self._session.delete(*args, **kwargs) - return HttpResponse(response) - - async def post(self, *args, **kwargs): - response = await self._session.post(*args, **kwargs) - return HttpResponse(response) - - async def __aexit__(self, *args, **kwargs): - await self._session.close() - - -async def aiohttp_session(*args, **kwargs): - if not has_aiohttp: - raise ModuleNotFoundError("aiohttp module not found!") - return aiohttp.ClientSession(*args, **kwargs) - - -async def httpx_client(*args, **kwargs): - if not has_httpx: - raise ModuleNotFoundError("httpx module not found!") - return httpx.AsyncClient(*args, **kwargs) diff --git a/discovery/engine/aiohttp/__init__.py b/discovery/engine/aiohttp/__init__.py new file mode 100644 index 0000000..752fa02 --- /dev/null +++ b/discovery/engine/aiohttp/__init__.py @@ -0,0 +1,2 @@ +from discovery.engine.aiohttp.engine import AIOHTTPEngine +from discovery.engine.aiohttp.response import AIOHTTPResponse diff --git a/discovery/engine/aiohttp/engine.py b/discovery/engine/aiohttp/engine.py new file mode 100644 index 0000000..6ff42f6 --- /dev/null +++ b/discovery/engine/aiohttp/engine.py @@ -0,0 +1,59 @@ +from contextlib import asynccontextmanager + +from aiohttp import ClientSession + +from discovery import log +from discovery.engine.abc import Engine +from discovery.engine.aiohttp.response import AIOHTTPResponse +from discovery.engine.response import Response + + +class AIOHTTPEngine(Engine): + def __init__(self, *args, **kwargs) -> None: + """AIOHTTPEngine. + + args: host, port and scheme + kwargs: session arguments + """ + super().__init__(*args) + self._session_kwargs = kwargs + + @asynccontextmanager + async def get(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with ClientSession(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.get(*args, **kwargs) + response.raise_for_status() + yield Response(AIOHTTPResponse(response)) + + @asynccontextmanager + async def put(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with ClientSession(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.put(*args, **kwargs) + response.raise_for_status() + yield Response(AIOHTTPResponse(response)) + + @asynccontextmanager + async def delete(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with ClientSession(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.delete(*args, **kwargs) + response.raise_for_status() + yield Response(AIOHTTPResponse(response)) + + @asynccontextmanager + async def post(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with ClientSession(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.post(*args, **kwargs) + response.raise_for_status() + yield Response(AIOHTTPResponse(response)) diff --git a/discovery/engine/aiohttp/response.py b/discovery/engine/aiohttp/response.py new file mode 100644 index 0000000..cb2377b --- /dev/null +++ b/discovery/engine/aiohttp/response.py @@ -0,0 +1,36 @@ +from discovery.engine.base_response import BaseResponse + + +class AIOHTTPResponse(BaseResponse): + def __init__(self, response) -> None: + self._response = response + + @property + def status(self) -> int: + return int(self._response.status) + + @property + def url(self) -> str: + return str(self._response.url) + + @property + def content_type(self) -> str: + return str(self._response.content_type) + + @property + def version(self) -> str: + http_version = self._response.version + return f"{http_version.major}.{http_version.minor}" + + @property + def raw_response(self): + return self._response + + async def json(self): + return await self._response.json() + + async def text(self): + return await self._response.text() + + async def content(self, *args, **kwargs) -> bytes: + return await self._response.content.read(*args, **kwargs) # type: ignore diff --git a/discovery/engine/base_response.py b/discovery/engine/base_response.py new file mode 100644 index 0000000..d78bc36 --- /dev/null +++ b/discovery/engine/base_response.py @@ -0,0 +1,36 @@ +from abc import ABC + + +class BaseResponse(ABC): + @property + def status(self) -> int: + raise NotImplementedError + + @property + def url(self) -> str: + raise NotImplementedError + + @property + def content_type(self) -> str: + raise NotImplementedError + + @property + def version(self) -> str: + raise NotImplementedError + + @property + def raw_response(self): + raise NotImplementedError + + async def json(self) -> dict: + raise NotImplementedError + + async def text(self) -> str: + raise NotImplementedError + + async def content(self, *args, **kwargs) -> bytes: + raise NotImplementedError + + def __repr__(self) -> str: + *_, name = str(self.__class__).split(".") + return f"{name[:-2]}(status={self.status}, http_version='{self.version}', url='{self.url}')" diff --git a/discovery/engine/httpx/__init__.py b/discovery/engine/httpx/__init__.py new file mode 100644 index 0000000..dc9fc39 --- /dev/null +++ b/discovery/engine/httpx/__init__.py @@ -0,0 +1,2 @@ +from discovery.engine.httpx.engine import HTTPXEngine +from discovery.engine.httpx.response import HTTPXResponse diff --git a/discovery/engine/httpx/engine.py b/discovery/engine/httpx/engine.py new file mode 100644 index 0000000..5e34dc6 --- /dev/null +++ b/discovery/engine/httpx/engine.py @@ -0,0 +1,62 @@ +from contextlib import asynccontextmanager + +try: + from httpx import AsyncClient +except ModuleNotFoundError: + AsyncClient = None # type: ignore + +from discovery import log +from discovery.engine.abc import Engine +from discovery.engine.httpx.response import HTTPXResponse +from discovery.engine.response import Response + + +class HTTPXEngine(Engine): + def __init__(self, *args, **kwargs) -> None: + """HTTPXEngine. + + args: host, port scheme + kwargs: session arguments + """ + super().__init__(*args) + self._session_kwargs = kwargs + + @asynccontextmanager + async def get(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with AsyncClient(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.get(*args, **kwargs) + response.raise_for_status() + yield Response(HTTPXResponse(response)) + + @asynccontextmanager + async def put(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with AsyncClient(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.put(*args, **kwargs) + response.raise_for_status() + yield Response(HTTPXResponse(response)) + + @asynccontextmanager + async def delete(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with AsyncClient(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.delete(*args, **kwargs) + response.raise_for_status() + yield Response(HTTPXResponse(response)) + + @asynccontextmanager + async def post(self, *args, **kwargs): + log.debug(f"args: {args}") + log.debug(f"kwargs: {kwargs}") + async with AsyncClient(**self._session_kwargs) as session: + log.debug(f"session_kwargs: {self._session_kwargs}") + response = await session.post(*args, **kwargs) + response.raise_for_status() + yield Response(HTTPXResponse(response)) diff --git a/discovery/engine/httpx/response.py b/discovery/engine/httpx/response.py new file mode 100644 index 0000000..04d52eb --- /dev/null +++ b/discovery/engine/httpx/response.py @@ -0,0 +1,41 @@ +try: + from httpx import Response +except ModuleNotFoundError: + Response = None # type: ignore + +from discovery.engine.base_response import BaseResponse + + +class HTTPXResponse(BaseResponse): + def __init__(self, response: Response) -> None: + self._strategy = response + + @property + def status(self) -> int: + return self._strategy.status_code + + @property + def url(self) -> str: + return str(self._strategy.url) + + @property + def content_type(self) -> str: + return str(self._strategy.headers["Content-Type"]) + + @property + def version(self) -> str: + _, http_version = self._strategy.http_version.split("/") + return f"{http_version}" + + @property + def raw_strategy(self): + return self._strategy + + async def json(self): + return self._strategy.json() + + async def text(self) -> str: + return self._strategy.text + + async def content(self, *args, **kwargs) -> bytes: + return bytes(self._strategy.content) diff --git a/discovery/engine/response.py b/discovery/engine/response.py index 8ead487..f7ff338 100644 --- a/discovery/engine/response.py +++ b/discovery/engine/response.py @@ -1,54 +1,35 @@ -class HttpResponse: - def __init__(self, response): - self._response = response +from discovery.engine.base_response import BaseResponse + + +class Response(BaseResponse): + def __init__(self, response: BaseResponse) -> None: + self._strategy = response @property def status(self) -> int: - try: - return int(self._response.status) - except AttributeError: - return int(self._response.status_code) + return self._strategy.status @property def url(self) -> str: - return str(self._response.url) + return self._strategy.url @property def content_type(self) -> str: - try: - return str(self._response.content_type) - except AttributeError: - return str(self._response.headers["content-type"]) + return self._strategy.content_type @property def version(self) -> str: - try: - http_version = self._response.version - return f"{http_version.major}.{http_version.minor}" - except AttributeError: - return str(self._response.http_version.split("/")[1]) + return self._strategy.version @property def raw_response(self): - return self._response + return self._strategy.raw_response async def json(self): - try: - response = await self._response.json() - return response - except TypeError: - return self._response.json() - - async def text(self): - try: - response = await self._response.text() - return response - except TypeError: - return self._response.text - - async def content(self) -> bytes: - try: - response = await self._response.content.read() - return bytes(response) - except AttributeError: - return bytes(self._response.content) + return await self._strategy.json() + + async def text(self) -> str: + return await self._strategy.text() + + async def content(self, *args, **kwargs) -> bytes: + return await self._strategy.content(*args, **kwargs) diff --git a/discovery/exceptions.py b/discovery/exceptions.py index f44ea0c..19b2473 100644 --- a/discovery/exceptions.py +++ b/discovery/exceptions.py @@ -1,10 +1,3 @@ -class ServiceNotFoundException(Exception): - pass - - -class ClientOperationException(Exception): - pass - - class NoConsulLeaderException(Exception): - pass + def __init__(self, message="Error to identify Consul's leader."): + super().__init__(message) diff --git a/discovery/model/__init__.py b/discovery/model/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discovery/model/agent/__init__.py b/discovery/model/agent/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discovery/model/agent/checks.py b/discovery/model/agent/checks.py deleted file mode 100644 index b8515d8..0000000 --- a/discovery/model/agent/checks.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from uuid import uuid4 - - -def script(args, name=None, interval="10s", timeout="5s"): - script_id = f"script-{uuid4().hex}" - name = name or script_id - return json.dumps( - {"args": args, "interval": interval, "timeout": timeout, "name": name} - ) - - -def http( - url, - name=None, - tls_skip_verify=True, - method="GET", - header={}, - body="", - interval="10s", - timeout="5s", - deregister_after="1m", -): - http_id = f"http-{uuid4().hex}" - name = name or http_id - return json.dumps( - { - "http": url, - "tls_skip_verify": tls_skip_verify, - "method": method, - "header": header, - "body": body, - "interval": interval, - "timeout": timeout, - "deregister_critical_service_after": deregister_after, - "name": name, - } - ) - - -def tcp(tcp, name=None, interval="10s", timeout="5s"): - tcp_id = f"tcp-{uuid4().hex}" - name = name or tcp_id - return json.dumps( - {"tcp": tcp, "interval": interval, "timeout": timeout, "name": name} - ) - - -def ttl(notes, name=None, ttl="30s"): - ttl_id = f"ttl-{uuid4().hex}" - name = name or ttl_id - return json.dumps({"notes": notes, "ttl": ttl, "name": name}) - - -def docker(container_id, args, shell=None, name=None, interval="10s"): - docker_id = f"docker-{uuid4().hex}" - name = name or docker_id - return json.dumps( - { - "docker_container_id": container_id, - "shell": shell, - "args": args, - "interval": interval, - "name": name, - } - ) - - -def grpc(grpc, name=None, tls=True, interval="10s"): - grpc_id = f"grpc-{uuid4().hex}" - name = name or grpc_id - return json.dumps( - {"grpc": grpc, "grpc_use_tls": tls, "interval": interval, "name": name} - ) - - -def alias(service_id, alias_service, name=None): - """Consul's alias check. - - alias_service: backend - service_id: frontent - """ - name = name or f"alias-{uuid4().hex}" - return json.dumps( - {"name": name, "service_id": service_id, "aliasservice": alias_service} - ) diff --git a/discovery/model/agent/service.py b/discovery/model/agent/service.py deleted file mode 100644 index 3798052..0000000 --- a/discovery/model/agent/service.py +++ /dev/null @@ -1,59 +0,0 @@ -import json -import socket -from functools import singledispatch -from uuid import uuid4 - - -def service( - name, - port, - dc="", - address=None, - tags=None, - meta=None, - namespace="default", - check=None, -): - service_id = f"{name}-{uuid4().hex}" - address = address = f"{socket.gethostbyname(socket.gethostname())}" - meta = meta or {} - tags = tags or [] - response = { - "name": name, - "id": service_id, - "address": address, - "port": port, - "tags": tags, - "meta": meta, - } - if check: - response.update(register_check(check)) - tags_is_valid(tags) - meta_is_valid(meta) - return json.dumps(response) - - -def tags_is_valid(tags): - if not isinstance(tags, list): - raise ValueError("tags must be list") - return True - - -def meta_is_valid(meta): - if not isinstance(meta, dict): - raise ValueError("meta must be dict") - return True - - -@singledispatch -def register_check(check): - response = {"check": json.loads(check)} - return response - - -@register_check.register(list) -def _(check): - response = {"checks": []} - for chk in check: - response["checks"].append(json.loads(chk)) - return response diff --git a/discovery/utils.py b/discovery/utils.py index 438ca3f..924a937 100644 --- a/discovery/utils.py +++ b/discovery/utils.py @@ -1,29 +1,88 @@ import collections import random +import socket import uuid +from dataclasses import asdict, dataclass +from functools import singledispatch +from typing import List, Optional, Union class _InnerServices: def __init__(self): self.services = {} - def add(self, key, value): + def add(self, key, value) -> None: if key not in self.services or len(self.services.get(key)) == 0: self.services.update({key: collections.deque(value)}) def get(self, value): - return self.services.get(value).popleft() + try: + return self.services.get(value).popleft() + except IndexError: + return None rr_services = _InnerServices() -def select_one_random(services): +def select_one_random(services: str): service_selected = random.randint(0, (len(services) - 1)) return services[service_selected] -def select_one_rr(services): +def select_one_rr(services: str): key_ = uuid.uuid5(uuid.NAMESPACE_DNS, str(services)).hex rr_services.add(key_, services) return rr_services.get(key_) + + +@dataclass +class Service: + name: str + port: int + id: Optional[str] = None + address: Optional[str] = None + tags: Optional[List[str]] = None + meta: Optional[dict] = None + enable_tag_override: bool = False + weights: Optional[dict] = None + check: Optional[Union[dict, List[dict]]] = None + + def __post_init__(self): + self.id = self.id or f"{self.name}-{uuid.uuid4().hex}" + self.address = self.address or f"{socket.gethostbyname(socket.gethostname())}" + self.meta = self.meta or {} + self.tags = self.tags or [] + tags_is_valid(self.tags) + meta_is_valid(self.meta) + + def dict(self) -> dict: + data = asdict(self) + if self.check: + data.update(register_check(self.check)) + return data + + def __getitem__(self, key: str): + return self.__dict__[key] + + +def tags_is_valid(tags: Optional[list]) -> bool: + if not isinstance(tags, list): + raise ValueError("tags must be list") + return True + + +def meta_is_valid(meta: Optional[dict]) -> bool: + if not isinstance(meta, dict): + raise ValueError("meta must be dict") + return True + + +@singledispatch +def register_check(check) -> dict: + return {"check": check} + + +@register_check.register(list) +def _(check) -> dict: + return {"checks": check} diff --git a/docker-compose.yml b/docker-compose.yml index ad41a8f..a4d651c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,6 @@ services: - 8600:8600/tcp - 8600:8600/udp - 8300:8300 - # client: - # image: consul - # container_name: consul_client - # ports: - # - 8500:8500 - # - 8600:8600/tcp - # - 8600:8600/udp - # #- 8501:8501 # HTTP off by default - # - 8502:8502 - # + extra_hosts: + - "aio-client:" + - "httpx-client:" diff --git a/docs/1.-overview.md b/docs/1.-overview.md deleted file mode 100644 index 2415621..0000000 --- a/docs/1.-overview.md +++ /dev/null @@ -1,2 +0,0 @@ -async client for [consul](https://consul.io). - diff --git a/docs/api/acl/auth_methods.md b/docs/api/acl/auth_methods.md new file mode 100644 index 0000000..77099e4 --- /dev/null +++ b/docs/api/acl/auth_methods.md @@ -0,0 +1,9 @@ +## [Auth Methods](https://developer.hashicorp.com/consul/api-docs/acl/auth-methods) + +| Category | Endpoint | Status +| --------------------- | ------------------------ | ------ +| Create an Auth Method | `/acl/auth-method` | ✅ +| Read an Auth Method | `/acl/auth-method/:name` | ✅ +| Update an Auth Method | `/acl/auth-method/:name` | ✅ +| Delete an Auth Method | `/acl/auth-method/:name` | ✅ +| List Auth Methods | `/acl/auth-methods` | ✅ diff --git a/docs/api/acl/binding-rules.md b/docs/api/acl/binding-rules.md new file mode 100644 index 0000000..0f23665 --- /dev/null +++ b/docs/api/acl/binding-rules.md @@ -0,0 +1,9 @@ +## [Binding Rules](https://developer.hashicorp.com/consul/api-docs/acl/binding-rules) + +| Category | Endpoint | Status +| --------------------- | ----------------------- | ------ +| Create a Binding Rule | `/acl/binding-rule` | ✅ +| Read a Binding Rule | `/acl/binding-rule/:id` | ✅ +| Update a Binding Rule | `/acl/binding-rule/:id` | ✅ +| Delete a Binding Rule | `/acl/binding-rule/:id` | ✅ +| List Binding Rules | `/acl/binding-rules` | ✅ diff --git a/docs/api/acl/overview.md b/docs/api/acl/overview.md new file mode 100644 index 0000000..fcab37d --- /dev/null +++ b/docs/api/acl/overview.md @@ -0,0 +1,30 @@ +## [ACL](https://developer.hashicorp.com/consul/api-docs/acl) + +Category | Endpoint | Status +-------- | ------|-------- +Bootstrap ACLs | `/acl/bootstrap` | ✅ +Check ACL Replication | `/acl/replication` | ✅ +Translate Rules | `/acl/rules/translate` | ✅ +Translate a Legacy Token's Rules | `/acl/rules/translate/:accessor_id` | ❌ +Login to Auth Method | `/acl/login` | ❌ +Logout from Auth Method | `/acl/logout` | ❌ +OIDC Authorization URL Request | `/acl/logout` | ❌ +OIDC Callback | `/acl/logout` | ❌ + +## Usage + +```python +from discovery import Consul + + +consul = Consul() + +# bootstrap +await consul.acl.bootstrap() + +# replication +await consul.acl.replication() + +# translate +await consul.acl.translate({"policy": "read"}) +``` diff --git a/docs/api/acl/policies.md b/docs/api/acl/policies.md new file mode 100644 index 0000000..58045ac --- /dev/null +++ b/docs/api/acl/policies.md @@ -0,0 +1,10 @@ +## [Policies](https://developer.hashicorp.com/consul/api-docs/acl/policies) + +| Category | Endpoint | Status +|-----------------------| ----------------- | ------ +| Create a Policy | `/acl/policy` | ✅ +| Read a Policy | `/acl/policy/:id` | ✅ +| Read a Policy by Name | `/acl/policy/:id` | ✅ +| Update a Policy | `/acl/policy/:id` | ✅ +| Delete a Policy | `/acl/policy/:id` | ✅ +| List Policies | `/acl/policies` | ✅ diff --git a/docs/api/acl/roles.md b/docs/api/acl/roles.md new file mode 100644 index 0000000..9330d52 --- /dev/null +++ b/docs/api/acl/roles.md @@ -0,0 +1,10 @@ +## [Roles](https://developer.hashicorp.com/consul/api-docs/acl/roles) + +| Category | Endpoint | Status +|---------------------| ---------------------- | ------ +| Create a Role | `/acl/role` | ✅ +| Read a Role by ID | `/acl/role/:id` | ✅ +| Read a Role by Name | `/acl/role/name/:name` | ✅ +| Update a Role | `/acl/role/:id` | ✅ +| Delete a Role | `/acl/role/:id` | ✅ +| List Roles | `/acl/roles` | ✅ diff --git a/docs/api/acl/tokens.md b/docs/api/acl/tokens.md new file mode 100644 index 0000000..e08f4e6 --- /dev/null +++ b/docs/api/acl/tokens.md @@ -0,0 +1,55 @@ +## [Tokens](https://developer.hashicorp.com/consul/api-docs/acl/tokens) + +| Category | Endpoint | Status +| --------------- |--------------------------------| ------ +| Create a Token | `/acl/token` | ✅ +| Read a Token | `/acl/token/:AccessorID` | ✅ +| Read Self Token | `/acl/token/self` | ✅ +| Update a Token | `/acl/token/:AccessorID` | ✅ +| Clone a Token | `/acl/token/:AccessorID/clone` | ✅ +| Delete a Token | `/acl/token/:AccessorID` | ✅ +| List Tokens | `/acl/tokens` | ✅ + +## [Legacy Tokens](https://developer.hashicorp.com/consul/api-docs/acl/legacy) + +| Category | Endpoint | Status | +| ---------------- | -------------------- | ------ | +| Create ACL Token | `/acl/create` | ❌ | +| Update ACL Token | `/acl/update` | ❌ | +| Delete ACL Token | `/acl/destroy/:uuid` | ❌ | +| Read ACL Token | `/acl/info/:uuid` | ❌ | +| Clone ACL Token | `/acl/clone/:uuid` | ❌ | +| List ACLs | `/acl/list` | ❌ | + +## Usage + +```python +from discovery import Consul + + +consul = Consul() + +# Create a Token +await consul.acl.token.create( + "Agent token for 'node1", + [{"ID": "165d4317-e379-f732-ce70-86278c4558f7"}, {"Name": "node-read"}], +) + +# Read a Token +await consul.acl.token.read() + +# Read Self Token +await consul.acl.token.details() + +# Update a Token +await consul.acl.token.update() + +# Clone a Token +await consul.acl.token.clone() + +# Delete a Token +await consul.acl.token.delete() + +# List Tokens +await consul.acl.token.list() +``` diff --git a/docs/api/admin-partitions.md b/docs/api/admin-partitions.md new file mode 100644 index 0000000..c7deda7 --- /dev/null +++ b/docs/api/admin-partitions.md @@ -0,0 +1,3 @@ +## 🚨 [Admin Partitions](https://developer.hashicorp.com/consul/api-docs/admin-partitions) + +Not implemented. diff --git a/docs/api/agent/checks.md b/docs/api/agent/checks.md new file mode 100644 index 0000000..76d37c3 --- /dev/null +++ b/docs/api/agent/checks.md @@ -0,0 +1,42 @@ +## [Checks](https://developer.hashicorp.com/consul/api-docs/agent/check) + +| Category | Endpoint | Status +| ----------------------- | ---------------------- | ------ +| List Checks | `/agent/check` | ✅ +| Register Check | `/agent/check/register`| ✅ +| Deregister Check | `/agent/check/deregister/:check_id` | ✅ +| TTL Check Pass | `/agent/check/pass/:check_id` | ✅ +| TTL Check Warn | `/agent/check/warn/:check_id` | ✅ +| TTL Check Fail | `/agent/check/fail/:check_id` | ✅ +| TTL Check Update | `/agent/check/update/:check_id` | ✅ + +## Usage + +```python +from discovery import Consul + +consul = Consul() + + +# list +await consul.agent.checks.list() +await consul.agent.checks.list('ServiceName=="my-service"') + +# register +await consul.agent.checks.register() + +# deregister +await consul.agent.checks.deregister() + +# check_pass +await consul.agent.checks.check_pass('my-check-id') + +# check_warn +await consul.agent.checks.check_warn('my-check-id') + +# check_fail +await consul.agent.checks.check_fail('my-check-id') + +# check_update +await consul.agent.checks.check_update('my-check-id') +``` diff --git a/docs/api/agent/connect.md b/docs/api/agent/connect.md new file mode 100644 index 0000000..6bab67e --- /dev/null +++ b/docs/api/agent/connect.md @@ -0,0 +1,7 @@ +## [Connect](https://developer.hashicorp.com/consul/api-docs/agent/connect) + +| Category | Endpoint | Status +| -------------------------------- | --------------------------------- | ------ +| Authorize | `/agent/connect/authorize` | ✅ +| Certificate Authority (CA) Roots | `/agent/connect/ca/roots` | ✅ +| Service Leaf Certificate | `/agent/connect/ca/leaf/:service` | ✅ diff --git a/docs/api/agent/overview.md b/docs/api/agent/overview.md new file mode 100644 index 0000000..ba1337b --- /dev/null +++ b/docs/api/agent/overview.md @@ -0,0 +1,15 @@ +## [Agent](https://developer.hashicorp.com/consul/api-docs/agent) + +| Category | Endpoint | Status +|-----------------------------|----------------------------| ------ +| Host information | `/agent/host` | ✅ +| List Members | `/agent/members` | ✅ +| Read Configuration | `/agent/self` | ✅ +| Reload Agent | `/agent/reload` | ✅ +| Enable Maintenance Mode | `/agent/maintenance` | ✅ +| View Metrics | `/agent/metrics` | ✅ +| Stream Logs | `/agent/monitor` | ✅ +| Join Agent | `/agent/join/:address` | ✅ +| Graceful Leave and Shutdown | `/agent/leave` | ✅ +| Force Leave and Shutdown | `/agent/force-leave/:node` | ✅ +| Update ACL Tokens | `/token/acl_token` | ✅ diff --git a/docs/api/agent/services.md b/docs/api/agent/services.md new file mode 100644 index 0000000..aaa18bf --- /dev/null +++ b/docs/api/agent/services.md @@ -0,0 +1,41 @@ +## [Services](https://developer.hashicorp.com/consul/api-docs/agent/service) + +| Category | Endpoint | Status +|--------------------------------| ------------------------------------------ | ------ +| List Services | `/agent/services` | ✅ +| Get Service Configuration | `/agent/service/:service_id` | ✅ +| Get local service health by Name | `/agent/health/service/name/:service_name` | ✅ +| Get local service health by ID | `/agent/health/service/id/:service_id` | ✅ +| Register Service | `/agent/service/register` | ✅ +| Deregister Service | `/agent/service/deregister/:service_id` | ✅ +| Enable Maintenance Mode | `/agent/service/maintenance/:service_id` | ✅ + +## Usage + +```python +from discovery import Consul + + +consul = Consul() + +# list +await consul.agent.service.list() + +# configuration +await consul.agent.service.configuration('my-service-id') + +# health_by_name +await consul.agent.service.health_by_name('') + +# health_by_id +await consul.agent.service.health_by_id() + +# register +await consul.agent.service.register() + +# deregister +await consul.agent.service.deregister() + +# enable_maintenance +await consul.agent.service.enable_maintenance() +``` diff --git a/docs/api/catalog.md b/docs/api/catalog.md new file mode 100644 index 0000000..964cd97 --- /dev/null +++ b/docs/api/catalog.md @@ -0,0 +1,49 @@ +## [Catalog](https://developer.hashicorp.com/consul/api-docs/catalog) + +| Category | Endpoint | Status +|----------------------------------------|--------------------------------------| ----- +| Register Entity | `/catalog/register` | ✅ +| Deregister Entity | `/catalog/deregister` | ✅ +| List Datacenters | `/catalog/datacenters` | ✅ +| List Nodes | `/catalog/nodes` | ✅ +| List Services | `/catalog/services` | ✅ +| List Nodes for Service | `/catalog/service/:service` | ✅ +| List Nodes for Connect-capable Service | `/catalog/connect/:service` | ✅ +| Retrieve Map of Services for a Node | `/catalog/node/:node` | ✅ +| List Services for Node | `/catalog/node-services/:node` | ✅ +| List Services for Gateway | `/catalog/gateway-services/:gateway` | ✅ + +## Usage + +```python +from discovery import Consul + +consul = Consul() + +# register_entity +await consul.catalog.register_entity(service) + +# deregister_entity +await consul.catalog.register_entity(service) + +# list datacenters +await consul.catalog.list_datacenters() + +# list_nodes +await consul.catalog.list_nodes() + +# list services +await consul.catalog.list_services() + +# list_nodes_for_service +await c.catalog.list_nodes_for_service('consul') + +# list_nodes_for_connect + +# services_for_node +await c.catalog.services_for_node('localhost') + +# list_services_for_node + +# list_services_for_gateway +``` diff --git a/docs/api/cluster-peering.md b/docs/api/cluster-peering.md new file mode 100644 index 0000000..ce618fa --- /dev/null +++ b/docs/api/cluster-peering.md @@ -0,0 +1,3 @@ +## [Cluster Perring](https://developer.hashicorp.com/consul/api-docs/peering) + +Not implemented diff --git a/docs/api/config.md b/docs/api/config.md new file mode 100644 index 0000000..d02bb41 --- /dev/null +++ b/docs/api/config.md @@ -0,0 +1,21 @@ +## [Config](https://developer.hashicorp.com/consul/api-docs/config) + +| Category | Endpoint | Status +| -------------------- | --------------------- | ------ +| Apply Configuration | `/config` | ✅ +| Get Configuration | `/config/:kind/:name` | ✅ +| List Configurations | `/config/:kind` | ✅ +| Delete Configuration | `/config/:kind/:name` | ✅ + + +## Usage + +```python +from discovery import Consul, Kind + + +consult = Consul() + +# list configurations +await consul.config.list(Kind.SERVICE_DEFAULTS) +``` diff --git a/docs/api/connect/ca.md b/docs/api/connect/ca.md new file mode 100644 index 0000000..a776b90 --- /dev/null +++ b/docs/api/connect/ca.md @@ -0,0 +1,7 @@ +## [CA]() + +| Category | Endpoint | Status +| ------------------------- | --------------------------- | ------ +| List CA Root Certificates | `/connect/ca/roots` | ✅ +| Get CA Configuration | `/connect/ca/configuration` | ✅ +| Update CA Configuration | `/connect/ca/configuration` | ✅ diff --git a/docs/api/connect/intentions.md b/docs/api/connect/intentions.md new file mode 100644 index 0000000..a5d62b6 --- /dev/null +++ b/docs/api/connect/intentions.md @@ -0,0 +1,14 @@ +## [Intentions](https://developer.hashicorp.com/consul/api-docs/connect/intentions) + +| Category | Endpoint | Status +|---------------------------------|----------------------------| ----- +| Upsert Intention by Name | `/connect/intentions/exact` | ✅ +| Create Intention with ID | `/connect/intentions` | ❌ +| Update Intention by ID | `/connect/intentions/:uuid` | ❌ +| Read Specific Intention by Name | `/connect/intentions/exact` | ✅ +| Read Specific Intention by ID | `/connect/intentions/:uuid` | ❌ +| List Intentions | `/connect/intentions` | ✅ +| Delete Intention by Name | `/connect/intentions/exact` | ✅ +| Delete Intention by ID | `/connect/intentions/:uuid` | ❌ +| Check Intention Result | `/connect/intentions/check` | ✅ +| List Matching Intentions | `/connect/intentions/match` | ✅ diff --git a/docs/api/connect/overview.md b/docs/api/connect/overview.md new file mode 100644 index 0000000..27c457e --- /dev/null +++ b/docs/api/connect/overview.md @@ -0,0 +1 @@ +## [Connect](https://developer.hashicorp.com/consul/api-docs/connect) diff --git a/docs/api/coordinates.md b/docs/api/coordinates.md new file mode 100644 index 0000000..d1a94d7 --- /dev/null +++ b/docs/api/coordinates.md @@ -0,0 +1,8 @@ +## [Coordinates](https://developer.hashicorp.com/consul/api-docs/coordinate) + +Category | Endpoint | Status +-------- | -------- | ------ +Read WAN Coordinates | `/coordinate/datacenters` | ✅ +Read LAN Coordinates for all nodes | `/coordinate/nodes` | ✅ +Read LAN Coordinates for a node | `/coordinate/node/:node` | ✅ +Update LAN Coordinates for a node | `/coordinate/update` | ✅ diff --git a/docs/api/discovery-chain.md b/docs/api/discovery-chain.md new file mode 100644 index 0000000..d5202b9 --- /dev/null +++ b/docs/api/discovery-chain.md @@ -0,0 +1,5 @@ +## [Discovery Chain](https://developer.hashicorp.com/consul/api-docs/discovery-chain) + +Category | Endpoint | Status +-------- | -------- | ------ +Read Compiled Discovery Chain | `/discovery-chain/:service` | ❌ diff --git a/docs/api/events.md b/docs/api/events.md new file mode 100644 index 0000000..90da9d5 --- /dev/null +++ b/docs/api/events.md @@ -0,0 +1,6 @@ +## [Events](https://developer.hashicorp.com/consul/api-docs/event) + +| Category | Endpoint | Status +| ----------- | --------------------------- | ------ +| Fire Event | `/discovery-chain/:service` | ✅ +| List Events | `/event/list` | ✅ diff --git a/docs/api/health.md b/docs/api/health.md new file mode 100644 index 0000000..0357a80 --- /dev/null +++ b/docs/api/health.md @@ -0,0 +1,38 @@ +## [Health](https://developer.hashicorp.com/consul/api-docs/health) + +Category | Endpoint | Status +-------- |----------------------------| ------ +List Checks for Node | `/health/node/:node` | ✅ +List Checks for Service | `/health/checks/:service` | ✅ +List Service Instances for Service | `/health/service/:service` | ✅ +List Service Instances for Connect-enabled Service | `/health/connect/:service` | ✅ +List Service Instances for Ingress Gateways Associated with a Service | `/health/ingress/:service` | ✅ +List Checks in State | `/health/state/:state` | ✅ + +## Usage + +```python +from discovery import Consul, HealthState + + +consul = Consul() + +# checks_for_node +await consul.health.checks_for_node('7f6d0d2ecb6d') + +# checks_for_service +await consul.health.checks_for_service('consul') + +# service_instances +await consul.health.service_instances('consul') + +# service_instances_for_connect +await consul.health.service_instances_for_connect('consul') + +# service_instances_for_ingress +await consul.health.service_instances_for_ingress('consul') + +# checks_in_state +await consul.health.checks_in_state() +await consul.health.checks_in_state(HealthState.CRITICAL) +``` diff --git a/docs/api/kv-store.md b/docs/api/kv-store.md new file mode 100644 index 0000000..c915f1e --- /dev/null +++ b/docs/api/kv-store.md @@ -0,0 +1,31 @@ +## [KV](https://developer.hashicorp.com/consul/api-docs/kv) + +Category | Endpoint | Status +-------- | -------- | ------ +Read Key | `/kv/:key` | ✅ +Create/Update Key | `/kv/:key` | ✅ +Delete Key | `/kv/:key` | ✅ + +## Usage + +```python +from discovery.client import Consul + + +consul = Consul() + +# create key +await consul.kv.create("mysecret", "my super secret") + +# read key +await consul.kv.read('mysecret') + +# update key +await consul.kv.update("mysecret", "my new super secret") + +# read key value +await consul.kv.read_value('mysecret') + +# delete delete +await consul.kv.delete("mysecret") +``` diff --git a/docs/api/namespaces.md b/docs/api/namespaces.md new file mode 100644 index 0000000..a6aa51e --- /dev/null +++ b/docs/api/namespaces.md @@ -0,0 +1,25 @@ +## 🚨 [Namespaces](https://developer.hashicorp.com/consul/api-docs/namespaces) + +| Category | Endpoint | Status +| ------------------- | ------------------ | ------ +| Create a Namespace | `/namespace` | ✅ +| Read a Namespace | `/namespace/:name` | ✅ +| Update a Namespace | `/namespace/:name` | ✅ +| Delete a Namespace | `/namespace/:name` | ✅ +| List all Namespaces | `/namespaces` | ✅ + + +## Usage + +```python +from discovery import Consul + + +consul = Consul() + +# leader +await consul.status.leader() + +# peers +await consul.status.peers() +``` diff --git a/docs/api/operator/area.md b/docs/api/operator/area.md new file mode 100644 index 0000000..16b7837 --- /dev/null +++ b/docs/api/operator/area.md @@ -0,0 +1,11 @@ +## 🚨 [Area](https://developer.hashicorp.com/consul/api-docs/operator/area) + +| Category | Endpoint | Status +| -------------------------- | ------------------------------ | ------ +| Create Network Area | `/operator/area` | ✅ +| List Network Areas | `/operator/area` | ✅ +| Update Network Area | `/operator/area/:uuid` | ✅ +| List Specific Network Area | `/operator/area/:uuid` | ✅ +| Delete Network Area | `/operator/area/:uuid` | ✅ +| Join Network Area | `/operator/area/:uuid/join` | ✅ +| List Network Area Members | `/operator/area/:uuid/members` | ✅ diff --git a/docs/api/operator/autopilot.md b/docs/api/operator/autopilot.md new file mode 100644 index 0000000..3c9a328 --- /dev/null +++ b/docs/api/operator/autopilot.md @@ -0,0 +1,29 @@ +## [Autopilot](https://developer.hashicorp.com/consul/api-docs/operator/autopilot) + +| Category | Endpoint | Status +| -------------------- | ----------------------------------- | ------ +| Read Configuration | `/operator/autopilot/configuration` | ✅ +| Update Configuration | `/operator/autopilot/configuration` | ✅ +| Read Health | `/operator/autopilot/health` | ✅ +| Read the Autopilot State | `/operator/autopilot/state` | ✅ + +## Usage + +```python +from discovery import Consul + +consul = Consul() + + +# read_configuration +await consul.operator.autopilot.read_configuration() + +# update_configuration +await consul.operator.autopilot.update_configuration() + +# read_health +await consul.operator.autopilot.read_health() + +# read_state +await consul.operator.autopilot.read_state() +``` diff --git a/docs/api/operator/keyring.md b/docs/api/operator/keyring.md new file mode 100644 index 0000000..a0fa7ff --- /dev/null +++ b/docs/api/operator/keyring.md @@ -0,0 +1,8 @@ +## [Keyring](https://developer.hashicorp.com/consul/api-docs/operator/keyring) + +| Category | Endpoint | Status +| ------------------------------------ | ------------------- | ------ +| List Gossip Encryption Keys | `/operator/keyring` | ✅ +| Add New Gossip Encryption Key | `/operator/keyring` | ✅ +| Change Primary Gossip Encryption Key | `/operator/keyring` | ✅ +| Delete Gossip Encryption Key | `/operator/keyring` | ✅ diff --git a/docs/api/operator/license.md b/docs/api/operator/license.md new file mode 100644 index 0000000..1e7c411 --- /dev/null +++ b/docs/api/operator/license.md @@ -0,0 +1,29 @@ +## 🚨 [License](https://developer.hashicorp.com/consul/api-docs/operator/license) + +Category | Endpoint | Status +-------- | -------- | ------ +Getting the Consul License | `/operator/license` | ✅ +Update the Consul License | `/operator/license` | ✅ +Resetting the Consul License | `/operator/license` | ✅ + +## Usage + +```python +from discovery import Consul + +consul = Consul() + + +# current +await consul.operator.license.current() + +# update_configuration +await consul.operator.autopilot.update_configuration() + +# update +my_license = b'mylicense' +await consul.operator.license.update(my_license) + +# reset +await consul.operator.license.reset() +``` diff --git a/docs/api/operator/overview.md b/docs/api/operator/overview.md new file mode 100644 index 0000000..711a68b --- /dev/null +++ b/docs/api/operator/overview.md @@ -0,0 +1 @@ +## [Operator](https://developer.hashicorp.com/consul/api-docs/operator) diff --git a/docs/api/operator/raft.md b/docs/api/operator/raft.md new file mode 100644 index 0000000..a1c954c --- /dev/null +++ b/docs/api/operator/raft.md @@ -0,0 +1,21 @@ +## [Raft](https://developer.hashicorp.com/consul/api-docs/operator/raft) + +| Category | Endpoint | Status +| ------------------ | ------------------------------ | ------ +| Read Configuration | `/operator/raft/configuration` | ✅ +| Delete Raft Peer | `/operator/raft/peer` | ✅ + +## Usage + +```python +from discovery.client import Consul + + +consul = Consul() + +# read_configuration +await consul.operator.raft.read_configuration() + +# delete_peer +await consul.operator.raft.delete_peer() +``` diff --git a/docs/api/operator/segment.md b/docs/api/operator/segment.md new file mode 100644 index 0000000..749b8a5 --- /dev/null +++ b/docs/api/operator/segment.md @@ -0,0 +1,5 @@ +## 🚨 [Segment](https://developer.hashicorp.com/consul/api-docs/operator/segment) + +| Category | Endpoint | Status +| --------------------- | ------------------- | ------ +| List Network Segments | `/operator/segment` | ✅ diff --git a/docs/api/prepared-queries.md b/docs/api/prepared-queries.md new file mode 100644 index 0000000..25e3860 --- /dev/null +++ b/docs/api/prepared-queries.md @@ -0,0 +1,11 @@ +## [Prepared Queries](https://developer.hashicorp.com/consul/api-docs/query) + +| Category | Endpoint | Status +| -------- |-------- | ------ +| Create Prepared Query | `/query` | ✅ +| List Prepared Queries | `/query` | ✅ +| Update Prepared Query | `/query/:uuid` | ✅ +| Read Prepared Query | `/query/:uuid`| ✅ +| Delete Prepared Query | `/query/:uuid` | ✅ +| Execute Prepared Query | `/query/:uuid/execute` | ✅ +| Explain Prepared Query | `/query/:uuid/explain` | ✅ diff --git a/docs/api/sessions.md b/docs/api/sessions.md new file mode 100644 index 0000000..1ea60dd --- /dev/null +++ b/docs/api/sessions.md @@ -0,0 +1,10 @@ +## [Sessions](https://developer.hashicorp.com/consul/api-docs/session) + +Category | Endpoint | Status +-------- | -------- | ------ +Create Session | `/session/create` | ✅ +Delete Session | `/session/destroy/:uuid` | ✅ +Read Session | `/session/info/:uuid` | ✅ +List Sessions for Node | `/session/node/:node` | ✅ +List Sessions | `/session/list` | ✅ +Renew Session | `/session/renew/:uuid` | ✅ diff --git a/docs/api/snapshots.md b/docs/api/snapshots.md new file mode 100644 index 0000000..255b82a --- /dev/null +++ b/docs/api/snapshots.md @@ -0,0 +1,6 @@ +## [Snapshots](https://developer.hashicorp.com/consul/api-docs/snapshot) + +Category | Endpoint | Status +-------- | -------- | ------ +Generate Snapshot | `/snapshot` | ✅ +Restore Snapshot | `/snapshot` | ✅ diff --git a/docs/api/status.md b/docs/api/status.md new file mode 100644 index 0000000..da2d024 --- /dev/null +++ b/docs/api/status.md @@ -0,0 +1,21 @@ +## [Status](https://developer.hashicorp.com/consul/api-docs/status) + +Category | Endpoint | Status +-------- |------------------| ------ +Get Raft Leader | `/status/leader` | ✅ +List Raft Peers | `/status/peers` | ✅ + +## Usage + +```python +from discovery import Consul + + +consul = Consul() + +# leader +await consul.status.leader() + +# peers +await consul.status.peers() +``` diff --git a/docs/api/transactions.md b/docs/api/transactions.md new file mode 100644 index 0000000..1209e84 --- /dev/null +++ b/docs/api/transactions.md @@ -0,0 +1,5 @@ +## [Transactions](https://developer.hashicorp.com/consul/api-docs/txn) + +Category | Endpoint | Status +-------- | -------- | ------ +Create Transaction | `/txn` | ✅ diff --git a/docs/engine/aiohttp.md b/docs/engine/aiohttp.md new file mode 100644 index 0000000..85569e3 --- /dev/null +++ b/docs/engine/aiohttp.md @@ -0,0 +1,33 @@ +# [AIOHTTP](https://docs.aiohttp.org/en/stable/) + +## default client + +Using default client: + +```python +from discovery import Consul + +consul = Consul() +``` + +## custom client + +```python +import logging + +from discovery import Consul +from discovery.engine import AIOHTTPEngine + + +logging.basicConfig(level=logging.DEBUG) + +# *args must be: host, port and scheme +# **kwargs session arguments +consul = Consul( + client=AIOHTTPEngine( + "localhost", 8500, 'http', + headers={'engine': 'discovery-client with AIOHTTP'} + ) +) +await consul.leader_ip() +``` diff --git a/docs/engine/httpx.md b/docs/engine/httpx.md new file mode 100644 index 0000000..d56c4ca --- /dev/null +++ b/docs/engine/httpx.md @@ -0,0 +1,24 @@ +# [httpx](https://www.python-httpx.org/) + +Using httpx [**async client**](https://www.python-httpx.org/async/). + +```python +import logging + +from discovery import Consul +from discovery.engine import HTTPXEngine + + +logging.basicConfig(level=logging.DEBUG) + +# *args must be: host, port and scheme +# **kwargs session arguments +consul = Consul( + client=HTTPXEngine( + "localhost", 8500, 'http', + headers={'engine': 'discovery-client with HTTPX'} + ) +) + +await consul.leader_ip() +``` diff --git a/docs/examples/1.-aiohttp.md b/docs/examples/1.-aiohttp.md deleted file mode 100644 index 722c9d0..0000000 --- a/docs/examples/1.-aiohttp.md +++ /dev/null @@ -1,66 +0,0 @@ - - -# aiohttp - -## client - -Using aiohttp client on the AioEngine. - -```python -import asyncio -from discovery import Consul, aiohttp_session - -loop = asyncio.get_event_loop() - -session = loop.run_until_complete(aiohttp_session()) -consul = Consul(session) - -# query a service from catalog filtering by health status -async def query(service_name): - resp = await consul.catalog.service('myapp') - resp = await resp.json() - return resp - -loop.run_until_complete(query('myapp')) -``` - -## aiohttp server + client - -Using discovery-client to display some queries to Consul API. - -```python -from aiohttp import web - -from discovery import Consul, aiohttp_session - - -app = web.Application() -routes = web.RouteTableDef() - - -async def consul(app): - session = await aiohttp_session() - consul = Consul(session) - app['consul'] = consul - - -@routes.get('/status/leader') -async def index(request): - response = await app['consul'].status.leader() - response = await response.text() - return web.Response(text=response) - - -@routes.get('/catalog/service/{service}') -async def svc(request): - response = await app['consul'].catalog.service( - f"{request.match_info['service']}" - ) - response = await response.json() - return web.json_response(response) - - -app.add_routes(routes) -app.on_startup.append(consul) -web.run_app(app) -``` diff --git a/docs/examples/2.-httpx.md b/docs/examples/2.-httpx.md deleted file mode 100644 index 672c9ea..0000000 --- a/docs/examples/2.-httpx.md +++ /dev/null @@ -1,60 +0,0 @@ -# httpx - -Using httpx async client on the AioEngine. - -```python -import asyncio -from discovery import Consul, httpx_client - - -loop = asyncio.get_event_loop() -session = loop.run_until_complete(httpx_client()) -consul = Consul(session) - -# registering a service -consul.agent.service.register('myapp', 5000) -# or -consul.register('myapp', 5000) - - -# query a service from catalog filtering by health status -async def query(service_name): - resp = await consul.catalog.service(service_name) - return resp.json() - -loop.run_until_complete(query('myapp')) -``` - -## Flask + httpx - -Using discovery-client to display some queries to Consul API. - -```python -import asyncio - -from flask import Flask, jsonify -import httpx - -from discovery import Consul, AioEngine, httpx_client - - -loop = asyncio.get_event_loop() -app = Flask(__name__) - -session = loop.run_until_complete(httpx_client()) -consul = Consul(AioEngine(session)) - - -@app.route('/status/leader') -def leader(): - response = loop.run_until_complete(consul.status.leader()) - return response.text - -@app.route('/catalog/service/') -def catalog(service): - response = loop.run_until_complete( - consul.catalog.service(service) - ) - return jsonify(response.json()) - -``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a77f630 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,37 @@ +[![ci](https://github.com/amenezes/discovery-client/workflows/ci/badge.svg)](https://github.com/amenezes/discovery-client/actions) +[![codecov](https://codecov.io/gh/amenezes/discovery-client/branch/master/graph/badge.svg)](https://codecov.io/gh/amenezes/discovery-client) +[![PyPI version](https://badge.fury.io/py/discovery-client.svg)](https://badge.fury.io/py/discovery-client) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/discovery-client) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +# discovery-client + +Async Python client for [consul](https://consul.io). + +HTTP engine options available: + +- aiohttp `default`; +- httpx. + +## Installing + +Install and update using pip: + +### default client + +````bash +pip install -U discovery-client +```` + +### httpx client + +````bash +pip install -U 'discovery-client[httpx]' +```` + +## Links + +- License: [Apache License](https://choosealicense.com/licenses/apache-2.0/) +- Code: [https://github.com/amenezes/discovery-client](https://github.com/amenezes/discovery-client) +- Issue tracker: [https://github.com/amenezes/discovery-client/issues](https://github.com/amenezes/discovery-client/issues) +- Docs: [https://discovery-client.amenezes.net](https://discovery-client.amenezes.net) diff --git a/examples/Dockerfile-aio b/examples/Dockerfile-aio deleted file mode 100644 index 9d2149e..0000000 --- a/examples/Dockerfile-aio +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3.6.8-alpine - -COPY . . - -RUN pip install -r requirements-example.txt - -EXPOSE 5000 - -ENTRYPOINT ["python", "test_aio.py"] diff --git a/examples/Dockerfile-client b/examples/Dockerfile-client deleted file mode 100644 index 871f856..0000000 --- a/examples/Dockerfile-client +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.6.8-alpine - -COPY . . - -RUN pip install -r requirements-example.txt - -EXPOSE 5000 - -ENV FLASK_APP test_client.py - -ENTRYPOINT ["flask", "run", "-h", "0.0.0.0"] diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 8f82f64..0000000 --- a/examples/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Discovery-Client Examples - -## Using docker and docker-compose - -From inside the `example` folder. - -````bash -docker-compose up -d -```` - -## On bare metal - -From inside the `example` folder. - -### setup - - -````yml -# uncomment the following lines and change MY_IP value to your local IP. -# lines 8-10 -extra_hosts: - - "aio-client:MY_IP" - - "standard-client:MY_IP" -```` - -### run - -````bash -# first install virtualenv -pip install virtualenv - -# create and activate venv -virtualenv venv -source venv/bin/activate - -# install dependencies -pip install -r requirements-example.txt - -# start consul container -docker-compose up -d discovery - -# start aio-client -python test_aio.py - -# OR - -# start standard-client -FLASK_APP=test_client.py flask run -h 0.0.0.0 -```` diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml deleted file mode 100644 index bd3a1f0..0000000 --- a/examples/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: '2' -services: - discovery: - image: consul - container_name: discovery - ports: - - 8500:8500 - # extra_hosts: - # - "aio-client:MY_IP" - # - "standard-client:MY_IP" - standard-client: - build: - context: . - dockerfile: Dockerfile-client - container_name: standard-client - ports: - - 5001:5000 - aio-client: - build: - context: . - dockerfile: Dockerfile-aio - container_name: aio-client - ports: - - 5002:5000 diff --git a/examples/requirements-example.txt b/examples/requirements-example.txt deleted file mode 100644 index 19cf9fd..0000000 --- a/examples/requirements-example.txt +++ /dev/null @@ -1,10 +0,0 @@ -aiohttp==3.5.4 -certifi==2019.6.16 -chardet==3.0.4 -idna==2.8 -python-consul==1.1.0 -requests==2.22.0 -six==1.12.0 -urllib3==1.25.3 -Flask==1.0.3 -discovery-client==0.2.4 diff --git a/examples/test_aio.py b/examples/test_aio.py deleted file mode 100644 index 898e482..0000000 --- a/examples/test_aio.py +++ /dev/null @@ -1,69 +0,0 @@ -import asyncio -import logging - -from aiohttp import web - -from discovery.aioclient import Consul -from discovery.service import Service -from discovery.check import Check, http - - -async def service_discovery(app): - app.loop.create_task(dc.register()) - asyncio.sleep(15) - app.loop.create_task(dc.check_consul_health()) - - -async def handle_info(request): - return web.json_response({'app': 'aio-client'}) - - -async def handle_status(request): - return web.json_response({'status': 'UP'}) - - -async def handle_services(request): - service_name = request.match_info.get('service_name', "consul") - response = await dc.find_services(service_name) - return web.json_response(response) - - -async def handle_service(request): - service_name = request.match_info.get('service_name', "consul") - response = {} - - try: - response = await dc.find_services(service_name) - except IndexError: - logging.info(f'Service {service_name} not found!') - return web.json_response(response) - - -async def shutdown_server(app): - app.loop.run_until_complete(dc.deregister()) - app.loop.close() - - -app = web.Application() - -dc = Consul( - host='discovery', - port=8500, - app=app.loop, - service=Service( - 'aio-client', - 5000, - check=Check( - 'app-check', - http('http://aio-client:5000/manage/health') - ) - ) -) - -app.on_startup.append(service_discovery) -app.on_shutdown.append(shutdown_server) -app.add_routes([web.get('/manage/health', handle_status), - web.get('/manage/info', handle_info), - web.get('/services/{service_name}', handle_services), - web.get('/service/{service_name}', handle_service)]) -web.run_app(app, host='0.0.0.0', port=5000) diff --git a/examples/test_client.py b/examples/test_client.py deleted file mode 100644 index b917281..0000000 --- a/examples/test_client.py +++ /dev/null @@ -1,42 +0,0 @@ -import json -import threading - -from discovery.client import Consul -from discovery.check import Check, http -from discovery.service import Service - -from flask import Flask - - -app = Flask(__name__) -dc = Consul( - host='discovery', - port=8500, - service=Service( - 'standard-client', - 5000, - check=Check( - 'standard-client-check', - http('http://standard-client:5000/manage/health') - ) - ) -) -dc.register() - - -@app.route('/manage/health') -def health(): - return json.dumps({'status': 'UP'}) - - -@app.route('/manage/info') -def info(): - return json.dumps({'app': 'standard-client'}) - - -@app.before_first_request -def enable_service_registry(): - def probe_discovery_connection(): - dc.consul_is_healthy() - thread = threading.Thread(target=probe_discovery_connection) - thread.start() diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a786ee9 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,84 @@ +site_name: discovery-client +repo_url: https://github.com/amenezes/discovery-client +repo_name: amenezes/discovery-client +theme: + name: material + features: + - navigation.instant + - navigation.top + - navigation.prune + - navigation.sections + - toc.integrate + - search.highlight + - search.suggest + - search.share + - content.code.annotate + - content.tooltips + - toc.follow + palette: + - scheme: default + primary: pink + accent: red + toggle: + icon: material/lightbulb-on + name: Switch to dark mode + - scheme: slate + primary: pink + accent: red + toggle: + icon: material/lightbulb + name: Switch to light mode + icon: + repo: fontawesome/brands/github-alt +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/amenezes/discovery-client + - icon: fontawesome/solid/bug + link: https://github.com/amenezes/discovery-client/issues + - icon: fontawesome/solid/envelope + link: mailto:alexandre.fmenezes@gmail.com +nav: +- API: + - ACLs: + - Overview: api/acl/overview.md + - Auth Methods: api/acl/auth_methods.md + - Binding Rules: api/acl/binding-rules.md + - Policies: api/acl/policies.md + - Roles: api/acl/roles.md + - Tokens: api/acl/tokens.md + - Admin Partitions: api/admin-partitions.md + - Agent: + - Overview: api/agent/overview.md + - Checks: api/agent/checks.md + - Services: api/agent/services.md + - Connect: api/agent/connect.md + - Catalog: api/catalog.md + - Cluster Peering: api/cluster-peering.md + - Config: api/config.md + - Connect: + - Overview: api/connect/overview.md + - CA: api/connect/ca.md + - Intentions: api/connect/intentions.md + - Coordinates: api/coordinates.md + - Discovery Chain: api/discovery-chain.md + - Events: api/events.md + - Health: api/health.md + - KV Store: api/kv-store.md + - Operator: + - Overview: api/operator/overview.md + - Area: api/operator/area.md + - Autopilot: api/operator/autopilot.md + - Keyring: api/operator/keyring.md + - License: api/operator/license.md + - Raft: api/operator/raft.md + - Segment: api/operator/segment.md + - Namespaces: api/namespaces.md + - Prepared Queries: api/prepared-queries.md + - Sessions: api/sessions.md + - Snapshots: api/snapshots.md + - Status: api/status.md + - Transactions: api/transactions.md +- Engine: + - AIOHTTP: engine/aiohttp.md + - httpx: engine/httpx.md diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 0584e0e..0000000 --- a/mypy.ini +++ /dev/null @@ -1,13 +0,0 @@ -[mypy] -platform=linux - -files = discovery -show_error_context = True -verbosity = 0 -ignore_missing_imports = True -no_implicit_optional = True - -warn_unused_configs = True -warn_return_any = True -warn_unused_ignores = True -warn_unreachable = True diff --git a/pyproject.toml b/pyproject.toml index 52d8cfc..b3491a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,3 @@ -[tool.portray] -modules = ["discovery"] - -[tool.portray.mkdocs.theme] -name = "material" -palette = {primary = "pink", accent = "gray"} +[build-system] +requires = ["setuptools >= 46.4.0"] +build-backend = "setuptools.build_meta" diff --git a/requirements-dev.txt b/requirements-dev.txt index 8ca4d8b..83233af 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,17 +1,15 @@ -# engines -aiohttp==3.6.2 -httpx==0.12.0 -# dev dependencies -mypy==0.770 -flake8==3.7.9 -pytest==5.4.1 -pytest-cov==2.8.1 -codecov==2.0.22 -pytest-asyncio==0.10.0 -pycodestyle==2.5.0 -isort==4.3.21 -black==19.10b0 -tox==3.14.5 -tox-asdf==0.1.0 -# docs -portray==1.3.1 +-r requirements.txt +mypy +black +flake8 +isort +build +pytest +codecov +pytest-cov +pytest-mock +pytest-asyncio +tox +tox-asdf +mkdocs-material +pre-commit diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b71902a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +aiohttp +httpx +click diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..885f0fd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,92 @@ +[bdist_wheel] +universal = 1 + +[metadata] +name = discovery-client +version = attr: discovery.__version__ +author = Alexandre Menezes +author_email = alexandre.fmenezes@gmail.com +description = async consul client +long_description = file: README.md +long_description_content_type = text/markdown +license = Apache-2.0 +license_file = LICENSE +url = https://github.com/amenezes/discovery-client +project_urls = + Documentation = https://discovery-client.amenezes.net + Code = https://github.com/amenezes/discovery-client + Issue tracker = https://github.com/amenezes/discovery-client/issues + Changes = https://github.com/amenezes/discovery-client/releases +classifiers = + Development Status :: 5 - Production/Stable + Framework :: AsyncIO + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Framework :: AsyncIO + Intended Audience :: Developers + Topic :: System :: Distributed Computing + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Libraries +keywords = "asyncio", "consul", "python-consul", "python-consul2" + +[options] +packages = find: +install_requires = + aiohttp >= 3.6.2 +python_requires = >= 3.8 + +[options.extras_require] +docs = mkdocs-material +cli = click>=8.1.3; rich>=12.6.0 +httpx = httpx>=0.16.1 +httpxcli = httpx>=0.16.1; click>=8.1.3; rich>=12.6.0 +all = mkdocs-material; aiohttp>=3.6.2; httpx>=0.16.1; click>=8.1.3; rich>=12.6.0 + +[options.entry_points] +console_scripts = + discovery = discovery.__main__:cli + +[flake8] +exclude = venv + __pycache__ + *.pyc + __init__.py + setup.py + examples +ignore = E501 W503 +verbose = 2 +doctests = True +show_source = True +statistics = True +count = True + +[mypy] +platform=linux +files = discovery +show_error_context = True +verbosity = 0 +ignore_missing_imports = True +no_implicit_optional = True +warn_unused_configs = True +warn_return_any = True +warn_unused_ignores = True +warn_unreachable = True + +[tox:tox] +envlist = py{38,39,310,311} + +[tool:pytest] +testpaths = tests +asyncio_mode = auto + +[testenv] +deps = -rrequirements-dev.txt +whitelist_externals = make +commands = make ci diff --git a/setup.py b/setup.py index 179a9ce..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,3 @@ -import setuptools +from setuptools import setup -from discovery import __version__ - -from collections import OrderedDict - - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="discovery-client", - version=f"{__version__}", - author="alexandre menezes", - author_email="alexandre.fmenezes@gmail.com", - description="async consul client", - long_description=long_description, - long_description_content_type="text/markdown", - license="Apache-2.0", - url="https://github.com/amenezes/discovery-client", - packages=setuptools.find_packages(include=["discovery", "discovery.*"]), - python_requires=">=3.6.0", - project_urls=OrderedDict(( - ('Documentation', 'https://discovery-client.amenezes.net'), - ('Code', 'https://github.com/amenezes/discovery-client'), - ('Issue tracker', 'https://github.com/amenezes/discovery-client/issues') - )), - install_requires=[ - "aiohttp<=3.6.2", - ], - extras_require={ - "aio": ["aiohttp<=3.6.2"], - "httpx": ["httpx>=0.12.0"], - "cli": ["cleo"], - "all": ["aiohttp<=3.6.2", "httpx>=0.12.0", "cleo"], - }, - setup_requires=["setuptools>=38.6.0"], - entry_points={"console_scripts": ["discovery=discovery.__main__:application.run [cli]"]}, - classifiers=[ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Framework :: AsyncIO", - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: System :: Distributed Computing", - "Topic :: Software Development :: Libraries", - ], -) +setup() diff --git a/conftest.py b/tests/conftest.py similarity index 60% rename from conftest.py rename to tests/conftest.py index 13c274c..148151f 100644 --- a/conftest.py +++ b/tests/conftest.py @@ -1,25 +1,55 @@ -import pytest +from contextlib import asynccontextmanager -import aiohttp +import pytest from discovery import api -from discovery.engine import AioEngine -from discovery.engine.aio import httpx_client +from discovery.client import Consul +from discovery.engine.httpx import HTTPXEngine + + +@pytest.fixture(scope="session") +def area(consul_api): + return api.Area(client=consul_api) + + +@pytest.fixture(scope="session") +def consul_api(expected=None): + yield ApiMock(expected=expected) + + +@pytest.fixture(scope="session") +def consul(consul_api): + return Consul() + + +@pytest.fixture(scope="session") +def consul_httpx(): + return Consul(client=HTTPXEngine()) @pytest.fixture -@pytest.mark.asyncio -async def aiohttp_client(): - session = aiohttp.ClientSession() - yield AioEngine(session) - await session.close() +async def segment(consul_api): + return api.Segment(client=consul_api) @pytest.fixture -@pytest.mark.asyncio -async def httpx_engine(): - client = await httpx_client() - return AioEngine(client) +async def raft(consul_api): + return api.Raft(client=consul_api) + + +@pytest.fixture +async def license(consul_api): + return api.License(client=consul_api) + + +@pytest.fixture +async def keyring(consul_api): + return api.Keyring(client=consul_api) + + +@pytest.fixture +async def autopilot(consul_api): + return api.AutoPilot(client=consul_api) class ResponseMock: @@ -45,55 +75,18 @@ def __init__(self, expected=None): self.url = "" self.expected = expected + @asynccontextmanager async def get(self, *args, **kwargs): - return ResponseMock(expected=self.expected) + yield ResponseMock(expected=self.expected) + @asynccontextmanager async def delete(self, *args, **kwargs): - return ResponseMock(expected=self.expected) + yield ResponseMock(expected=self.expected) + @asynccontextmanager async def put(self, *args, **kwargs): - return ResponseMock(expected=self.expected) + yield ResponseMock(expected=self.expected) + @asynccontextmanager async def post(self, *args, **kwargs): - return ResponseMock(expected=self.expected) - - -@pytest.fixture -def consul_api(expected=None): - return ApiMock(expected=expected) - - -@pytest.fixture -@pytest.mark.asyncio -async def segment(consul_api): - return api.Segment(client=consul_api) - - -@pytest.fixture -@pytest.mark.asyncio -async def raft(consul_api): - return api.Raft(client=consul_api) - - -@pytest.fixture -@pytest.mark.asyncio -async def license(consul_api): - return api.License(client=consul_api) - - -@pytest.fixture -@pytest.mark.asyncio -async def keyring(consul_api): - return api.Keyring(client=consul_api) - - -@pytest.fixture -@pytest.mark.asyncio -async def autopilot(consul_api): - return api.AutoPilot(client=consul_api) - - -@pytest.fixture -@pytest.mark.asyncio -def area(consul_api): - return api.Area(client=consul_api) + yield ResponseMock(expected=self.expected) diff --git a/tests/unit/api/test_acl.py b/tests/unit/api/test_acl.py index 6ed0af5..d734571 100644 --- a/tests/unit/api/test_acl.py +++ b/tests/unit/api/test_acl.py @@ -42,33 +42,26 @@ def translate_response(): @pytest.fixture -@pytest.mark.asyncio async def acl(consul_api): return api.Acl(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [bootstrap_response()]) async def test_bootstrap(acl, expected): acl.client.expected = expected response = await acl.bootstrap() - response = await response.json() assert response == bootstrap_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [replication_response()]) async def test_replication(acl, expected): acl.client.expected = expected response = await acl.replication() - response = await response.json() assert response == replication_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [translate_response()]) async def test_translate(acl, expected): acl.client.expected = expected response = await acl.translate(translate_payload()) - response = await response.text() assert response == translate_response() diff --git a/tests/unit/api/test_agent.py b/tests/unit/api/test_agent.py index 5ab7306..50bece4 100644 --- a/tests/unit/api/test_agent.py +++ b/tests/unit/api/test_agent.py @@ -1,6 +1,6 @@ import pytest -from discovery import api +from discovery import TokenType, api def stream_logs_sample(): @@ -142,7 +142,6 @@ def metrics_response(): @pytest.fixture -@pytest.mark.asyncio async def agent(consul_api): return api.Agent( api.Checks(client=consul_api), @@ -155,90 +154,70 @@ async def agent(consul_api): ) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [members_response()]) async def test_members(agent, expected): agent.client.expected = expected response = await agent.members() - response = await response.json() assert response == members_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [configuration_response()]) async def test_read_configuration(agent, expected): agent.client.expected = expected response = await agent.read_configuration() - response = await response.json() assert response == configuration_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_reload(agent, expected): - agent.client.expected = expected - response = await agent.reload() - assert response.status == 200 +async def test_reload(agent, mocker): + spy = mocker.spy(agent.client, "put") + await agent.reload() + spy.assert_called_with("/v1/agent/reload") -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_maintenance(agent, expected): - agent.client.expected = expected - response = await agent.maintenance() - assert response.status == 200 +async def test_maintenance(agent, mocker): + spy = mocker.spy(agent.client, "put") + await agent.maintenance() + spy.assert_called_with("/v1/agent/maintenance?enable=True") -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [metrics_response()]) async def test_metrics(agent, expected): agent.client.expected = expected response = await agent.metrics() - response = await response.json() assert response == metrics_response() -# @pytest.mark.skip -# @pytest.mark.asyncio -# @pytest.mark.parametrize("expected", [metrics_response()]) -# async def test_stream_logs(agent, expected): -# response = await agent.stream_logs(stream=True) -# assert response - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_join(agent, expected): - agent.client.expected = expected - response = await agent.join("1.2.3.4") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_leave(agent, expected): - agent.client.expected = expected - response = await agent.leave() - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_force_leave(agent, expected): - agent.client.expected = expected - response = await agent.force_leave("agent-one") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update_acl_token(agent, expected): - agent.client.expected = expected - response = await agent.update_acl_token("default") - assert response.status == 200 - - -@pytest.mark.asyncio -async def test_update_acl_token_invalid(agent): - with pytest.raises(ValueError): - await agent.update_acl_token("invalid") +async def test_join(agent, mocker): + spy = mocker.spy(agent.client, "put") + await agent.join("1.2.3.4") + spy.assert_called_with("/v1/agent/join/1.2.3.4") + + +async def test_leave(agent, mocker): + spy = mocker.spy(agent.client, "put") + await agent.leave() + spy.assert_called_with("/v1/agent/leave") + + +async def test_force_leave(agent, mocker): + spy = mocker.spy(agent.client, "put") + await agent.force_leave("agent-one") + spy.assert_called_with("/v1/agent/force-leave/agent-one") + + +@pytest.mark.parametrize( + "token_type", + [ + TokenType.DEFAULT, + TokenType.AGENT, + TokenType.AGENT_RECOVERY, + TokenType.REPLICATION, + TokenType.AGENT_MASTER, + TokenType.ACL_TOKEN, + TokenType.ACL_AGENT_TOKEN, + TokenType.ACL_AGENT_MASTER_TOKEN, + TokenType.ACL_REPLICATION_TOKEN, + ], +) +async def test_update_acl_token(agent, token_type): + await agent.update_acl_token("token", token_type) diff --git a/tests/unit/api/test_area.py b/tests/unit/api/test_area.py index 0b80885..9866fc6 100644 --- a/tests/unit/api/test_area.py +++ b/tests/unit/api/test_area.py @@ -3,16 +3,6 @@ import pytest -def sample_payload(): - return json.dumps( - { - "PeerDatacenter": "dc2", - "RetryJoin": ["10.1.2.3", "10.1.2.4", "10.1.2.5"], - "UseTLS": False, - } - ) - - def create_area_response(): return {"ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05"} @@ -39,10 +29,6 @@ def list_area_response(): ] -def update_area_payload(): - return {"UseTLS": True} - - def join_payload(): return ["10.1.2.3", "10.1.2.4", "10.1.2.5"] @@ -76,64 +62,53 @@ def members_response(): ) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [create_area_response()]) -async def test_create(area, expected): +async def test_create_network(area, expected): area.client.expected = expected - response = await area.create(sample_payload()) - response = await response.json() + response = await area.create_network("dc2") assert response == create_area_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_area_response()]) -async def test_list_areas(area, expected): +async def test_list_network(area, expected): area.client.expected = expected - response = await area.list() - response = await response.json() + response = await area.list_network() assert response == list_area_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_area_response()]) -async def test_list_specific_area(area, expected): +async def test_list_specific_network(area, expected): area.client.expected = expected - response = await area.list("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - response = await response.json() + response = await area.list_specific_network("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") assert response == list_area_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update(area, expected): - area.client.expected = expected - response = await area.update( - "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", update_area_payload() +async def test_update_network(area, mocker): + spy = mocker.spy(area.client, "put") + await area.update_network("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") + spy.assert_called_with( + "/v1/operator/area/8f246b77-f3e1-ff88-5b48-8ec93abf3e05", json={"UseTLS": True} ) - assert response.status == 200 -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(area, expected): - area.client.expected = expected - response = await area.delete("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - assert response.status == 200 +async def test_delete_network(area, mocker): + spy = mocker.spy(area.client, "delete") + await area.delete_network("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") + spy.assert_called_with("/v1/operator/area/8f246b77-f3e1-ff88-5b48-8ec93abf3e05") -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [join_response()]) -async def test_join(area, expected): +async def test_join_network(area, expected): area.client.expected = expected - response = await area.join("8f246b77-f3e1-ff88-5b48-8ec93abf3e05", join_payload(),) - response = await response.json() + response = await area.join_network( + "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + join_payload(), + ) assert response == join_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [members_response()]) -async def test_members(area, expected): +async def test_list_network_members(area, expected): area.client.expected = expected - response = await area.members("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - response = await response.json() + response = await area.list_network_members("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") assert response == members_response() diff --git a/tests/unit/api/test_auth_method.py b/tests/unit/api/test_auth_method.py index ab05f12..0e47cee 100644 --- a/tests/unit/api/test_auth_method.py +++ b/tests/unit/api/test_auth_method.py @@ -1,25 +1,8 @@ -import json - import pytest from discovery import api -def create_payload(): - return json.dumps( - { - "Name": "minikube", - "Type": "kubernetes", - "Description": "dev minikube cluster", - "Config": { - "Host": "https://192.0.2.42:8443", - "CACert": "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----\n", - "ServiceAccountJWT": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9...", - }, - } - ) - - def create_response(): return { "Name": "minikube", @@ -50,18 +33,6 @@ def read_response(): } -def update_payload(): - return { - "Name": "minikube", - "Description": "updated name", - "Config": { - "Host": "https://192.0.2.42:8443", - "CACert": "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----\n", - "ServiceAccountJWT": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9...", - }, - } - - def update_response(): return { "Name": "minikube", @@ -97,50 +68,57 @@ def list_response(): @pytest.fixture -@pytest.mark.asyncio async def auth_method(consul_api): return api.AuthMethod(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [create_response()]) async def test_create(auth_method, expected): auth_method.client.expected = expected - response = await auth_method.create(create_payload()) - response = await response.json() + response = await auth_method.create( + "minikube", + "kubernetes", + "dev minikube cluster", + { + "Host": "https://192.0.2.42:8443", + "CACert": "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----\n", + "ServiceAccountJWT": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9...", + }, + ) assert response == create_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [read_response()]) async def test_read(auth_method, expected): auth_method.client.expected = expected response = await auth_method.read("minikube") - response = await response.json() assert response == read_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [update_response()]) async def test_update(auth_method, expected): auth_method.client.expected = expected - response = await auth_method.update("minikube", update_payload()) - response = await response.json() + response = await auth_method.update( + "minikube", + "kubernetes", + "updated name", + { + "Host": "https://192.0.2.42:8443", + "CACert": "-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----\n", + "ServiceAccountJWT": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9...", + }, + ) assert response == update_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(auth_method, expected): - auth_method.client.expected = expected +async def test_delete(auth_method): + auth_method.client.expected = True response = await auth_method.delete("minikube") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_response()]) async def test_list(auth_method, expected): auth_method.client.expected = expected response = await auth_method.list() - response = await response.json() assert response == list_response() diff --git a/tests/unit/api/test_autopilot.py b/tests/unit/api/test_autopilot.py index 2f28bcf..27a5bca 100644 --- a/tests/unit/api/test_autopilot.py +++ b/tests/unit/api/test_autopilot.py @@ -1,97 +1,132 @@ -import json +SAMPLE_PAYLOAD = { + "CleanupDeadServers": True, + "LastContactThreshold": "100ms", + "MaxTrailingLogs": 250, + "MinQuorum": 3, + "ServerStabilizationTime": "5s", + "RedundancyZoneTag": "", + "DisableUpgradeMigration": False, + "UpgradeVersionTag": "", + "CreateIndex": 4, + "ModifyIndex": 4, +} -import pytest +CONFIG_RESPONSE = { + "CleanupDeadServers": True, + "LastContactThreshold": "200ms", + "MaxTrailingLogs": 250, + "ServerStabilizationTime": "10s", + "RedundancyZoneTag": "", + "DisableUpgradeMigration": False, + "UpgradeVersionTag": "", + "CreateIndex": 4, + "ModifyIndex": 4, +} -def sample_payload(): - return json.dumps( +HEALTH_RESPONSE = { + "Healthy": True, + "FailureTolerance": 0, + "Servers": [ { - "CleanupDeadServers": True, - "LastContactThreshold": "100ms", - "MaxTrailingLogs": 250, - "MinQuorum": 3, - "ServerStabilizationTime": "5s", - "RedundancyZoneTag": "", - "DisableUpgradeMigration": False, - "UpgradeVersionTag": "", - "CreateIndex": 4, - "ModifyIndex": 4, - } - ) - - -def config_response(): - return { - "CleanupDeadServers": True, - "LastContactThreshold": "200ms", - "MaxTrailingLogs": 250, - "ServerStabilizationTime": "10s", - "RedundancyZoneTag": "", - "DisableUpgradeMigration": False, - "UpgradeVersionTag": "", - "CreateIndex": 4, - "ModifyIndex": 4, - } + "ID": "e349749b-3303-3ddf-959c-b5885a0e1f6e", + "Name": "node1", + "Address": "127.0.0.1:8300", + "SerfStatus": "alive", + "Version": "0.7.4", + "Leader": True, + "LastContact": "0s", + "LastTerm": 2, + "LastIndex": 46, + "Healthy": True, + "Voter": True, + "StableSince": "2017-03-06T22:07:51Z", + }, + { + "ID": "e36ee410-cc3c-0a0c-c724-63817ab30303", + "Name": "node2", + "Address": "127.0.0.1:8205", + "SerfStatus": "alive", + "Version": "0.7.4", + "Leader": False, + "LastContact": "27.291304ms", + "LastTerm": 2, + "LastIndex": 46, + "Healthy": True, + "Voter": False, + "StableSince": "2017-03-06T22:18:26Z", + }, + ], +} +READ_AUTOPILOT_STATE_RESP = { + "Healthy": True, + "FailureTolerance": 1, + "OptimisticFailureTolerance": 4, + "Servers": { + "5e26a3af-f4fc-4104-a8bb-4da9f19cb278": {}, + "10b71f14-4b08-4ae5-840c-f86d39e7d330": {}, + "1fd52e5e-2f72-47d3-8cfc-2af760a0c8c2": {}, + "63783741-abd7-48a9-895a-33d01bf7cb30": {}, + "6cf04fd0-7582-474f-b408-a830b5471285": {}, + }, + "Leader": "5e26a3af-f4fc-4104-a8bb-4da9f19cb278", + "Voters": [ + "5e26a3af-f4fc-4104-a8bb-4da9f19cb278", + "10b71f14-4b08-4ae5-840c-f86d39e7d330", + "1fd52e5e-2f72-47d3-8cfc-2af760a0c8c2", + ], + "RedundancyZones": {"az1": {}, "az2": {}, "az3": {}}, + "ReadReplicas": [ + "63783741-abd7-48a9-895a-33d01bf7cb30", + "6cf04fd0-7582-474f-b408-a830b5471285", + ], + "Upgrade": {}, +} -def health_response(): - return { - "Healthy": True, - "FailureTolerance": 0, - "Servers": [ - { - "ID": "e349749b-3303-3ddf-959c-b5885a0e1f6e", - "Name": "node1", - "Address": "127.0.0.1:8300", - "SerfStatus": "alive", - "Version": "0.7.4", - "Leader": True, - "LastContact": "0s", - "LastTerm": 2, - "LastIndex": 46, - "Healthy": True, - "Voter": True, - "StableSince": "2017-03-06T22:07:51Z", - }, - { - "ID": "e36ee410-cc3c-0a0c-c724-63817ab30303", - "Name": "node2", - "Address": "127.0.0.1:8205", - "SerfStatus": "alive", - "Version": "0.7.4", - "Leader": False, - "LastContact": "27.291304ms", - "LastTerm": 2, - "LastIndex": 46, - "Healthy": True, - "Voter": False, - "StableSince": "2017-03-06T22:18:26Z", - }, - ], - } - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [config_response()]) -async def test_read_configuration(autopilot, expected): - autopilot.client.expected = expected +async def test_read_configuration(autopilot): + autopilot.client.expected = CONFIG_RESPONSE response = await autopilot.read_configuration() - response = await response.json() - assert response == config_response() + assert response == CONFIG_RESPONSE -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update_configuration(autopilot, expected): - autopilot.client.expected = expected - response = await autopilot.update_configuration(sample_payload) - assert response.status == 200 +async def test_update_configuration(autopilot, mocker): + spy = mocker.spy(autopilot.client, "put") + await autopilot.update_configuration(SAMPLE_PAYLOAD) + spy.assert_called_with( + "/v1/operator/autopilot/configuration", + json={ + "CleanupDeadServers": { + "CleanupDeadServers": True, + "LastContactThreshold": "100ms", + "MaxTrailingLogs": 250, + "MinQuorum": 3, + "ServerStabilizationTime": "5s", + "RedundancyZoneTag": "", + "DisableUpgradeMigration": False, + "UpgradeVersionTag": "", + "CreateIndex": 4, + "ModifyIndex": 4, + }, + "LastContactThreshold": "200ms", + "MaxTrailingLogs": 250, + "MinQuorum": 0, + "ServerStabilizationTime": "10s", + "RedundancyZoneTag": "", + "DisableUpgradeMigration": False, + "UpgradeVersionTag": "", + }, + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [health_response()]) -async def test_read_health(autopilot, expected): - autopilot.client.expected = expected +async def test_read_health(autopilot): + autopilot.client.expected = HEALTH_RESPONSE response = await autopilot.read_health() - response = await response.json() - assert response == health_response() + assert response == HEALTH_RESPONSE + + +async def test_read_state(autopilot): + autopilot.client.expected = READ_AUTOPILOT_STATE_RESP + response = await autopilot.read_state() + assert response == READ_AUTOPILOT_STATE_RESP diff --git a/tests/unit/api/test_binding_rule.py b/tests/unit/api/test_binding_rule.py index 5cf09bd..f6d0d35 100644 --- a/tests/unit/api/test_binding_rule.py +++ b/tests/unit/api/test_binding_rule.py @@ -3,25 +3,6 @@ from discovery import api -def sample_payload(): - return { - "Description": "example rule", - "AuthMethod": "minikube", - "Selector": "serviceaccount.namespace==default", - "BindType": "service", - "BindName": "{{ serviceaccount.name }}", - } - - -def update_payload(): - return { - "Description": "updated rule", - "Selector": "serviceaccount.namespace=dev", - "BindType": "role", - "BindName": "{{ serviceaccount.name }}", - } - - def update_response(): return { "ID": "000ed53c-e2d3-e7e6-31a5-c19bc3518a3d", @@ -72,52 +53,46 @@ def list_binding_response(): @pytest.fixture -@pytest.mark.asyncio async def binding_rule(consul_api): return api.BindingRule(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_create(binding_rule, expected): binding_rule.client.expected = expected - response = await binding_rule.create(sample_payload()) - response = await response.json() + response = await binding_rule.create( + "minikube", "service", r"{{ serviceaccount.name }}" + ) assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_read(binding_rule, expected): binding_rule.client.expected = expected response = await binding_rule.read("000ed53c-e2d3-e7e6-31a5-c19bc3518a3d") - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [update_response()]) async def test_update(binding_rule, expected): binding_rule.client.expected = expected response = await binding_rule.update( - "000ed53c-e2d3-e7e6-31a5-c19bc3518a3d", update_payload() + "000ed53c-e2d3-e7e6-31a5-c19bc3518a3d", + "minikube", + "role", + r"{{ serviceaccount.name }}", ) - response = await response.json() assert response == update_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(binding_rule, expected): - binding_rule.client.expected = expected +async def test_delete(binding_rule): + binding_rule.client.expected = True response = await binding_rule.delete("000ed53c-e2d3-e7e6-31a5-c19bc3518a3d") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_binding_response()]) async def test_list(binding_rule, expected): binding_rule.client.expected = expected response = await binding_rule.list() - response = await response.json() assert response == list_binding_response() diff --git a/tests/unit/api/test_ca.py b/tests/unit/api/test_ca.py index d0b0e83..781c242 100644 --- a/tests/unit/api/test_ca.py +++ b/tests/unit/api/test_ca.py @@ -58,32 +58,44 @@ def update_payload(): @pytest.fixture -@pytest.mark.asyncio async def ca(consul_api): return api.CA(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [ca_roots_response()]) -async def test_roots(ca, expected): +async def test_list_root_certificates(ca, expected): ca.client.expected = expected - response = await ca.roots() - response = await response.json() + response = await ca.list_root_certificates() assert response == ca_roots_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [ca_configuration_response()]) async def test_configuration(ca, expected): ca.client.expected = expected response = await ca.configuration() - response = await response.json() assert response == ca_configuration_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update(ca, expected): - ca.client.expected = expected - response = await ca.update(update_payload()) - response.status == 200 +async def test_update_configuration(ca, mocker): + spy = mocker.spy(ca.client, "put") + await ca.update_configuration( + "consul", + { + "LeafCertTTL": "72h", + "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----...", + "RootCert": "-----BEGIN CERTIFICATE-----...", + "IntermediateCertTTL": "8760h", + }, + ) + spy.assert_called_with( + "/v1/connect/ca/configuration", + json={ + "Provider": "consul", + "Config": { + "LeafCertTTL": "72h", + "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----...", + "RootCert": "-----BEGIN CERTIFICATE-----...", + "IntermediateCertTTL": "8760h", + }, + }, + ) diff --git a/tests/unit/api/test_catalog.py b/tests/unit/api/test_catalog.py index 9304efe..9616ddd 100644 --- a/tests/unit/api/test_catalog.py +++ b/tests/unit/api/test_catalog.py @@ -5,50 +5,6 @@ from discovery import api -def sample_payload(): - return json.dumps( - { - "Datacenter": "dc1", - "ID": "40e4a748-2192-161a-0510-9bf59fe950b5", - "Node": "foobar", - "Address": "192.168.10.10", - "TaggedAddresses": {"lan": "192.168.10.10", "wan": "10.0.10.10"}, - "NodeMeta": {"somekey": "somevalue"}, - "Service": { - "ID": "redis1", - "Service": "redis", - "Tags": ["primary", "v1"], - "Address": "127.0.0.1", - "TaggedAddresses": { - "lan": {"address": "127.0.0.1", "port": 8000}, - "wan": {"address": "198.18.0.1", "port": 80}, - }, - "Meta": {"redis_version": "4.0"}, - "Port": 8000, - }, - "Check": { - "Node": "foobar", - "CheckID": "service:redis1", - "Name": "Redis health check", - "Notes": "Script based health check", - "Status": "passing", - "ServiceID": "redis1", - "Definition": { - "TCP": "localhost:8888", - "Interval": "5s", - "Timeout": "1s", - "DeregisterCriticalServiceAfter": "30s", - }, - }, - "SkipNodeUpdate": False, - } - ) - - -def deregister_payload(): - return {"Datacenter": "dc1", "Node": "foobar"} - - def list_nodes_response(): return [ { @@ -153,71 +109,67 @@ async def catalog(consul_api): return api.Catalog(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_register(catalog, expected): - catalog.client.expected = expected - response = await catalog.register(sample_payload()) - assert response.status == 200 +async def test_register_entity(catalog, mocker): + spy = mocker.spy(catalog.client, "put") + await catalog.register_entity( + "192.168.10.10", "dc1", "t2.320", "40e4a748-2192-161a-0510-9bf59fe950b5" + ) + spy.assert_called_with( + "/v1/catalog/register", + json={ + "Datacenter": "dc1", + "ID": "40e4a748-2192-161a-0510-9bf59fe950b5", + "Node": "t2.320", + "Address": "192.168.10.10", + }, + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_deregister(catalog, expected): - catalog.client.expected = expected - response = await catalog.deregister(deregister_payload()) - assert response.status == 200 +async def test_deregister_entity(catalog, mocker): + spy = mocker.spy(catalog.client, "put") + await catalog.deregister_entity("t2.320", "dc1") + spy.assert_called_with( + "/v1/catalog/deregister", json={"Datacenter": "dc1", "Node": "t2.320"} + ) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_datacenters_response()]) async def test_datacenters(catalog, expected): catalog.client.expected = expected - response = await catalog.datacenters() - response = await response.json() + response = await catalog.list_datacenters() assert response == list_datacenters_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_nodes_response()]) -async def test_nodes(catalog, expected): +async def test_list_nodes(catalog, expected): catalog.client.expected = expected - response = await catalog.nodes() - response = await response.json() + response = await catalog.list_nodes() assert response == list_nodes_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [services_response()]) -async def test_services(catalog, expected): +async def test_list_services(catalog, expected): catalog.client.expected = expected - response = await catalog.services() - response = await response.json() + response = await catalog.list_services() assert response == services_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_response()]) -async def test_service(catalog, expected): +async def test_list_nodes_for_service(catalog, expected): catalog.client.expected = expected - response = await catalog.service("my-service") - response = await response.json() + response = await catalog.list_nodes_for_service("my-service") assert response == service_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_response()]) -async def test_connect(catalog, expected): +async def test_list_nodes_for_connect(catalog, expected): catalog.client.expected = expected - response = await catalog.connect("my-service") - response = await response.json() + response = await catalog.list_nodes_for_connect("my-service") assert response == service_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [map_services_node_response()]) -async def test_node(catalog, expected): +async def test_services_for_node(catalog, expected): catalog.client.expected = expected - response = await catalog.node("my-node") - response = await response.json() + response = await catalog.services_for_node("my-node") assert response == map_services_node_response() diff --git a/tests/unit/api/test_checks.py b/tests/unit/api/test_checks.py index 6fc983f..2e64ac3 100644 --- a/tests/unit/api/test_checks.py +++ b/tests/unit/api/test_checks.py @@ -1,108 +1,120 @@ import pytest from discovery import api - - -def checks_response(): - return { - "service:redis": { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis", - "ServiceTags": ["primary"], - } - } - - -def register_payload(): - return { - "ID": "mem", - "Name": "Memory utilization", - "Notes": "Ensure we don't oversubscribe memory", - "DeregisterCriticalServiceAfter": "90m", - "Args": ["/usr/local/bin/check_mem.py"], - "DockerContainerID": "f972c95ebf0e", - "Shell": "/bin/bash", - "HTTP": "https://example.com", - "Method": "POST", - "Header": {"Content-Type": "application/json"}, - "Body": '{"check":"mem"}', - "TCP": "example.com:22", - "Interval": "10s", - "Timeout": "5s", - "TLSSkipVerify": True, +from discovery.api.check_status import CheckStatus + +CHECKS_RESPONSE = { + "service:redis": { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis", + "ServiceTags": ["primary"], } +} + + +REGISTER_PAYLOAD = { + "ID": "mem", + "Name": "Memory utilization", + "Notes": "Ensure we don't oversubscribe memory", + "DeregisterCriticalServiceAfter": "90m", + "Args": ["/usr/local/bin/check_mem.py"], + "DockerContainerID": "f972c95ebf0e", + "Shell": "/bin/bash", + "HTTP": "https://example.com", + "Method": "POST", + "Header": {"Content-Type": "application/json"}, + "Body": '{"check":"mem"}', + "TCP": "example.com:22", + "Interval": "10s", + "Timeout": "5s", + "TLSSkipVerify": True, +} @pytest.fixture -@pytest.mark.asyncio async def checks(consul_api): return api.Checks(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [checks_response()]) -async def test_list_checks(checks, expected): - checks.client.expected = expected - response = await checks.checks() - response = await response.json() - assert response == checks_response() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_register(checks, expected): - checks.client.expected = expected - response = await checks.register(register_payload) - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_deregister(checks, expected): - checks.client.expected = expected - response = await checks.deregister("my-check-id") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_check_pass(checks, expected): - checks.client.expected = expected - response = await checks.check_pass("my-check-id") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_check_warn(checks, expected): - checks.client.expected = expected - response = await checks.check_warn("my-check-id") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_check_fail(checks, expected): - checks.client.expected = expected - response = await checks.check_fail("my-check-id") - assert response.status == 200 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_check_update(checks, expected): - checks.client.expected = expected - response = await checks.check_update("my-check-id", "passing") - assert response.status == 200 - - -@pytest.mark.asyncio -async def test_check_update_value_error(checks): +async def test_list_checks(checks): + checks.client.expected = CHECKS_RESPONSE + response = await checks.list() + assert response == CHECKS_RESPONSE + + +async def test_register(checks, mocker): + spy = mocker.spy(checks.client, "put") + await checks.register(REGISTER_PAYLOAD) + spy.assert_called_with( + "/v1/agent/check/register", + json={ + "ID": "mem", + "Name": "Memory utilization", + "Notes": "Ensure we don't oversubscribe memory", + "DeregisterCriticalServiceAfter": "90m", + "Args": ["/usr/local/bin/check_mem.py"], + "DockerContainerID": "f972c95ebf0e", + "Shell": "/bin/bash", + "HTTP": "https://example.com", + "Method": "POST", + "Header": {"Content-Type": "application/json"}, + "Body": '{"check":"mem"}', + "TCP": "example.com:22", + "Interval": "10s", + "Timeout": "5s", + "TLSSkipVerify": True, + }, + ) + + +async def test_deregister(checks, mocker): + spy = mocker.spy(checks.client, "put") + await checks.deregister("my-check-id") + spy.assert_called_with("/v1/agent/check/deregister/my-check-id") + + +async def test_check_pass(checks, mocker): + spy = mocker.spy(checks.client, "put") + await checks.check_pass("my-check-id") + spy.assert_called_with("/v1/agent/check/pass/my-check-id") + + +async def test_check_warn(checks, mocker): + spy = mocker.spy(checks.client, "put") + await checks.check_warn("my-check-id") + spy.assert_called_with("/v1/agent/check/warn/my-check-id") + + +async def test_check_fail(checks, mocker): + spy = mocker.spy(checks.client, "put") + await checks.check_fail("my-check-id") + spy.assert_called_with("/v1/agent/check/fail/my-check-id") + + +@pytest.mark.parametrize( + "status, expected", + [ + ("passing", "passing"), + (CheckStatus.WARNING, CheckStatus.WARNING), + (CheckStatus.CRITICAL, "critical"), + ], +) +async def test_check_update(checks, mocker, status, expected): + spy = mocker.spy(checks.client, "put") + await checks.check_update("my-check-id", status) + spy.assert_called_with( + "/v1/agent/check/update/my-check-id", + json={"status": expected, "output": ""}, + ) + + +@pytest.mark.parametrize("status", ["ok", "pass", "", "PASSING"]) +async def test_check_update_invalid_status(checks, status): with pytest.raises(ValueError): - await checks.check_update("my-check-id", "ok") + await checks.check_update("my-check-id", status) diff --git a/tests/unit/api/test_config.py b/tests/unit/api/test_config.py index 86d5049..75eebba 100644 --- a/tests/unit/api/test_config.py +++ b/tests/unit/api/test_config.py @@ -1,6 +1,7 @@ import pytest from discovery import api +from discovery.api.kind import Kind def sample_payload(): @@ -41,58 +42,34 @@ def list_response(): @pytest.fixture -@pytest.mark.asyncio def config(consul_api): return api.Config(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_apply(config, expected): - config.client.expected = expected - response = await config.apply(sample_payload()) - assert response.status == 200 +async def test_apply(config, mocker): + spy = mocker.spy(config.client, "put") + await config.apply(sample_payload()) + spy.assert_called_with( + "/v1/config", + json={"Kind": "service-defaults", "Name": "web", "Protocol": "http"}, + ) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [config_response()]) -async def test_get_success(config, expected): +async def test_get(config, expected): config.client.expected = expected - response = await config.get("service-defaults", "web") - response = await response.json() + response = await config.get(Kind.SERVICE_DEFAULTS, "web") assert response == config_response() -@pytest.mark.asyncio -async def test_get_value_error(config): - with pytest.raises(ValueError): - await config.get("service", "web") - - -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_response()]) -async def test_list_success(config, expected): +async def test_list(config, expected): config.client.expected = expected - response = await config.list("service-defaults") - response = await response.json() + response = await config.list(Kind.SERVICE_DEFAULTS) assert response == list_response() -@pytest.mark.asyncio -async def test_list_value_error(config): - with pytest.raises(ValueError): - await config.list("service") - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete_success(config, expected): - config.client.expected = expected - response = await config.delete("service-defaults", "web") - assert response.status == 200 - - -@pytest.mark.asyncio -async def test_delete_value_error(config): - with pytest.raises(ValueError): - await config.delete("service", "web") +async def test_delete(config, mocker): + spy = mocker.spy(config.client, "delete") + await config.delete(Kind.SERVICE_DEFAULTS, "web") + spy.assert_called_with("/v1/config/service-defaults/web") diff --git a/tests/unit/api/test_connect.py b/tests/unit/api/test_connect.py index 5713945..1178c52 100644 --- a/tests/unit/api/test_connect.py +++ b/tests/unit/api/test_connect.py @@ -49,12 +49,10 @@ @pytest.fixture -@pytest.mark.asyncio async def connect(consul_api): return api.Connect(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [authorize_response]) async def test_authorize(connect, expected): connect.client.expected = expected @@ -63,11 +61,9 @@ async def test_authorize(connect, expected): "spiffe://dc1-7e567ac2-551d-463f-8497-f78972856fc1.consul/ns/default/dc/dc1/svc/web", "04:00:00:00:00:01:15:4b:5a:c3:94", ) - response = await response.json() assert response == authorize_response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [authorize_response]) async def test_authorize_with_namespace(connect, expected): connect.client.expected = expected @@ -77,23 +73,18 @@ async def test_authorize_with_namespace(connect, expected): "04:00:00:00:00:01:15:4b:5a:c3:94", "default", ) - response = await response.json() assert response == authorize_response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [ca_roots_response]) async def test_ca_roots(connect, expected): connect.client.expected = expected response = await connect.ca_roots() - response = await response.json() assert response == ca_roots_response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [leaf_certificate_response]) async def test_leaf_certificate(connect, expected): connect.client.expected = expected response = await connect.leaf_certificate("web") - response = await response.json() assert response == leaf_certificate_response diff --git a/tests/unit/api/test_coordinate.py b/tests/unit/api/test_coordinate.py index eb1911b..d847fe0 100644 --- a/tests/unit/api/test_coordinate.py +++ b/tests/unit/api/test_coordinate.py @@ -38,41 +38,44 @@ def lan_response(): @pytest.fixture -@pytest.mark.asyncio async def coordinate(consul_api): return api.Coordinate(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [wan_response()]) async def test_read_wan(coordinate, expected): coordinate.client.expected = expected response = await coordinate.read_wan() - response = await response.json() assert response == wan_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [lan_response()]) -async def test_read_lan(coordinate, expected): +async def test_read_lan_for_all_nodes(coordinate, expected): coordinate.client.expected = expected - response = await coordinate.read_lan() - response = await response.json() + response = await coordinate.read_lan_for_all_nodes() assert response == lan_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [lan_response()]) -async def test_read_lan_node(coordinate, expected): +async def test_read_lan_for_node(coordinate, expected): coordinate.client.expected = expected - response = await coordinate.read_lan_node("agent-one") - response = await response.json() + response = await coordinate.read_lan_for_node("agent-one") assert response == lan_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update_lan_node(coordinate, expected): - coordinate.client.expected = expected - response = await coordinate.update_lan_node(sample_payload()) - assert response.status == 200 +async def test_update_lan_for_node(coordinate, mocker): + spy = mocker.spy(coordinate.client, "put") + await coordinate.update_lan_for_node(sample_payload()) + spy.assert_called_with( + "/v1/coordinate/update", + json={ + "Node": "agent-one", + "Segment": "", + "Coord": { + "Adjustment": 0, + "Error": 1.5, + "Height": 0, + "Vec": [0, 0, 0, 0, 0, 0, 0, 0], + }, + }, + ) diff --git a/tests/unit/api/test_event.py b/tests/unit/api/test_event.py index 59ae038..db49d30 100644 --- a/tests/unit/api/test_event.py +++ b/tests/unit/api/test_event.py @@ -25,24 +25,19 @@ def list_response(): @pytest.fixture -@pytest.mark.asyncio async def events(consul_api): return api.Events(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_fire(events, expected): events.client.expected = expected - response = await events.fire("my-event", sample_payload()) - response = await response.json() + response = await events.fire_event("my-event", sample_payload()) assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_response()]) async def test_list(events, expected): events.client.expected = expected - response = await events.list("my-event") - response = await response.json() + response = await events.list() assert response == list_response() diff --git a/tests/unit/api/test_health.py b/tests/unit/api/test_health.py index f840eaf..ee31c3c 100644 --- a/tests/unit/api/test_health.py +++ b/tests/unit/api/test_health.py @@ -136,57 +136,40 @@ def sample_state_response(): @pytest.fixture -@pytest.mark.asyncio async def health(consul_api): return api.Health(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) -async def test_node(health, expected): +async def test_checks_for_node(health, expected): health.client.expected = expected - response = await health.node("my-node") - response = await response.json() + response = await health.checks_for_node("my-node") assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_response()]) -async def test_checks(health, expected): +async def test_checks_for_service(health, expected): health.client.expected = expected - response = await health.checks("my-service") - response = await response.json() + response = await health.checks_for_service("my-service") assert response == service_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [nodes_for_service_response()]) -async def test_service(health, expected): +async def test_service_instances(health, expected): health.client.expected = expected - response = await health.service("my-service") - response = await response.json() + response = await health.service_instances("my-service") assert response == nodes_for_service_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_response()]) -async def test_connect(health, expected): +async def test_service_instances_for_connect(health, expected): health.client.expected = expected - response = await health.connect("consul") - response = await response.json() + response = await health.service_instances_for_connect("consul") assert response == service_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_state_response()]) -async def test_state_success(health, expected): +async def test_checks_in_state(health, expected): health.client.expected = expected - response = await health.state("passing") - response = await response.json() + response = await health.checks_in_state() assert response == sample_state_response() - - -@pytest.mark.asyncio -async def test_state_value_error(health): - with pytest.raises(ValueError): - await health.state("ok") diff --git a/tests/unit/api/test_intention.py b/tests/unit/api/test_intention.py index d06aa92..987564a 100644 --- a/tests/unit/api/test_intention.py +++ b/tests/unit/api/test_intention.py @@ -1,21 +1,8 @@ -import json - import pytest from discovery import api -def sample_payload(): - return json.dumps( - { - "SourceName": "web", - "DestinationName": "db", - "SourceType": "consul", - "Action": "allow", - } - ) - - def sample_response(): return {"ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05"} @@ -90,87 +77,47 @@ def check_response(): return {"Allowed": True} -def update_payload(): - return json.dumps( - { - "SourceName": "web", - "DestinationName": "other-db", - "SourceType": "consul", - "Action": "allow", - } - ) - - @pytest.fixture -@pytest.mark.asyncio async def intention(consul_api): return api.Intentions(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) -async def test_create_intention(intention, expected): +async def test_upsert_by_name(intention, expected): intention.client.expected = expected - response = await intention.create(sample_payload()) - response = await response.json() + response = await intention.upsert_by_name("web", "db") assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_intent_response()]) -async def test_read(intention, expected): +async def test_read_by_name(intention, expected): intention.client.expected = expected - response = await intention.read("e9ebc19f-d481-42b1-4871-4d298d3acd5c") - response = await response.json() + response = await intention.read_by_name("web", "db") assert response == sample_intent_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_intentions_response()]) async def test_list(intention, expected): intention.client.expected = expected response = await intention.list() - response = await response.json() assert response == sample_intentions_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update(intention, expected): - intention.client.expected = expected - response = await intention.update( - "e9ebc19f-d481-42b1-4871-4d298d3acd5c", update_payload() - ) - assert response.status == 200 +async def test_delete_by_name(intention, mocker): + spy = mocker.spy(intention.client, "delete") + await intention.delete_by_name("web", "db") + spy.assert_called_with("/v1/connect/intentions/exact?source=web&destination=db") -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(intention, expected): - intention.client.expected = expected - response = await intention.delete("e9ebc19f-d481-42b1-4871-4d298d3acd5c") - assert response.status == 200 - - -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [check_response()]) async def test_check(intention, expected): intention.client.expected = expected response = await intention.check("web", "db") - response = await response.json() assert response == check_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_match_response()]) -async def test_match_success(intention, expected): +async def test_list_match(intention, expected): intention.client.expected = expected - response = await intention.match("source", "web") - response = await response.json() + response = await intention.list_match("web") assert response == list_match_response() - - -@pytest.mark.asyncio -async def test_match_invalid(intention): - with pytest.raises(ValueError): - await intention.match("origin", "web") diff --git a/tests/unit/api/test_keyring.py b/tests/unit/api/test_keyring.py index b29fd41..13bc814 100644 --- a/tests/unit/api/test_keyring.py +++ b/tests/unit/api/test_keyring.py @@ -34,34 +34,32 @@ def sample_response(): ] -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) -async def test_list(keyring, expected): +async def test_list_keys(keyring, expected): keyring.client.expected = expected - response = await keyring.list() - response = await response.json() + response = await keyring.list_keys() assert response == sample_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_add(keyring, expected): - keyring.client.expected = expected - response = await keyring.add(sample_payload()) - assert response.status == 200 +async def test_add_encryption_key(keyring, mocker): + spy = mocker.spy(keyring.client, "post") + await keyring.add_encryption_key(sample_payload()) + spy.assert_called_with( + "/v1/operator/keyring", json={"Key": '{"Key": "3lg9DxVfKNzI8O+IQ5Ek+Q=="}'} + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_change(keyring, expected): - keyring.client.expected = expected - response = await keyring.change(sample_payload()) - assert response.status == 200 +async def test_change_encryption_key(keyring, mocker): + spy = mocker.spy(keyring.client, "put") + await keyring.change_encryption_key(sample_payload()) + spy.assert_called_with( + "/v1/operator/keyring", json={"Key": '{"Key": "3lg9DxVfKNzI8O+IQ5Ek+Q=="}'} + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(keyring, expected): - keyring.client.expected = expected - response = await keyring.delete(sample_payload()) - assert response.status == 200 +async def test_delete_encryption_key(keyring, mocker): + spy = mocker.spy(keyring.client, "delete") + await keyring.delete_encryption_key(sample_payload()) + spy.assert_called_with( + "/v1/operator/keyring", json={"Key": '{"Key": "3lg9DxVfKNzI8O+IQ5Ek+Q=="}'} + ) diff --git a/tests/unit/api/test_kv.py b/tests/unit/api/test_kv.py index 8087e21..3e530bc 100644 --- a/tests/unit/api/test_kv.py +++ b/tests/unit/api/test_kv.py @@ -25,39 +25,31 @@ def read_response(): @pytest.fixture -@pytest.mark.asyncio async def kv(consul_api): return api.Kv(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_create(kv, expected): - kv.client.expected = expected +async def test_create(kv): + kv.client.expected = True + response = await kv.create("test_key", sample_data()) - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [read_response()]) async def test_read(kv, expected): kv.client.expected = expected response = await kv.read("test_key") - response = await response.json() assert response == read_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update(kv, expected): - kv.client.expected = expected +async def test_update(kv): + kv.client.expected = True response = await kv.update("test_key", sample_data()) - assert response.status == 200 + assert response -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(kv, expected): - kv.client.expected = expected +async def test_delete(kv): + kv.client.expected = True response = await kv.delete("test_key") - assert response.status == 200 + assert response diff --git a/tests/unit/api/test_license.py b/tests/unit/api/test_license.py index e6e46de..344ac3b 100644 --- a/tests/unit/api/test_license.py +++ b/tests/unit/api/test_license.py @@ -31,28 +31,22 @@ def sample_payload(*args, **kwargs): ) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_payload()]) async def test_current(license, expected): license.client.expected = expected response = await license.current() - response = await response.json() assert response == sample_payload() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_payload()]) async def test_update(license, expected): license.client.expected = expected response = await license.update(data={}) - data = await response.json() - assert data == sample_payload() + assert response == sample_payload() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_payload()]) async def test_reset(license, expected): license.client.expected = expected response = await license.reset() - data = await response.json() - assert data == sample_payload() + assert response == sample_payload() diff --git a/tests/unit/api/test_namespace.py b/tests/unit/api/test_namespace.py index 15bebc3..b8807ae 100644 --- a/tests/unit/api/test_namespace.py +++ b/tests/unit/api/test_namespace.py @@ -51,21 +51,21 @@ def create_response(): } -def update_payload(): - return { - "Description": "Namespace for Team 1", - "ACLs": { - "PolicyDefaults": [ - {"ID": "77117cf6-d976-79b0-d63b-5a36ac69c8f1"}, - {"Name": "node-read"}, - ], - "RoleDefaults": [ - {"ID": "69748856-ae69-d620-3ec4-07844b3c6be7"}, - {"Name": "ns-team-2-read"}, - ], - }, - "Meta": {"foo": "bar"}, - } +# def update_payload(): +# return { +# "Description": "Namespace for Team 1", +# "ACLs": { +# "PolicyDefaults": [ +# {"ID": "77117cf6-d976-79b0-d63b-5a36ac69c8f1"}, +# {"Name": "node-read"}, +# ], +# "RoleDefaults": [ +# {"ID": "69748856-ae69-d620-3ec4-07844b3c6be7"}, +# {"Name": "ns-team-2-read"}, +# ], +# }, +# "Meta": {"foo": "bar"}, +# } def update_response(): @@ -159,51 +159,40 @@ def list_all_response(): @pytest.fixture -@pytest.mark.asyncio async def namespace(consul_api): return api.Namespace(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [create_response()]) async def test_create(namespace, expected): namespace.client.expected = expected response = await namespace.create(create_payload()) - response = await response.json() assert response == create_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [create_response()]) async def test_read(namespace, expected): namespace.client.expected = expected response = await namespace.read("team-1") - data = await response.json() - assert data == create_response() + assert response == create_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [update_response()]) async def test_update(namespace, expected): namespace.client.expected = expected - response = await namespace.update("team-1", data=update_payload()) - response = await response.json() + response = await namespace.update("team-1", "Namespace for Team 1") assert response == update_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [delete_response()]) async def test_delete(namespace, expected): namespace.client.expected = expected response = await namespace.delete("team-1") - response = await response.json() assert response == delete_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_all_response()]) async def test_list_all(namespace, expected): namespace.client.expected = expected response = await namespace.list_all() - data = await response.json() - assert data == list_all_response() + assert response == list_all_response() diff --git a/tests/unit/api/test_operator.py b/tests/unit/api/test_operator.py index b92c605..361cacd 100644 --- a/tests/unit/api/test_operator.py +++ b/tests/unit/api/test_operator.py @@ -4,38 +4,37 @@ @pytest.fixture -@pytest.mark.asyncio async def operator(consul_api, area, autopilot, keyring, license, raft, segment): return api.Operator( - area, autopilot, keyring, license, raft, segment, client=consul_api, + area, + autopilot, + keyring, + license, + raft, + segment, + client=consul_api, ) -@pytest.mark.asyncio async def test_area(operator, area): assert operator.area == area -@pytest.mark.asyncio async def test_autopilot(operator, autopilot): assert operator.autopilot == autopilot -@pytest.mark.asyncio async def test_keyring(operator, keyring): assert operator.keyring == keyring -@pytest.mark.asyncio async def test_license(operator, license): assert operator.license == license -@pytest.mark.asyncio async def test_raft(operator, raft): assert operator.raft == raft -@pytest.mark.asyncio async def test_segment(operator, segment): assert operator.segment == segment diff --git a/tests/unit/api/test_policy.py b/tests/unit/api/test_policy.py index 08914c1..099c50f 100644 --- a/tests/unit/api/test_policy.py +++ b/tests/unit/api/test_policy.py @@ -3,15 +3,6 @@ from discovery import api -def sample_payload(): - return { - "Name": "node-read", - "Description": "Grants read access to all node information", - "Rules": "node_prefix '' { policy = 'read'}", - "Datacenters": ["dc1"], - } - - def sample_response(): return { "ID": "e359bd81-baca-903e-7e64-1ccd9fdc78f5", @@ -49,52 +40,50 @@ def sample_list_response(): @pytest.fixture -@pytest.mark.asyncio async def policy(consul_api): return api.Policy(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_create(policy, expected): policy.client.expected = expected - response = await policy.create(sample_payload()) - response = await response.json() + response = await policy.create( + "node-read", + "Grants read access to all node information", + 'node_prefix "" { policy = "read"}', + ["dc1"], + ) assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_read(policy, expected): policy.client.expected = expected response = await policy.read("e359bd81-baca-903e-7e64-1ccd9fdc78f5") - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_update(policy, expected): policy.client.expected = expected response = await policy.update( - "e359bd81-baca-903e-7e64-1ccd9fdc78f5", sample_payload() + "c01a1f82-44be-41b0-a686-685fb6e0f485", + "register-app-service", + "Grants write permissions necessary to register the 'app' service", + 'service "app" { policy = "write"}', + ["dc1"], ) - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(policy, expected): - policy.client.expected = expected +async def test_delete(policy): + policy.client.expected = True response = await policy.delete("e359bd81-baca-903e-7e64-1ccd9fdc78f5") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_list_response()]) async def test_list(policy, expected): policy.client.expected = expected response = await policy.list() - response = await response.json() assert response == sample_list_response() diff --git a/tests/unit/api/test_query.py b/tests/unit/api/test_query.py index 1c34d1e..b3c3b5b 100644 --- a/tests/unit/api/test_query.py +++ b/tests/unit/api/test_query.py @@ -3,24 +3,6 @@ from discovery import api -def sample_payload(): - return { - "Name": "my-query", - "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", - "Token": "", - "Service": { - "Service": "redis", - "Failover": {"NearestN": 3, "Datacenters": ["dc1", "dc2"]}, - "Near": "node1", - "OnlyPassing": False, - "Tags": ["primary", "!experimental"], - "NodeMeta": {"instance_type": "m3.large"}, - "ServiceMeta": {"environment": "production"}, - }, - "DNS": {"TTL": "10s"}, - } - - def sample_response(): return {"ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05"} @@ -121,69 +103,71 @@ def sample_explain_response(): @pytest.fixture -@pytest.mark.asyncio def query(consul_api): return api.Query(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_create(query, expected): query.client.expected = expected - response = await query.create(sample_payload()) - response = await response.json() + response = await query.create( + "my-query", + { + "Service": "redis", + "Failover": {"NearestN": 3, "Datacenters": ["dc1", "dc2"]}, + "Near": "node1", + "OnlyPassing": False, + "Tags": ["primary", "!experimental"], + "NodeMeta": {"instance_type": "m3.large"}, + "ServiceMeta": {"environment": "production"}, + }, + session="adf4238a-882b-9ddc-4a9d-5b6758e4159e", + dns={"TTL": "10s"}, + ) assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_read_response()]) -async def test_read_without_uuid(query, expected): - query.client.expected = expected - response = await query.read() - response = await response.json() - assert response == sample_read_response() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [sample_read_response()]) -async def test_read_with_uuid(query, expected): +async def test_read(query, expected): query.client.expected = expected response = await query.read("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - response = await response.json() assert response == sample_read_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(query, expected): - query.client.expected = expected - response = await query.delete("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - assert response.status == 200 +async def test_delete(query, mocker): + spy = mocker.spy(query.client, "delete") + await query.delete("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") + spy.assert_called_with("/v1/query/8f246b77-f3e1-ff88-5b48-8ec93abf3e05") -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_update(query, expected): - query.client.expected = expected - response = await query.update( - "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", sample_payload() +async def test_update(query): + await query.update( + "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + name="my-query", + session="adf4238a-882b-9ddc-4a9d-5b6758e4159e", + token="", + service={ + "Service": "redis", + "Failover": {"NearestN": 3, "Datacenters": ["dc1", "dc2"]}, + "Near": "node1", + "OnlyPassing": False, + "Tags": ["primary", "!experimental"], + "NodeMeta": {"instance_type": "m3.large"}, + "ServiceMeta": {"environment": "production"}, + }, + dns={"TTL": "10s"}, ) - assert response.status == 200 -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_execute_response()]) async def test_execute(query, expected): query.client.expected = expected response = await query.execute("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - response = await response.json() assert response == sample_execute_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_explain_response()]) async def test_explain(query, expected): query.client.expected = expected response = await query.explain("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - response = await response.json() assert response == sample_explain_response() diff --git a/tests/unit/api/test_raft.py b/tests/unit/api/test_raft.py index 3935469..6f635e8 100644 --- a/tests/unit/api/test_raft.py +++ b/tests/unit/api/test_raft.py @@ -1,18 +1,32 @@ import pytest -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) +@pytest.mark.parametrize("expected", [dict]) async def test_read_configuration(raft, expected): - raft.client.expected = expected + raft.client.expected = {} response = await raft.read_configuration() - assert response.status == 200 + assert isinstance(response, dict) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [["127.0.0.1:8300"]]) -async def test_delete_peer(raft, expected): - raft.client.expected = expected - response = await raft.delete_peer(dc="dc1", address="127.0.0.1:8300") - resp = await response.text() - assert resp is not None +async def test_delete_peer_with_address(raft, mocker): + spy = mocker.spy(raft.client, "delete") + await raft.delete_peer(dc="dc1", address="127.0.0.1:8300") + spy.assert_called_with("/v1/operator/raft/peer?address=127.0.0.1:8300&dc=dc1") + + +async def test_delete_peer_with_id(raft, mocker): + spy = mocker.spy(raft.client, "delete") + await raft.delete_peer(dc="dc1", peer_id="123-456-789") + spy.assert_called_with("/v1/operator/raft/peer?id=123-456-789&dc=dc1") + + +async def test_delete_peer_error(raft): + with pytest.raises(ValueError): + await raft.delete_peer("123-456", "127.0.0.1:8300", "dc1") + + +async def test_delete_peer_without_query_param(raft, mocker): + # spy = mocker.spy(raft.client, "delete") + with pytest.raises(ValueError): + await raft.delete_peer(dc="dc1") + # spy.assert_called_with("/v1/operator/raft/peer?dc=dc1") diff --git a/tests/unit/api/test_role.py b/tests/unit/api/test_role.py index 7f506cc..8c82a6c 100644 --- a/tests/unit/api/test_role.py +++ b/tests/unit/api/test_role.py @@ -3,21 +3,6 @@ from discovery import api -def sample_payload(): - return { - "Name": "example-role", - "Description": "Showcases all input parameters", - "Policies": [ - {"ID": "783beef3-783f-f41f-7422-7087dc272765"}, - {"Name": "node-read"}, - ], - "ServiceIdentities": [ - {"ServiceName": "web"}, - {"ServiceName": "db", "Datacenters": ["dc1"]}, - ], - } - - def sample_response(): return { "ID": "aa770e5b-8b0b-7fcf-e5a1-8535fcc388b4", @@ -68,61 +53,58 @@ def sample_list_response(): @pytest.fixture -@pytest.mark.asyncio async def role(consul_api): return api.Role(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_create(role, expected): role.client.expected = expected - response = await role.create(sample_payload()) - response = await response.json() + response = await role.create( + "example-role", + "Showcases all input parameters", + [{"ID": "783beef3-783f-f41f-7422-7087dc272765"}, {"Name": "node-read"}], + [{"ServiceName": "web"}, {"ServiceName": "db", "Datacenters": ["dc1"]}], + [{"NodeName": "node-1", "Datacenter": "dc2"}], + ) assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_read_by_id(role, expected): role.client.expected = expected response = await role.read_by_id("aa770e5b-8b0b-7fcf-e5a1-8535fcc388b4") - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_read_by_name(role, expected): role.client.expected = expected response = await role.read_by_name("example-role") - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_update(role, expected): role.client.expected = expected response = await role.update( - "8bec74a4-5ced-45ed-9c9d-bca6153490bb", sample_payload() + "8bec74a4-5ced-45ed-9c9d-bca6153490bb", + "example-two", + [{"Name": "node-read"}], + [{"ServiceName": "db"}], + [{"NodeName": "node-1", "Datacenter": "dc2"}], ) - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(role, expected): - role.client.expected = expected +async def test_delete(role): + role.client.expected = True response = await role.delete("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_list_response()]) async def test_list(role, expected): role.client.expected = expected response = await role.list() - response = await response.json() assert response == sample_list_response() diff --git a/tests/unit/api/test_segment.py b/tests/unit/api/test_segment.py index 1956bfe..4df674b 100644 --- a/tests/unit/api/test_segment.py +++ b/tests/unit/api/test_segment.py @@ -5,10 +5,8 @@ def sample_response(): return ["", "alpha", "beta"] -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_list(segment, expected): segment.client.expected = expected - response = await segment.list() - resp = await response.json() + resp = await segment.list() assert resp == sample_response() diff --git a/tests/unit/api/test_service.py b/tests/unit/api/test_service.py index 6ec2caa..e5aca89 100644 --- a/tests/unit/api/test_service.py +++ b/tests/unit/api/test_service.py @@ -158,75 +158,81 @@ def status_response(): @pytest.fixture -@pytest.mark.asyncio async def service(consul_api): return api.Service(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [list_services_response()]) -async def test_services(service, expected): +async def test_list(service, expected): service.client.expected = expected - response = await service.services() - response = await response.json() + response = await service.list() assert response == list_services_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [service_payload_response()]) -async def test_service(service, expected): - service.client.expected = expected - response = await service.service("web-sidecar-proxy") - response = await response.json() - assert response == service_payload_response() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_register(service, expected): - service.client.expected = expected - response = await service.register(register_payload()) - assert response.status == 200 +async def test_register(service, mocker): + spy = mocker.spy(service.client, "put") + await service.register(register_payload()) + spy.assert_called_with( + "/v1/agent/service/register", + json={ + "ID": "redis1", + "Name": "redis", + "Tags": ["primary", "v1"], + "Address": "127.0.0.1", + "Port": 8000, + "Meta": {"redis_version": "4.0"}, + "EnableTagOverride": False, + "Check": { + "DeregisterCriticalServiceAfter": "90m", + "Args": ["/usr/local/bin/check_redis.py"], + "Interval": "10s", + "Timeout": "5s", + }, + "Weights": {"Passing": 10, "Warning": 1}, + }, + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_deregister(service, expected): - service.client.expected = expected - response = await service.deregister("my-service-id") - assert response.status == 200 +async def test_deregister(service, mocker): + spy = mocker.spy(service.client, "put") + await service.deregister("my-service-id") + spy.assert_called_with( + "/v1/agent/service/deregister/my-service-id", + ) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_maintenance(service, expected): - service.client.expected = expected - response = await service.maintenance("my-service-id", True, "For the tests") - assert response.status == 200 +@pytest.mark.parametrize( + "reason, expected", + [ + (None, "/v1/agent/service/maintenance/my-service-id?enable=True"), + ( + "For the tests", + "/v1/agent/service/maintenance/my-service-id?enable=True&reason=For+the+tests", + ), + ], +) +async def test_enable_maintenance(reason, expected, service, mocker): + spy = mocker.spy(service.client, "put") + await service.enable_maintenance("my-service-id", True, reason) + spy.assert_called_with(expected) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_payload_response()]) async def test_configuration(service, expected): service.client.expected = expected response = await service.configuration("web-sidecar-proxy") - response = await response.json() assert response == service_payload_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_health_name_response()]) -async def test_service_health_by_name(service, expected): +async def test_health_by_name(service, expected): service.client.expected = expected - response = await service.service_health_by_name("web") - response = await response.json() + response = await service.health_by_name("web") assert response == service_health_name_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [service_health_id_response()]) -async def test_service_health_by_id(service, expected): +async def test_health_by_id(service, expected): service.client.expected = expected - response = await service.service_health_by_id("web1") - response = await response.json() + response = await service.health_by_id("web1") assert response == service_health_id_response() diff --git a/tests/unit/api/test_session.py b/tests/unit/api/test_session.py index 7d96a42..5d0b74d 100644 --- a/tests/unit/api/test_session.py +++ b/tests/unit/api/test_session.py @@ -3,17 +3,6 @@ from discovery import api -def sample_payload(): - { - "LockDelay": "15s", - "Name": "my-service-lock", - "Node": "foobar", - "Checks": ["a", "b", "c"], - "Behavior": "release", - "TTL": "30s", - } - - def sample_response(): return [ { @@ -31,58 +20,43 @@ def sample_response(): @pytest.fixture -@pytest.mark.asyncio async def session(consul_api): return api.Session(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_create(session, expected): - session.client.expected = expected - response = await session.create(sample_payload()) - assert response.status == 200 +async def test_create(session): + await session.create("my-service-lock", "foobar", ["a", "b", "c"], ttl="30s") -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(session, expected): - session.client.expected = expected +async def test_delete(session): + session.client.expected = True response = await session.delete("adf4238a-882b-9ddc-4a9d-5b6758e4159e") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_read(session, expected): session.client.expected = expected resp = await session.read("adf4238a-882b-9ddc-4a9d-5b6758e4159e") - resp = await resp.json() assert resp == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) -async def test_list_node_session(session, expected): +async def test_list_sessions_for_node(session, expected): session.client.expected = expected - response = await session.list_node_session("raja-laptop-02") - response = await response.json() + response = await session.list_sessions_for_node("node-abcd1234") assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_list(session, expected): session.client.expected = expected response = await session.list() - response = await response.json() assert response == sample_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_response()]) async def test_renew(session, expected): session.client.expected = expected resp = await session.renew("adf4238a-882b-9ddc-4a9d-5b6758e4159e") - resp = await resp.json() assert resp == sample_response() diff --git a/tests/unit/api/test_snapshot.py b/tests/unit/api/test_snapshot.py index 266a15d..c177f64 100644 --- a/tests/unit/api/test_snapshot.py +++ b/tests/unit/api/test_snapshot.py @@ -1,27 +1,24 @@ +from pathlib import Path + import pytest from discovery import api @pytest.fixture -@pytest.mark.asyncio async def snapshot(consul_api): return api.Snapshot(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_generate(snapshot, expected): - snapshot.client.expected = expected - response = await snapshot.generate() - assert response.status == 200 +async def test_generate(snapshot): + await snapshot.generate() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_restore(snapshot, expected): - snapshot.client.expected = expected - snap = await snapshot.generate() - data = await snap.content() - response = await snapshot.restore(data=data) - assert response.status == 200 +@pytest.mark.skip +async def test_restore(snapshot, mocker): + spy = mocker.spy(snapshot.client, "put") + await snapshot.restore(data=Path("README.md").read_bytes()) + spy.assert_called_with( + "/v1/snapshot", + data=b"[![ci](https://github.com/amenezes/discovery-client/workflows/ci/badge.svg)](https://github.com/amenezes/discovery-client/actions)\n[![Maintainability](https://api.codeclimate.com/v1/badges/fc7916aab464c8b7d742/maintainability)](https://codeclimate.com/github/amenezes/discovery-client/maintainability)\n[![codecov](https://codecov.io/gh/amenezes/discovery-client/branch/master/graph/badge.svg)](https://codecov.io/gh/amenezes/discovery-client)\n[![PyPI version](https://badge.fury.io/py/discovery-client.svg)](https://badge.fury.io/py/discovery-client)\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/discovery-client)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\n# discovery-client\n\nAsync Python client for [consul](https://consul.io).\n\nHTTP engine options available:\n\n- aiohttp `default`;\n- httpx.\n\n## Installing\n\nInstall and update using pip:\n\n### default client\n\n````bash\npip install -U discovery-client\n````\n\n### httpx client\n\n````bash\npip install -U 'discovery-client[httpx]'\n````\n\n## Links\n\n- License: [Apache License](https://choosealicense.com/licenses/apache-2.0/)\n- Code: [https://github.com/amenezes/discovery-client](https://github.com/amenezes/discovery-client)\n- Issue tracker: [https://github.com/amenezes/discovery-client/issues](https://github.com/amenezes/discovery-client/issues)\n- Docs: [https://discovery-client.amenezes.net](https://discovery-client.amenezes.net)\n", + ) diff --git a/tests/unit/api/test_status.py b/tests/unit/api/test_status.py index aa8d10e..f55599c 100644 --- a/tests/unit/api/test_status.py +++ b/tests/unit/api/test_status.py @@ -4,27 +4,22 @@ @pytest.fixture -@pytest.mark.asyncio async def status(consul_api): return api.Status(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", ["127.0.0.1:8300"]) async def test_leader(status, expected): status.client.expected = expected response = await status.leader() - resp = await response.json() - assert resp == "127.0.0.1:8300" + assert response == "127.0.0.1:8300" -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [["127.0.0.1:8300"]]) async def test_peers(status, expected): status.client.expected = expected response = await status.peers() - resp = await response.json() - assert resp == ["127.0.0.1:8300"] + assert response == ["127.0.0.1:8300"] def test_repr(status): diff --git a/tests/unit/api/test_token.py b/tests/unit/api/test_token.py index 9f940d6..f7d1897 100644 --- a/tests/unit/api/test_token.py +++ b/tests/unit/api/test_token.py @@ -1,5 +1,3 @@ -import json - import pytest from discovery import api @@ -50,73 +48,50 @@ def sample_token_response(*args, **kwargs): } -def sample_payload(*args, **kwargs): - return json.dumps( - { - "ID": "8bec74a4-5ced-45ed-9c9d-bca6153490bb", - "Name": "example-two", - "Policies": [{"Name": "node-read"}], - "ServiceIdentities": [{"ServiceName": "db"}], - } - ) - - @pytest.fixture -@pytest.mark.asyncio async def token(consul_api): return api.Token(client=consul_api) -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_token_response()]) async def test_create(token, expected): token.client.expected = expected - response = await token.create(sample_payload()) - response = await response.json() - assert response == sample_token_response() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [sample_token_response()]) -async def test_read_by_id(token, expected): - token.client.expected = expected - response = await token.read_by_id("8bec74a4-5ced-45ed-9c9d-bca6153490bb") - response = await response.json() + response = await token.create( + "Agent token for 'node1", + [{"ID": "165d4317-e379-f732-ce70-86278c4558f7"}, {"Name": "node-read"}], + ) assert response == sample_token_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_token_response()]) -async def test_read_by_name(token, expected): +async def test_read(token, expected): token.client.expected = expected - response = await token.read_by_name("example-two") - response = await response.json() + response = await token.read("6a1253d2-1785-24fd-91c2-f8e78c745511") assert response == sample_token_response() -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_token_response()]) async def test_update(token, expected): token.client.expected = expected response = await token.update( - "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", sample_payload() + "Agent token for 'node1'", + [ + {"ID": "165d4317-e379-f732-ce70-86278c4558f7"}, + {"Name": "node-read"}, + {"Name": "service-read"}, + ], ) - response = await response.json() assert response == sample_token_response() -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_delete(token, expected): - token.client.expected = expected +async def test_delete(token): + token.client.expected = True response = await token.delete("8f246b77-f3e1-ff88-5b48-8ec93abf3e05") - assert response.status == 200 + assert response -@pytest.mark.asyncio @pytest.mark.parametrize("expected", [sample_list_response()]) async def test_list(token, expected): token.client.expected = expected response = await token.list() - response = await response.json() assert response == sample_list_response() diff --git a/tests/unit/api/test_txn.py b/tests/unit/api/test_txn.py index 1e7d9c5..6dd0607 100644 --- a/tests/unit/api/test_txn.py +++ b/tests/unit/api/test_txn.py @@ -54,15 +54,63 @@ def sample_payload(): ) +SAMPLE_RESPONSE = { + "Results": [ + { + "KV": { + "LockIndex": "", + "Key": "", + "Flags": "", + "Value": "", + "CreateIndex": "", + "ModifyIndex": "", + } + }, + { + "Node": { + "ID": "67539c9d-b948-ba67-edd4-d07a676d6673", + "Node": "bar", + "Address": "192.168.0.1", + "Datacenter": "dc1", + "TaggedAddresses": None, + "Meta": {"instance_type": "m2.large"}, + "CreateIndex": 32, + "ModifyIndex": 32, + } + }, + { + "Check": { + "Node": "bar", + "CheckID": "service:web1", + "Name": "Web HTTP Check", + "Status": "critical", + "Notes": "", + "Output": "", + "ServiceID": "web1", + "ServiceName": "web", + "ServiceTags": None, + "Definition": {"HTTP": "http://localhost:8080", "Interval": "10s"}, + "CreateIndex": 22, + "ModifyIndex": 35, + } + }, + ], + "Errors": [ + { + "OpIndex": "", + "What": "", + }, + ..., + ], +} + + @pytest.fixture -@pytest.mark.asyncio async def txn(consul_api): return api.Txn(client=consul_api) -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_create(txn, expected): - txn.client.expected = expected +async def test_create(txn): + txn.client.expected = SAMPLE_RESPONSE response = await txn.create(sample_payload()) - assert response.status == 200 + assert response == SAMPLE_RESPONSE diff --git a/tests/unit/engine/test_aio.py b/tests/unit/engine/test_aio.py index bb016e9..a3ac3b9 100644 --- a/tests/unit/engine/test_aio.py +++ b/tests/unit/engine/test_aio.py @@ -1,29 +1,46 @@ -import pytest +import aiohttp -@pytest.mark.asyncio -async def test_get(aiohttp_client): - response = await aiohttp_client.get("https://httpbin.org/get") - assert response.status == 200 +async def test_get(consul): + async with consul.client.get("https://httpbin.org/get") as response: + assert response.status == 200 + assert response.version == "1.1" -@pytest.mark.asyncio -async def test_put(aiohttp_client): - response = await aiohttp_client.put("https://httpbin.org/put") - assert response.status == 200 +async def test_put(consul): + async with consul.client.put("https://httpbin.org/put") as response: + assert response.status == 200 -@pytest.mark.asyncio -async def test_delete(aiohttp_client): - response = await aiohttp_client.delete("https://httpbin.org/delete") - assert response.status == 200 +async def test_delete(consul): + async with consul.client.delete("https://httpbin.org/delete") as response: + assert response.status == 200 -@pytest.mark.asyncio -async def test_post(aiohttp_client): - response = await aiohttp_client.post("https://httpbin.org/post") - assert response.status == 200 +async def test_post(consul): + async with consul.client.post("https://httpbin.org/post") as response: + assert response.status == 200 -def test_url(aiohttp_client): - assert aiohttp_client.url == "http://localhost:8500" +def test_url(consul): + assert consul.client.url == "http://localhost:8500" + + +def test_repr(consul): + assert ( + str(consul) + == "Consul(engine=AIOHTTPEngine(host='localhost', port=8500, scheme='http'))" + ) + + +async def test_response_repr(consul): + async with consul.client.get("https://httpbin.org/get") as response: + assert ( + str(response) + == "Response(status=200, http_version='1.1', url='https://httpbin.org/get')" + ) + + +async def test_raw_response(consul): + async with consul.client.get("https://httpbin.org/get") as response: + assert isinstance(response.raw_response, aiohttp.client_reqrep.ClientResponse) diff --git a/tests/unit/engine/test_custom_engine.py b/tests/unit/engine/test_custom_engine.py deleted file mode 100644 index 0c20fd8..0000000 --- a/tests/unit/engine/test_custom_engine.py +++ /dev/null @@ -1,36 +0,0 @@ -import pytest - -from discovery.engine.abc import Engine - - -class CustomEngine(Engine): - pass - - -@pytest.fixture -def custom_engine(): - return CustomEngine() - - -@pytest.mark.asyncio -async def test_get(custom_engine): - with pytest.raises(NotImplementedError): - await custom_engine.get("https://httpbin.org/get") - - -@pytest.mark.asyncio -async def test_put(custom_engine): - with pytest.raises(NotImplementedError): - await custom_engine.put("https://httpbin.org/put") - - -@pytest.mark.asyncio -async def test_delete(custom_engine): - with pytest.raises(NotImplementedError): - await custom_engine.delete("https://httpbin.org/delete") - - -@pytest.mark.asyncio -async def test_post(custom_engine): - with pytest.raises(NotImplementedError): - await custom_engine.post("https://httpbin.org/post") diff --git a/tests/unit/engine/test_httpx.py b/tests/unit/engine/test_httpx.py new file mode 100644 index 0000000..6227ebf --- /dev/null +++ b/tests/unit/engine/test_httpx.py @@ -0,0 +1,63 @@ +import pytest + + +@pytest.fixture +async def httpx_response(consul_httpx): + async with consul_httpx.client.get("https://httpbin.org/json") as response: + yield response + + +async def test_get(httpx_response): + response = httpx_response + assert response.status == 200 + assert response.content_type == "application/json" + + +async def test_json(httpx_response): + response = await httpx_response.json() + assert isinstance(response, dict) + + +async def test_text(httpx_response): + content = await httpx_response.text() + assert isinstance(content, str) + + +async def test_content(httpx_response): + response = await httpx_response.content() + assert isinstance(response, bytes) + + +async def test_put(consul_httpx): + async with consul_httpx.client.put("https://httpbin.org/put") as response: + assert response.status == 200 + assert response.version == "1.1" + + +async def test_delete(consul_httpx): + async with consul_httpx.client.delete("https://httpbin.org/delete") as response: + assert response.status == 200 + + +async def test_post(consul_httpx): + async with consul_httpx.client.post("https://httpbin.org/post") as response: + assert response.status == 200 + + +def test_url(consul_httpx): + assert consul_httpx.client.url == "http://localhost:8500" + + +def test_repr(consul_httpx): + assert ( + str(consul_httpx) + == "Consul(engine=HTTPXEngine(host='localhost', port=8500, scheme='http'))" + ) + + +async def test_response_repr(consul_httpx): + async with consul_httpx.client.get("https://httpbin.org/get") as response: + assert ( + str(response) + == "Response(status=200, http_version='1.1', url='https://httpbin.org/get')" + ) diff --git a/tests/unit/engine/test_response.py b/tests/unit/engine/test_response.py index a584802..47b0d04 100644 --- a/tests/unit/engine/test_response.py +++ b/tests/unit/engine/test_response.py @@ -1,81 +1,44 @@ import pytest +from discovery.engine.response import Response -class TestHttpxClient: - @pytest.mark.asyncio - async def test_status(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/status/200") - assert resp.status == 200 - - @pytest.mark.asyncio - async def test_url(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/status/200") - assert resp.url == "https://httpbin.org/status/200" - - @pytest.mark.asyncio - async def test_content_type(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/status/200") - assert resp.content_type == "text/html; charset=utf-8" - - @pytest.mark.asyncio - async def test_version(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/status/200") - assert resp.version == "1.1" - - @pytest.mark.asyncio - async def test_json(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/json") - content = await resp.json() - assert isinstance(content, dict) - - @pytest.mark.asyncio - async def test_text(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/html") - content = await resp.text() - assert isinstance(content, str) - - @pytest.mark.asyncio - async def test_content(self, httpx_engine): - resp = await httpx_engine.get("https://httpbin.org/html") - content = await resp.content() - assert isinstance(content, bytes) - - -class TestAioHttpClient: - @pytest.mark.asyncio - async def test_status(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/status/200") - assert resp.status == 200 - - @pytest.mark.asyncio - async def test_url(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/status/200") - assert resp.url == "https://httpbin.org/status/200" - - @pytest.mark.asyncio - async def test_content_type(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/status/200") - assert resp.content_type == "text/html" - - @pytest.mark.asyncio - async def test_version(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/status/200") - assert resp.version == "1.1" - - @pytest.mark.asyncio - async def test_json(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/json") - content = await resp.json() - assert isinstance(content, dict) - - @pytest.mark.asyncio - async def test_text(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/html") - content = await resp.text() - assert isinstance(content, str) - - @pytest.mark.asyncio - async def test_content(self, aiohttp_client): - resp = await aiohttp_client.get("https://httpbin.org/html") - content = await resp.content() - assert isinstance(content, bytes) + +@pytest.fixture +async def response(consul): + async with consul.client.get("https://httpbin.org/json") as resp: + yield resp + + +def test_resp_instance(response): + assert isinstance(response, Response) + + +def test_status(response): + assert response.status == 200 + + +def test_url(response): + assert response.url == "https://httpbin.org/json" + + +def test_content_type(response): + assert response.content_type == "application/json" + + +async def test_version(response): + assert response.version == "1.1" + + +async def test_json(response): + content = await response.json() + assert isinstance(content, dict) + + +async def test_text(response): + content = await response.text() + assert isinstance(content, str) + + +async def test_content(response): + content = await response.content() + assert isinstance(content, bytes) diff --git a/tests/unit/setup.py b/tests/unit/setup.py index 3fd0019..619c202 100644 --- a/tests/unit/setup.py +++ b/tests/unit/setup.py @@ -2,14 +2,13 @@ import pytest from discovery import api -from discovery.engine import AioEngine +from discovery.engine import AIOHTTPEngine @pytest.fixture -@pytest.mark.asyncio async def aiohttp_client(): session = aiohttp.ClientSession() - yield AioEngine(session) + yield AIOHTTPEngine(session) await session.close() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index e16914b..ba8a1ae 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1,11 +1,3 @@ -import pytest - -from discovery.client import Consul -from discovery.engine import aiohttp_session -from discovery.exceptions import ServiceNotFoundException -from discovery.model.agent import checks -from discovery.utils import select_one_random - SERVICE_RESPONSE = { "ID": "154d3a48-e665-a22e-c75a-c093de56a188", "Node": "6a4e48904f35", @@ -101,122 +93,39 @@ ] -@pytest.fixture -@pytest.mark.asyncio -async def client(consul_api): - session = await aiohttp_session() - yield Consul(consul_api) - await session.close() - - -@pytest.mark.asyncio -async def test_default_timeout(client): - assert client.timeout == 30 - - -@pytest.mark.asyncio -async def test_changing_default_timeout(aiohttp_client, monkeypatch): - monkeypatch.setenv("DEFAULT_TIMEOUT", "5") - client = Consul(aiohttp_client) - assert client.timeout == 5 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [SERVICES_RESPONSE]) -async def test_find_services(client, expected): - client.client.expected = expected - response = await client.find_services("consul") - assert response == SERVICES_RESPONSE - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [SERVICES_RESPONSE]) -async def test_find_service_rr(client, expected): - client.client.expected = expected - response = await client.find_service("consul") - assert response == SERVICE_RESPONSE - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [SERVICES_RESPONSE]) -async def test_find_service_random(client, expected): - client.client.expected = expected - response = await client.find_service("consul", select_one_random) - assert response == SERVICE_RESPONSE - - -@pytest.mark.asyncio -async def test_service_not_found(client): - with pytest.raises(ServiceNotFoundException): - await client.find_service("myapp") - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", ["127.0.0.1:8300"]) -async def test_leader_ip(client, expected): - client.client.expected = expected - leader_ip = await client.leader_ip() - assert leader_ip == "127.0.0.1" - - -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [HEALTHY_INSTANCES_RESPONSE]) -async def test_consul_healthy_instances(client, expected): - client.client.expected = expected - response = await client.consul_healthy_instances() - assert response == HEALTHY_INSTANCES_RESPONSE - - -@pytest.mark.skip -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [HEALTHY_INSTANCES_RESPONSE]) -async def test_leader_current_id(client, expected): - client.client.expected = expected - leader_id = await client.leader_current_id() - assert leader_id == "620b350c-5384-7797-b6be-f51696e6afc8" - - -@pytest.mark.skip -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_register(client, expected): - client.client.expected = expected - response = await client.register( - "myapp", 5000, checks.http("http://myapp:5000/status") - ) - assert response is None - - -@pytest.mark.skip -@pytest.mark.asyncio -@pytest.mark.parametrize("expected", [200]) -async def test_deregister(client, expected): - client.client.expected = expected - response = await client.deregister("myapp") - assert response is None - - # async def test_register_additional_check(client): - # """Test the registration of an additional check for a service registered.""" - # await self.dc.register_additional_check( - # check.Check( - # name='additional-check', - # check=check.alias('consul') - # ) - # ) - - # async def test_register_additional_check_failed(client): - # with pytest.raises(TypeError): - # await self.dc.register_additional_check('invalid-check') - - # async def test_deregister_additional_check(client): - # """Test the registration of an additional check for a service registered.""" - # await self.dc.deregister_additional_check( - # check.Check( - # name='additional-check', - # check=check.alias('consul') - # ) - # ) - - # async def test_deregister_additional_check_failed(client): - # with pytest.raises(TypeError): - # await self.dc.deregister_additional_check('invalid-check') +async def test_default_timeout(consul): + assert consul.reconnect_timeout == 30 + + +def test_client_props(consul): + assert hasattr(consul, "catalog") + assert hasattr(consul, "config") + assert hasattr(consul, "coordinate") + assert hasattr(consul, "events") + assert hasattr(consul, "health") + assert hasattr(consul, "kv") + assert hasattr(consul, "namespace") + assert hasattr(consul, "query") + assert hasattr(consul, "session") + assert hasattr(consul, "snapshot") + assert hasattr(consul, "status") + assert hasattr(consul, "txn") + assert hasattr(consul, "agent") + assert hasattr(consul, "connect") + assert hasattr(consul, "acl") + assert hasattr(consul, "operator") + assert hasattr(consul.operator, "area") + assert hasattr(consul.operator, "autopilot") + assert hasattr(consul.operator, "keyring") + assert hasattr(consul.operator, "license") + assert hasattr(consul.operator, "raft") + assert hasattr(consul.operator, "segment") + assert hasattr(consul, "binding_rule") + assert hasattr(consul, "policy") + assert hasattr(consul, "role") + assert hasattr(consul, "token") + assert hasattr(consul, "check") + assert hasattr(consul, "services") + assert hasattr(consul, "ca") + assert hasattr(consul, "intentions") + assert hasattr(consul, "event") diff --git a/tests/unit/test_service_module.py b/tests/unit/test_service_module.py index 4313d5a..2abd558 100644 --- a/tests/unit/test_service_module.py +++ b/tests/unit/test_service_module.py @@ -1,21 +1,16 @@ -import json - import pytest -from discovery.model.agent import checks -from discovery.model.agent.service import meta_is_valid, service, tags_is_valid +from discovery import checks, utils +from discovery.utils import Service def test_service_with_check(): - resp = service("myapp", 5000, check=checks.http("http://localhost:5000/health")) - resp = json.loads(resp) - assert tuple(["name", "id", "address", "port", "tags", "meta", "check"]) == tuple( - resp - ) + svc = Service("myapp", 5000, check=checks.http("http://localhost:5000/health")) + assert svc["check"] is not None def test_service_with_multi_check(): - resp = service( + svc = Service( "myapp", 5000, check=[ @@ -23,89 +18,91 @@ def test_service_with_multi_check(): checks.tcp("localhost:22"), ], ) - assert tuple(["name", "id", "address", "port", "tags", "meta", "checks"]) == tuple( - json.loads(resp) - ) + assert len(svc["check"]) == 2 def test_create_service_without_check(): - resp = service("myapp2", 5001) - assert "check" not in resp + svc = Service("myapp2", 5001) + assert hasattr(svc, "check") -def test_json(): - resp = service("myapp2", 5001) - assert tuple(["name", "id", "address", "port", "tags", "meta"]) == tuple( - json.loads(resp) - ) +def test_service_return(): + svc = Service("myapp2", 5001) + assert isinstance(svc, Service) -def test_alias_check(): - resp = checks.alias("myapp", "other_service_id") - assert tuple(["name", "service_id", "aliasservice"]) == tuple(json.loads(resp)) +@pytest.mark.parametrize("prop", ["name", "service_id", "alias_service"]) +def test_alias_check(prop): + assert prop in checks.alias("myapp", "other_service_id") -def test_script_check(): - resp = checks.script(["/usr/local/bin/check_mem.py", "-limit", "256MB"]) - assert tuple(["args", "interval", "timeout", "name"]) == tuple(json.loads(resp)) +@pytest.mark.parametrize("prop", ["args", "interval", "timeout", "name"]) +def test_script_check(prop): + assert prop in checks.script(["/usr/local/bin/check_mem.py", "-limit", "256MB"]) -def test_http_check(): - resp = checks.http("http://localhost:5000/manage/health") - assert tuple( - [ - "http", - "tls_skip_verify", - "method", - "header", - "body", - "interval", - "timeout", - "deregister_critical_service_after", - "name", - ] - ) == tuple(json.loads(resp)) +@pytest.mark.parametrize("prop", ["id", "name", "h2ping", "interval", "h2ping_use_tls"]) +def test_h2ping_check(prop): + assert prop in checks.h2ping([]) -def test_tcp_check(): - resp = checks.tcp("localhost:22") - assert tuple(["tcp", "interval", "timeout", "name"]) == tuple(json.loads(resp)) +@pytest.mark.parametrize( + "prop", + [ + "http", + "tls_skip_verify", + "tls_server_name", + "method", + "header", + "body", + "interval", + "timeout", + "deregister_critical_service_after", + "disable_redirects", + "name", + ], +) +def test_http_check(prop): + assert prop in checks.http("http://localhost:5000/manage/health") -def test_ttl_check(): - resp = checks.ttl("my custom ttl", "30s") - assert tuple(["notes", "ttl", "name"]) == tuple(json.loads(resp)) +@pytest.mark.parametrize("prop", ["tcp", "interval", "timeout", "name"]) +def test_tcp_check(prop): + assert prop in checks.tcp("localhost:22") -def test_docker_check(): - resp = checks.docker( +@pytest.mark.parametrize("prop", ["notes", "ttl", "name"]) +def test_ttl_check(prop): + assert prop in checks.ttl("my custom ttl", "30s") + + +@pytest.mark.parametrize( + "prop", ["docker_container_id", "shell", "args", "interval", "name"] +) +def test_docker_check(prop): + assert prop in checks.docker( container_id="f972c95ebf0e", args=["/usr/local/bin/check_mem.py"] ) - assert tuple(["docker_container_id", "shell", "args", "interval", "name"]) == tuple( - json.loads(resp).keys() - ) -def test_grpc_check(): - resp = checks.grpc("127.0.0.1:12345") - assert tuple(["grpc", "grpc_use_tls", "interval", "name"]) == tuple( - json.loads(resp) - ) +@pytest.mark.parametrize("prop", ["grpc", "grpc_use_tls", "interval", "name"]) +def test_grpc_check(prop): + assert prop in checks.grpc("127.0.0.1:12345") def test_tags_validation(): - tags_is_valid(["python", "ia"]) + utils.tags_is_valid(["python", "ia"]) def test_invalid_tags(): with pytest.raises(ValueError): - tags_is_valid("python") + utils.tags_is_valid("python") def test_metadata_validation(): - meta_is_valid({"lang": "python", "env": "production"}) + utils.meta_is_valid({"lang": "python", "env": "production"}) def test_invalid_metadata(): with pytest.raises(ValueError): - meta_is_valid("python") + utils.meta_is_valid("python") diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8019b88..1a72053 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -19,13 +19,15 @@ def test_select_one_rr(services): assert select_one_rr(service) == service # group 2: select alternate services - servicesB = ["d", "e"] - servicesC = ["f", "g", "h"] - - assert select_one_rr(servicesB) == "d" - assert select_one_rr(servicesC) == "f" - assert select_one_rr(servicesC) == "g" - assert select_one_rr(servicesB) == "e" - assert select_one_rr(servicesB) == "d" - assert select_one_rr(servicesC) == "h" - assert select_one_rr(servicesC) == "f" + servicesB = ["a", "b"] + servicesC = ["c", "d", "e"] + + # B service + assert select_one_rr(servicesB) == "a" + assert select_one_rr(servicesB) == "b" + assert select_one_rr(servicesB) == "a" + # C service + assert select_one_rr(servicesC) == "c" + assert select_one_rr(servicesC) == "d" + assert select_one_rr(servicesC) == "e" + assert select_one_rr(servicesC) == "c" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index af52ec9..0000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[tox] -envlist = py{36,37,38} - -[testenv] -deps = -rrequirements-dev.txt -whitelist_externals = make -commands = make ci