cancan-microstack 0.0.1__py3-none-any.whl
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.
- cancan_microstack/__init__.py +14 -0
- cancan_microstack/__version__.py +10 -0
- cancan_microstack/assets/__init__.py +6 -0
- cancan_microstack/assets/builds/caddy/Caddyfile +187 -0
- cancan_microstack/assets/builds/caddy/DEPLOYMENT.md +303 -0
- cancan_microstack/assets/builds/caddy/Dockerfile +46 -0
- cancan_microstack/assets/builds/caddy/README.md +343 -0
- cancan_microstack/assets/builds/caddy/geoip/README.md +5 -0
- cancan_microstack/assets/builds/caddy/start.sh +78 -0
- cancan_microstack/assets/builds/caddy/waf/coraza.conf +179 -0
- cancan_microstack/assets/builds/service/Dockerfile +59 -0
- cancan_microstack/assets/builds/service/README.md +13 -0
- cancan_microstack/assets/ddl/create_db.sql +22 -0
- cancan_microstack/assets/ddl/infra/execution_log_tbl.sql +46 -0
- cancan_microstack/assets/ddl/infra/node_instance_tbl.sql +56 -0
- cancan_microstack/assets/ddl/infra/service_action_log_tbl.sql +36 -0
- cancan_microstack/assets/ddl/infra/service_config_tbl.sql +26 -0
- cancan_microstack/assets/ddl/infra/service_info_tbl.sql +45 -0
- cancan_microstack/assets/ddl/infra/service_instance_tbl.sql +54 -0
- cancan_microstack/assets/ddl/infra/service_operation_tbl.sql +47 -0
- cancan_microstack/assets/ddl/infra/workflow_definition_tbl.sql +60 -0
- cancan_microstack/assets/ddl/infra/workflow_definition_version_tbl.sql +35 -0
- cancan_microstack/assets/ddl/infra/workflow_engine_alert_tbl.sql +34 -0
- cancan_microstack/assets/ddl/infra/workflow_run_tbl.sql +52 -0
- cancan_microstack/assets/ddl/ops/admin_user_tbl.sql +34 -0
- cancan_microstack/assets/ddl/ops/caddy_access_log_tbl.sql +91 -0
- cancan_microstack/assets/ddl/ops/caddy_certificate_tbl.sql +59 -0
- cancan_microstack/assets/ddl/ops/caddy_rate_limit_tbl.sql +64 -0
- cancan_microstack/assets/ddl/ops/caddy_route_tbl.sql +63 -0
- cancan_microstack/assets/ddl/ops/caddy_stats_tbl.sql +77 -0
- cancan_microstack/assets/ddl/trigger.sql +21 -0
- cancan_microstack/assets/docker/docker-compose.infra.yml +401 -0
- cancan_microstack/assets/scripts/README.md +195 -0
- cancan_microstack/assets/scripts/docker/build_images.sh +44 -0
- cancan_microstack/assets/scripts/docker/force_rebuild_images.sh +38 -0
- cancan_microstack/assets/scripts/docker/rebuild_all.sh +34 -0
- cancan_microstack/assets/scripts/docker/rebuild_compose.sh +61 -0
- cancan_microstack/assets/scripts/docker/restart.sh +35 -0
- cancan_microstack/assets/scripts/docker/restart_compose.sh +35 -0
- cancan_microstack/assets/scripts/docker/start.sh +78 -0
- cancan_microstack/assets/scripts/docker/start_all.sh +46 -0
- cancan_microstack/assets/scripts/docker/start_compose.sh +66 -0
- cancan_microstack/assets/scripts/docker/stop.sh +67 -0
- cancan_microstack/assets/scripts/docker/stop_all.sh +38 -0
- cancan_microstack/assets/scripts/docker/stop_compose.sh +38 -0
- cancan_microstack/assets/scripts/podman/build_images_podman.sh +59 -0
- cancan_microstack/assets/scripts/podman/cleanup_podman.sh +25 -0
- cancan_microstack/assets/scripts/podman/force_rebuild_images_podman.sh +56 -0
- cancan_microstack/assets/scripts/podman/rebuild_all_podman.sh +37 -0
- cancan_microstack/assets/scripts/podman/rebuild_compose_podman.sh +60 -0
- cancan_microstack/assets/scripts/podman/restart_compose_podman.sh +73 -0
- cancan_microstack/assets/scripts/podman/start_all_podman.sh +66 -0
- cancan_microstack/assets/scripts/podman/start_compose_podman.sh +80 -0
- cancan_microstack/assets/scripts/podman/start_podman.sh +91 -0
- cancan_microstack/assets/scripts/podman/stop.sh +73 -0
- cancan_microstack/assets/scripts/podman/stop_all_podman.sh +34 -0
- cancan_microstack/assets/scripts/podman/stop_compose_podman.sh +58 -0
- cancan_microstack/assets/scripts/start_controllersrv.sh +9 -0
- cancan_microstack/assets/scripts/utils/check_all_db_tables.sh +104 -0
- cancan_microstack/assets/scripts/utils/check_env.sh +177 -0
- cancan_microstack/assets/scripts/utils/check_service_management_deployment.sh +225 -0
- cancan_microstack/assets/scripts/utils/deploy_service_management.sh +176 -0
- cancan_microstack/assets/scripts/utils/force_reload_infrasrv.sh +52 -0
- cancan_microstack/assets/scripts/utils/monitor_service_management.sh +187 -0
- cancan_microstack/assets/scripts/utils/reset_postgres_volume.sh +68 -0
- cancan_microstack/assets/scripts/utils/test_async_operations.sh +141 -0
- cancan_microstack/assets/scripts/utils/verify_real_operations.sh +76 -0
- cancan_microstack/assets/service/Dockerfile +65 -0
- cancan_microstack/assets/www/adminops/assets/AppEmpty.vue_vue_type_script_setup_true_lang-BOKUurnM.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConfigManage-DKV5YOUz.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConfigManage-Y5bhy7wG.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ConsoleManage-8ljYvCW2.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ConsoleManage-BWpyqbuQ.css +1 -0
- cancan_microstack/assets/www/adminops/assets/DashboardNew-B9Nf1OPl.js +1 -0
- cancan_microstack/assets/www/adminops/assets/DashboardNew-DYWZKQ1V.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LogSearch-CA0Jhe78.js +1 -0
- cancan_microstack/assets/www/adminops/assets/LogSearch-CCZfTNPF.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LoginView-BId3kP3M.css +1 -0
- cancan_microstack/assets/www/adminops/assets/LoginView-BQZTV_Qy.js +1 -0
- cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-BdEYwqFq.js +1 -0
- cancan_microstack/assets/www/adminops/assets/OperationProgressDialog-D-pASR8G.css +1 -0
- cancan_microstack/assets/www/adminops/assets/PageContainer-Byss-yUC.js +1 -0
- cancan_microstack/assets/www/adminops/assets/PageContainer-C3nSZwM7.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RateLimitManage-BDI8jLpC.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RateLimitManage-DJY4NiF-.js +1 -0
- cancan_microstack/assets/www/adminops/assets/RouteManage-DaUQ4QLw.css +1 -0
- cancan_microstack/assets/www/adminops/assets/RouteManage-w9XCU0UA.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceCard-BFzHe6Tw.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceCard-BJUhWnA-.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceDetail-Cw24WuKp.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceDetail-Yum47zdB.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceList-C7ryvbhE.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceList-Cgd01fUx.css +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceLogs-COpG9H0h.js +1 -0
- cancan_microstack/assets/www/adminops/assets/ServiceLogs-H_Alq0cf.css +1 -0
- cancan_microstack/assets/www/adminops/assets/StatsOverview-D0TwMQkA.js +39 -0
- cancan_microstack/assets/www/adminops/assets/StatsOverview-lqAN6pqM.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpBindView-CWlAmzFt.js +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpBindView-HoQC1lhx.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpVerifyView-BHN1VtX1.css +1 -0
- cancan_microstack/assets/www/adminops/assets/TotpVerifyView-D3w_lZk8.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowCenter-DU_mpIA0.css +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowCenter-i50rZyxN.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-CnHokPL9.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowDesigner-DaZaZpLd.css +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowRuns-B09hK48c.js +1 -0
- cancan_microstack/assets/www/adminops/assets/WorkflowRuns-wGutKIIU.css +1 -0
- cancan_microstack/assets/www/adminops/assets/caddy-nnCKf8fG.js +1 -0
- cancan_microstack/assets/www/adminops/assets/format-Cuzxgna9.js +1 -0
- cancan_microstack/assets/www/adminops/assets/index-CiFlm8oc.js +64 -0
- cancan_microstack/assets/www/adminops/assets/index-UW0T1Dkc.css +1 -0
- cancan_microstack/assets/www/adminops/assets/service-BYlgGPs_.js +1 -0
- cancan_microstack/assets/www/adminops/assets/service-operation-6GzLw2Z1.js +1 -0
- cancan_microstack/assets/www/adminops/assets/style-CcIXnQ5y.css +1 -0
- cancan_microstack/assets/www/adminops/assets/style-lRnStdGu.js +39 -0
- cancan_microstack/assets/www/adminops/assets/useDebounce-BRlqfXqf.js +1 -0
- cancan_microstack/assets/www/adminops/assets/workflow-CUXs39Ac.js +1 -0
- cancan_microstack/assets/www/adminops/index.html +16 -0
- cancan_microstack/assets/www/adminops/vite.svg +1 -0
- cancan_microstack/cli/__init__.py +14 -0
- cancan_microstack/cli/__main__.py +9 -0
- cancan_microstack/cli/main.py +552 -0
- cancan_microstack/cmd/__init__.py +54 -0
- cancan_microstack/cmd/cancan/__init__.py +12 -0
- cancan_microstack/cmd/cancan/run.py +395 -0
- cancan_microstack/cmd/controllersrv/__init__.py +0 -0
- cancan_microstack/cmd/controllersrv/run.py +131 -0
- cancan_microstack/cmd/infrasrv/__init__.py +5 -0
- cancan_microstack/cmd/infrasrv/run.py +100 -0
- cancan_microstack/cmd/opsbffsrv/__init__.py +5 -0
- cancan_microstack/cmd/opsbffsrv/run.py +96 -0
- cancan_microstack/core/__init__.py +5 -0
- cancan_microstack/core/assets.py +123 -0
- cancan_microstack/core/compose_builder.py +102 -0
- cancan_microstack/core/doctor.py +152 -0
- cancan_microstack/core/microstack.py +71 -0
- cancan_microstack/core/runner.py +56 -0
- cancan_microstack/core/stack_manager.py +186 -0
- cancan_microstack/public/__init__.py +7 -0
- cancan_microstack/public/api/__init__.py +1 -0
- cancan_microstack/public/api/controllersrv_client.py +277 -0
- cancan_microstack/public/api/infrasrv_client.py +404 -0
- cancan_microstack/public/const/__init__.py +1 -0
- cancan_microstack/public/const/action_consts.py +18 -0
- cancan_microstack/public/const/app_consts.py +42 -0
- cancan_microstack/public/const/caddy_consts.py +22 -0
- cancan_microstack/public/const/controllersrv_consts.py +163 -0
- cancan_microstack/public/const/docker_consts.py +15 -0
- cancan_microstack/public/const/error.py +56 -0
- cancan_microstack/public/const/health_consts.py +52 -0
- cancan_microstack/public/const/hook_enums.py +56 -0
- cancan_microstack/public/const/logging_enums.py +13 -0
- cancan_microstack/public/const/metrics_enums.py +36 -0
- cancan_microstack/public/const/monitor_enums.py +26 -0
- cancan_microstack/public/const/operation_consts.py +53 -0
- cancan_microstack/public/const/opsbffsrv_error.py +92 -0
- cancan_microstack/public/const/overrides_consts.py +13 -0
- cancan_microstack/public/const/redis.py +17 -0
- cancan_microstack/public/const/service_consts.py +15 -0
- cancan_microstack/public/const/workflow_consts.py +65 -0
- cancan_microstack/public/error.py +41 -0
- cancan_microstack/public/logging/__init__.py +0 -0
- cancan_microstack/public/logging/initializer.py +109 -0
- cancan_microstack/public/logging/mq_handler.py +279 -0
- cancan_microstack/public/schemas/__init__.py +1 -0
- cancan_microstack/public/schemas/caddy/__init__.py +381 -0
- cancan_microstack/public/schemas/caddy/analysis.py +90 -0
- cancan_microstack/public/schemas/caddy/route.py +18 -0
- cancan_microstack/public/schemas/common.py +79 -0
- cancan_microstack/public/schemas/controllersrv/__init__.py +3 -0
- cancan_microstack/public/schemas/controllersrv/async_requests.py +30 -0
- cancan_microstack/public/schemas/controllersrv/compose_models.py +47 -0
- cancan_microstack/public/schemas/controllersrv/const.py +24 -0
- cancan_microstack/public/schemas/controllersrv/docker_models.py +45 -0
- cancan_microstack/public/schemas/controllersrv/docker_responses.py +104 -0
- cancan_microstack/public/schemas/controllersrv/requests.py +54 -0
- cancan_microstack/public/schemas/controllersrv/responses.py +124 -0
- cancan_microstack/public/schemas/controllersrv/task_models.py +102 -0
- cancan_microstack/public/schemas/controllersrv/validation.py +23 -0
- cancan_microstack/public/schemas/hook_metrics.py +124 -0
- cancan_microstack/public/schemas/hooks.py +39 -0
- cancan_microstack/public/schemas/infra/__init__.py +0 -0
- cancan_microstack/public/schemas/infra/cleanup.py +25 -0
- cancan_microstack/public/schemas/infra/container.py +74 -0
- cancan_microstack/public/schemas/infra/enums.py +135 -0
- cancan_microstack/public/schemas/infra/health_check.py +42 -0
- cancan_microstack/public/schemas/infra/hook_log.py +42 -0
- cancan_microstack/public/schemas/infra/operation.py +90 -0
- cancan_microstack/public/schemas/infra/overview.py +25 -0
- cancan_microstack/public/schemas/infra/push.py +33 -0
- cancan_microstack/public/schemas/infra/service_action_log.py +47 -0
- cancan_microstack/public/schemas/infra/service_config.py +10 -0
- cancan_microstack/public/schemas/infra/service_info.py +69 -0
- cancan_microstack/public/schemas/infra/service_instance.py +93 -0
- cancan_microstack/public/schemas/infra/service_management.py +152 -0
- cancan_microstack/public/schemas/infra/service_operation.py +79 -0
- cancan_microstack/public/schemas/infra/service_registry.py +158 -0
- cancan_microstack/public/schemas/infra/status_types.py +19 -0
- cancan_microstack/public/schemas/infra/workflow.py +566 -0
- cancan_microstack/public/schemas/logging/__init__.py +1 -0
- cancan_microstack/public/schemas/logging/log_event.py +121 -0
- cancan_microstack/public/schemas/opsbffsrv/__init__.py +1 -0
- cancan_microstack/public/schemas/opsbffsrv/async_ops.py +17 -0
- cancan_microstack/public/schemas/opsbffsrv/db_admin.py +147 -0
- cancan_microstack/public/schemas/opsbffsrv/db_init.py +48 -0
- cancan_microstack/public/schemas/opsbffsrv/service_config.py +89 -0
- cancan_microstack/public/schemas/opsbffsrv/service_logs.py +54 -0
- cancan_microstack/public/schemas/service_operation.py +24 -0
- cancan_microstack/public/schemas/service_registry.py +40 -0
- cancan_microstack/public/types/__init__.py +7 -0
- cancan_microstack/public/web/__init__.py +0 -0
- cancan_microstack/public/web/config_value.py +105 -0
- cancan_microstack/public/web/server.py +385 -0
- cancan_microstack/py.typed +0 -0
- cancan_microstack/runtime/__init__.py +0 -0
- cancan_microstack/runtime/compose_cmd.py +228 -0
- cancan_microstack/runtime/host_daemon.py +318 -0
- cancan_microstack/runtime/overrides.py +103 -0
- cancan_microstack/runtime/resources.py +25 -0
- cancan_microstack/runtime/workspace.py +94 -0
- cancan_microstack/services/__init__.py +0 -0
- cancan_microstack/services/controllersrv/__init__.py +8 -0
- cancan_microstack/services/controllersrv/application/__init__.py +0 -0
- cancan_microstack/services/controllersrv/application/docker_compose_app.py +427 -0
- cancan_microstack/services/controllersrv/conf/__init__.py +0 -0
- cancan_microstack/services/controllersrv/conf/config.py +76 -0
- cancan_microstack/services/controllersrv/conf/settings.py +54 -0
- cancan_microstack/services/controllersrv/domain/__init__.py +0 -0
- cancan_microstack/services/controllersrv/domain/docker_compose/__init__.py +0 -0
- cancan_microstack/services/controllersrv/domain/docker_compose/docker_compose_domain.py +278 -0
- cancan_microstack/services/controllersrv/domain/service_validator.py +327 -0
- cancan_microstack/services/controllersrv/domain/task/__init__.py +17 -0
- cancan_microstack/services/controllersrv/domain/task/task_queue.py +286 -0
- cancan_microstack/services/controllersrv/domain/task/task_worker.py +495 -0
- cancan_microstack/services/controllersrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/controllersrv/interface/api/docker_control_api.py +470 -0
- cancan_microstack/services/controllersrv/router.py +132 -0
- cancan_microstack/services/infrasrv/__init__.py +4 -0
- cancan_microstack/services/infrasrv/application/__init__.py +0 -0
- cancan_microstack/services/infrasrv/application/health_check_app.py +24 -0
- cancan_microstack/services/infrasrv/application/logging/__init__.py +1 -0
- cancan_microstack/services/infrasrv/application/logging/log_ingestion_service.py +183 -0
- cancan_microstack/services/infrasrv/application/service_config.py +22 -0
- cancan_microstack/services/infrasrv/application/service_logs_app.py +53 -0
- cancan_microstack/services/infrasrv/application/service_management_app.py +689 -0
- cancan_microstack/services/infrasrv/application/service_operation_tracker.py +251 -0
- cancan_microstack/services/infrasrv/application/service_registry.py +53 -0
- cancan_microstack/services/infrasrv/application/workflow/__init__.py +0 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_app.py +991 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_queue.py +302 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_tasks.py +46 -0
- cancan_microstack/services/infrasrv/application/workflow/workflow_worker_runtime.py +122 -0
- cancan_microstack/services/infrasrv/conf/__init__.py +0 -0
- cancan_microstack/services/infrasrv/conf/config.py +98 -0
- cancan_microstack/services/infrasrv/domain/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/health_check/__init__.py +3 -0
- cancan_microstack/services/infrasrv/domain/health_check/health_check_domain.py +576 -0
- cancan_microstack/services/infrasrv/domain/hooks/__init__.py +19 -0
- cancan_microstack/services/infrasrv/domain/hooks/builtin_hooks.py +308 -0
- cancan_microstack/services/infrasrv/domain/hooks/hook_registry.py +43 -0
- cancan_microstack/services/infrasrv/domain/hooks/hooks_log_utils.py +275 -0
- cancan_microstack/services/infrasrv/domain/hooks/init.py +17 -0
- cancan_microstack/services/infrasrv/domain/hooks/metrics.py +205 -0
- cancan_microstack/services/infrasrv/domain/hooks/pre_registration_hooks.py +490 -0
- cancan_microstack/services/infrasrv/domain/registry/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/registry/service_registry.py +509 -0
- cancan_microstack/services/infrasrv/domain/service_config/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/service_config/service_config.py +50 -0
- cancan_microstack/services/infrasrv/domain/service_logs/__init__.py +0 -0
- cancan_microstack/services/infrasrv/domain/service_logs/service_logs_domain.py +51 -0
- cancan_microstack/services/infrasrv/domain/workflow/__init__.py +4 -0
- cancan_microstack/services/infrasrv/domain/workflow/engine.py +159 -0
- cancan_microstack/services/infrasrv/domain/workflow/node_handlers.py +509 -0
- cancan_microstack/services/infrasrv/domain/workflow/workflow_domain.py +164 -0
- cancan_microstack/services/infrasrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/api/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/api/controllersrv_api.py +165 -0
- cancan_microstack/services/infrasrv/infrastructure/cache/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/cache/service_registry_cache.py +174 -0
- cancan_microstack/services/infrasrv/infrastructure/db/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/execution_log_tbl.py +53 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/node_instance_tbl.py +55 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_config_tbl.py +30 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_info_tbl.py +59 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_instance_tbl.py +88 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/service_operation_tbl.py +73 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_tbl.py +55 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_definition_version_tbl.py +43 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_engine_alert_tbl.py +57 -0
- cancan_microstack/services/infrasrv/infrastructure/db/model/workflow_run_tbl.py +56 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/__init__.py +0 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_action_log_op.py +239 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config.py +80 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_config_manager.py +198 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_info_op.py +297 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_instance_op.py +688 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_operation_op.py +387 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/service_registry.py +124 -0
- cancan_microstack/services/infrasrv/infrastructure/db/operate/workflow_op.py +804 -0
- cancan_microstack/services/infrasrv/infrastructure/ddl_manager.py +31 -0
- cancan_microstack/services/infrasrv/infrastructure/mongo/__init__.py +1 -0
- cancan_microstack/services/infrasrv/infrastructure/mongo/log_repository.py +129 -0
- cancan_microstack/services/infrasrv/interface/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/api/health_check_api.py +29 -0
- cancan_microstack/services/infrasrv/interface/api/hooks.py +284 -0
- cancan_microstack/services/infrasrv/interface/api/internal.py +49 -0
- cancan_microstack/services/infrasrv/interface/api/internal_instance_api.py +265 -0
- cancan_microstack/services/infrasrv/interface/api/internal_operation_api.py +206 -0
- cancan_microstack/services/infrasrv/interface/api/service_config.py +50 -0
- cancan_microstack/services/infrasrv/interface/api/service_logs_api.py +49 -0
- cancan_microstack/services/infrasrv/interface/api/service_management_api.py +113 -0
- cancan_microstack/services/infrasrv/interface/api/service_registry.py +117 -0
- cancan_microstack/services/infrasrv/interface/api/workflow_api.py +303 -0
- cancan_microstack/services/infrasrv/interface/schedule/__init__.py +0 -0
- cancan_microstack/services/infrasrv/interface/schedule/cleanup.py +13 -0
- cancan_microstack/services/infrasrv/interface/schedule/health_check.py +27 -0
- cancan_microstack/services/infrasrv/interface/schedule/log_cleanup.py +26 -0
- cancan_microstack/services/infrasrv/interface/schedule/operation_tracker.py +25 -0
- cancan_microstack/services/infrasrv/interface/schedule/scheduler.py +39 -0
- cancan_microstack/services/infrasrv/interface/schedule/workflow_scheduler.py +115 -0
- cancan_microstack/services/infrasrv/router.py +341 -0
- cancan_microstack/services/opsbffsrv/__init__.py +4 -0
- cancan_microstack/services/opsbffsrv/application/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/application/async_operation_app.py +150 -0
- cancan_microstack/services/opsbffsrv/application/auth_app.py +285 -0
- cancan_microstack/services/opsbffsrv/application/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/application/caddy/access_log_analysis_app.py +344 -0
- cancan_microstack/services/opsbffsrv/application/caddy/access_log_ingestion_service.py +169 -0
- cancan_microstack/services/opsbffsrv/application/caddy/certificate_management_app.py +355 -0
- cancan_microstack/services/opsbffsrv/application/caddy/rate_limit_management_app.py +496 -0
- cancan_microstack/services/opsbffsrv/application/caddy/route_management_app.py +401 -0
- cancan_microstack/services/opsbffsrv/application/caddy/stats_aggregation_app.py +364 -0
- cancan_microstack/services/opsbffsrv/application/db_admin_app.py +103 -0
- cancan_microstack/services/opsbffsrv/application/db_init_app.py +283 -0
- cancan_microstack/services/opsbffsrv/application/logging/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/application/logging/log_query_app.py +28 -0
- cancan_microstack/services/opsbffsrv/application/service_config.py +158 -0
- cancan_microstack/services/opsbffsrv/application/service_logs_app.py +74 -0
- cancan_microstack/services/opsbffsrv/application/service_registry.py +36 -0
- cancan_microstack/services/opsbffsrv/application/workflow_ops_app.py +730 -0
- cancan_microstack/services/opsbffsrv/conf/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/conf/config.py +224 -0
- cancan_microstack/services/opsbffsrv/domain/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/auth/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/auth/admin_init.py +38 -0
- cancan_microstack/services/opsbffsrv/domain/auth/auth_domain.py +108 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/access_log_analysis.py +358 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/certificate_management.py +325 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/default_routes.py +53 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/rate_limit_management.py +308 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/route_management.py +279 -0
- cancan_microstack/services/opsbffsrv/domain/caddy/stats_aggregation.py +654 -0
- cancan_microstack/services/opsbffsrv/domain/db_admin/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/db_admin/db_admin_domain.py +118 -0
- cancan_microstack/services/opsbffsrv/domain/db_init/__init__.py +3 -0
- cancan_microstack/services/opsbffsrv/domain/db_init/db_init_domain.py +358 -0
- cancan_microstack/services/opsbffsrv/domain/logging/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/domain/logging/log_query_domain.py +99 -0
- cancan_microstack/services/opsbffsrv/domain/service_config/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/service_config/service_config.py +81 -0
- cancan_microstack/services/opsbffsrv/domain/service_registry/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/domain/service_registry/service_registry.py +292 -0
- cancan_microstack/services/opsbffsrv/infrastructure/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/api/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/api/infrasrv_api.py +242 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/captcha_service.py +67 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/password_service.py +12 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/redis_store.py +131 -0
- cancan_microstack/services/opsbffsrv/infrastructure/auth/totp_service.py +59 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/access_log_parser.py +307 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/admin_api_client.py +678 -0
- cancan_microstack/services/opsbffsrv/infrastructure/caddy/ip_geo_locator.py +176 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/admin_user_tbl.py +33 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_access_log_tbl.py +90 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_certificate_tbl.py +65 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_rate_limit_tbl.py +69 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_route_tbl.py +66 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/caddy_stats_tbl.py +78 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_action_log_tbl.py +44 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_config_tbl.py +30 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_info_tbl.py +51 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/model/service_instance_tbl.py +68 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/admin_user_operate.py +59 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_access_log.py +531 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_certificate.py +451 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_rate_limit.py +360 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_route.py +271 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/caddy_stats.py +343 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_action_log_op.py +57 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_config.py +86 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_info_op.py +79 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_instance.py +58 -0
- cancan_microstack/services/opsbffsrv/infrastructure/db/operate/service_registry.py +138 -0
- cancan_microstack/services/opsbffsrv/infrastructure/ddl_manager.py +31 -0
- cancan_microstack/services/opsbffsrv/infrastructure/mongo/__init__.py +1 -0
- cancan_microstack/services/opsbffsrv/infrastructure/mongo/log_query_repository.py +87 -0
- cancan_microstack/services/opsbffsrv/interface/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/api/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/api/async_operation_api.py +137 -0
- cancan_microstack/services/opsbffsrv/interface/api/auth_api.py +113 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/__init__.py +3 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/access_log_api.py +174 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/certificate_api.py +235 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/rate_limit_api.py +302 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/route_api.py +250 -0
- cancan_microstack/services/opsbffsrv/interface/api/caddy/stats_api.py +243 -0
- cancan_microstack/services/opsbffsrv/interface/api/db_admin_api.py +62 -0
- cancan_microstack/services/opsbffsrv/interface/api/db_init_api.py +109 -0
- cancan_microstack/services/opsbffsrv/interface/api/instance_management_api.py +165 -0
- cancan_microstack/services/opsbffsrv/interface/api/log_query_api.py +41 -0
- cancan_microstack/services/opsbffsrv/interface/api/mongo_express_proxy_api.py +181 -0
- cancan_microstack/services/opsbffsrv/interface/api/pgweb_proxy_api.py +154 -0
- cancan_microstack/services/opsbffsrv/interface/api/rabbitmq_mgmt_proxy_api.py +518 -0
- cancan_microstack/services/opsbffsrv/interface/api/redis_commander_proxy_api.py +133 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_config.py +146 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_logs_api.py +81 -0
- cancan_microstack/services/opsbffsrv/interface/api/service_registry.py +66 -0
- cancan_microstack/services/opsbffsrv/interface/api/workflow_ops_api.py +413 -0
- cancan_microstack/services/opsbffsrv/interface/middleware/__init__.py +0 -0
- cancan_microstack/services/opsbffsrv/interface/middleware/auth_middleware.py +52 -0
- cancan_microstack/services/opsbffsrv/router.py +901 -0
- cancan_microstack/utils/__init__.py +1 -0
- cancan_microstack/utils/container_env.py +218 -0
- cancan_microstack-0.0.1.dist-info/METADATA +155 -0
- cancan_microstack-0.0.1.dist-info/RECORD +440 -0
- cancan_microstack-0.0.1.dist-info/WHEEL +5 -0
- cancan_microstack-0.0.1.dist-info/entry_points.txt +2 -0
- cancan_microstack-0.0.1.dist-info/licenses/LICENSE +21 -0
- cancan_microstack-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import http
|
|
3
|
+
|
|
4
|
+
from cancan_microstack.public.const.error import ErrorCode
|
|
5
|
+
from cancan_microstack.public.schemas.common import (
|
|
6
|
+
APIResponse,
|
|
7
|
+
MessageResp,
|
|
8
|
+
)
|
|
9
|
+
from cancan_microstack.public.schemas.infra.enums import (
|
|
10
|
+
HookExecutionResult,
|
|
11
|
+
ServiceType,
|
|
12
|
+
)
|
|
13
|
+
from cancan_microstack.public.schemas.infra.service_registry import (
|
|
14
|
+
ServiceInstance,
|
|
15
|
+
ServiceInstanceList,
|
|
16
|
+
ServiceRegistryCreate,
|
|
17
|
+
)
|
|
18
|
+
from linglong_web import build_success_response
|
|
19
|
+
from cancan_microstack.public.error import HTTPException
|
|
20
|
+
from cancan_microstack.services.infrasrv.application.service_registry import ServiceRegistryApp
|
|
21
|
+
from cancan_microstack.services.infrasrv.domain.hooks import get_hook_manager
|
|
22
|
+
from cancan_microstack.public.schemas.hooks import HookContext
|
|
23
|
+
from linglong_web.utils import logger
|
|
24
|
+
|
|
25
|
+
_service_registry_app = ServiceRegistryApp()
|
|
26
|
+
_hook_manager = get_hook_manager()
|
|
27
|
+
|
|
28
|
+
SERVICE_NAME_TO_TYPE = {
|
|
29
|
+
"infrasrv": ServiceType.INFRASTRUCTURE,
|
|
30
|
+
"opsbffsrv": ServiceType.OPS,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# 内置校验类钩子:这些钩子的 FAILURE 视为"校验未通过",必须真正拒绝注册(返回 400),
|
|
34
|
+
# 否则空 host / 坏端口 / 非法服务名等明显非法注册会被放行,使"校验"名不副实。
|
|
35
|
+
# 钩子名称见 domain/hooks/builtin_hooks.py。
|
|
36
|
+
ENFORCING_VALIDATION_HOOKS = frozenset({
|
|
37
|
+
"service_name_validation",
|
|
38
|
+
"host_validation",
|
|
39
|
+
"port_range_validation",
|
|
40
|
+
"service_quota",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def register_service_handler(
|
|
45
|
+
service: ServiceRegistryCreate,
|
|
46
|
+
) -> APIResponse[MessageResp | None]:
|
|
47
|
+
"""
|
|
48
|
+
注册服务
|
|
49
|
+
供业务服务启动时注册使用
|
|
50
|
+
"""
|
|
51
|
+
logger.info(
|
|
52
|
+
f"Received registration request for service: {service.service_name}, host: '{service.host}', port: {service.port}")
|
|
53
|
+
|
|
54
|
+
# 推导服务类型
|
|
55
|
+
service_type = SERVICE_NAME_TO_TYPE.get(service.service_name, ServiceType.BUSINESS)
|
|
56
|
+
|
|
57
|
+
# 创建钩子执行上下文
|
|
58
|
+
hook_context = HookContext(
|
|
59
|
+
service_name=service.service_name,
|
|
60
|
+
service_type=service_type,
|
|
61
|
+
instance_id=service.instance_id,
|
|
62
|
+
host=service.host,
|
|
63
|
+
port=service.port,
|
|
64
|
+
metadata=service.service_metadata or {}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 执行预注册钩子
|
|
68
|
+
hook_results = await _hook_manager.execute_hooks(hook_context)
|
|
69
|
+
|
|
70
|
+
# 检查钩子执行结果
|
|
71
|
+
for hook_result in hook_results:
|
|
72
|
+
if hook_result.result == HookExecutionResult.TERMINATE: # 钩子主动中断整条链,拒绝注册
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=http.HTTPStatus.BAD_REQUEST.value,
|
|
75
|
+
error_code=ErrorCode.REGISTRATION_REJECTED,
|
|
76
|
+
msg=f"Registration rejected by hook {hook_result.hook_name}: {hook_result.message}",
|
|
77
|
+
)
|
|
78
|
+
elif hook_result.result == HookExecutionResult.FAILURE:
|
|
79
|
+
# 内置校验类钩子失败 = 注册信息非法,真正拒绝注册(让"校验"名副其实)。
|
|
80
|
+
if hook_result.hook_name in ENFORCING_VALIDATION_HOOKS:
|
|
81
|
+
raise HTTPException(
|
|
82
|
+
status_code=http.HTTPStatus.BAD_REQUEST.value,
|
|
83
|
+
error_code=ErrorCode.REGISTRATION_REJECTED,
|
|
84
|
+
msg=f"Registration rejected by validation hook {hook_result.hook_name}: {hook_result.message}",
|
|
85
|
+
)
|
|
86
|
+
# 其它(非内置校验)钩子失败维持原行为:仅告警、不拦截。
|
|
87
|
+
logger.warning(f"Hook {hook_result.hook_name} failed: {hook_result.message or hook_result.error}")
|
|
88
|
+
|
|
89
|
+
# 如果钩子修改了服务信息,使用修改后的信息
|
|
90
|
+
# 这里可以添加逻辑处理钩子对上下文的修改
|
|
91
|
+
|
|
92
|
+
await _service_registry_app.register_service(service)
|
|
93
|
+
return build_success_response(data=MessageResp(message="Service registered successfully"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def deregister_service_handler(
|
|
97
|
+
service_name: str,
|
|
98
|
+
instance_id: str,
|
|
99
|
+
) -> APIResponse[MessageResp]:
|
|
100
|
+
"""
|
|
101
|
+
注销服务
|
|
102
|
+
供业务服务关闭时注销使用
|
|
103
|
+
"""
|
|
104
|
+
await _service_registry_app.deregister_service(service_name, instance_id)
|
|
105
|
+
return build_success_response(data=MessageResp(message="Service deregistered successfully"))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def get_service_instances_handler(
|
|
109
|
+
service_name: str,
|
|
110
|
+
only_healthy: bool = True,
|
|
111
|
+
) -> APIResponse[ServiceInstanceList]:
|
|
112
|
+
"""
|
|
113
|
+
获取服务实例列表
|
|
114
|
+
供业务服务进行服务发现使用
|
|
115
|
+
"""
|
|
116
|
+
instances: List[ServiceInstance] = await _service_registry_app.get_service_instances(service_name, only_healthy)
|
|
117
|
+
return build_success_response(data=ServiceInstanceList(instances=instances))
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
工作流引擎 API 接口层
|
|
3
|
+
Workflow Engine API Interface Layer
|
|
4
|
+
"""
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import (
|
|
7
|
+
Dict,
|
|
8
|
+
Any,
|
|
9
|
+
Optional,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from fastapi import Query
|
|
13
|
+
|
|
14
|
+
from linglong_web import build_success_response
|
|
15
|
+
from cancan_microstack.public.schemas.common import APIResponse
|
|
16
|
+
from cancan_microstack.public.schemas.infra import workflow as workflow_types
|
|
17
|
+
from cancan_microstack.services.infrasrv.application.workflow.workflow_app import workflow_app
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def create_workflow_handler(
|
|
21
|
+
payload: workflow_types.WorkflowDefinitionCreate,
|
|
22
|
+
) -> APIResponse[workflow_types.WorkflowDefinition]:
|
|
23
|
+
"""
|
|
24
|
+
创建一个新的工作流定义
|
|
25
|
+
Create a new workflow definition.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
payload: 工作流定义数据
|
|
29
|
+
Workflow definition data.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
已创建的工作流定义
|
|
33
|
+
The created workflow definition.
|
|
34
|
+
"""
|
|
35
|
+
result = await workflow_app.create_workflow_definition(payload)
|
|
36
|
+
return build_success_response(data=result)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def list_workflows_handler(
|
|
40
|
+
page: int = Query(1, ge=1, description="页码"),
|
|
41
|
+
page_size: int = Query(20, ge=1, le=100, description="每页大小"),
|
|
42
|
+
) -> APIResponse[workflow_types.WorkflowListResponse]:
|
|
43
|
+
"""
|
|
44
|
+
列出所有工作流定义
|
|
45
|
+
List all workflow definitions.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
工作流定义列表
|
|
49
|
+
A list of workflow definitions.
|
|
50
|
+
"""
|
|
51
|
+
result = await workflow_app.list_workflow_definitions(page, page_size)
|
|
52
|
+
return build_success_response(data=result)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def get_workflow_handler(
|
|
56
|
+
workflow_id: str,
|
|
57
|
+
) -> APIResponse[workflow_types.WorkflowDefinition]:
|
|
58
|
+
"""
|
|
59
|
+
根据 ID 获取指定的工作流定义
|
|
60
|
+
Get a specific workflow definition by ID.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
workflow_id: 工作流定义的唯一标识符
|
|
64
|
+
The unique identifier of the workflow definition.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
工作流定义
|
|
68
|
+
The workflow definition.
|
|
69
|
+
"""
|
|
70
|
+
result = await workflow_app.get_workflow_definition(workflow_id)
|
|
71
|
+
return build_success_response(data=result)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def list_workflow_versions_handler(
|
|
75
|
+
workflow_id: str,
|
|
76
|
+
limit: int = Query(50, ge=1, le=200, description="返回的版本数量限制"),
|
|
77
|
+
) -> APIResponse[workflow_types.WorkflowVersionListResponse]:
|
|
78
|
+
"""列出指定工作流的版本历史
|
|
79
|
+
List workflow definition versions for the given workflow."""
|
|
80
|
+
|
|
81
|
+
result = await workflow_app.list_workflow_versions(workflow_id, limit)
|
|
82
|
+
return build_success_response(data=result)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def list_runs_handler(
|
|
86
|
+
workflow_id: Optional[str] = Query(None, description="按工作流ID过滤"),
|
|
87
|
+
reqid: Optional[str] = Query(None, description="按请求追踪 ID 过滤"),
|
|
88
|
+
status: Optional[str] = Query(None, description="运行状态过滤"),
|
|
89
|
+
date_from: Optional[datetime] = Query(None, description="开始时间过滤"),
|
|
90
|
+
date_to: Optional[datetime] = Query(None, description="结束时间过滤"),
|
|
91
|
+
page: int = Query(1, ge=1, description="页码"),
|
|
92
|
+
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
|
93
|
+
) -> APIResponse[workflow_types.WorkflowRunListResponse]:
|
|
94
|
+
"""
|
|
95
|
+
列出工作流的运行实例
|
|
96
|
+
List workflow runs.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
workflow_id: (可选) 按工作流 ID 过滤
|
|
100
|
+
(Optional) Filter by workflow ID.
|
|
101
|
+
page: 页码
|
|
102
|
+
Page number.
|
|
103
|
+
page_size: 每页大小
|
|
104
|
+
Size per page.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
工作流运行实例列表
|
|
108
|
+
A list of workflow runs.
|
|
109
|
+
"""
|
|
110
|
+
query = workflow_types.WorkflowRunQuery(
|
|
111
|
+
workflow_id=workflow_id,
|
|
112
|
+
reqid=reqid,
|
|
113
|
+
status=status,
|
|
114
|
+
date_from=date_from,
|
|
115
|
+
date_to=date_to,
|
|
116
|
+
page=page,
|
|
117
|
+
page_size=page_size,
|
|
118
|
+
)
|
|
119
|
+
result = await workflow_app.list_workflow_runs(query)
|
|
120
|
+
return build_success_response(data=result)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def trigger_workflow_handler(
|
|
124
|
+
workflow_id: str, payload: workflow_types.WorkflowTriggerRequest,
|
|
125
|
+
) -> APIResponse[workflow_types.WorkflowTriggerResponse]:
|
|
126
|
+
"""
|
|
127
|
+
触发一次工作流运行
|
|
128
|
+
Trigger a workflow run.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
workflow_id: 要触发的工作流的 ID
|
|
132
|
+
The ID of the workflow to trigger.
|
|
133
|
+
payload: 触发工作流的上下文数据
|
|
134
|
+
The context data for triggering the workflow.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
包含运行 ID 和状态的字典
|
|
138
|
+
A dictionary containing the run ID and status.
|
|
139
|
+
"""
|
|
140
|
+
result = await workflow_app.trigger_workflow(workflow_id, payload)
|
|
141
|
+
return build_success_response(data=result)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def get_run_graph_status_handler(
|
|
145
|
+
run_id: str,
|
|
146
|
+
) -> APIResponse[workflow_types.RunGraphResponse]:
|
|
147
|
+
"""
|
|
148
|
+
获取工作流运行实例中所有节点的状态
|
|
149
|
+
Get the status of all nodes in a workflow run graph.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
run_id: 工作流运行实例的 ID
|
|
153
|
+
The ID of the workflow run instance.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
节点状态列表
|
|
157
|
+
A list of node statuses.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
result = await workflow_app.get_run_graph_status(run_id)
|
|
161
|
+
return build_success_response(data=result)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def get_node_history_handler(
|
|
165
|
+
run_id: str, node_id: str,
|
|
166
|
+
) -> APIResponse[workflow_types.NodeExecutionHistoryResponse]:
|
|
167
|
+
"""
|
|
168
|
+
获取运行实例中特定节点的执行历史
|
|
169
|
+
Get the execution history of a specific node in a run.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
run_id: 工作流运行实例的 ID
|
|
173
|
+
The ID of the workflow run instance.
|
|
174
|
+
node_id: 节点的 ID
|
|
175
|
+
The ID of the node.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
节点的执行历史列表
|
|
179
|
+
A list of execution history for the node.
|
|
180
|
+
"""
|
|
181
|
+
result = await workflow_app.get_node_history(run_id, node_id)
|
|
182
|
+
return build_success_response(data=result)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def external_callback_handler(
|
|
186
|
+
node_instance_id: str, payload: Dict[str, Any],
|
|
187
|
+
) -> APIResponse[workflow_types.CallbackAckResponse]:
|
|
188
|
+
"""
|
|
189
|
+
处理等待外部回调的节点的恢复操作
|
|
190
|
+
Handle external callbacks for suspended nodes.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
node_instance_id: 节点实例的唯一标识符
|
|
194
|
+
The unique identifier of the node instance.
|
|
195
|
+
payload: 外部服务返回的数据
|
|
196
|
+
Data returned from the external service.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
确认接受回调的状态
|
|
200
|
+
A status confirming acceptance of the callback.
|
|
201
|
+
"""
|
|
202
|
+
result = await workflow_app.handle_external_callback(node_instance_id, payload)
|
|
203
|
+
return build_success_response(data=result)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def get_workflow_stats_handler() -> APIResponse[workflow_types.WorkflowStats]:
|
|
207
|
+
"""
|
|
208
|
+
获取工作流统计信息
|
|
209
|
+
Get workflow statistics.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
工作流统计信息
|
|
213
|
+
Workflow statistics.
|
|
214
|
+
"""
|
|
215
|
+
result = await workflow_app.get_workflow_stats()
|
|
216
|
+
return build_success_response(data=result)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def update_workflow_handler(
|
|
220
|
+
workflow_id: str,
|
|
221
|
+
payload: workflow_types.WorkflowDefinitionUpdate,
|
|
222
|
+
) -> APIResponse[workflow_types.WorkflowDefinition]:
|
|
223
|
+
"""
|
|
224
|
+
更新工作流定义
|
|
225
|
+
Update workflow definition.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
workflow_id: 工作流唯一标识符 / Workflow unique identifier.
|
|
229
|
+
payload: 更新数据 / Update data.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
更新后的工作流定义 / The updated workflow definition.
|
|
233
|
+
"""
|
|
234
|
+
result = await workflow_app.update_workflow_definition(workflow_id, payload)
|
|
235
|
+
return build_success_response(data=result)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def delete_workflow_handler(
|
|
239
|
+
workflow_id: str,
|
|
240
|
+
) -> APIResponse[Dict[str, str]]:
|
|
241
|
+
"""
|
|
242
|
+
删除工作流定义
|
|
243
|
+
Delete workflow definition.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
workflow_id: 工作流唯一标识符 / Workflow unique identifier.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
删除确认消息 / Deletion confirmation message.
|
|
250
|
+
"""
|
|
251
|
+
await workflow_app.delete_workflow_definition(workflow_id)
|
|
252
|
+
return build_success_response(data={"message": "Workflow deleted successfully"})
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
async def rollback_workflow_handler(
|
|
256
|
+
workflow_id: str,
|
|
257
|
+
payload: workflow_types.WorkflowRollbackRequest,
|
|
258
|
+
) -> APIResponse[workflow_types.WorkflowDefinition]:
|
|
259
|
+
"""回滚工作流定义到指定版本
|
|
260
|
+
Roll back workflow definition to a historical version."""
|
|
261
|
+
|
|
262
|
+
result = await workflow_app.rollback_workflow_definition(workflow_id, payload)
|
|
263
|
+
return build_success_response(data=result)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def list_engine_alerts_handler(
|
|
267
|
+
status: Optional[workflow_types.WorkflowEngineAlertStatus] = Query(None, description="按状态过滤"),
|
|
268
|
+
severity: Optional[workflow_types.WorkflowEngineAlertSeverity] = Query(None, description="按严重程度过滤"),
|
|
269
|
+
run_id: Optional[str] = Query(None, description="按运行 ID 过滤"),
|
|
270
|
+
page: int = Query(1, ge=1, description="页码"),
|
|
271
|
+
page_size: int = Query(20, ge=1, le=200, description="每页大小"),
|
|
272
|
+
) -> APIResponse[workflow_types.WorkflowEngineAlertListResponse]:
|
|
273
|
+
"""列出工作流引擎告警"""
|
|
274
|
+
|
|
275
|
+
query = workflow_types.WorkflowEngineAlertQuery(
|
|
276
|
+
status=status,
|
|
277
|
+
severity=severity,
|
|
278
|
+
run_id=run_id,
|
|
279
|
+
page=page,
|
|
280
|
+
page_size=page_size,
|
|
281
|
+
)
|
|
282
|
+
result = await workflow_app.list_engine_alerts(query)
|
|
283
|
+
return build_success_response(data=result)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def acknowledge_engine_alert_handler(
|
|
287
|
+
alert_id: str,
|
|
288
|
+
payload: workflow_types.WorkflowEngineAlertAckRequest,
|
|
289
|
+
) -> APIResponse[workflow_types.WorkflowEngineAlert]:
|
|
290
|
+
"""标记引擎告警为已知晓"""
|
|
291
|
+
|
|
292
|
+
result = await workflow_app.acknowledge_engine_alert(alert_id, payload)
|
|
293
|
+
return build_success_response(data=result)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
async def resolve_engine_alert_handler(
|
|
297
|
+
alert_id: str,
|
|
298
|
+
payload: workflow_types.WorkflowEngineAlertAckRequest,
|
|
299
|
+
) -> APIResponse[workflow_types.WorkflowEngineAlert]:
|
|
300
|
+
"""标记引擎告警为已解决"""
|
|
301
|
+
|
|
302
|
+
result = await workflow_app.resolve_engine_alert(alert_id, payload)
|
|
303
|
+
return build_success_response(data=result)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from linglong_web.utils import logger
|
|
2
|
+
from cancan_microstack.services.infrasrv.application.service_registry import ServiceRegistryApp
|
|
3
|
+
|
|
4
|
+
_service_registry_app = ServiceRegistryApp()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def cleanup_dead_instances_task():
|
|
8
|
+
"""定期清理已下线的服务实例"""
|
|
9
|
+
try:
|
|
10
|
+
logger.info("Running scheduled cleanup of dead service instances...")
|
|
11
|
+
await _service_registry_app.cleanup_dead_instances()
|
|
12
|
+
except Exception as e:
|
|
13
|
+
logger.error(f"Error in cleanup task: {e}", exc_info=True)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from linglong_web.utils import logger
|
|
2
|
+
from cancan_microstack.services.infrasrv.application.health_check_app import HealthCheckApp
|
|
3
|
+
|
|
4
|
+
_health_check_app = HealthCheckApp()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def health_check_task():
|
|
8
|
+
"""
|
|
9
|
+
定期健康检查任务
|
|
10
|
+
|
|
11
|
+
支持:
|
|
12
|
+
- 多实例健康检查
|
|
13
|
+
- 操作窗口期豁免
|
|
14
|
+
- 区分正常/异常关闭
|
|
15
|
+
- 自动重启不健康实例
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
logger.info("Running scheduled health check (multi-instance)...")
|
|
19
|
+
result = await _health_check_app.check_all_instances()
|
|
20
|
+
|
|
21
|
+
logger.info(
|
|
22
|
+
f"Health check completed: {result.healthy} healthy, "
|
|
23
|
+
f"{result.degraded} degraded, {result.unhealthy} unhealthy, "
|
|
24
|
+
f"{result.exempted} exempted, {result.expected_stopped} expected_stopped"
|
|
25
|
+
)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
logger.error(f"Error in health check task: {e}", exc_info=True)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""日志清理定时任务 / Scheduled task for log retention."""
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
|
|
4
|
+
from linglong_web import LinglongConfig
|
|
5
|
+
from linglong_web.utils import logger
|
|
6
|
+
|
|
7
|
+
from cancan_microstack.services.infrasrv.application.logging.log_ingestion_service import get_log_ingestion_service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def cleanup_expired_logs_task() -> None:
|
|
11
|
+
"""删除超过保留期的日志 / Delete logs that exceed retention."""
|
|
12
|
+
# 从运行时配置读取日志保留策略,提供安全默认值。
|
|
13
|
+
# Read retention policy from runtime config with safe defaults.
|
|
14
|
+
retention_days = int(LinglongConfig.LOG_RETENTION_DAYS)
|
|
15
|
+
batch_limit = int(LinglongConfig.LOG_RETENTION_BATCH_LIMIT)
|
|
16
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
|
|
17
|
+
|
|
18
|
+
repo = get_log_ingestion_service().get_repository()
|
|
19
|
+
if repo is None:
|
|
20
|
+
logger.warning("Log repository not available, skipping cleanup task")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
deleted = await repo.cleanup_older_than(cutoff, batch_limit)
|
|
24
|
+
logger.info(
|
|
25
|
+
"Log retention cleanup finished: deleted=%s cutoff=%s", deleted, cutoff.isoformat()
|
|
26
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""定时同步 controllersrv 任务状态 / Periodic controllersrv task synchronisation"""
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
from linglong_web.utils import logger
|
|
5
|
+
|
|
6
|
+
from cancan_microstack.services.infrasrv.application.service_operation_tracker import (
|
|
7
|
+
ServiceOperationTrackerApp,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
_tracker_app = ServiceOperationTrackerApp()
|
|
11
|
+
_poll_lock = asyncio.Lock()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def operation_tracker_task() -> None:
|
|
15
|
+
"""每次执行时轮询 controllersrv 并刷新操作状态 / Poll controllersrv and refresh operation states"""
|
|
16
|
+
|
|
17
|
+
if _poll_lock.locked():
|
|
18
|
+
logger.warning("Operation tracker still running, skipping this tick")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
async with _poll_lock:
|
|
22
|
+
try:
|
|
23
|
+
await _tracker_app.run_once()
|
|
24
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
25
|
+
logger.error("Operation tracker failed: %s", exc, exc_info=True)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from aioclock import Every
|
|
2
|
+
|
|
3
|
+
from linglong_web import (
|
|
4
|
+
BaseScheduler,
|
|
5
|
+
to_group,
|
|
6
|
+
)
|
|
7
|
+
from cancan_microstack.services.infrasrv.interface.schedule.health_check import health_check_task
|
|
8
|
+
from cancan_microstack.services.infrasrv.interface.schedule.cleanup import cleanup_dead_instances_task
|
|
9
|
+
from cancan_microstack.services.infrasrv.interface.schedule.log_cleanup import cleanup_expired_logs_task
|
|
10
|
+
from cancan_microstack.services.infrasrv.interface.schedule.workflow_scheduler import workflow_scheduler_task
|
|
11
|
+
from cancan_microstack.services.infrasrv.interface.schedule.operation_tracker import operation_tracker_task
|
|
12
|
+
|
|
13
|
+
scheduler_group = to_group([
|
|
14
|
+
BaseScheduler(
|
|
15
|
+
name="service_health_check",
|
|
16
|
+
trigger=Every(seconds=30, first_run_strategy="wait"),
|
|
17
|
+
func=health_check_task,
|
|
18
|
+
),
|
|
19
|
+
BaseScheduler(
|
|
20
|
+
name="operation_tracker",
|
|
21
|
+
trigger=Every(seconds=10, first_run_strategy="wait"),
|
|
22
|
+
func=operation_tracker_task,
|
|
23
|
+
),
|
|
24
|
+
BaseScheduler(
|
|
25
|
+
name="cleanup_dead_instances",
|
|
26
|
+
trigger=Every(minutes=5, first_run_strategy="wait"),
|
|
27
|
+
func=cleanup_dead_instances_task,
|
|
28
|
+
),
|
|
29
|
+
BaseScheduler(
|
|
30
|
+
name="cleanup_log_documents",
|
|
31
|
+
trigger=Every(hours=24, first_run_strategy="immediate"),
|
|
32
|
+
func=cleanup_expired_logs_task,
|
|
33
|
+
),
|
|
34
|
+
BaseScheduler(
|
|
35
|
+
name="workflow_scheduler",
|
|
36
|
+
trigger=Every(minutes=1, first_run_strategy="immediate"),
|
|
37
|
+
func=workflow_scheduler_task,
|
|
38
|
+
),
|
|
39
|
+
])
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow Scheduler Task
|
|
3
|
+
"""
|
|
4
|
+
from datetime import (
|
|
5
|
+
datetime,
|
|
6
|
+
timedelta,
|
|
7
|
+
)
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from croniter import croniter
|
|
11
|
+
|
|
12
|
+
from cancan_microstack.public.schemas.infra import workflow as workflow_types
|
|
13
|
+
from linglong_web.utils import (
|
|
14
|
+
get_request_id,
|
|
15
|
+
logger,
|
|
16
|
+
set_request_id,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from cancan_microstack.services.infrasrv.application.workflow.workflow_app import workflow_app
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def workflow_scheduler_task():
|
|
23
|
+
"""扫描并触发需要调度的工作流
|
|
24
|
+
Scan for due workflows and trigger them via the workflow app.
|
|
25
|
+
"""
|
|
26
|
+
logger.info("Workflow scheduler task started.")
|
|
27
|
+
try:
|
|
28
|
+
await scan_and_trigger_workflows()
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
logger.error(f"Error in workflow scheduler: {exc}", exc_info=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def scan_and_trigger_workflows(reference_time: Optional[datetime] = None):
|
|
34
|
+
"""Scan registered definitions and trigger due workflows.
|
|
35
|
+
扫描已注册的工作流定义并触发符合条件的调度。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
now = (reference_time or datetime.now()).replace(microsecond=0)
|
|
39
|
+
scheduled_workflows = await workflow_app.get_scheduled_workflows()
|
|
40
|
+
|
|
41
|
+
if not scheduled_workflows:
|
|
42
|
+
logger.info("No scheduled workflows found.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
triggered_count = 0
|
|
46
|
+
for workflow in scheduled_workflows:
|
|
47
|
+
cron_expr = (workflow.schedule or "").strip()
|
|
48
|
+
if not cron_expr:
|
|
49
|
+
logger.warning(f"Empty cron string for workflow {workflow.id}")
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
second_at_beginning = _cron_has_seconds(cron_expr)
|
|
53
|
+
if not croniter.is_valid(cron_expr, second_at_beginning=second_at_beginning):
|
|
54
|
+
logger.warning(f"Invalid cron string for workflow {workflow.id}: {workflow.schedule}")
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
base_time = _normalize_base_time(cron_expr, now)
|
|
58
|
+
if not _cron_due(cron_expr, base_time):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
logger.info(f"Triggering scheduled workflow: {workflow.name} ({workflow.id})")
|
|
62
|
+
try:
|
|
63
|
+
# 每个调度触发分配独立 reqid,便于全链路检索
|
|
64
|
+
# Allocate dedicated reqid per scheduled trigger for end-to-end traceability
|
|
65
|
+
set_request_id(None)
|
|
66
|
+
scheduled_reqid = get_request_id()
|
|
67
|
+
await workflow_app.trigger_workflow(
|
|
68
|
+
workflow_id_str=str(workflow.id),
|
|
69
|
+
payload=workflow_types.WorkflowTriggerRequest(
|
|
70
|
+
trigger_context={
|
|
71
|
+
"scheduled_time": base_time.isoformat(),
|
|
72
|
+
"reqid": scheduled_reqid,
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
trigger_type=workflow_types.TriggerType.SCHEDULE,
|
|
76
|
+
)
|
|
77
|
+
triggered_count += 1
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Failed to trigger workflow {workflow.id}: {e}", exc_info=True)
|
|
80
|
+
|
|
81
|
+
logger.info(f"Workflow scheduler task finished. Triggered {triggered_count} workflows.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _cron_due(expression: str, base_time: datetime) -> bool:
|
|
85
|
+
"""Return True when the cron expression fires near *base_time*.
|
|
86
|
+
当 cron 表达式在基准时间附近触发时返回 True。
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
second_at_beginning = _cron_has_seconds(expression)
|
|
91
|
+
cron = croniter(
|
|
92
|
+
expression,
|
|
93
|
+
base_time + timedelta(seconds=1),
|
|
94
|
+
second_at_beginning=second_at_beginning,
|
|
95
|
+
)
|
|
96
|
+
prev_fire = cron.get_prev(datetime)
|
|
97
|
+
except Exception as exc: # noqa: BLE001 - croniter raises generic exceptions
|
|
98
|
+
logger.warning("Failed to iterate cron '%s': %s", expression, exc)
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
tolerance_seconds = 1 if _cron_has_seconds(expression) else 60
|
|
102
|
+
delta = (base_time - prev_fire).total_seconds()
|
|
103
|
+
return 0 <= delta <= tolerance_seconds
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _cron_has_seconds(expression: str) -> bool:
|
|
107
|
+
parts = [part for part in expression.split() if part]
|
|
108
|
+
return len(parts) >= 6
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _normalize_base_time(expression: str, reference_time: datetime) -> datetime:
|
|
112
|
+
normalized = reference_time.replace(microsecond=0)
|
|
113
|
+
if _cron_has_seconds(expression):
|
|
114
|
+
return normalized
|
|
115
|
+
return normalized.replace(second=0)
|