onex-cli 1.9.4__tar.gz → 1.9.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {onex_cli-1.9.4 → onex_cli-1.9.6}/CHANGELOG.md +24 -0
- {onex_cli-1.9.4/onex_cli.egg-info → onex_cli-1.9.6}/PKG-INFO +1 -1
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/__init__.py +1 -1
- onex_cli-1.9.6/onex/commands/es.py +488 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/server.py +245 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/service_descriptor.py +248 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6/onex_cli.egg-info}/PKG-INFO +1 -1
- onex_cli-1.9.4/onex/commands/es.py +0 -140
- {onex_cli-1.9.4 → onex_cli-1.9.6}/LICENSE +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/MANIFEST.in +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/README.md +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/__main__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/create.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/create_e2e.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/debug.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/deploy.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/dev.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/env.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/init.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/invoke.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/login.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/logout.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/logs.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/mcp.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/platform.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/provision.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/replay.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/status.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/switch.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/test.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/trace.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/undeploy.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/validate.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/vpn.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/config.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/tools/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/runtime/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/runtime/local_runtime.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/validator.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/README.md.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/service.yml.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/apis/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/models/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/models/schemas.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/repositories/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/services/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/e2e/data_service.yml.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/e2e/test_service_e2e.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/README.md.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/service.yml.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/events.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/schedules.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/README.md.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/service.yml.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/apis/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/apis/hello.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/README.md.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/service.yml.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/apis/__init__.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/apis/handlers.py.j2 +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/auth.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/crypto.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/email.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/helpers.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/__init__.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/platform_detector.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/setup_vpn.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/wireguard_manager.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/SOURCES.txt +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/dependency_links.txt +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/entry_points.txt +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/requires.txt +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/top_level.txt +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/setup.cfg +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/setup.py +0 -0
- {onex_cli-1.9.4 → onex_cli-1.9.6}/tests/test_mcp_logs_e2e.py +0 -0
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.6] - 2026-03-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`onex es status`** - View es_sync trigger status, ES index health, and execution stats
|
|
12
|
+
- Shows active/paused/error triggers with sync ratio (ES vs MongoDB doc counts)
|
|
13
|
+
- Execution stats, failure counts, last error, recent execution logs
|
|
14
|
+
- Filter by service name, JSON output support
|
|
15
|
+
- MCP tool: `onex_es_status`
|
|
16
|
+
- **`onex es test`** - End-to-end es_sync verification
|
|
17
|
+
- Inserts test doc into MongoDB, verifies sync to ES within 10s, cleans up
|
|
18
|
+
- Per-trigger pass/fail with step-by-step details
|
|
19
|
+
- MCP tool: `onex_es_test`
|
|
20
|
+
|
|
21
|
+
## [1.9.5] - 2026-03-24
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Schema: `docs` field on HTTP triggers** for OpenAPI/Swagger documentation
|
|
25
|
+
- `request_model` / `response_model`: reference Pydantic models for request/response body schemas
|
|
26
|
+
- `query_params`: document query parameters in Swagger UI
|
|
27
|
+
- `error_responses`: document additional error responses by status code
|
|
28
|
+
- `summary`, `description`, `tags`, `status_code`: standard OpenAPI fields
|
|
29
|
+
- Validation: `module:ClassName` format enforced (e.g., `doc_models:RegisterRequest`)
|
|
30
|
+
- New schema models: `DocsConfig`, `QueryParamSpec`, `ErrorResponseSpec`
|
|
31
|
+
|
|
8
32
|
## [1.7.9] - 2026-03-02
|
|
9
33
|
|
|
10
34
|
### Fixed
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# OneXEOS Services CLI
|
|
2
|
-
__version__ = "1.9.
|
|
2
|
+
__version__ = "1.9.6"
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ES command group - Elasticsearch management commands
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
status - View es_sync trigger status, ES index health, and execution stats
|
|
6
|
+
test - Test es_sync by inserting a doc, verifying sync, then cleaning up
|
|
7
|
+
reindex - Reindex ES from MongoDB for a service's es_sync collections
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import json
|
|
12
|
+
import requests
|
|
13
|
+
from onex.config import get_config, get_access_token, get_active_environment
|
|
14
|
+
from onex.utils import print_error, print_info, print_success, print_warning
|
|
15
|
+
from onex.utils.auth import auto_refresh_token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def es():
|
|
20
|
+
"""Elasticsearch management commands."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_headers(env):
|
|
25
|
+
"""Build auth headers for the given environment."""
|
|
26
|
+
access_token = get_access_token(env)
|
|
27
|
+
headers = {}
|
|
28
|
+
if access_token:
|
|
29
|
+
headers['Authorization'] = f'Bearer {access_token}'
|
|
30
|
+
return headers
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _handle_auth_retry(env, headers, make_request):
|
|
34
|
+
"""Handle 401 with token refresh. Returns response or None."""
|
|
35
|
+
if env != "local":
|
|
36
|
+
print_warning("Token expired, refreshing...")
|
|
37
|
+
if auto_refresh_token(env, silent=False):
|
|
38
|
+
access_token = get_access_token(env)
|
|
39
|
+
headers['Authorization'] = f'Bearer {access_token}'
|
|
40
|
+
return make_request(headers)
|
|
41
|
+
else:
|
|
42
|
+
print_error("Authentication failed - could not refresh token")
|
|
43
|
+
print_info(f"Please login again: onex login --env {env}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@es.command()
|
|
48
|
+
@click.argument('service_name', required=False, default=None)
|
|
49
|
+
@click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
|
|
50
|
+
@click.option('--platform-url', help='Override platform URL')
|
|
51
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
52
|
+
def status(service_name, env, platform_url, output_json):
|
|
53
|
+
"""View es_sync trigger status, ES index health, and execution stats.
|
|
54
|
+
|
|
55
|
+
Shows all es_sync triggers with their current status, ES index document counts,
|
|
56
|
+
MongoDB source counts, execution stats, and recent execution logs.
|
|
57
|
+
|
|
58
|
+
\b
|
|
59
|
+
Examples:
|
|
60
|
+
# View all es_sync triggers across all services
|
|
61
|
+
onex es status
|
|
62
|
+
|
|
63
|
+
# View es_sync triggers for a specific service
|
|
64
|
+
onex es status crm
|
|
65
|
+
|
|
66
|
+
# Output as JSON
|
|
67
|
+
onex es status crm --json
|
|
68
|
+
"""
|
|
69
|
+
if env is None:
|
|
70
|
+
env = get_active_environment()
|
|
71
|
+
|
|
72
|
+
config = get_config()
|
|
73
|
+
base_url = platform_url or config.get_platform_url(env)
|
|
74
|
+
headers = _build_headers(env)
|
|
75
|
+
|
|
76
|
+
click.echo(f"🔍 ES Sync Status ({env})...")
|
|
77
|
+
|
|
78
|
+
params = {}
|
|
79
|
+
if service_name:
|
|
80
|
+
params['service_name'] = service_name
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
response = requests.get(
|
|
84
|
+
f"{base_url}/_internal/es-sync",
|
|
85
|
+
params=params,
|
|
86
|
+
headers=headers,
|
|
87
|
+
timeout=30,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if response.status_code == 401 and env != "local":
|
|
91
|
+
retry_resp = _handle_auth_retry(env, headers, lambda h: requests.get(
|
|
92
|
+
f"{base_url}/_internal/es-sync", params=params, headers=h, timeout=30))
|
|
93
|
+
if retry_resp:
|
|
94
|
+
response = retry_resp
|
|
95
|
+
else:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if response.status_code == 200:
|
|
99
|
+
data = response.json()
|
|
100
|
+
|
|
101
|
+
if output_json:
|
|
102
|
+
click.echo(json.dumps(data, indent=2))
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
total = data.get('total', 0)
|
|
106
|
+
summary = data.get('summary', {})
|
|
107
|
+
|
|
108
|
+
if total == 0:
|
|
109
|
+
if service_name:
|
|
110
|
+
print_info(f"No es_sync triggers found for service '{service_name}'.")
|
|
111
|
+
else:
|
|
112
|
+
print_info("No es_sync triggers registered.")
|
|
113
|
+
print_info("Deploy a service with es_sync in service.yml to register triggers.")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Summary header
|
|
117
|
+
click.echo()
|
|
118
|
+
active = summary.get('active', 0)
|
|
119
|
+
paused = summary.get('paused', 0)
|
|
120
|
+
error = summary.get('error', 0)
|
|
121
|
+
total_exec = summary.get('total_executions', 0)
|
|
122
|
+
total_fail = summary.get('total_failures', 0)
|
|
123
|
+
|
|
124
|
+
click.echo(f" Total: {total} trigger(s) | "
|
|
125
|
+
f"{click.style(f'{active} active', fg='green')} | "
|
|
126
|
+
f"{click.style(f'{paused} paused', fg='yellow') if paused else f'{paused} paused'} | "
|
|
127
|
+
f"{click.style(f'{error} error', fg='red') if error else f'{error} error'}")
|
|
128
|
+
click.echo(f" Executions: {total_exec:,} total | {total_fail:,} failures")
|
|
129
|
+
click.echo()
|
|
130
|
+
|
|
131
|
+
# Per-service details
|
|
132
|
+
for svc in data.get('services', []):
|
|
133
|
+
svc_name = svc.get('service_name', '?')
|
|
134
|
+
triggers = svc.get('triggers', [])
|
|
135
|
+
click.echo(click.style(f" ┌─ {svc_name} ({len(triggers)} trigger(s))", fg='cyan', bold=True))
|
|
136
|
+
|
|
137
|
+
for i, t in enumerate(triggers):
|
|
138
|
+
is_last = (i == len(triggers) - 1)
|
|
139
|
+
prefix = " └──" if is_last else " ├──"
|
|
140
|
+
indent = " " if is_last else " │ "
|
|
141
|
+
|
|
142
|
+
# Status indicator
|
|
143
|
+
t_status = t.get('status', 'unknown')
|
|
144
|
+
if t_status == 'active':
|
|
145
|
+
status_str = click.style('● ACTIVE', fg='green')
|
|
146
|
+
elif t_status == 'paused':
|
|
147
|
+
status_str = click.style('○ PAUSED', fg='yellow')
|
|
148
|
+
else:
|
|
149
|
+
status_str = click.style('✖ ERROR', fg='red')
|
|
150
|
+
|
|
151
|
+
collection = t.get('collection', '?')
|
|
152
|
+
index = t.get('index', '?')
|
|
153
|
+
click.echo(f"{prefix} {collection} → {index} {status_str}")
|
|
154
|
+
|
|
155
|
+
# ES index info
|
|
156
|
+
es_info = t.get('es_index', {})
|
|
157
|
+
mongo_count = t.get('mongo_doc_count', 0)
|
|
158
|
+
es_count = es_info.get('doc_count', 0)
|
|
159
|
+
es_size = es_info.get('size', 'N/A')
|
|
160
|
+
es_exists = es_info.get('exists', False)
|
|
161
|
+
|
|
162
|
+
if es_exists:
|
|
163
|
+
# Show sync ratio
|
|
164
|
+
sync_indicator = ''
|
|
165
|
+
if mongo_count > 0 and es_count > 0:
|
|
166
|
+
ratio = es_count / mongo_count * 100
|
|
167
|
+
if ratio >= 99:
|
|
168
|
+
sync_indicator = click.style(' (in sync)', fg='green')
|
|
169
|
+
elif ratio >= 80:
|
|
170
|
+
sync_indicator = click.style(f' ({ratio:.0f}% synced)', fg='yellow')
|
|
171
|
+
else:
|
|
172
|
+
sync_indicator = click.style(f' ({ratio:.0f}% synced)', fg='red')
|
|
173
|
+
click.echo(f"{indent}MongoDB: {mongo_count:,} docs | ES: {es_count:,} docs ({es_size}){sync_indicator}")
|
|
174
|
+
else:
|
|
175
|
+
click.echo(f"{indent}MongoDB: {mongo_count:,} docs | " +
|
|
176
|
+
click.style("ES index missing!", fg='red'))
|
|
177
|
+
|
|
178
|
+
# Execution stats
|
|
179
|
+
exec_count = t.get('execution_count', 0)
|
|
180
|
+
fail_count = t.get('failure_count', 0)
|
|
181
|
+
last_exec = t.get('last_execution')
|
|
182
|
+
last_error = t.get('last_error')
|
|
183
|
+
|
|
184
|
+
exec_line = f"{indent}Executions: {exec_count:,}"
|
|
185
|
+
if fail_count > 0:
|
|
186
|
+
exec_line += f" | " + click.style(f"Failures: {fail_count:,}", fg='red')
|
|
187
|
+
if last_exec:
|
|
188
|
+
exec_line += f" | Last: {last_exec}"
|
|
189
|
+
click.echo(exec_line)
|
|
190
|
+
|
|
191
|
+
if last_error:
|
|
192
|
+
click.echo(f"{indent}" + click.style(f"Last error: {last_error}", fg='red'))
|
|
193
|
+
|
|
194
|
+
# Operations & field mapping
|
|
195
|
+
ops = t.get('operations', [])
|
|
196
|
+
if ops:
|
|
197
|
+
click.echo(f"{indent}Operations: {', '.join(ops)}")
|
|
198
|
+
fm = t.get('field_mapping')
|
|
199
|
+
if fm:
|
|
200
|
+
click.echo(f"{indent}Field mapping: {len(fm)} field(s)")
|
|
201
|
+
|
|
202
|
+
# Recent logs (compact)
|
|
203
|
+
recent = t.get('recent_logs', [])
|
|
204
|
+
if recent:
|
|
205
|
+
click.echo(f"{indent}Recent:")
|
|
206
|
+
for log in recent[:3]:
|
|
207
|
+
op = log.get('operation', '?')
|
|
208
|
+
ok = '✓' if log.get('success') else '✗'
|
|
209
|
+
ms = log.get('execution_time_ms', 0)
|
|
210
|
+
ts = log.get('executed_at', '')
|
|
211
|
+
if ts and len(ts) > 19:
|
|
212
|
+
ts = ts[:19]
|
|
213
|
+
color = 'green' if log.get('success') else 'red'
|
|
214
|
+
click.echo(f"{indent} {click.style(ok, fg=color)} {op} ({ms}ms) {ts}")
|
|
215
|
+
|
|
216
|
+
click.echo()
|
|
217
|
+
|
|
218
|
+
elif response.status_code == 503:
|
|
219
|
+
print_error("Stream Trigger Service unavailable")
|
|
220
|
+
print_info("Make sure platform is running: ./scripts/deploy-dev.sh status")
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
print_error(f"Failed: HTTP {response.status_code}")
|
|
224
|
+
try:
|
|
225
|
+
detail = response.json().get('detail', response.text)
|
|
226
|
+
print_error(f" {detail}")
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
except requests.exceptions.ConnectionError:
|
|
231
|
+
print_error(f"Cannot connect to platform at {base_url}")
|
|
232
|
+
print_info("Make sure platform is running")
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print_error(f"Error: {e}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@es.command()
|
|
239
|
+
@click.argument('service_name')
|
|
240
|
+
@click.option('--collection', '-c', default=None, help='Specific collection to test (all if omitted)')
|
|
241
|
+
@click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
|
|
242
|
+
@click.option('--platform-url', help='Override platform URL')
|
|
243
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
244
|
+
def test(service_name, collection, env, platform_url, output_json):
|
|
245
|
+
"""Test es_sync by inserting a doc, verifying sync, then cleaning up.
|
|
246
|
+
|
|
247
|
+
For each es_sync trigger, this command:
|
|
248
|
+
1. Checks ES index exists
|
|
249
|
+
2. Inserts a test document into MongoDB
|
|
250
|
+
3. Waits up to 10s for it to appear in ES via change stream
|
|
251
|
+
4. Cleans up the test document from both MongoDB and ES
|
|
252
|
+
|
|
253
|
+
\b
|
|
254
|
+
Examples:
|
|
255
|
+
# Test all es_sync triggers for crm service
|
|
256
|
+
onex es test crm
|
|
257
|
+
|
|
258
|
+
# Test a specific collection
|
|
259
|
+
onex es test crm --collection adminService
|
|
260
|
+
|
|
261
|
+
# Test on dev environment
|
|
262
|
+
onex es test crm --env dev
|
|
263
|
+
"""
|
|
264
|
+
if env is None:
|
|
265
|
+
env = get_active_environment()
|
|
266
|
+
|
|
267
|
+
config = get_config()
|
|
268
|
+
base_url = platform_url or config.get_platform_url(env)
|
|
269
|
+
headers = _build_headers(env)
|
|
270
|
+
|
|
271
|
+
click.echo(f"🧪 Testing ES Sync for {service_name} ({env})...")
|
|
272
|
+
click.echo()
|
|
273
|
+
|
|
274
|
+
body = {'service_name': service_name}
|
|
275
|
+
if collection:
|
|
276
|
+
body['collection'] = collection
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
response = requests.post(
|
|
280
|
+
f"{base_url}/_internal/es-sync/test",
|
|
281
|
+
json=body,
|
|
282
|
+
headers=headers,
|
|
283
|
+
timeout=120,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if response.status_code == 401 and env != "local":
|
|
287
|
+
retry_resp = _handle_auth_retry(env, headers, lambda h: requests.post(
|
|
288
|
+
f"{base_url}/_internal/es-sync/test", json=body, headers=h, timeout=120))
|
|
289
|
+
if retry_resp:
|
|
290
|
+
response = retry_resp
|
|
291
|
+
else:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
if response.status_code == 200:
|
|
295
|
+
data = response.json()
|
|
296
|
+
|
|
297
|
+
if output_json:
|
|
298
|
+
click.echo(json.dumps(data, indent=2))
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
results = data.get('results', [])
|
|
302
|
+
summary = data.get('summary', {})
|
|
303
|
+
error = data.get('error')
|
|
304
|
+
|
|
305
|
+
if error and not results:
|
|
306
|
+
print_error(error)
|
|
307
|
+
print_info("Deploy a service with es_sync in service.yml first.")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
for r in results:
|
|
311
|
+
coll = r.get('collection', '?')
|
|
312
|
+
index = r.get('index', '?')
|
|
313
|
+
passed = r.get('passed', False)
|
|
314
|
+
duration = r.get('duration_ms', 0)
|
|
315
|
+
steps = r.get('steps', [])
|
|
316
|
+
|
|
317
|
+
if passed:
|
|
318
|
+
click.echo(click.style(
|
|
319
|
+
f" ✅ {coll} → {index}: PASSED ({duration}ms)", fg='green'))
|
|
320
|
+
else:
|
|
321
|
+
click.echo(click.style(
|
|
322
|
+
f" ❌ {coll} → {index}: FAILED ({duration}ms)", fg='red'))
|
|
323
|
+
|
|
324
|
+
for step in steps:
|
|
325
|
+
if step.startswith('OK:'):
|
|
326
|
+
click.echo(f" {click.style('✓', fg='green')} {step[4:]}")
|
|
327
|
+
elif step.startswith('FAIL:'):
|
|
328
|
+
click.echo(f" {click.style('✗', fg='red')} {step[6:]}")
|
|
329
|
+
elif step.startswith('WARN:'):
|
|
330
|
+
click.echo(f" {click.style('!', fg='yellow')} {step[6:]}")
|
|
331
|
+
elif step.startswith('SKIP:'):
|
|
332
|
+
click.echo(f" {click.style('○', fg='yellow')} {step[6:]}")
|
|
333
|
+
else:
|
|
334
|
+
click.echo(f" {step}")
|
|
335
|
+
|
|
336
|
+
click.echo()
|
|
337
|
+
total = summary.get('total', 0)
|
|
338
|
+
passed_count = summary.get('passed', 0)
|
|
339
|
+
failed_count = summary.get('failed', 0)
|
|
340
|
+
|
|
341
|
+
if failed_count == 0:
|
|
342
|
+
print_success(f"All {total} es_sync trigger(s) passed")
|
|
343
|
+
else:
|
|
344
|
+
print_warning(f"{passed_count}/{total} passed, {failed_count} failed")
|
|
345
|
+
|
|
346
|
+
elif response.status_code == 503:
|
|
347
|
+
print_error("Stream Trigger Service unavailable")
|
|
348
|
+
print_info("Make sure platform is running: ./scripts/deploy-dev.sh status")
|
|
349
|
+
|
|
350
|
+
else:
|
|
351
|
+
print_error(f"Test failed: HTTP {response.status_code}")
|
|
352
|
+
try:
|
|
353
|
+
detail = response.json().get('detail', response.text)
|
|
354
|
+
print_error(f" {detail}")
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
except requests.exceptions.ConnectionError:
|
|
359
|
+
print_error(f"Cannot connect to platform at {base_url}")
|
|
360
|
+
print_info("Make sure platform is running")
|
|
361
|
+
|
|
362
|
+
except requests.exceptions.Timeout:
|
|
363
|
+
print_error("Test timed out (>2 minutes)")
|
|
364
|
+
print_info("This can happen if change streams are slow. Check stream-trigger logs.")
|
|
365
|
+
|
|
366
|
+
except Exception as e:
|
|
367
|
+
print_error(f"Error: {e}")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@es.command()
|
|
371
|
+
@click.argument('service_name')
|
|
372
|
+
@click.option('--collection', '-c', default=None, help='Specific collection to reindex (all if omitted)')
|
|
373
|
+
@click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
|
|
374
|
+
@click.option('--platform-url', help='Override platform URL')
|
|
375
|
+
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
376
|
+
def reindex(service_name, collection, env, platform_url, output_json):
|
|
377
|
+
"""Reindex Elasticsearch from MongoDB for a service.
|
|
378
|
+
|
|
379
|
+
Reads all documents from MongoDB collections that have es_sync triggers
|
|
380
|
+
registered and bulk-indexes them into Elasticsearch.
|
|
381
|
+
|
|
382
|
+
\b
|
|
383
|
+
Examples:
|
|
384
|
+
# Reindex all ES collections for crm service
|
|
385
|
+
onex es reindex crm
|
|
386
|
+
|
|
387
|
+
# Reindex a specific collection
|
|
388
|
+
onex es reindex crm --collection adminService
|
|
389
|
+
|
|
390
|
+
# Reindex on dev environment
|
|
391
|
+
onex es reindex crm --env dev
|
|
392
|
+
"""
|
|
393
|
+
if env is None:
|
|
394
|
+
env = get_active_environment()
|
|
395
|
+
|
|
396
|
+
config = get_config()
|
|
397
|
+
base_url = platform_url or config.get_platform_url(env)
|
|
398
|
+
|
|
399
|
+
# Build auth headers
|
|
400
|
+
access_token = get_access_token(env)
|
|
401
|
+
headers = {}
|
|
402
|
+
if access_token:
|
|
403
|
+
headers['Authorization'] = f'Bearer {access_token}'
|
|
404
|
+
|
|
405
|
+
click.echo(f"🔄 Reindexing ES for {service_name} ({env})...")
|
|
406
|
+
|
|
407
|
+
body = {'service_name': service_name}
|
|
408
|
+
if collection:
|
|
409
|
+
body['collection'] = collection
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
response = requests.post(
|
|
413
|
+
f"{base_url}/_internal/reindex",
|
|
414
|
+
json=body,
|
|
415
|
+
headers=headers,
|
|
416
|
+
timeout=300,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if response.status_code == 401 and env != "local":
|
|
420
|
+
print_warning("Token expired, refreshing...")
|
|
421
|
+
if auto_refresh_token(env, silent=False):
|
|
422
|
+
access_token = get_access_token(env)
|
|
423
|
+
headers['Authorization'] = f'Bearer {access_token}'
|
|
424
|
+
response = requests.post(
|
|
425
|
+
f"{base_url}/_internal/reindex",
|
|
426
|
+
json=body,
|
|
427
|
+
headers=headers,
|
|
428
|
+
timeout=300,
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
print_error("Authentication failed - could not refresh token")
|
|
432
|
+
print_info(f"Please login again: onex login --env {env}")
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
if response.status_code == 200:
|
|
436
|
+
data = response.json()
|
|
437
|
+
|
|
438
|
+
if output_json:
|
|
439
|
+
click.echo(json.dumps(data, indent=2))
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
collections = data.get('collections', [])
|
|
443
|
+
total = data.get('total_docs_synced', 0)
|
|
444
|
+
|
|
445
|
+
if not collections:
|
|
446
|
+
print_info("No es_sync collections found for this service.")
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
click.echo()
|
|
450
|
+
for col in collections:
|
|
451
|
+
name = col.get('collection', '?')
|
|
452
|
+
index = col.get('index', '?')
|
|
453
|
+
docs = col.get('docs_synced', 0)
|
|
454
|
+
duration = col.get('duration_ms', 0)
|
|
455
|
+
error = col.get('error')
|
|
456
|
+
|
|
457
|
+
if error:
|
|
458
|
+
click.echo(click.style(
|
|
459
|
+
f" ❌ {name} → {index}: {error}", fg='red'))
|
|
460
|
+
else:
|
|
461
|
+
click.echo(
|
|
462
|
+
f" ✅ {name} → {index}: {docs} docs ({duration}ms)")
|
|
463
|
+
|
|
464
|
+
click.echo()
|
|
465
|
+
print_success(f"Total: {total} docs synced across {len(collections)} collection(s)")
|
|
466
|
+
|
|
467
|
+
elif response.status_code == 404:
|
|
468
|
+
print_error(response.json().get('detail', 'No es_sync triggers found'))
|
|
469
|
+
print_info("Make sure the service has es_sync configured in service.yml and is deployed.")
|
|
470
|
+
|
|
471
|
+
else:
|
|
472
|
+
print_error(f"Reindex failed: HTTP {response.status_code}")
|
|
473
|
+
try:
|
|
474
|
+
detail = response.json().get('detail', response.text)
|
|
475
|
+
print_error(f" {detail}")
|
|
476
|
+
except Exception:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
except requests.exceptions.ConnectionError:
|
|
480
|
+
print_error(f"Cannot connect to platform at {base_url}")
|
|
481
|
+
print_info("Make sure platform is running")
|
|
482
|
+
|
|
483
|
+
except requests.exceptions.Timeout:
|
|
484
|
+
print_error("Reindex timed out (>5 minutes)")
|
|
485
|
+
print_info("This can happen with very large collections. Try reindexing one collection at a time with --collection.")
|
|
486
|
+
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print_error(f"Error: {e}")
|