Skip to content

Commit fcbbff8

Browse files
hrefgaudenz
authored andcommitted
Add objects-user fixture and cleanup
1 parent bbbccae commit fcbbff8

5 files changed

Lines changed: 144 additions & 1 deletion

File tree

api.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
import boto3
12
import re
23
import requests
34
import time
45

56
from constants import API_TOKEN
67
from constants import API_URL
78
from constants import LOCKS_PATH
9+
from constants import OBJECTS_URL
810
from constants import PROCESS_ID
911
from constants import RUNNER_ID
1012
from errors import Timeout
1113
from events import trigger
1214
from filelock import FileLock
1315
from requests.adapters import HTTPAdapter
1416
from requests.packages.urllib3.util.retry import Retry
17+
from resources import ObjectsUser
1518
from urllib.parse import urlparse
1619

1720

@@ -81,11 +84,13 @@ def __init__(self, scope, zone=None, read_only=False):
8184
agent_suffix = ''
8285

8386
self.api_url = API_URL
87+
self.objects_url = OBJECTS_URL
8488
self.headers['Authorization'] = f'Bearer {API_TOKEN}'
8589
self.headers['User-Agent'] = f'Acceptance Tests{agent_suffix}'
8690
self.hooks = {'response': self.on_response}
8791
self.scope = scope
8892
self.read_only = read_only
93+
self.zone = zone
8994

9095
# 8 Retries @ 2.5 backoff_factor = 10.6 minutes
9196
retry_strategy = RetryStrategy(
@@ -99,6 +104,10 @@ def __init__(self, scope, zone=None, read_only=False):
99104

100105
self.mount("https://", adapter)
101106

107+
# This is None, when running "invoke cleanup"
108+
if self.zone:
109+
self.objects_endpoint = self.objects_endpoint_for(self.zone)
110+
102111
def post(self, url, data=None, json=None, add_tags=True, **kwargs):
103112
assert not data, "Please only use json, not data"
104113

@@ -107,6 +116,7 @@ def post(self, url, data=None, json=None, add_tags=True, **kwargs):
107116
'runner': RUNNER_ID,
108117
'process': PROCESS_ID,
109118
'scope': self.scope,
119+
'zone': self.zone,
110120
}
111121

112122
return super().post(url, data=data, json=json, **kwargs)
@@ -156,6 +166,7 @@ def resources(path):
156166
yield from resources('/networks')
157167
yield from resources('/server-groups')
158168
yield from resources('/custom-images')
169+
yield from resources('/objects-users')
159170

160171
def cleanup(self, limit_to_scope=True, limit_to_process=True):
161172
""" Deletes resources created by this API object. """
@@ -179,6 +190,9 @@ def cleanup(self, limit_to_scope=True, limit_to_process=True):
179190
if exceptions:
180191
raise ExceptionGroup("Failures during cleanup.", exceptions)
181192

193+
def objects_endpoint_for(self, zone):
194+
return self.objects_url.format(region=zone.rstrip('0123456789'))
195+
182196

183197
def delete_handler(path):
184198
""" Registers the decorated function as delete handler for the given
@@ -244,3 +258,25 @@ def delete_volume_snapshots(api, url):
244258
f'Snapshot failed to delete within 60 seconds. Status '
245259
f'is still "{snapshot.json()["status"]}".'
246260
)
261+
262+
263+
@delete_handler(path='/v1/objects-users/.+')
264+
def delete_objects_users(api, url):
265+
""" Before deleting an objects user, we have to delete owned buckets. """
266+
267+
user = ObjectsUser.from_href(None, api, url, name="")
268+
user.wait_for_access()
269+
270+
session = boto3.Session(
271+
aws_access_key_id=user.keys[0]['access_key'],
272+
aws_secret_access_key=user.keys[0]['secret_key'],
273+
)
274+
275+
objects_endpoint = api.objects_endpoint_for(zone=user.tags['zone'])
276+
s3 = session.resource('s3', endpoint_url=objects_endpoint)
277+
278+
for bucket in s3.buckets.all():
279+
bucket.objects.all().delete()
280+
bucket.delete()
281+
282+
api.request("DELETE", url)

conftest.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import boto3
12
import os
23
import pytest
34
import random
@@ -21,6 +22,7 @@
2122
from resources import FloatingIP
2223
from resources import LoadBalancer
2324
from resources import Network
25+
from resources import ObjectsUser
2426
from resources import Server
2527
from resources import ServerGroup
2628
from resources import Volume
@@ -829,3 +831,51 @@ def factory(num_backends,
829831
return load_balancer, listener, pool, backends, private_network
830832

831833
return factory
834+
835+
836+
@pytest.fixture(scope='session')
837+
def create_objects_user(request, session_api, objects_endpoint):
838+
""" Factory to create an objects user. """
839+
840+
factory = ObjectsUser.factory(
841+
request=request,
842+
api=session_api,
843+
)
844+
845+
def wrapper(*args, **kwargs):
846+
user = factory(*args, **kwargs)
847+
848+
# We need to wait for a moment for the key to become available.
849+
user.wait_for_access()
850+
851+
return user
852+
853+
return wrapper
854+
855+
856+
@pytest.fixture(scope='session')
857+
def objects_user(create_objects_user):
858+
""" An object user that can be used with objects_endpoint. """
859+
860+
return create_objects_user(name=f'at-{secrets.token_hex(8)}')
861+
862+
863+
@pytest.fixture(scope='session')
864+
def objects_endpoint(session_api):
865+
""" An objects endpoint of the given zone. """
866+
867+
return session_api.objects_endpoint
868+
869+
870+
@pytest.fixture(scope='session')
871+
def access_key(objects_user):
872+
""" An S3 access key for the objects endpoint. """
873+
874+
return objects_user.keys[0]["access_key"]
875+
876+
877+
@pytest.fixture(scope='session')
878+
def secret_key(objects_user):
879+
""" An S3 secret key for the objects endpoint. """
880+
881+
return objects_user.keys[0]["secret_key"]

constants.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
if not os.environ.get('CLOUDSCALE_API_TOKEN'):
1010
raise RuntimeError(
1111
"No valid API token found in the CLOUDSCALE_API_TOKEN "
12-
"environment variable"
12+
"environment variable."
13+
)
14+
15+
if (('CLOUDSCALE_API_URL' in os.environ)
16+
!= ('CLOUDSCALE_OBJECTS_URL' in os.environ)):
17+
raise RuntimeError(
18+
"Either the CLOUDSCALE_API_URL or the CLOUDSCALE_OBJECTS_URL "
19+
"environment variable is set, but not both. Set both variables or "
20+
"use the defaults."
1321
)
1422

1523
# The API token is used to distinguish tests from various runners. If you have
@@ -24,6 +32,15 @@
2432
API_URL = os.environ.get('CLOUDSCALE_API_URL', 'https://api.cloudscale.ch/v1')
2533
API_URL = API_URL.rstrip('/')
2634

35+
OBJECTS_URL = os.environ.get(
36+
'CLOUDSCALE_OBJECTS_URL', 'https://objects.{region}.cloudscale.ch')
37+
OBJECTS_URL = OBJECTS_URL.rstrip('/')
38+
if '{region}' not in OBJECTS_URL:
39+
raise RuntimeError(
40+
'The CLOUDSCALE_OBJECTS_URL variable must contain the "{region}" '
41+
'template.'
42+
)
43+
2744
# One external ping target per IP version, that is assumed to be online
2845
PUBLIC_PING_TARGETS = {
2946
4: '8.8.8.8',

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
boto3
12
dnspython
23
filelock
34
flake8

resources.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import boto3
2+
import botocore
13
import re
24
import secrets
35
import textwrap
@@ -1454,3 +1456,40 @@ def verify_backend(self, prober, backend, count=1, port=None):
14541456
for i in range(count):
14551457
assert (prober.http_get(self.build_url(url='/hostname', port=port))
14561458
== backend.name)
1459+
1460+
1461+
class ObjectsUser(CloudscaleResource):
1462+
1463+
def __init__(self, request, api, name):
1464+
super().__init__(request, api)
1465+
1466+
self.spec = {
1467+
'display_name': f'{RESOURCE_NAME_PREFIX}-{name}',
1468+
}
1469+
1470+
@with_trigger('objects-user.create')
1471+
def create(self):
1472+
self.info = self.api.post('/objects-users', json=self.spec).json()
1473+
1474+
def wait_for_access(self, timeout=30):
1475+
objects_endpoint = self.api.objects_endpoint_for(self.tags['zone'])
1476+
1477+
s3 = boto3.client(
1478+
's3',
1479+
endpoint_url=objects_endpoint,
1480+
aws_access_key_id=self.keys[0]["access_key"],
1481+
aws_secret_access_key=self.keys[0]["secret_key"],
1482+
)
1483+
1484+
timeout = time.monotonic() + 30
1485+
exception = None
1486+
1487+
while time.monotonic() < timeout:
1488+
try:
1489+
s3.list_buckets()
1490+
break
1491+
except botocore.exceptions.ClientError as e:
1492+
exception = e
1493+
time.sleep(1)
1494+
else:
1495+
raise TimeoutError from exception

0 commit comments

Comments
 (0)