Skip to content

Commit

Permalink
feat: support agent tokens (#2)
Browse files Browse the repository at this point in the history
* feat: support agent tokens

* use params instead

* rename to get and define default value for agent address

* add tokens to documentation

* sort the endpoints alphabetically

* reorder tokens methods

* fix: add missing default value

* fix: return empty dict and not list

* validate that response is an empty list
  • Loading branch information
martinm82 authored Feb 25, 2021
1 parent 3d8ad39 commit 7428403
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Builds
.. autoclass:: quickbuild.endpoints.builds.Builds
:members:

Tokens
~~~~~~

.. autoclass:: quickbuild.endpoints.tokens.Tokens
:members:

Users
~~~~~

Expand Down
2 changes: 2 additions & 0 deletions quickbuild/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from quickbuild.endpoints.audits import Audits
from quickbuild.endpoints.builds import Builds
from quickbuild.endpoints.tokens import Tokens
from quickbuild.endpoints.users import Users
from quickbuild.exceptions import (
QBError,
Expand All @@ -25,6 +26,7 @@ class QuickBuild(ABC):
def __init__(self):
self.audits = Audits(self)
self.builds = Builds(self)
self.tokens = Tokens(self)
self.users = Users(self)

@staticmethod
Expand Down
81 changes: 81 additions & 0 deletions quickbuild/endpoints/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import List, Optional

import xmltodict


class Tokens:
"""With tokens, one can authorize/unauthorize agents, or access agent
details including token value and latest usage information.
"""

def __init__(self, quickbuild):
self.quickbuild = quickbuild

def get(self, agent_address: Optional[str] = None) -> List[dict]:
"""
Get token value and latest used information of agents.
Args:
agent_address (Optional[str]):
Build agent address, eg. my-agent:8811.
If param address is set to None, details of all agents will be returned.
Returns:
List[dict]: List of token and agent details
"""
def callback(response: str) -> List[dict]:
root = xmltodict.parse(response)

tokens = []
if root['list'] is not None:
tokens = root['list']['com.pmease.quickbuild.model.Token']
if isinstance(tokens, list) is False:
tokens = [tokens]
return tokens

params_agent_address = dict(address=agent_address) if agent_address else {}

return self.quickbuild._request(
'GET',
'tokens',
callback,
params=params_agent_address,
)

def authorize(self, agent_ip: str, agent_port: int = 8811) -> str:
"""
Authorize a build agent to join the build grid.
Args:
agent_ip (str): The build agent IP address.
agent_port (int): The build agent port (default: 8811).
Returns:
str: identifier of the newly created token for the build agent
"""
response = self.quickbuild._request(
'GET',
'tokens/authorize',
params=dict(ip=agent_ip, port=agent_port),
)

return response

def unauthorize(self, agent_ip: str, agent_port: int = 8811) -> str:
"""
Unauthorize an already authorized build agent.
Args:
agent_ip (str): The build agent IP address.
agent_port (int): The build agent port (default: 8811).
Returns:
str: identifier of the removed token representing the build agent.
"""
response = self.quickbuild._request(
'GET',
'tokens/unauthorize',
params=dict(ip=agent_ip, port=agent_port),
)

return response
189 changes: 189 additions & 0 deletions tests/test_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@

import re

import pytest
import responses

from quickbuild import AsyncQBClient, QBClient


REGEX_IP = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
REGEX_PORT = '\d+'

TOKEN_XML = r"""<?xml version="1.0" encoding="UTF-8"?>
<list>
<com.pmease.quickbuild.model.Token>
<id>120204</id>
<value>84858611-a1fe-4f88-a49c-f600cf0ecf11</value>
<ip>192.168.1.100</ip>
<port>8811</port>
<test>false</test>
<lastUsedDate>2021-02-08T20:01:09.426Z</lastUsedDate>
<lastUsedReason>Run step (configuration: root/pipelineC, build: B.50906, step: master&gt;build)</lastUsedReason>
<hostName>quickbuild-agent-192-168-1-100</hostName>
<offlineAlert>true</offlineAlert>
</com.pmease.quickbuild.model.Token>
</list>
"""

TOKENS_XML = r"""<?xml version="1.0" encoding="UTF-8"?>
<list>
<com.pmease.quickbuild.model.Token>
<id>117554</id>
<value>bee7e8ff-fd9a-475a-8cf5-42a5353b8875</value>
<ip>192.168.1.100</ip>
<port>8811</port>
<test>false</test>
<lastUsedDate>2021-02-08T20:08:29.360Z</lastUsedDate>
<lastUsedReason>Check build condition (configuration: root/pipelineA)</lastUsedReason>
<hostName>quickbuild-agent-192-168-1-100</hostName>
<offlineAlert>true</offlineAlert>
</com.pmease.quickbuild.model.Token>
<com.pmease.quickbuild.model.Token>
<id>115672</id>
<value>27350640-d9f9-4a10-96ae-b6ec8fee998b</value>
<ip>192.168.1.101</ip>
<port>8811</port>
<test>false</test>
<lastUsedDate>2021-02-08T20:01:10.175Z</lastUsedDate>
<lastUsedReason>Run step (configuration: root/pipelineA, build: B.1234, step: master&gt;finalize)</lastUsedReason>
<hostName>quickbuild-agent-192-168-1-101</hostName>
<offlineAlert>true</offlineAlert>
</com.pmease.quickbuild.model.Token>
<com.pmease.quickbuild.model.Token>
<id>116545</id>
<value>8f604c48-b9f4-4bbe-847c-c073b2aebc81</value>
<ip>192.168.1.102</ip>
<port>8811</port>
<test>false</test>
<lastUsedDate>2021-02-08T20:01:10.013Z</lastUsedDate>
<lastUsedReason>Run step (configuration: root/pipelineB, build: B.123, step: master&gt;publish)</lastUsedReason>
<hostName>quickbuild-agent-192-168-1-102</hostName>
<offlineAlert>true</offlineAlert>
</com.pmease.quickbuild.model.Token>
</list>
"""

EMPTY_TOKEN_XML = r"""<?xml version="1.0" encoding="UTF-8"?>
<list/>
"""


@responses.activate
def test_authorize():
RESPONSE_DATA = '120123'

responses.add(
responses.GET,
re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
content_type='text/plain',
body=RESPONSE_DATA,
match_querystring=True,
)

response = QBClient('http://server').tokens.authorize('192.168.1.100', 8811)
assert response == '120123'

response = QBClient('http://server').tokens.authorize('192.168.1.100')
assert response == '120123'


@responses.activate
def test_unauthorize():
RESPONSE_DATA = '120123'

responses.add(
responses.GET,
re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
content_type='text/plain',
body=RESPONSE_DATA,
match_querystring=True,
)

response = QBClient('http://server').tokens.unauthorize('192.168.1.100', 8811)
assert response == '120123'

response = QBClient('http://server').tokens.unauthorize('192.168.1.100')
assert response == '120123'


@responses.activate
def test_token_and_agent_details():
responses.add(
responses.GET,
re.compile(r'.*/rest/tokens\?address=quickbuild-agent-192-168-1-100%3A8811'),
content_type='application/xml',
body=TOKEN_XML
)

response = QBClient('http://server').tokens.get('quickbuild-agent-192-168-1-100:8811')
assert len(response) == 1
assert response[0]['id'] == '120204'


@responses.activate
def test_tokens_and_agent_details():
responses.add(
responses.GET,
re.compile(r'.*/rest/tokens'),
content_type='application/xml',
body=TOKENS_XML,
)

response = QBClient('http://server').tokens.get()
assert len(response) == 3
assert response[0]['id'] == '117554'
assert response[1]['id'] == '115672'
assert response[2]['id'] == '116545'


@responses.activate
def test_tokens_and_agent_details_with_unknown_address():
responses.add(
responses.GET,
re.compile(r'.*/rest/tokens\?address=unknown'),
content_type='application/xml',
body=EMPTY_TOKEN_XML,
match_querystring=True,
)

response = QBClient('http://server').tokens.get('unknown')
assert len(response) == 0
assert response == []


@pytest.mark.asyncio
async def test_authorize_async(aiohttp_mock):
RESPONSE_DATA = '120123'

client = AsyncQBClient('http://server')
try:
aiohttp_mock.get(
re.compile(r'.*/rest/tokens/authorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
body=RESPONSE_DATA,
)

response = await client.tokens.authorize('192.168.1.100')
assert response == '120123'
finally:
await client.close()


@pytest.mark.asyncio
async def test_unauthorize_async(aiohttp_mock):
RESPONSE_DATA = '120123'

client = AsyncQBClient('http://server')
try:
aiohttp_mock.get(
re.compile(r'.*/rest/tokens/unauthorize\?ip={}&port={}'.format(REGEX_IP, REGEX_PORT)),
body=RESPONSE_DATA,
)

response = await client.tokens.unauthorize('192.168.1.100')
assert response == '120123'
finally:
await client.close()

0 comments on commit 7428403

Please sign in to comment.