Skip to content

Commit ae2fd97

Browse files
committed
feat(sandbox): expose resource APIs
Expose sandbox resource management and support inline Kodo credentials. Add template disk size configuration and regression coverage.
1 parent 36135e1 commit ae2fd97

4 files changed

Lines changed: 157 additions & 6 deletions

File tree

‎qiniu/services/sandbox/client.py‎

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,27 @@ def _normalize_injection(injection):
6565
def _normalize_resources(resources):
6666
if resources is None:
6767
return None
68-
return [_to_dict(resource) for resource in resources]
68+
normalized = [_to_dict(resource) for resource in resources]
69+
for resource in normalized:
70+
if not isinstance(resource, dict) or resource.get('type') != 'kodo':
71+
continue
72+
if (resource.get('access_key') is None) != (
73+
resource.get('secret_key') is None):
74+
raise ValueError(
75+
'access_key and secret_key must be provided together')
76+
if (resource.get('access_key') is not None and
77+
(not resource.get('access_key') or
78+
not resource.get('secret_key'))):
79+
raise ValueError(
80+
'access_key and secret_key must not be empty')
81+
return normalized
6982

7083

7184
def _has_kodo_resource(resources):
7285
for resource in resources or []:
73-
if isinstance(resource, KodoResource):
74-
return True
7586
data = _to_dict(resource)
76-
if isinstance(data, dict) and data.get('type') == 'kodo':
87+
if (isinstance(data, dict) and data.get('type') == 'kodo' and
88+
(not data.get('access_key') or not data.get('secret_key'))):
7789
return True
7890
return False
7991

@@ -179,6 +191,17 @@ def _normalize_list_options(opts):
179191
return opts
180192

181193

194+
def _normalize_template_create_options(opts):
195+
body = dict(opts or {})
196+
disk_size_mb = _single_alias_value(
197+
body, 'disk_size_mb', 'diskSizeMB')
198+
body.pop('disk_size_mb', None)
199+
body.pop('diskSizeMB', None)
200+
if disk_size_mb is not None:
201+
body['diskSizeMB'] = disk_size_mb
202+
return body
203+
204+
182205
def _sandbox_api_key_from_env():
183206
return (
184207
os.getenv('QINIU_SANDBOX_API_KEY') or
@@ -475,6 +498,32 @@ def get_sandbox_injections(self, sandbox_id):
475498

476499
getSandboxInjections = get_sandbox_injections
477500

501+
def get_sandbox_resources(self, sandbox_id):
502+
_require_sandbox_id(sandbox_id)
503+
return self._request(
504+
'GET',
505+
'/sandboxes/{0}/resources'.format(encode_path(sandbox_id)),
506+
)
507+
508+
getSandboxResources = get_sandbox_resources
509+
510+
def update_git_repository_resource_token(
511+
self, sandbox_id, resource_id, authorization_token):
512+
_require_sandbox_id(sandbox_id)
513+
if not resource_id:
514+
raise SandboxError('resource_id is required')
515+
if not authorization_token:
516+
raise SandboxError('authorization_token is required')
517+
return self._request(
518+
'PATCH',
519+
'/sandboxes/{0}/resources/{1}'.format(
520+
encode_path(sandbox_id), encode_path(resource_id)),
521+
body={'authorization_token': authorization_token},
522+
empty=True,
523+
)
524+
525+
updateGitRepositoryResourceToken = update_git_repository_resource_token
526+
478527
def update_sandbox_injections(self, sandbox_id, injections):
479528
_require_sandbox_id(sandbox_id)
480529
if injections is None:
@@ -583,7 +632,11 @@ def get_sandbox_logs(self, sandbox_id, **opts):
583632
getLogs = get_sandbox_logs
584633

585634
def create_template(self, **opts):
586-
return self._request('POST', '/v3/templates', body=opts)
635+
return self._request(
636+
'POST',
637+
'/v3/templates',
638+
body=_normalize_template_create_options(opts),
639+
)
587640

588641
createTemplate = create_template
589642
createTemplateV3 = create_template

‎qiniu/services/sandbox/resources.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ def to_dict(self):
2121

2222

2323
class KodoResource(object):
24-
def __init__(self, bucket, mount_path, prefix=None, read_only=None):
24+
def __init__(self, bucket, mount_path, prefix=None, read_only=None,
25+
access_key=None, secret_key=None):
2526
self.bucket = bucket
2627
self.mount_path = mount_path
2728
self.prefix = prefix
2829
self.read_only = read_only
30+
self.access_key = access_key
31+
self.secret_key = secret_key
2932

3033
def to_dict(self):
3134
data = {
@@ -37,4 +40,11 @@ def to_dict(self):
3740
data['prefix'] = self.prefix
3841
if self.read_only is not None:
3942
data['read_only'] = self.read_only
43+
if (self.access_key is None) != (self.secret_key is None):
44+
raise ValueError('access_key and secret_key must be provided together')
45+
if self.access_key is not None:
46+
if not self.access_key or not self.secret_key:
47+
raise ValueError('access_key and secret_key must not be empty')
48+
data['access_key'] = self.access_key
49+
data['secret_key'] = self.secret_key
4050
return data

‎qiniu/services/sandbox/sandbox.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,18 @@ def get_injections(self):
258258

259259
getInjections = get_injections
260260

261+
def get_resources(self):
262+
return self.client.get_sandbox_resources(self.sandbox_id)
263+
264+
getResources = get_resources
265+
266+
def update_git_repository_resource_token(
267+
self, resource_id, authorization_token):
268+
return self.client.update_git_repository_resource_token(
269+
self.sandbox_id, resource_id, authorization_token)
270+
271+
updateGitRepositoryResourceToken = update_git_repository_resource_token
272+
261273
def update_injections(self, injections):
262274
return self.client.update_sandbox_injections(
263275
self.sandbox_id, injections)

‎tests/cases/test_services/test_sandbox/test_client.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,52 @@ def test_client_uses_default_endpoint_and_api_key_headers():
123123
}
124124

125125

126+
def test_create_template_maps_disk_size_mb_to_api_field():
127+
session = RecordingSession([DummyResponse(202, {
128+
'templateID': 'tpl123',
129+
'buildID': 'build123',
130+
})])
131+
client = SandboxClient(api_key='api-key', session=session)
132+
133+
client.create_template(name='disk-size-test', disk_size_mb=15360)
134+
135+
req = session.requests[0]
136+
assert req.method == 'POST'
137+
assert req.url == DEFAULT_ENDPOINT + '/v3/templates'
138+
assert body_of(req) == {
139+
'name': 'disk-size-test',
140+
'diskSizeMB': 15360,
141+
}
142+
143+
144+
def test_sandbox_resource_apis_list_and_update_git_token():
145+
session = RecordingSession([
146+
DummyResponse(200, {'resources': [{
147+
'type': 'github_repository',
148+
'resource_id': 'res123',
149+
'url': 'https://github.com/qiniu/python-sdk.git',
150+
'mount_path': '/workspace/repo',
151+
}]}),
152+
DummyResponse(204),
153+
])
154+
client = SandboxClient(api_key='api-key', session=session)
155+
156+
resources = client.get_sandbox_resources('sbx/123')
157+
client.update_git_repository_resource_token(
158+
'sbx/123', 'res/123', 'new-token')
159+
160+
assert resources['resources'][0]['resource_id'] == 'res123'
161+
assert session.requests[0].method == 'GET'
162+
assert session.requests[0].url == (
163+
DEFAULT_ENDPOINT + '/sandboxes/sbx%2F123/resources')
164+
assert session.requests[1].method == 'PATCH'
165+
assert session.requests[1].url == (
166+
DEFAULT_ENDPOINT + '/sandboxes/sbx%2F123/resources/res%2F123')
167+
assert body_of(session.requests[1]) == {
168+
'authorization_token': 'new-token',
169+
}
170+
171+
126172
@pytest.mark.parametrize('status_code', [408, 500])
127173
def test_create_sandbox_retries_retryable_status_and_reuses_idempotency_key(
128174
monkeypatch, status_code):
@@ -331,6 +377,36 @@ def test_create_with_kodo_resource_uses_qiniu_signature():
331377
}]
332378

333379

380+
def test_create_with_inline_kodo_credentials_uses_api_key_auth():
381+
session = RecordingSession(
382+
[DummyResponse(201, {'sandboxID': 'sbx123', 'templateID': 'base'})])
383+
client = SandboxClient(api_key='api-key', session=session)
384+
385+
client.create_sandbox(resources=[KodoResource(
386+
bucket='bucket',
387+
mount_path='/mnt/bucket',
388+
access_key='resource-ak',
389+
secret_key='resource-sk',
390+
)])
391+
392+
req = session.requests[0]
393+
assert req.headers['Authorization'] == 'Bearer api-key'
394+
assert body_of(req)['resources'][0]['access_key'] == 'resource-ak'
395+
assert body_of(req)['resources'][0]['secret_key'] == 'resource-sk'
396+
397+
398+
def test_create_rejects_partial_inline_kodo_credentials():
399+
client = SandboxClient(api_key='api-key', session=RecordingSession())
400+
401+
with pytest.raises(ValueError, match='access_key and secret_key'):
402+
client.create_sandbox(resources=[{
403+
'type': 'kodo',
404+
'bucket': 'bucket',
405+
'mount_path': '/mnt/bucket',
406+
'access_key': 'resource-ak',
407+
}])
408+
409+
334410
def test_create_with_saved_injection_rule_requires_qiniu_credentials(
335411
monkeypatch):
336412
monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False)

0 commit comments

Comments
 (0)