Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support agent tokens #2

Merged
merged 9 commits into from
Feb 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions quickbuild/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Callable, NamedTuple, Optional

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 @@ -24,6 +25,7 @@ class QuickBuild(ABC):
def __init__(self):
self.builds = Builds(self)
self.users = Users(self)
self.tokens = Tokens(self)

@staticmethod
def _callback(response: Response, fcb: Optional[Callable] = None) -> str:
Expand Down
80 changes: 80 additions & 0 deletions quickbuild/endpoints/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from typing import List

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 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.

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.

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

def get_token_and_agent_details(self, agent_address: str) -> List[dict]:
"""
Get token value and latest used information of agents.

Args:
agent_address (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,
)
188 changes: 188 additions & 0 deletions tests/test_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@

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_token_and_agent_details('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_token_and_agent_details(None)
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_token_and_agent_details('unknown')
assert len(response) == 0


@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()