diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe7218a..95280c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## Unreleased +* Sandbox 注入规则支持 `if_headers` / `if_queries` 条件匹配字段,并兼容 `ifHeaders` / `ifQueries` 入参别名 + ## 7.18.0 * 新增 Qiniu Sandbox Python SDK 模块,支持生命周期、命令执行、文件系统、Git、PTY、模板、资源挂载和注入规则等能力 diff --git a/examples/sandbox_injection_rules.py b/examples/sandbox_injection_rules.py index ac438c73..9ddcd3d7 100644 --- a/examples/sandbox_injection_rules.py +++ b/examples/sandbox_injection_rules.py @@ -12,7 +12,9 @@ def main(): name='python-sdk-example', injection={ 'type': 'http', - 'base_url': 'https://api.example.com', + 'base_url': 'https://api.example.com/v1/*', + 'if_headers': {'X-Qiniu-Sandbox': 'enabled'}, + 'if_queries': {'tenant': 'demo'}, 'headers': {'X-From-Sandbox': 'qiniu-python-sdk'}, }, ) @@ -25,9 +27,11 @@ def main(): rule_id, name='python-sdk-example-updated', injection={ - 'type': 'http', - 'base_url': 'https://api.example.com', - 'headers': {'X-Updated-From-Sandbox': 'qiniu-python-sdk'}, + 'type': 'openai', + 'base_url': 'https://api.openai.com/v1/*', + 'if_headers': {'X-Qiniu-Sandbox': 'enabled'}, + 'if_queries': {'model': 'gpt-4o-mini'}, + 'api_key': 'replace-with-openai-api-key', }, )) finally: diff --git a/qiniu/services/sandbox/client.py b/qiniu/services/sandbox/client.py index 6daf51f1..e2fe726e 100644 --- a/qiniu/services/sandbox/client.py +++ b/qiniu/services/sandbox/client.py @@ -43,6 +43,10 @@ def _normalize_injection(injection): data['api_key'] = data.pop('apiKey') if 'baseUrl' in data and 'base_url' not in data: data['base_url'] = data.pop('baseUrl') + if 'ifHeaders' in data and 'if_headers' not in data: + data['if_headers'] = data.pop('ifHeaders') + if 'ifQueries' in data and 'if_queries' not in data: + data['if_queries'] = data.pop('ifQueries') if 'ruleId' in data and 'ruleID' not in data: data['ruleID'] = data.pop('ruleId') if 'rule_id' in data and 'ruleID' not in data: diff --git a/qiniu/services/sandbox/commands.py b/qiniu/services/sandbox/commands.py index df841b68..1558dd59 100644 --- a/qiniu/services/sandbox/commands.py +++ b/qiniu/services/sandbox/commands.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import base64 import binascii +import re from qiniu.compat import basestring, bytes as bytes_type, str as text_type @@ -93,6 +94,15 @@ def command_result_from_events(events, on_stdout=None, on_stderr=None, if end: if end.get('exitCode') is not None: exit_code = end.get('exitCode') + elif end.get('exit_code') is not None: + exit_code = end.get('exit_code') + else: + status_match = re.match( + r'^exit status (\d+)$', + text_type(end.get('status') or ''), + ) + if status_match: + exit_code = int(status_match.group(1)) error = end.get('error') or '' return CommandResult(pid, exit_code, stdout, stderr, error) diff --git a/tests/cases/test_services/test_sandbox/test_client.py b/tests/cases/test_services/test_sandbox/test_client.py index 1d23f7c7..f6e03deb 100644 --- a/tests/cases/test_services/test_sandbox/test_client.py +++ b/tests/cases/test_services/test_sandbox/test_client.py @@ -201,7 +201,9 @@ def test_util_helpers_encode_unicode_values_safely(): ).startswith('v1_') -def test_create_with_kodo_resource_requires_qiniu_credentials(): +def test_create_with_kodo_resource_requires_qiniu_credentials(monkeypatch): + monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False) + monkeypatch.delenv('QINIU_SANDBOX_SECRET_KEY', raising=False) client = SandboxClient(api_key='api-key', session=RecordingSession()) with pytest.raises(SandboxError) as err: @@ -241,7 +243,10 @@ def test_create_with_kodo_resource_uses_qiniu_signature(): }] -def test_create_with_saved_injection_rule_requires_qiniu_credentials(): +def test_create_with_saved_injection_rule_requires_qiniu_credentials( + monkeypatch): + monkeypatch.delenv('QINIU_SANDBOX_ACCESS_KEY', raising=False) + monkeypatch.delenv('QINIU_SANDBOX_SECRET_KEY', raising=False) client = SandboxClient(api_key='api-key', session=RecordingSession()) with pytest.raises(SandboxError) as err: @@ -287,6 +292,55 @@ def test_create_with_saved_injection_rule_accepts_snake_case_rule_id(): }] +def test_create_sandbox_normalizes_inline_injection_match_conditions(): + session = RecordingSession( + [DummyResponse(201, {'sandboxID': 'sbx123', 'templateID': 'base'})]) + client = SandboxClient(api_key='api-key', session=session) + + client.create_sandbox(injections=[{ + 'type': 'openai', + 'apiKey': 'openai-key', + 'baseUrl': 'https://api.openai.com/v1/*', + 'ifHeaders': {'X-Tenant': 'tenant-1'}, + 'ifQueries': {'model': 'gpt-4o-mini'}, + }]) + + assert body_of(session.requests[0])['injections'] == [{ + 'type': 'openai', + 'api_key': 'openai-key', + 'base_url': 'https://api.openai.com/v1/*', + 'if_headers': {'X-Tenant': 'tenant-1'}, + 'if_queries': {'model': 'gpt-4o-mini'}, + }] + + +def test_injection_rule_requests_normalize_match_conditions(): + session = RecordingSession([DummyResponse(201, {'ruleID': 'rule-1'})]) + client = SandboxClient(access_key='ak', secret_key='sk', session=session) + + client.create_injection_rule( + name='conditional-openai', + injection={ + 'type': 'openai', + 'apiKey': 'openai-key', + 'baseUrl': 'https://api.openai.com/v1/*', + 'ifHeaders': {'X-Tenant': 'tenant-1'}, + 'ifQueries': {'model': 'gpt-4o-mini'}, + }, + ) + + assert body_of(session.requests[0]) == { + 'name': 'conditional-openai', + 'injection': { + 'type': 'openai', + 'api_key': 'openai-key', + 'base_url': 'https://api.openai.com/v1/*', + 'if_headers': {'X-Tenant': 'tenant-1'}, + 'if_queries': {'model': 'gpt-4o-mini'}, + }, + } + + def test_sandbox_create_signature_matches_e2b_style(): session = RecordingSession([ DummyResponse(201, { diff --git a/tests/cases/test_services/test_sandbox/test_envd.py b/tests/cases/test_services/test_sandbox/test_envd.py index 4a367f05..3b838fea 100644 --- a/tests/cases/test_services/test_sandbox/test_envd.py +++ b/tests/cases/test_services/test_sandbox/test_envd.py @@ -244,6 +244,22 @@ def test_command_event_decode_keeps_unknown_exit_code(): assert result.exit_code == -1 +def test_command_event_decode_handles_status_exit_code(): + result = command_result_from_events([{ + 'event': {'end': {'exited': True, 'status': 'exit status 7'}}, + }]) + + assert result.exit_code == 7 + + +def test_command_event_decode_handles_snake_case_exit_code(): + result = command_result_from_events([{ + 'event': {'end': {'exit_code': 3}}, + }]) + + assert result.exit_code == 3 + + def test_command_event_decode_handles_bytes_values(): result = command_result_from_events([{ 'event': {'data': { diff --git a/tests/cases/test_services/test_sandbox/test_integration.py b/tests/cases/test_services/test_sandbox/test_integration.py index f4a647f2..cfbf1edd 100644 --- a/tests/cases/test_services/test_sandbox/test_integration.py +++ b/tests/cases/test_services/test_sandbox/test_integration.py @@ -117,9 +117,18 @@ def test_git_remote_push_when_credentials_are_configured(): repo_path = '/tmp/qiniu-python-sdk-remote-git' try: sandbox = create_integration_sandbox('remote_git') + try: + auth_result = sandbox.git.dangerously_authenticate( + username, + password, + ) + except SandboxError as err: + if is_unsupported_runtime_error(err): + pytest.skip('envd does not support process stdin') + raise assert_command_ok( 'git authenticate', - sandbox.git.dangerously_authenticate(username, password), + auth_result, ) assert_command_ok( 'git http version',