Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
64 changes: 58 additions & 6 deletions qiniu/services/sandbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from .constants import DEFAULT_TEMPLATE
from .errors import SandboxError, TemplateBuildError
from .resources import KodoResource
from .util import (
encode_path,
json_dumps,
Expand Down Expand Up @@ -65,15 +64,27 @@ def _normalize_injection(injection):
def _normalize_resources(resources):
if resources is None:
return None
return [_to_dict(resource) for resource in resources]
normalized = [_to_dict(resource) for resource in resources]
for resource in normalized:
if not isinstance(resource, dict) or resource.get('type') != 'kodo':
continue
if (resource.get('access_key') is None) != (
resource.get('secret_key') is None):
raise ValueError(
'access_key and secret_key must be provided together')
if (resource.get('access_key') is not None and
(not resource.get('access_key') or
not resource.get('secret_key'))):
raise ValueError(
'access_key and secret_key must not be empty')
return normalized


def _has_kodo_resource(resources):
for resource in resources or []:
if isinstance(resource, KodoResource):
return True
data = _to_dict(resource)
if isinstance(data, dict) and data.get('type') == 'kodo':
if (isinstance(data, dict) and data.get('type') == 'kodo' and
(not data.get('access_key') or not data.get('secret_key'))):
return True
return False

Expand Down Expand Up @@ -179,6 +190,17 @@ def _normalize_list_options(opts):
return opts


def _normalize_template_create_options(opts):
body = dict(opts or {})
disk_size_mb = _single_alias_value(
body, 'disk_size_mb', 'diskSizeMB')
body.pop('disk_size_mb', None)
body.pop('diskSizeMB', None)
if disk_size_mb is not None:
body['diskSizeMB'] = disk_size_mb
return body


def _sandbox_api_key_from_env():
return (
os.getenv('QINIU_SANDBOX_API_KEY') or
Expand Down Expand Up @@ -475,6 +497,32 @@ def get_sandbox_injections(self, sandbox_id):

getSandboxInjections = get_sandbox_injections

def get_sandbox_resources(self, sandbox_id):
_require_sandbox_id(sandbox_id)
return self._request(
'GET',
'/sandboxes/{0}/resources'.format(encode_path(sandbox_id)),
)

getSandboxResources = get_sandbox_resources

def update_git_repository_resource_token(
self, sandbox_id, resource_id, authorization_token):
_require_sandbox_id(sandbox_id)
if not resource_id:
raise SandboxError('resource_id is required')
if not authorization_token:
raise SandboxError('authorization_token is required')
return self._request(
'PATCH',
'/sandboxes/{0}/resources/{1}'.format(
encode_path(sandbox_id), encode_path(resource_id)),
body={'authorization_token': authorization_token},
empty=True,
)

updateGitRepositoryResourceToken = update_git_repository_resource_token

def update_sandbox_injections(self, sandbox_id, injections):
_require_sandbox_id(sandbox_id)
if injections is None:
Expand Down Expand Up @@ -583,7 +631,11 @@ def get_sandbox_logs(self, sandbox_id, **opts):
getLogs = get_sandbox_logs

def create_template(self, **opts):
return self._request('POST', '/v3/templates', body=opts)
return self._request(
'POST',
'/v3/templates',
body=_normalize_template_create_options(opts),
)

createTemplate = create_template
createTemplateV3 = create_template
Expand Down
12 changes: 11 additions & 1 deletion qiniu/services/sandbox/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ def to_dict(self):


class KodoResource(object):
def __init__(self, bucket, mount_path, prefix=None, read_only=None):
def __init__(self, bucket, mount_path, prefix=None, read_only=None,
access_key=None, secret_key=None):
self.bucket = bucket
self.mount_path = mount_path
self.prefix = prefix
self.read_only = read_only
self.access_key = access_key
self.secret_key = secret_key

def to_dict(self):
data = {
Expand All @@ -37,4 +40,11 @@ def to_dict(self):
data['prefix'] = self.prefix
if self.read_only is not None:
data['read_only'] = self.read_only
if (self.access_key is None) != (self.secret_key is None):
raise ValueError('access_key and secret_key must be provided together')
if self.access_key is not None:
if not self.access_key or not self.secret_key:
raise ValueError('access_key and secret_key must not be empty')
data['access_key'] = self.access_key
data['secret_key'] = self.secret_key
return data
12 changes: 12 additions & 0 deletions qiniu/services/sandbox/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,18 @@ def get_injections(self):

getInjections = get_injections

def get_resources(self):
return self.client.get_sandbox_resources(self.sandbox_id)

getResources = get_resources

def update_git_repository_resource_token(
self, resource_id, authorization_token):
return self.client.update_git_repository_resource_token(
self.sandbox_id, resource_id, authorization_token)

updateGitRepositoryResourceToken = update_git_repository_resource_token

def update_injections(self, injections):
return self.client.update_sandbox_injections(
self.sandbox_id, injections)
Expand Down
76 changes: 76 additions & 0 deletions tests/cases/test_services/test_sandbox/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,52 @@ def test_client_uses_default_endpoint_and_api_key_headers():
}


def test_create_template_maps_disk_size_mb_to_api_field():
session = RecordingSession([DummyResponse(202, {
'templateID': 'tpl123',
'buildID': 'build123',
})])
client = SandboxClient(api_key='api-key', session=session)

client.create_template(name='disk-size-test', disk_size_mb=15360)

req = session.requests[0]
assert req.method == 'POST'
assert req.url == DEFAULT_ENDPOINT + '/v3/templates'
assert body_of(req) == {
'name': 'disk-size-test',
'diskSizeMB': 15360,
}


def test_sandbox_resource_apis_list_and_update_git_token():
session = RecordingSession([
DummyResponse(200, {'resources': [{
'type': 'github_repository',
'resource_id': 'res123',
'url': 'https://github.com/qiniu/python-sdk.git',
'mount_path': '/workspace/repo',
}]}),
DummyResponse(204),
])
client = SandboxClient(api_key='api-key', session=session)

resources = client.get_sandbox_resources('sbx/123')
client.update_git_repository_resource_token(
'sbx/123', 'res/123', 'new-token')

assert resources['resources'][0]['resource_id'] == 'res123'
assert session.requests[0].method == 'GET'
assert session.requests[0].url == (
DEFAULT_ENDPOINT + '/sandboxes/sbx%2F123/resources')
assert session.requests[1].method == 'PATCH'
assert session.requests[1].url == (
DEFAULT_ENDPOINT + '/sandboxes/sbx%2F123/resources/res%2F123')
assert body_of(session.requests[1]) == {
'authorization_token': 'new-token',
}


@pytest.mark.parametrize('status_code', [408, 500])
def test_create_sandbox_retries_retryable_status_and_reuses_idempotency_key(
monkeypatch, status_code):
Expand Down Expand Up @@ -331,6 +377,36 @@ def test_create_with_kodo_resource_uses_qiniu_signature():
}]


def test_create_with_inline_kodo_credentials_uses_api_key_auth():
session = RecordingSession(
[DummyResponse(201, {'sandboxID': 'sbx123', 'templateID': 'base'})])
client = SandboxClient(api_key='api-key', session=session)

client.create_sandbox(resources=[KodoResource(
bucket='bucket',
mount_path='/mnt/bucket',
access_key='resource-ak',
secret_key='resource-sk',
)])

req = session.requests[0]
assert req.headers['Authorization'] == 'Bearer api-key'
assert body_of(req)['resources'][0]['access_key'] == 'resource-ak'
assert body_of(req)['resources'][0]['secret_key'] == 'resource-sk'


def test_create_rejects_partial_inline_kodo_credentials():
client = SandboxClient(api_key='api-key', session=RecordingSession())

with pytest.raises(ValueError, match='access_key and secret_key'):
client.create_sandbox(resources=[{
'type': 'kodo',
'bucket': 'bucket',
'mount_path': '/mnt/bucket',
'access_key': 'resource-ak',
}])


def test_create_with_saved_injection_rule_requires_qiniu_credentials(
monkeypatch):
monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False)
Expand Down
Loading