From b6071fcc872bc446d485393152f04d2eb85d9fbb Mon Sep 17 00:00:00 2001 From: rainystevn1 Date: Fri, 17 Apr 2026 22:16:18 +0800 Subject: [PATCH 1/4] refactor: restructure codebase into modular architecture Major reorganization from flat service/producer/consumer layers to a clean module-based architecture with clear separation of concerns: - New `src/app/` module: application lifecycle and mode orchestration - New `src/infra/` module: infrastructure concerns (k8s, redis, db, etcd, harbor, helm, loki, tracing, buildkit, chaos) - New `src/interface/` module: HTTP controllers, workers, receivers - New `src/module/` packages: auth, chaossystem, container, dataset, evaluation, execution, group, injection, label, metric, notification, project, rbac, sdk, system, systemmetric, task, team, trace, user - New `src/router/` module: route registration and grouping - Delete deprecated DTOs: analyzer, audit, auth, debug, group, metrics, redis, resource, role, system - Delete deprecated handlers: debug, system/*, v2/{auth,permissions,projects,resources,roles} - Delete deprecated clients: etcd, harbor, helm, k8s, redis - Delete deprecated repositories and service/producer files Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + .openapi-generator/typescript/sdk/config.json | 20 + .../typescript/sdk/templates/package.mustache | 58 + .../sdk/templates/tsconfig.mustache | 34 + docs/access-key-signature-spec.md | 168 ++ docs/aegisctl-cli-spec.md | 64 +- docs/backend-fx-refactor-plan.md | 650 ++++++++ docs/frontend-redesign.md | 4 +- docs/model-dto-refactor-todo.md | 179 ++ docs/swagger-audience-marking-report.md | 201 +++ docs/todo.md | 584 +++++++ justfile | 7 +- project-index.yaml | 11 +- scripts/command/src/formatter/python.py | 29 +- scripts/command/src/swagger/__init__.py | 3 +- scripts/command/src/swagger/common.py | 2 + scripts/command/src/swagger/init.py | 389 ++++- scripts/command/src/swagger/python.py | 4 +- scripts/command/src/swagger/typescript.py | 68 +- scripts/command/uv.lock | 2 +- scripts/generate_swagger_audience_report.py | 281 ++++ scripts/migrate_swagger_comments.py | 1104 +++++++++++++ sdk/python/pyproject.toml | 4 +- sdk/python/src/rcabench/__init__.py | 2 +- sdk/python/src/rcabench/client/http_client.py | 101 +- src/app/app.go | 32 + src/app/both.go | 38 + src/app/consumer.go | 32 + src/app/http_modules.go | 53 + src/app/options.go | 13 + src/app/producer.go | 22 + src/app/producer_init.go | 44 + src/app/startup_smoke_test.go | 331 ++++ src/app/startup_validate_test.go | 25 + src/client/debug/status_registry.go | 247 --- src/client/etcd_client.go | 160 -- src/client/harbor_client.go | 151 -- src/client/helm.go | 323 ---- src/client/helm_test.go | 194 --- src/client/k8s/client.go | 88 - src/client/k8s/k8s_test.go | 104 -- src/client/redis_client.go | 164 -- src/cmd/aegisctl/client/auth.go | 175 +- src/cmd/aegisctl/client/auth_test.go | 93 ++ src/cmd/aegisctl/client/client.go | 20 +- src/cmd/aegisctl/cmd/auth.go | 305 +++- src/cmd/aegisctl/cmd/root.go | 18 +- src/cmd/aegisctl/config/config.go | 2 + src/database/database.go | 159 -- src/database/view.go | 120 -- src/docs/docs_test.go | 167 ++ src/dto/analyzer.go | 64 - src/dto/audit.go | 133 -- src/dto/auth.go | 119 -- src/dto/container.go | 709 +------- src/dto/dataset.go | 328 +--- src/dto/debug.go | 10 - src/dto/dynamic_config.go | 272 ---- src/dto/group.go | 47 - src/dto/injection.go | 860 +--------- src/dto/label.go | 212 --- src/dto/log.go | 8 - src/dto/metrics.go | 67 - src/dto/permission.go | 243 --- src/dto/project.go | 196 +-- src/dto/redis.go | 10 - src/dto/resource.go | 65 - src/dto/role.go | 173 -- src/dto/system.go | 76 - src/dto/task.go | 179 +- src/dto/trace.go | 222 --- src/go.mod | 3 + src/go.sum | 6 + src/handlers/debug.go | 45 - src/handlers/system/audit.go | 85 - src/handlers/system/configs.go | 335 ---- src/handlers/system/health.go | 289 ---- src/handlers/system/monitor.go | 152 -- src/handlers/v2/auth.go | 220 --- src/handlers/v2/permissions.go | 119 -- src/handlers/v2/projects.go | 504 ------ src/handlers/v2/resources.go | 117 -- src/handlers/v2/roles.go | 276 ---- src/{handlers => httpx}/common.go | 12 +- src/infra/buildkit/gateway.go | 52 + src/infra/buildkit/module.go | 7 + src/infra/chaos/module.go | 15 + src/infra/config/module.go | 19 + src/infra/db/config.go | 38 + src/infra/db/migration.go | 125 ++ src/infra/db/module.go | 77 + src/infra/etcd/gateway.go | 126 ++ src/infra/etcd/module.go | 7 + src/infra/harbor/gateway.go | 118 ++ src/infra/harbor/module.go | 7 + src/infra/helm/gateway.go | 262 +++ src/infra/helm/module.go | 7 + src/{client => infra}/k8s/controller.go | 14 +- src/{client => infra}/k8s/crd.go | 8 +- src/infra/k8s/gateway.go | 142 ++ src/{client => infra}/k8s/job.go | 70 +- src/infra/k8s/k8s_test.go | 186 +++ src/infra/k8s/module.go | 21 + src/infra/logger/module.go | 37 + src/{client/loki.go => infra/loki/client.go} | 50 +- src/infra/loki/module.go | 7 + src/infra/redis/gateway.go | 309 ++++ src/infra/redis/module.go | 7 + src/infra/redis/task_queue.go | 194 +++ src/infra/runtime/module.go | 23 + src/infra/tracing/module.go | 28 + .../jaeger.go => infra/tracing/provider.go} | 33 +- src/interface/controller/module.go | 79 + src/interface/http/module.go | 16 + src/interface/http/router.go | 12 + src/interface/http/server.go | 40 + src/interface/receiver/module.go | 76 + src/interface/worker/module.go | 114 ++ src/main.go | 162 +- src/middleware/audit.go | 14 +- src/middleware/deps.go | 409 +++++ src/middleware/permission.go | 19 +- src/{database => model}/entity.go | 21 +- src/{database => model}/entity_helper.go | 2 +- src/model/view.go | 46 + src/module/auth/api_types.go | 259 +++ src/module/auth/handler.go | 506 ++++++ src/module/auth/module.go | 14 + src/module/auth/repository.go | 198 +++ src/module/auth/service.go | 524 ++++++ src/module/auth/service_test.go | 230 +++ src/module/auth/token_store.go | 58 + .../chaossystem/api_types.go} | 32 +- .../chaossystem/handler.go} | 113 +- src/module/chaossystem/module.go | 9 + src/module/chaossystem/repository.go | 105 ++ .../chaossystem/service.go} | 104 +- src/module/container/api_types.go | 667 ++++++++ src/module/container/build_gateway.go | 87 + src/module/container/build_gateway_test.go | 84 + src/module/container/core.go | 23 + src/module/container/file_store.go | 86 + src/module/container/file_store_test.go | 84 + .../container/handler.go} | 380 +++-- src/module/container/module.go | 11 + src/module/container/repository.go | 361 ++++ src/module/container/service.go | 657 ++++++++ src/module/dataset/api_types.go | 348 ++++ src/module/dataset/core.go | 12 + src/module/dataset/file_store.go | 69 + src/module/dataset/file_store_test.go | 75 + .../datasets.go => module/dataset/handler.go} | 301 ++-- src/module/dataset/module.go | 10 + src/module/dataset/repository.go | 377 +++++ src/module/dataset/service.go | 529 ++++++ .../docs.go => module/docs/swagger_models.go} | 12 +- .../evaluation/api_types.go} | 94 +- .../evaluation/handler.go} | 75 +- src/module/evaluation/module.go | 9 + .../evaluation/repository.go} | 50 +- .../evaluation/service.go} | 151 +- .../execution/api_types.go} | 139 +- .../execution/handler.go} | 340 ++-- src/module/execution/module.go | 9 + src/module/execution/repository.go | 412 +++++ .../execution/result_types.go} | 45 +- src/module/execution/service.go | 340 ++++ src/module/execution/service_test.go | 255 +++ src/module/group/api_types.go | 119 ++ .../v2/groups.go => module/group/handler.go} | 63 +- src/module/group/module.go | 9 + src/module/group/repository.go | 38 + .../group.go => module/group/service.go} | 97 +- src/module/injection/api_types.go | 903 ++++++++++ src/module/injection/archive.go | 41 + src/module/injection/datapack_store.go | 353 ++++ src/module/injection/datapack_store_test.go | 83 + .../injection/handler.go} | 980 ++++++----- src/module/injection/module.go | 10 + .../injection}/query_datapack_arrow.go | 20 +- .../injection/query_datapack_noarrow.go | 16 + src/module/injection/repository.go | 652 ++++++++ src/module/injection/service.go | 956 +++++++++++ src/module/injection/service_test.go | 146 ++ src/module/injection/submit.go | 205 +++ .../injection/time_range.go} | 94 +- src/module/label/api_types.go | 219 +++ src/module/label/core.go | 40 + .../v2/labels.go => module/label/handler.go} | 126 +- src/module/label/module.go | 9 + src/module/label/repository.go | 275 ++++ src/module/label/service.go | 283 ++++ src/module/metric/api_types.go | 65 + .../metrics.go => module/metric/handler.go} | 52 +- src/module/metric/module.go | 9 + src/module/metric/repository.go | 39 + .../metrics.go => module/metric/service.go} | 288 ++-- .../notification/api_types.go} | 2 +- .../notification/handler.go} | 62 +- src/module/notification/module.go | 9 + src/module/notification/repository.go | 11 + src/module/notification/service.go | 32 + src/module/project/api_types.go | 183 +++ src/module/project/handler.go | 264 +++ src/module/project/module.go | 13 + src/module/project/repository.go | 322 ++++ src/module/project/service.go | 173 ++ src/module/project/service_test.go | 194 +++ src/module/rbac/api_types.go | 326 ++++ src/module/rbac/handler.go | 478 ++++++ src/module/rbac/module.go | 9 + src/module/rbac/repository.go | 358 ++++ src/module/rbac/service.go | 243 +++ src/module/rbac/service_test.go | 66 + .../sdk/api_types.go} | 12 +- .../sdk/handler.go} | 64 +- .../sdk_entities.go => module/sdk/models.go} | 6 +- src/module/sdk/module.go | 9 + .../sdk/repository.go} | 74 +- src/module/sdk/service.go | 52 + src/module/sdk/service_test.go | 117 ++ src/module/system/api_types.go | 431 +++++ src/module/system/handler.go | 500 ++++++ src/module/system/handler_test.go | 115 ++ src/module/system/module.go | 9 + src/module/system/repository.go | 171 ++ src/module/system/service.go | 731 +++++++++ src/module/system/service_test.go | 263 +++ src/module/systemmetric/api_types.go | 33 + src/module/systemmetric/collector.go | 44 + .../systemmetric/handler.go} | 27 +- src/module/systemmetric/module.go | 10 + src/module/systemmetric/repository.go | 11 + src/module/systemmetric/service.go | 227 +++ src/module/task/api_types.go | 186 +++ .../v2/tasks.go => module/task/handler.go} | 67 +- src/module/task/log_service.go | 272 ++++ src/module/task/log_types.go | 14 + src/module/task/loki_gateway.go | 24 + src/module/task/module.go | 12 + src/module/task/queue_store.go | 24 + src/module/task/repository.go | 83 + src/module/task/service.go | 107 ++ src/module/task/service_test.go | 105 ++ src/{dto/team.go => module/team/api_types.go} | 68 +- .../v2/teams.go => module/team/handler.go} | 237 ++- src/module/team/module.go | 9 + src/module/team/repository.go | 371 +++++ src/module/team/service.go | 213 +++ src/module/team/service_test.go | 63 + src/module/trace/api_types.go | 161 ++ .../v2/traces.go => module/trace/handler.go} | 73 +- src/module/trace/module.go | 9 + src/module/trace/repository.go | 63 + src/module/trace/service.go | 89 + src/module/trace/stream.go | 175 ++ src/{dto/user.go => module/user/api_types.go} | 191 +-- .../v2/users.go => module/user/handler.go} | 397 ++--- src/module/user/module.go | 9 + src/module/user/repository.go | 403 +++++ src/module/user/service.go | 330 ++++ src/module/user/service_test.go | 197 +++ src/repository/audit.go | 135 -- src/repository/common.go | 5 - src/repository/container.go | 139 +- src/repository/dataset.go | 107 +- src/repository/detector.go | 8 +- src/repository/dynamic_config.go | 196 --- src/repository/execution.go | 76 +- src/repository/granularity.go | 8 +- src/repository/injection.go | 139 +- src/repository/label.go | 62 +- src/repository/permission.go | 329 ---- src/repository/project.go | 363 ----- src/repository/query_builder.go | 51 +- src/repository/resource.go | 113 -- src/repository/role.go | 174 -- src/{database => repository}/scope.go | 8 +- src/repository/system.go | 95 -- src/repository/system_metadata.go | 73 - src/repository/task.go | 447 ----- src/repository/team.go | 257 --- src/repository/token.go | 98 -- src/repository/trace.go | 126 -- src/repository/user.go | 501 ------ src/router/admin.go | 112 ++ src/router/handlers.go | 93 ++ src/router/module.go | 7 + src/router/portal.go | 106 ++ src/router/public.go | 25 + src/router/router.go | 12 +- src/router/router_test.go | 57 + src/router/sdk.go | 21 + src/router/system.go | 39 +- src/router/v2.go | 429 +---- src/service/common/config_listener.go | 78 +- src/service/common/config_registry.go | 79 +- src/service/common/config_registry_test.go | 24 + src/service/common/config_store.go | 34 + src/service/common/container.go | 53 +- .../common.go => common/datapack_resolver.go} | 43 +- src/service/common/dataset.go | 25 +- src/service/common/dynamic_config.go | 13 +- src/service/common/injection.go | 8 +- src/service/common/label.go | 12 +- src/service/common/metadata_store.go | 57 +- src/service/common/task.go | 71 +- src/service/consumer/algo_execution.go | 51 +- src/service/consumer/build_container.go | 41 +- src/service/consumer/build_datapack.go | 30 +- src/service/consumer/collect_result.go | 37 +- src/service/consumer/common.go | 12 +- src/service/consumer/config_handlers.go | 38 +- src/service/consumer/distribute_tasks.go | 16 +- src/service/consumer/fault_injection.go | 58 +- src/service/consumer/jvm_runtime_mutator.go | 6 +- src/service/consumer/k8s_handler.go | 227 ++- src/service/consumer/monitor.go | 206 +-- .../consumer/namespace_catalog_store.go | 34 + src/service/consumer/namespace_lock_store.go | 128 ++ .../consumer/namespace_status_store.go | 43 + src/service/consumer/rate_limiter.go | 79 +- src/service/consumer/rate_limiter_store.go | 54 + src/service/consumer/redis.go | 50 + src/service/consumer/restart_pedestal.go | 60 +- src/service/consumer/runtime_deps.go | 23 + src/service/consumer/state_store.go | 100 ++ src/service/consumer/task.go | 118 +- src/service/consumer/trace.go | 62 +- src/service/initialization/bootstrap_store.go | 207 +++ src/service/initialization/common.go | 19 +- src/service/initialization/consumer.go | 73 +- src/service/initialization/producer.go | 170 +- src/service/initialization/systems.go | 24 +- src/service/initialization/types.go | 56 +- src/service/initialization/utils.go | 13 +- src/service/logreceiver/receiver.go | 16 +- src/service/producer/audit.go | 150 -- src/service/producer/auth.go | 286 ---- src/service/producer/container.go | 861 ---------- src/service/producer/dataset.go | 636 -------- src/service/producer/dynamic_config.go | 544 ------- src/service/producer/evaluation.go | 44 - src/service/producer/execution.go | 421 ----- src/service/producer/injection.go | 1447 ----------------- src/service/producer/label.go | 352 ---- src/service/producer/notification.go | 23 - src/service/producer/permission.go | 114 -- src/service/producer/project.go | 399 ----- .../producer/query_datapack_noarrow.go | 15 - src/service/producer/relation.go | 515 ------ src/service/producer/resource.go | 69 - src/service/producer/role.go | 164 -- src/service/producer/sdk_evaluation.go | 57 - src/service/producer/system.go | 283 ---- src/service/producer/task.go | 368 ----- src/service/producer/team.go | 381 ----- src/service/producer/trace.go | 283 ---- src/service/producer/upload.go | 262 --- src/service/producer/user.go | 213 --- src/testutil/redisstub.go | 132 ++ src/utils/access_key_crypto.go | 78 + src/utils/jwt.go | 38 +- 363 files changed, 32202 insertions(+), 23135 deletions(-) create mode 100644 .openapi-generator/typescript/sdk/config.json create mode 100644 .openapi-generator/typescript/sdk/templates/package.mustache create mode 100644 .openapi-generator/typescript/sdk/templates/tsconfig.mustache create mode 100644 docs/access-key-signature-spec.md create mode 100644 docs/backend-fx-refactor-plan.md create mode 100644 docs/model-dto-refactor-todo.md create mode 100644 docs/swagger-audience-marking-report.md create mode 100644 docs/todo.md create mode 100644 scripts/generate_swagger_audience_report.py create mode 100644 scripts/migrate_swagger_comments.py create mode 100644 src/app/app.go create mode 100644 src/app/both.go create mode 100644 src/app/consumer.go create mode 100644 src/app/http_modules.go create mode 100644 src/app/options.go create mode 100644 src/app/producer.go create mode 100644 src/app/producer_init.go create mode 100644 src/app/startup_smoke_test.go create mode 100644 src/app/startup_validate_test.go delete mode 100644 src/client/debug/status_registry.go delete mode 100644 src/client/etcd_client.go delete mode 100644 src/client/harbor_client.go delete mode 100644 src/client/helm.go delete mode 100644 src/client/helm_test.go delete mode 100644 src/client/k8s/client.go delete mode 100644 src/client/k8s/k8s_test.go delete mode 100644 src/client/redis_client.go create mode 100644 src/cmd/aegisctl/client/auth_test.go delete mode 100644 src/database/database.go delete mode 100644 src/database/view.go create mode 100644 src/docs/docs_test.go delete mode 100644 src/dto/analyzer.go delete mode 100644 src/dto/audit.go delete mode 100644 src/dto/auth.go delete mode 100644 src/dto/debug.go delete mode 100644 src/dto/group.go delete mode 100644 src/dto/metrics.go delete mode 100644 src/dto/redis.go delete mode 100644 src/dto/resource.go delete mode 100644 src/dto/role.go delete mode 100644 src/dto/system.go delete mode 100644 src/handlers/debug.go delete mode 100644 src/handlers/system/audit.go delete mode 100644 src/handlers/system/configs.go delete mode 100644 src/handlers/system/health.go delete mode 100644 src/handlers/system/monitor.go delete mode 100644 src/handlers/v2/auth.go delete mode 100644 src/handlers/v2/permissions.go delete mode 100644 src/handlers/v2/projects.go delete mode 100644 src/handlers/v2/resources.go delete mode 100644 src/handlers/v2/roles.go rename src/{handlers => httpx}/common.go (89%) create mode 100644 src/infra/buildkit/gateway.go create mode 100644 src/infra/buildkit/module.go create mode 100644 src/infra/chaos/module.go create mode 100644 src/infra/config/module.go create mode 100644 src/infra/db/config.go create mode 100644 src/infra/db/migration.go create mode 100644 src/infra/db/module.go create mode 100644 src/infra/etcd/gateway.go create mode 100644 src/infra/etcd/module.go create mode 100644 src/infra/harbor/gateway.go create mode 100644 src/infra/harbor/module.go create mode 100644 src/infra/helm/gateway.go create mode 100644 src/infra/helm/module.go rename src/{client => infra}/k8s/controller.go (98%) rename src/{client => infra}/k8s/crd.go (84%) create mode 100644 src/infra/k8s/gateway.go rename src/{client => infra}/k8s/job.go (80%) create mode 100644 src/infra/k8s/k8s_test.go create mode 100644 src/infra/k8s/module.go create mode 100644 src/infra/logger/module.go rename src/{client/loki.go => infra/loki/client.go} (69%) create mode 100644 src/infra/loki/module.go create mode 100644 src/infra/redis/gateway.go create mode 100644 src/infra/redis/module.go create mode 100644 src/infra/redis/task_queue.go create mode 100644 src/infra/runtime/module.go create mode 100644 src/infra/tracing/module.go rename src/{client/jaeger.go => infra/tracing/provider.go} (65%) create mode 100644 src/interface/controller/module.go create mode 100644 src/interface/http/module.go create mode 100644 src/interface/http/router.go create mode 100644 src/interface/http/server.go create mode 100644 src/interface/receiver/module.go create mode 100644 src/interface/worker/module.go create mode 100644 src/middleware/deps.go rename src/{database => model}/entity.go (98%) rename src/{database => model}/entity_helper.go (98%) create mode 100644 src/model/view.go create mode 100644 src/module/auth/api_types.go create mode 100644 src/module/auth/handler.go create mode 100644 src/module/auth/module.go create mode 100644 src/module/auth/repository.go create mode 100644 src/module/auth/service.go create mode 100644 src/module/auth/service_test.go create mode 100644 src/module/auth/token_store.go rename src/{dto/chaos_system.go => module/chaossystem/api_types.go} (84%) rename src/{handlers/v2/systems.go => module/chaossystem/handler.go} (69%) create mode 100644 src/module/chaossystem/module.go create mode 100644 src/module/chaossystem/repository.go rename src/{service/producer/chaos_system.go => module/chaossystem/service.go} (54%) create mode 100644 src/module/container/api_types.go create mode 100644 src/module/container/build_gateway.go create mode 100644 src/module/container/build_gateway_test.go create mode 100644 src/module/container/core.go create mode 100644 src/module/container/file_store.go create mode 100644 src/module/container/file_store_test.go rename src/{handlers/v2/containers.go => module/container/handler.go} (69%) create mode 100644 src/module/container/module.go create mode 100644 src/module/container/repository.go create mode 100644 src/module/container/service.go create mode 100644 src/module/dataset/api_types.go create mode 100644 src/module/dataset/core.go create mode 100644 src/module/dataset/file_store.go create mode 100644 src/module/dataset/file_store_test.go rename src/{handlers/v2/datasets.go => module/dataset/handler.go} (69%) create mode 100644 src/module/dataset/module.go create mode 100644 src/module/dataset/repository.go create mode 100644 src/module/dataset/service.go rename src/{handlers/docs.go => module/docs/swagger_models.go} (87%) rename src/{dto/evaluation.go => module/evaluation/api_types.go} (67%) rename src/{handlers/v2/evaluations.go => module/evaluation/handler.go} (71%) create mode 100644 src/module/evaluation/module.go rename src/{repository/evaluation.go => module/evaluation/repository.go} (58%) rename src/{service/analyzer/evaluation.go => module/evaluation/service.go} (61%) rename src/{dto/execution.go => module/execution/api_types.go} (69%) rename src/{handlers/v2/executions.go => module/execution/handler.go} (63%) create mode 100644 src/module/execution/module.go create mode 100644 src/module/execution/repository.go rename src/{dto/algorithm_result.go => module/execution/result_types.go} (84%) create mode 100644 src/module/execution/service.go create mode 100644 src/module/execution/service_test.go create mode 100644 src/module/group/api_types.go rename src/{handlers/v2/groups.go => module/group/handler.go} (76%) create mode 100644 src/module/group/module.go create mode 100644 src/module/group/repository.go rename src/{service/producer/group.go => module/group/service.go} (55%) create mode 100644 src/module/injection/api_types.go create mode 100644 src/module/injection/archive.go create mode 100644 src/module/injection/datapack_store.go create mode 100644 src/module/injection/datapack_store_test.go rename src/{handlers/v2/injections.go => module/injection/handler.go} (60%) create mode 100644 src/module/injection/module.go rename src/{service/producer => module/injection}/query_datapack_arrow.go (80%) create mode 100644 src/module/injection/query_datapack_noarrow.go create mode 100644 src/module/injection/repository.go create mode 100644 src/module/injection/service.go create mode 100644 src/module/injection/service_test.go create mode 100644 src/module/injection/submit.go rename src/{dto/request.go => module/injection/time_range.go} (56%) create mode 100644 src/module/label/api_types.go create mode 100644 src/module/label/core.go rename src/{handlers/v2/labels.go => module/label/handler.go} (74%) create mode 100644 src/module/label/module.go create mode 100644 src/module/label/repository.go create mode 100644 src/module/label/service.go create mode 100644 src/module/metric/api_types.go rename src/{handlers/v2/metrics.go => module/metric/handler.go} (75%) create mode 100644 src/module/metric/module.go create mode 100644 src/module/metric/repository.go rename src/{service/producer/metrics.go => module/metric/service.go} (52%) rename src/{dto/notification.go => module/notification/api_types.go} (93%) rename src/{handlers/v2/notifications.go => module/notification/handler.go} (73%) create mode 100644 src/module/notification/module.go create mode 100644 src/module/notification/repository.go create mode 100644 src/module/notification/service.go create mode 100644 src/module/project/api_types.go create mode 100644 src/module/project/handler.go create mode 100644 src/module/project/module.go create mode 100644 src/module/project/repository.go create mode 100644 src/module/project/service.go create mode 100644 src/module/project/service_test.go create mode 100644 src/module/rbac/api_types.go create mode 100644 src/module/rbac/handler.go create mode 100644 src/module/rbac/module.go create mode 100644 src/module/rbac/repository.go create mode 100644 src/module/rbac/service.go create mode 100644 src/module/rbac/service_test.go rename src/{dto/sdk_evaluation.go => module/sdk/api_types.go} (91%) rename src/{handlers/v2/sdk_evaluations.go => module/sdk/handler.go} (69%) rename src/{database/sdk_entities.go => module/sdk/models.go} (94%) create mode 100644 src/module/sdk/module.go rename src/{repository/sdk_evaluation.go => module/sdk/repository.go} (54%) create mode 100644 src/module/sdk/service.go create mode 100644 src/module/sdk/service_test.go create mode 100644 src/module/system/api_types.go create mode 100644 src/module/system/handler.go create mode 100644 src/module/system/handler_test.go create mode 100644 src/module/system/module.go create mode 100644 src/module/system/repository.go create mode 100644 src/module/system/service.go create mode 100644 src/module/system/service_test.go create mode 100644 src/module/systemmetric/api_types.go create mode 100644 src/module/systemmetric/collector.go rename src/{handlers/v2/system.go => module/systemmetric/handler.go} (68%) create mode 100644 src/module/systemmetric/module.go create mode 100644 src/module/systemmetric/repository.go create mode 100644 src/module/systemmetric/service.go create mode 100644 src/module/task/api_types.go rename src/{handlers/v2/tasks.go => module/task/handler.go} (79%) create mode 100644 src/module/task/log_service.go create mode 100644 src/module/task/log_types.go create mode 100644 src/module/task/loki_gateway.go create mode 100644 src/module/task/module.go create mode 100644 src/module/task/queue_store.go create mode 100644 src/module/task/repository.go create mode 100644 src/module/task/service.go create mode 100644 src/module/task/service_test.go rename src/{dto/team.go => module/team/api_types.go} (67%) rename src/{handlers/v2/teams.go => module/team/handler.go} (73%) create mode 100644 src/module/team/module.go create mode 100644 src/module/team/repository.go create mode 100644 src/module/team/service.go create mode 100644 src/module/team/service_test.go create mode 100644 src/module/trace/api_types.go rename src/{handlers/v2/traces.go => module/trace/handler.go} (77%) create mode 100644 src/module/trace/module.go create mode 100644 src/module/trace/repository.go create mode 100644 src/module/trace/service.go create mode 100644 src/module/trace/stream.go rename src/{dto/user.go => module/user/api_types.go} (56%) rename src/{handlers/v2/users.go => module/user/handler.go} (64%) create mode 100644 src/module/user/module.go create mode 100644 src/module/user/repository.go create mode 100644 src/module/user/service.go create mode 100644 src/module/user/service_test.go delete mode 100644 src/repository/audit.go delete mode 100644 src/repository/common.go delete mode 100644 src/repository/dynamic_config.go delete mode 100644 src/repository/permission.go delete mode 100644 src/repository/project.go delete mode 100644 src/repository/resource.go delete mode 100644 src/repository/role.go rename src/{database => repository}/scope.go (82%) delete mode 100644 src/repository/system.go delete mode 100644 src/repository/system_metadata.go delete mode 100644 src/repository/task.go delete mode 100644 src/repository/team.go delete mode 100644 src/repository/token.go delete mode 100644 src/repository/trace.go delete mode 100644 src/repository/user.go create mode 100644 src/router/admin.go create mode 100644 src/router/handlers.go create mode 100644 src/router/module.go create mode 100644 src/router/portal.go create mode 100644 src/router/public.go create mode 100644 src/router/router_test.go create mode 100644 src/router/sdk.go create mode 100644 src/service/common/config_registry_test.go create mode 100644 src/service/common/config_store.go rename src/service/{producer/common.go => common/datapack_resolver.go} (59%) create mode 100644 src/service/consumer/namespace_catalog_store.go create mode 100644 src/service/consumer/namespace_lock_store.go create mode 100644 src/service/consumer/namespace_status_store.go create mode 100644 src/service/consumer/rate_limiter_store.go create mode 100644 src/service/consumer/redis.go create mode 100644 src/service/consumer/runtime_deps.go create mode 100644 src/service/consumer/state_store.go create mode 100644 src/service/initialization/bootstrap_store.go delete mode 100644 src/service/producer/audit.go delete mode 100644 src/service/producer/auth.go delete mode 100644 src/service/producer/container.go delete mode 100644 src/service/producer/dataset.go delete mode 100644 src/service/producer/dynamic_config.go delete mode 100644 src/service/producer/evaluation.go delete mode 100644 src/service/producer/execution.go delete mode 100644 src/service/producer/injection.go delete mode 100644 src/service/producer/label.go delete mode 100644 src/service/producer/notification.go delete mode 100644 src/service/producer/permission.go delete mode 100644 src/service/producer/project.go delete mode 100644 src/service/producer/query_datapack_noarrow.go delete mode 100644 src/service/producer/relation.go delete mode 100644 src/service/producer/resource.go delete mode 100644 src/service/producer/role.go delete mode 100644 src/service/producer/sdk_evaluation.go delete mode 100644 src/service/producer/system.go delete mode 100644 src/service/producer/task.go delete mode 100644 src/service/producer/team.go delete mode 100644 src/service/producer/trace.go delete mode 100644 src/service/producer/upload.go delete mode 100644 src/service/producer/user.go create mode 100644 src/testutil/redisstub.go create mode 100644 src/utils/access_key_crypto.go diff --git a/.gitignore b/.gitignore index 00c98d1f..411fe7c2 100644 --- a/.gitignore +++ b/.gitignore @@ -179,6 +179,7 @@ logs/ .husky/ sdk/python-gen/ sdk/python/src/rcabench/openapi/ +sdk/typescript/ src/docs/converted/ src/docs/openapi2/ src/docs/openapi3/ @@ -196,6 +197,7 @@ scripts/command/command.bin CLAUDE.md +.codex .claude/ .vscode/ diff --git a/.openapi-generator/typescript/sdk/config.json b/.openapi-generator/typescript/sdk/config.json new file mode 100644 index 00000000..e12ab4d1 --- /dev/null +++ b/.openapi-generator/typescript/sdk/config.json @@ -0,0 +1,20 @@ +{ + "npmName": "@OperationsPAI/sdk", + "npmVersion": "0.0.0", + "npmDescription": "TypeScript SDK for RCABench API", + "githost": "github.com", + "gitUserId": "OperationsPAI", + "gitRepoId": "AegisLab", + "licenseName": "MIT", + "supportsES6": true, + "modelPropertyNaming": "original", + "withInterfaces": true, + "useSingleRequestParameter": true, + "typescriptThreePlus": true, + "enumNameSuffix": "", + "enumPropertyNaming": "original", + "hideGenerationTimestamp": true, + "disallowAdditionalPropertiesIfNotPresent": false, + "sortParamsByRequiredFlag": true, + "stringEnums": true +} diff --git a/.openapi-generator/typescript/sdk/templates/package.mustache b/.openapi-generator/typescript/sdk/templates/package.mustache new file mode 100644 index 00000000..136825d6 --- /dev/null +++ b/.openapi-generator/typescript/sdk/templates/package.mustache @@ -0,0 +1,58 @@ +{ + "name": "{{npmName}}", + "version": "{{npmVersion}}", + "description": "OpenAPI client for {{npmName}}", + "author": "OpenAPI-Generator Contributors", + "repository": { + "type": "git", + "url": "https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}.git" + }, + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "keywords": [ + "axios", + "typescript", + "openapi-client", + "openapi-generator", + "{{npmName}}" + ], + "license": "{{licenseName}}", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", +{{#supportsES6}} + "module": "./dist/esm/index.js", + "sideEffects": false, +{{/supportsES6}} + "exports": { + ".": { + "types": "./dist/index.d.ts", + {{#supportsES6}} + "import": "./dist/esm/index.js", + {{/supportsES6}} + "require": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc{{#supportsES6}} && tsc -p tsconfig.esm.json{{/supportsES6}}", + "prepare": "npm run build" + }, + "dependencies": { + "axios": "{{axiosVersion}}" + {{#withAWSV4Signature}} + "aws4-axios": "^3.3.4" + {{/withAWSV4Signature}} + }, + "devDependencies": { + "@types/node": "12.11.5 - 12.20.42", + "typescript": "^4.0 || ^5.0" + }{{#npmRepository}},{{/npmRepository}} +{{#npmRepository}} + "publishConfig": { + "registry": "{{npmRepository}}" + } +{{/npmRepository}} +} diff --git a/.openapi-generator/typescript/sdk/templates/tsconfig.mustache b/.openapi-generator/typescript/sdk/templates/tsconfig.mustache new file mode 100644 index 00000000..2a661dbb --- /dev/null +++ b/.openapi-generator/typescript/sdk/templates/tsconfig.mustache @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "ES2020", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + {{#supportsES6}} + "moduleResolution": "node", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + {{/supportsES6}} + {{^supportsES6}} + "lib": [ + "es6", + "dom" + ], + {{/supportsES6}} + }, +"include": ["*.ts"], + "exclude": [ + "dist", + "node_modules", + "**/*.test.ts" + ] +} diff --git a/docs/access-key-signature-spec.md b/docs/access-key-signature-spec.md new file mode 100644 index 00000000..5599cbab --- /dev/null +++ b/docs/access-key-signature-spec.md @@ -0,0 +1,168 @@ +# Access Key Signature Spec + +This document defines the canonical AK/SK signing flow used by SDK clients and `aegisctl` to exchange an access key for a short-lived bearer token. + +## Portal Workflow + +Recommended operator flow: + +1. Sign in to the Portal with your normal human account. +2. Open the access-key management page and create an access key for the specific automation use case. +3. Copy the returned `access_key` and one-time `secret_key` immediately and store them in your secret manager. +4. Use the signed-header token exchange flow in SDKs, `aegisctl`, CI, or other automation. +5. Rotate or disable the key from Portal when the automation changes or is no longer needed. + +Portal manages the credential lifecycle, while runtime callers only use: + +- `access_key` +- `secret_key` +- `POST /api/v2/auth/access-key/token` + +## Endpoint + +- `POST /api/v2/auth/access-key/token` + +This endpoint is the only place where `secret_key` is used directly. All normal business APIs still use: + +- `Authorization: Bearer ` + +## Required Headers + +Every token exchange request must include these headers: + +- `X-Access-Key`: the access key identifier, for example `ak_xxx` +- `X-Timestamp`: unix timestamp in seconds, for example `1713333333` +- `X-Nonce`: caller-generated unique nonce, max length `128` +- `X-Signature`: lowercase hex `HMAC-SHA256` + +## Canonical String + +The signature payload is the following newline-joined canonical string: + +```text +METHOD +PATH +ACCESS_KEY +TIMESTAMP +NONCE +``` + +For the token exchange endpoint, the canonical string looks like: + +```text +POST +/api/v2/auth/access-key/token +ak_demo +1713333333 +abc123 +``` + +Rules: + +- `METHOD` must be uppercase, for example `POST` +- `PATH` is the request path only, without scheme, host, or query string +- `ACCESS_KEY`, `TIMESTAMP`, and `NONCE` must exactly match the transmitted headers + +## Signature Algorithm + +Compute the signature as: + +```text +signature = hex(hmac_sha256(secret_key, canonical_string)) +``` + +Details: + +- hash: `SHA-256` +- MAC: `HMAC` +- output encoding: lowercase hexadecimal +- secret material: raw `secret_key` + +## Verification Rules + +The server currently enforces: + +- timestamp must be within `+- 5 minutes` +- nonce is single-use inside the validity window +- repeated nonce submissions are rejected as replay attempts +- disabled, deleted, or expired access keys cannot issue tokens + +Replay protection is implemented with Redis-backed nonce reservation. + +## Request Example + +```http +POST /api/v2/auth/access-key/token HTTP/1.1 +Host: aegislab.example.com +Accept: application/json +Content-Type: application/json +X-Access-Key: ak_demo +X-Timestamp: 1713333333 +X-Nonce: abc123 +X-Signature: 4cf2f2cbb93d... +``` + +The request body is empty. + +## curl Example + +The following example shows a full shell flow from `access_key` / `secret_key` to bearer token: + +```bash +ACCESS_KEY="ak_demo" +SECRET_KEY="sk_demo" +SERVER="http://localhost:8082" +PATH_URI="/api/v2/auth/access-key/token" +TIMESTAMP="$(date +%s)" +NONCE="$(openssl rand -hex 16)" +CANONICAL="POST\n${PATH_URI}\n${ACCESS_KEY}\n${TIMESTAMP}\n${NONCE}" +SIGNATURE="$(printf '%b' "${CANONICAL}" | openssl dgst -sha256 -hmac "${SECRET_KEY}" -hex | awk '{print $2}')" + +curl -X POST "${SERVER}${PATH_URI}" \ + -H "Accept: application/json" \ + -H "X-Access-Key: ${ACCESS_KEY}" \ + -H "X-Timestamp: ${TIMESTAMP}" \ + -H "X-Nonce: ${NONCE}" \ + -H "X-Signature: ${SIGNATURE}" +``` + +After receiving the response, extract `data.token` and use it as: + +```http +Authorization: Bearer +``` + +## Response Usage + +On success, the endpoint returns a bearer token payload similar to: + +```json +{ + "code": 0, + "message": "Access key token issued successfully", + "data": { + "token": "", + "token_type": "Bearer", + "expires_at": "2026-04-17T12:00:00Z", + "auth_type": "access_key", + "access_key": "ak_demo" + } +} +``` + +Clients must use the returned JWT for subsequent business API calls: + +```http +Authorization: Bearer +``` + +Do not send `X-Access-Key` / `X-Signature` headers to normal business endpoints. + +## aegisctl Debug Helpers + +`aegisctl` provides two local debugging commands for signature issues: + +- `aegisctl auth inspect`: inspect the stored auth context, token source, expiry, and access key metadata +- `aegisctl auth sign-debug --access-key ... --secret-key ...`: print the canonical string, signed headers, and a ready-to-run curl example +- `aegisctl auth sign-debug --execute`: execute the signed token exchange request immediately and print the API response +- `aegisctl auth sign-debug --execute --save-context`: execute the signed request and persist the returned bearer token into the current CLI context diff --git a/docs/aegisctl-cli-spec.md b/docs/aegisctl-cli-spec.md index 844ade43..1a51a5f7 100644 --- a/docs/aegisctl-cli-spec.md +++ b/docs/aegisctl-cli-spec.md @@ -115,30 +115,30 @@ Available on all commands: Authenticate and persist token. ```bash -# Interactive (prompts for password) -aegisctl auth login --server http://localhost:8082 --username admin - -# Non-interactive (for scripts/agents) -aegisctl auth login --server http://localhost:8082 --username admin --password admin +# Exchange AK/SK for a bearer token +aegisctl auth login --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo # With context name -aegisctl auth login --server http://localhost:8082 --username admin --password admin --context dev +aegisctl auth login --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --context dev ``` **Behavior**: -- Calls `POST /api/v2/auth/login` +- Computes the canonical string `METHOD\nPATH\nACCESS_KEY\nTIMESTAMP\nNONCE` +- Signs it with lowercase hex `HMAC-SHA256(secret_key, canonical_string)` +- Calls `POST /api/v2/auth/access-key/token` with `X-Access-Key`, `X-Timestamp`, `X-Nonce`, `X-Signature` - Saves token + server + expiry to `~/.aegisctl/config.yaml` - Sets as `current-context` if no context exists yet - Prints authentication status to stdout +- Does not persist `secret_key` **Flags**: | Flag | Required | Description | |------|----------|-------------| | `--server` | Yes | API server URL | -| `--username` | Yes | Username | -| `--password` | No | Password (prompts if omitted) | -| `--context` | No | Context name to save as (default: hostname-derived) | +| `--access-key` | Yes | Access key | +| `--secret-key` | Yes | Secret key | +| `--context` | No | Context name to save as (default: `default`) | #### `aegisctl auth status` @@ -156,6 +156,37 @@ aegisctl auth status **Behavior**: Calls `GET /api/v2/auth/profile` to verify token validity. +#### `aegisctl auth inspect` + +Inspect the current local auth context without sending credentials anywhere. + +```bash +aegisctl auth inspect +aegisctl auth inspect -o json +``` + +**Behavior**: +- Reads the active context from `~/.aegisctl/config.yaml` +- Prints `server`, `auth_type`, `access_key`, token preview, and expiry state + +#### `aegisctl auth sign-debug` + +Print the canonical string and signed headers for `AK/SK -> token` debugging. + +```bash +aegisctl auth sign-debug --access-key ak_demo --secret-key sk_demo +aegisctl auth sign-debug --access-key ak_demo --secret-key sk_demo --timestamp 1713333333 --nonce abc123 +aegisctl auth sign-debug --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --execute +aegisctl auth sign-debug --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --execute --save-context +``` + +**Behavior**: +- Rebuilds the canonical string `METHOD\nPATH\nACCESS_KEY\nTIMESTAMP\nNONCE` +- Prints the computed `X-Access-Key`, `X-Timestamp`, `X-Nonce`, `X-Signature` +- Prints a ready-to-run curl example for `POST /api/v2/auth/access-key/token` +- Optionally executes the request with `--execute` and prints the live response +- Optionally persists the returned bearer token into the active context with `--save-context` + #### `aegisctl auth token` Directly set an API token without login flow. @@ -166,6 +197,15 @@ aegisctl auth token --set eyJhbGci... **Use case**: CI/CD pipelines or agents that receive tokens from external secret managers. +#### Portal Access Key Workflow + +Recommended usage: + +1. Create or rotate the access key in Portal. +2. Store `access_key` and one-time `secret_key` in a secret manager. +3. Use `aegisctl auth login --access-key ... --secret-key ...` or direct curl signing to get a bearer token. +4. Use the bearer token for normal API calls. + --- ### `aegisctl context` — Multi-Environment Management @@ -906,7 +946,7 @@ func (r *WSReader) Stream(ctx context.Context) (<-chan string, error) set -e # Setup -aegisctl auth login --server http://aegislab:8082 --username agent --password secret +aegisctl auth login --server http://aegislab:8082 --access-key ak_agent --secret-key sk_agent # Discover resources ALGORITHMS=$(aegisctl container list --type algorithm -o json) @@ -1025,7 +1065,7 @@ Core capabilities needed for an agent to run a complete experiment cycle. | # | Command | API Endpoint | Description | |---|---------|-------------|-------------| -| 1 | `auth login` | `POST /api/v2/auth/login` | Authenticate and persist token | +| 1 | `auth login` | `POST /api/v2/auth/access-key/token` | Exchange AK/SK and persist token | | 2 | `auth token --set` | (local) | Set token directly | | 3 | Config file read/write | (local) | `~/.aegisctl/config.yaml` management | | 4 | `project list` | `GET /api/v2/projects` | List projects | diff --git a/docs/backend-fx-refactor-plan.md b/docs/backend-fx-refactor-plan.md new file mode 100644 index 00000000..55366636 --- /dev/null +++ b/docs/backend-fx-refactor-plan.md @@ -0,0 +1,650 @@ +# AegisLab Backend Fx Refactor Plan + +> 创建日期:2026-04-15 +> 状态:Draft +> 范围:后端模块边界、Fx 依赖装配、生命周期管理、HTTP / worker / controller / receiver 多入口治理 + +## TL;DR + +AegisLab 后端不只是一个 HTTP API 服务。它同时包含: + +- HTTP producer server +- background consumer +- scheduler +- K8s controller +- OTLP log receiver +- DB / Redis / Etcd / K8s / Loki / tracing 等基础设施资源 + +因此当前问题不是单纯缺少依赖注入,而是: + +- 模块边界不清 +- 全局初始化散落 +- 资源生命周期没有统一管理 +- HTTP / worker / controller 等入口互相交叉 +- handler / service / repository 依赖方向不够硬 + +结论:**优先采用 Fx,而不是继续沿旧 DI 骨架扩张。** + +Fx 在这里的价值不是“自动 new 对象”,而是: + +1. 把 app 启动和模块装配收回到 app 层。 +2. 用 module 明确业务域和基础设施边界。 +3. 用 lifecycle 管理 DB、Redis、HTTP server、consumer、scheduler、receiver、controller 的启动和关闭。 +4. 让 producer / consumer / both 三种模式共享基础模块,但启用不同入口。 + +## 1. Current Problems + +### 1.1 全局初始化散落 + +当前启动流程里存在多个全局初始化点: + +- `database.InitDB()` +- `client.InitTraceProvider()` +- `initChaosExperiment()` +- `k8s.GetK8sController()` +- `client.GetRedisClient()` +- `consumer.StartScheduler(ctx)` +- `consumer.ConsumeTasks(ctx)` +- `logreceiver.NewOTLPLogReceiver(...).Start(ctx)` + +这些初始化分散在 `main.go`、`client`、`service`、`repository` 等多个包里。结果是: + +- 启动顺序靠人工记忆。 +- 新人很难判断资源从哪里来。 +- 关闭逻辑不统一。 +- 测试很难替换基础设施。 +- producer / consumer / both 三种模式重复装配逻辑。 + +### 1.2 分层边界不够硬 + +期望依赖方向: + +```text +cmd + -> app + -> interface + -> module + -> domain + -> infra interface + -> infra implementation +``` + +当前实际情况: + +- handler 直接调用 `service/producer` 包级函数。 +- 少数 handler 直接 import `database` / `repository`。 +- service 大量直接使用全局 `database.DB`、Redis、K8s、Loki 等 client。 +- repository 中混入 Redis queue / token blacklist 等非 DB 能力。 +- middleware 直接依赖具体 producer service。 + +### 1.3 多入口没有统一 app 模型 + +当前有三种运行模式: + +- `producer`: HTTP API server +- `consumer`: background worker / scheduler / K8s controller / receiver +- `both`: 同时启动 producer 和 consumer 能力 + +这些模式本质上应该是三套 Fx option: + +```text +CommonOptions + ProducerOptions +CommonOptions + ConsumerOptions +CommonOptions + ProducerOptions + ConsumerOptions +``` + +而不是在 `main.go` 中手写多份初始化流程。 + +## 2. Target Module Boundary + +先确定模块边界,再谈 Fx 注入。 + +### 2.1 App Layer + +职责: + +- 程序启动入口 +- Fx app 创建 +- producer / consumer / both option 选择 +- 生命周期统一管理 +- graceful shutdown + +建议目录: + +```text +src/app/ + app.go + options.go + producer.go + consumer.go + both.go +``` + +### 2.2 Interface Layer + +职责: + +- HTTP / Gin router +- middleware +- handler +- worker entry +- scheduler entry +- K8s controller entry +- OTLP receiver entry + +建议目录: + +```text +src/interface/ + http/ + module.go + router.go + routes_public.go + routes_sdk.go + routes_portal.go + routes_admin.go + routes_system.go + worker/ + module.go + consumer.go + scheduler.go + controller/ + module.go + k8s.go + receiver/ + module.go + otlp.go +``` + +过渡期可以先不移动现有 `handlers/`、`router/`、`service/consumer/` 文件,只在 Fx module 中包装它们。 + +### 2.3 Business Module Layer + +按业务域拆模块,每个模块只暴露 `Module`、`NewService`、`NewHandler`、`NewRepository`、必要接口。 + +建议业务模块: + +```text +src/module/ + auth/ + user/ + rbac/ + team/ + project/ + container/ + dataset/ + injection/ + execution/ + task/ + evaluation/ + trace/ + metrics/ + notification/ + audit/ + system/ + dynamicconfig/ +``` + +每个模块的目标形态: + +```go +var Module = fx.Module("project", + fx.Provide( + NewRepository, + NewService, + NewHandler, + ), +) +``` + +### 2.4 Domain Layer + +职责: + +- 核心业务规则 +- domain entity / value object +- 纯逻辑校验 +- 不依赖 Gin、Gorm、Redis、K8s + +建议目录: + +```text +src/domain/ + project/ + task/ + injection/ + execution/ + permission/ +``` + +过渡期可以先继续使用 `database` entity 和 `dto`,等模块稳定后再抽 domain。 + +### 2.5 Infra Layer + +职责: + +- 配置 +- 日志 +- DB +- Redis +- Etcd +- K8s +- Loki +- Jaeger / tracing +- Harbor +- Helm +- BuildKit +- Chaos client + +建议目录: + +```text +src/infra/ + config/ + logger/ + db/ + redis/ + etcd/ + k8s/ + loki/ + tracing/ + harbor/ + helm/ + buildkit/ + chaos/ +``` + +每个 infra module 要明确: + +- 创建什么资源 +- 返回什么接口或 client +- 是否需要 `fx.Lifecycle` +- `OnStart` 做什么 +- `OnStop` 做什么 + +## 3. Dependency Rules + +### 3.1 允许依赖 + +```text +cmd -> app +app -> interface / module / infra +interface -> module service interface +module -> domain / infra interface +infra implementation -> external libraries +``` + +### 3.2 禁止依赖 + +```text +domain -> gin / gorm / redis / k8s +repository -> handler +repository -> service +handler -> database.DB +handler -> repository implementation +middleware -> concrete producer service +business module -> another module's implementation +``` + +跨业务模块调用优先依赖接口。例如 project 需要 RBAC 能力: + +```go +type PermissionChecker interface { + CheckUserPermission(ctx context.Context, params *dto.CheckPermissionParams) (bool, error) +} +``` + +由 rbac module 提供实现。 + +## 4. Fx App Design + +### 4.1 Common Options + +所有模式共享: + +```go +func CommonOptions() fx.Option { + return fx.Options( + config.Module, + logger.Module, + db.Module, + redis.Module, + tracing.Module, + etcd.Module, + BusinessModules(), + ) +} +``` + +### 4.2 Producer Options + +HTTP server 模式: + +```go +func ProducerOptions() fx.Option { + return fx.Options( + CommonOptions(), + http.Module, + ) +} +``` + +### 4.3 Consumer Options + +后台任务模式: + +```go +func ConsumerOptions() fx.Option { + return fx.Options( + CommonOptions(), + k8s.Module, + chaos.Module, + worker.Module, + controller.Module, + receiver.Module, + ) +} +``` + +### 4.4 Both Options + +本地或一体化部署模式: + +```go +func BothOptions() fx.Option { + return fx.Options( + CommonOptions(), + k8s.Module, + chaos.Module, + http.Module, + worker.Module, + controller.Module, + receiver.Module, + ) +} +``` + +### 4.5 main.go 目标形态 + +```go +func main() { + mode := parseMode() + + var opts fx.Option + switch mode { + case "producer": + opts = app.ProducerOptions() + case "consumer": + opts = app.ConsumerOptions() + case "both": + opts = app.BothOptions() + } + + fx.New(opts).Run() +} +``` + +`main.go` 不再直接初始化 DB、Redis、K8s controller、HTTP server、scheduler。 + +## 5. Lifecycle Plan + +Fx lifecycle 应统一管理这些资源: + +### 5.1 DB + +- `fx.Provide(NewGormDB)` +- `OnStop`: close underlying sql DB + +### 5.2 Redis + +- `fx.Provide(NewRedisClient)` +- `OnStop`: `Close()` + +### 5.3 HTTP Server + +- `fx.Provide(NewGinEngine, NewHTTPServer)` +- `OnStart`: `server.ListenAndServe()` in goroutine +- `OnStop`: `server.Shutdown(ctx)` + +### 5.4 K8s Controller + +- `fx.Provide(NewK8sController)` +- `OnStart`: start controller in goroutine +- `OnStop`: cancel controller context + +### 5.5 Worker / Scheduler + +- `fx.Provide(NewTaskConsumer, NewScheduler)` +- `OnStart`: start goroutines +- `OnStop`: cancel context and wait if needed + +### 5.6 OTLP Receiver + +- `fx.Provide(NewOTLPReceiver)` +- `OnStart`: start receiver +- `OnStop`: shutdown receiver + +### 5.7 Tracing + +- `fx.Provide(NewTraceProvider)` +- `OnStop`: flush / shutdown provider if supported + +## 6. HTTP Boundary Plan + +HTTP routes should be split by audience, not by current file size. + +### 6.1 Public + +- login +- register +- refresh +- health +- docs + +### 6.2 SDK + +Stable programmatic API: + +- project list / get / create +- container / dataset / version query +- submit injection / build / execution +- task status / logs +- injection / execution / evaluation query +- metrics query +- datapack download / query + +### 6.3 Portal + +普通登录用户前端页面 API: + +- profile +- teams / projects +- labels +- notifications +- user-scoped container / dataset / injection / execution +- upload / download / query + +### 6.4 Admin + +系统管理 API: + +- users +- roles +- permissions +- resources +- audit +- system configs +- global injections / executions +- chaos systems +- batch delete + +第一阶段只拆注册函数,不改 URL。 + +## 7. Migration Strategy + +### Phase 0: Stop Legacy DI Expansion + +当前已有的旧 DI 骨架可以视为短期试验。后续不要继续沿旧 provider 骨架深挖。 + +处理方式: + +- 暂时保留也可以,避免立即制造回滚噪音。 +- 开始引入 Fx 后,用 Fx app 替换 `app.InitializeProducerApp()`。 +- 最终删除旧 DI 骨架相关文件和依赖。 + +### Phase 1: Add Fx Skeleton + +目标:引入 Fx,但不重写业务逻辑。 + +任务: + +- 增加 `go.uber.org/fx` +- 新建 `app` Fx options +- 新建 `infra/config`、`infra/logger`、`infra/db` 的 module 草案 +- 先包装现有 `config.Init`、`database.InitDB` +- 保持现有 router / handler / service 行为 + +验收: + +- producer 可以通过 Fx 启动 +- consumer 旧逻辑暂不迁移或只包装 +- 现有 API 路由不变 + +### Phase 2: Move Lifecycle Into Fx + +目标:把启动和停止资源收回 app。 + +迁移顺序: + +1. DB lifecycle +2. Redis lifecycle +3. tracing lifecycle +4. HTTP server lifecycle +5. OTLP receiver lifecycle +6. scheduler lifecycle +7. consumer lifecycle +8. K8s controller lifecycle + +验收: + +- `main.go` 不再手写资源启动顺序 +- producer / consumer / both 使用不同 Fx options +- 资源关闭有 `OnStop` + +### Phase 3: Module Boundary Wrapper + +目标:先建立业务 module 壳,不急着重写内部逻辑。 + +优先模块: + +1. project +2. auth +3. task +4. injection +5. execution + +每个模块先暴露: + +```go +var Module = fx.Module("project", + fx.Provide(NewHandler), +) +``` + +如果 service / repository 尚未 struct 化,可以先由 handler wrapper 调旧函数。 + +验收: + +- router 依赖 module handler +- 新模块入口清晰 +- 旧包级函数逐步减少 + +### Phase 4: Structify Service / Repository + +目标:逐个业务模块把包级函数改成 struct。 + +每个模块执行: + +- `Repository` struct 化 +- `Service` struct 化 +- `Handler` struct 化 +- service 注入 repository / store / gateway +- handler 注入 service +- 移除 handler direct repository / database import +- 移除 service direct global DB usage + +验收: + +- 新迁移模块可单测 +- 依赖从 Fx 图中可见 +- 无新增全局 client 访问 + +### Phase 5: Split Store / Gateway + +目标:把基础设施访问从 repository / service 中抽出。 + +- Redis token blacklist -> `infra/redis` 或 `module/auth.TokenStore` +- Redis task queue -> `module/task.QueueStore` +- Loki -> `infra/loki.Gateway` +- K8s -> `infra/k8s.Gateway` +- Etcd -> `infra/etcd.Client` +- Harbor / Helm / BuildKit -> gateway + +验收: + +- repository 只处理 DB +- 外部系统访问都有接口边界 + +### Phase 6: SDK / Portal / Admin Governance + +目标:让 API 受众边界和 SDK 生成一致。 + +- 拆 route registration +- 审核 OpenAPI3 `x-api-type` audience 扩展 +- 修正 `sdk / portal / admin` 归属 +- 更新 SDK 生成脚本 + +## 8. First PR Scope + +第一批建议只做: + +1. 新增 Fx 依赖。 +2. 新增 `app` Fx options。 +3. 新增 `infra/config`、`infra/db`、`interface/http` module 壳。 +4. producer 模式通过 Fx 启动 HTTP server。 +5. 保持业务 handler/service/repository 不动。 +6. 标记当前旧 DI 骨架文件为待删除,或直接在本 PR 中移除旧骨架。 + +第一批不建议做: + +- 迁移所有 service +- 搬目录 +- 改 URL +- 清理所有 SDK 标记 +- 重写 consumer +- 重写 repository + +## 9. Completion Criteria + +最终完成后应满足: + +1. `main.go` 只负责解析 mode 和启动 Fx app。 +2. producer / consumer / both 由 Fx options 组合。 +3. DB / Redis / HTTP / worker / receiver / controller 都有 lifecycle。 +4. HTTP routes 按 Public / SDK / Portal / Admin / System 拆分。 +5. handler 不直接 import `database` / repository implementation。 +6. service 不直接使用全局 `database.DB`。 +7. repository 不操作 Redis / K8s / Loki / Etcd。 +8. business module 之间依赖接口,不依赖实现。 +9. 新模块只需要暴露 `Module` 和构造函数。 +10. SDK 只包含稳定外部 API。 + +## 10. Open Questions + +1. 是否要物理移动目录到 `module/`、`infra/`、`interface/`,还是先保持旧目录、只用 Fx module 约束? +2. `service/producer` / `service/consumer` 是否改名? +3. Redis task queue 放在 `infra/redis` 还是 `module/task`? +4. Permission checker 接口归属 `module/rbac` 还是 `interface/http/middleware`? +5. 是否在本轮移除已有旧 DI 骨架,还是等 Fx producer 跑通后再删? + +建议:先不做大规模目录迁移。第一阶段用 Fx module 包装旧代码,等启动生命周期稳定后,再逐个业务模块搬迁。 diff --git a/docs/frontend-redesign.md b/docs/frontend-redesign.md index 44b55aa4..6432b38a 100644 --- a/docs/frontend-redesign.md +++ b/docs/frontend-redesign.md @@ -509,8 +509,8 @@ FaultInjection CRD → HandleCRDSucceeded → BuildDatapack Job → HandleJobSuc `types/api.ts` 中手写的 `Team`, `TeamMember` 等类型需迁移到 SDK 生成类型 (`@rcabench/client`)。 步骤: -1. 确保所有相关 API 在后端标注了 `@x-api-type {"sdk":"true"}` -2. `just swag-init && just generate-typescript-client` +1. 确保 OpenAPI3 中相关接口带有正确的 `x-api-type` audience 标记(如 `portal` / `admin`) +2. `just swag-init && just generate-typescript-sdk` 3. 前端 `import type { ... } from '@rcabench/client'` 替换手写类型 ## 8. UI/UX Guidelines diff --git a/docs/model-dto-refactor-todo.md b/docs/model-dto-refactor-todo.md new file mode 100644 index 00000000..90d15aa7 --- /dev/null +++ b/docs/model-dto-refactor-todo.md @@ -0,0 +1,179 @@ +# Model / DTO Refactor TODO + +> 创建日期:2026-04-17 +> 目标:把原 `database` 语义收缩为持久化模型层 `model`,并把当前全局 `dto` 逐步拆回各模块,避免存储模型和接口契约继续混在一起。 + +## 设计原则 + +- `src/model` 只放持久化模型、GORM hook、scanner / valuer、只读 view model。 +- `src/infra/db` 负责连接、迁移、生命周期、view 创建。 +- 不把 `dto` 直接并入 `model`。 +- `dto` 优先按模块下沉到 `src/module/*`,只保留极少数真正跨模块共享类型。 +- 先改命名和目录边界,再做更细的 DTO 下沉,避免一轮里同时改太多语义。 + +## 阶段 1:`database` -> `model` + +- [x] 新建 `src/model` +- [x] 将 `src/database/*` 迁到 `src/model/*` +- [x] 将包名从 `database` 改为 `model` +- [x] 批量更新仓库内 `aegis/database` import +- [x] 批量更新 `database.*` 类型引用 +- [x] 跑主链测试确认编译通过 + - 已执行:`cd src && go test ./app -count=1` + - 已执行:`cd src && go test ./module/... ./router/... ./repository ...` + - 备注:沙箱内执行 `cd src && go test ./...` 时,`app` 中两条 loopback smoke test 在整仓并行场景下触发 `listen tcp 127.0.0.1:0: socket: operation not permitted`,单包执行通过,属于环境限制而非本轮重命名回归。 + +## 阶段 2:继续压缩 `src/model` + +- [x] 复查 `src/model` 是否只剩实体 / view model / scanner / valuer +- [x] 将模块专用读模型从 `src/model` 下沉回对应模块 +- [x] 优先处理 SDK 只读模型 + - `src/model/sdk_entities.go` 已删除 + - SDK 只读模型已迁到 `src/module/sdk/models.go` + +## 阶段 3:拆全局 `dto` + +- [x] 明确 `dto` 中每个文件对应的模块归属 + - 当前剩余 `src/dto/*` 已收敛为共享分页/搜索/响应壳与跨模块运行时载荷:`common.go`、`response.go`、`search.go`、`permission.go`、`project.go`、`dynamic_config.go`、`container.go`、`dataset.go`、`injection.go`、`task.go`、`trace.go`、`log.go`、`label.go` +- [x] 优先试点 `auth` / `sdk` / `system` + - `src/module/auth/api_types.go` 已落地,`src/dto/auth.go` 已删除 + - `src/module/sdk/api_types.go` / `src/module/sdk/models.go` 已落地,`src/dto/sdk_evaluation.go` 已删除 + - `src/module/system/api_types.go` 已落地,`src/dto/audit.go` 已删除,并缩减 `src/dto/system.go` / `src/dto/dynamic_config.go` +- [x] 再推进下一批明显模块内聚 DTO + - `src/module/chaossystem/api_types.go` 已落地,`src/dto/chaos_system.go` 已删除 + - `src/module/team/api_types.go` 已落地,`src/dto/team.go` 已删除 + - `src/module/label/api_types.go` 已落地,`src/dto/label.go` 已裁剪为仅保留共享 `LabelItem` + - `src/module/rbac/api_types.go` 已落地,`src/dto/resource.go` 已删除,`src/dto/role.go` 已裁剪掉 role mutation/list 请求类型 + - `src/module/user/api_types.go` 已落地,`src/dto/user.go` 已裁剪掉 user CRUD/detail 请求响应类型 + - `src/module/project/api_types.go` 已落地,`src/dto/project.go` 已裁剪为仅保留共享 search/statistics 结构 + - `src/module/dataset/api_types.go` 已落地,`src/dto/dataset.go` 已裁剪掉 dataset CRUD/detail/label 管理请求响应类型 + - `src/module/container/api_types.go` 已落地,`src/dto/container.go` 已裁剪掉 container CRUD/detail/label 管理请求响应类型 + - `src/module/evaluation/api_types.go` 已落地,`src/dto/evaluation.go` 已删除,并把批量评估逻辑收回 `src/module/evaluation/service.go` + - `src/module/metric/api_types.go` 已落地,`src/dto/metrics.go` 已删除 + - `src/module/task/api_types.go` 已落地,`src/dto/task.go` 已裁剪掉 task list/batch-delete/detail/queue 这批模块内 API 类型;trace 仍复用共享 `TaskResp` + - `src/module/notification/api_types.go` 已落地,`src/dto/notification.go` 已删除 + - `src/module/group/api_types.go` 已落地,`src/dto/group.go` 已删除,并把 group stats/stream 相关类型从 `src/dto/trace.go` 收回模块 + - `src/module/trace/api_types.go` 已落地,`src/dto/trace.go` 已裁剪掉 trace list/detail/stream 请求响应类型;`src/repository/trace.go` 也已去掉对 `dto.ListTraceFilters` 的依赖 + - `src/module/systemmetric/api_types.go` 已落地,`src/dto/system.go` 已删除;system 通过模块别名复用监控响应类型 + - `src/module/execution/api_types.go` 已落地,`src/dto/execution.go` 已删除;evaluation 改为直接复用 execution 模块公开执行引用类型 + - `src/module/execution/result_types.go` 已落地,执行结果上传请求/响应与 detector / granularity 结果项已迁回模块,`src/dto/algorithm_result.go` 已删除 + - `src/module/rbac/api_types.go` 已继续扩充 role / permission 响应与 permission list 查询契约,`src/dto/role.go` 已删除,`src/dto/permission.go` 已裁剪为仅保留 middleware / repository 共享的 `CheckPermissionParams` + - `src/module/injection/api_types.go` 已落地,`src/dto/injection.go` 已裁剪为仅保留 consumer / task 共享的 `InjectionItem` + - `src/module/injection/time_range.go` 已落地,注入分析查询时间窗契约已迁回模块,`src/dto/request.go` 已删除 + - `src/module/dataset/api_types.go` 已继续接管 search / dataset version / datapack relation 契约,`src/dto/dataset.go` 已裁剪为仅保留共享 `DatasetRef` + - `src/module/auth/api_types.go` 已接管 profile 响应契约,`src/dto/user.go` 已删除未使用的 `UserSearchReq` 并移出 `UserProfileResp` + - `src/module/user/api_types.go` 已继续接管 permission assignment / resource-role 视图契约,`src/dto/user.go` 已删除 + - `src/dto/project.go` 已删除未使用的 `SearchProjectReq`,当前仅保留 project/team 共用的 `ProjectStatistics` + - `src/dto/dynamic_config.go` 已删除未使用的 `ConfigStatsResp`,当前仅保留跨 `service/common` / `module/system` 共用的 `ConfigUpdateResponse` + - `src/module/task/log_types.go` 已落地,任务日志 WebSocket 消息已迁回模块,`src/dto/log.go` 已裁剪为仅保留共享 `LogEntry` + - container 构建请求已直接复用共享 `dto.BuildOptions`,模块内重复定义已删除 + - 未被引用的遗留全局 DTO 已继续清理:`src/dto/analyzer.go`、`src/dto/debug.go`、`src/dto/redis.go` 已删除;`src/dto/trace.go` 中未使用的 `TraceQuery` 已移除 +- [x] 将模块专用 request / response 移到 `src/module/*` + - Auth 请求/响应类型已迁到 `src/module/auth/api_types.go` + - SDK 请求/响应类型已迁到 `src/module/sdk/api_types.go` + - System 请求/响应类型已迁到 `src/module/system/api_types.go` + - ChaosSystem 请求/响应类型已迁到 `src/module/chaossystem/api_types.go` + - Team 请求/响应类型已迁到 `src/module/team/api_types.go` + - Label 请求/响应类型已迁到 `src/module/label/api_types.go` + - RBAC 的 role/resource 请求类型与 resource 响应类型已迁到 `src/module/rbac/api_types.go` + - User 的 CRUD/detail 请求响应类型已迁到 `src/module/user/api_types.go` + - Project 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/project/api_types.go` + - Dataset 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/dataset/api_types.go` + - Container 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/container/api_types.go` + - Evaluation 的 list/detail/batch evaluate 请求响应类型已迁到 `src/module/evaluation/api_types.go` + - Metric 的 query/response 类型已迁到 `src/module/metric/api_types.go` + - Task 的 list/batch-delete/detail/queue 请求响应类型已迁到 `src/module/task/api_types.go` + - Notification 的 stream 请求/事件类型已迁到 `src/module/notification/api_types.go` + - Group 的 stats/stream 请求响应类型已迁到 `src/module/group/api_types.go` + - Trace 的 list/detail/stream 请求响应类型已迁到 `src/module/trace/api_types.go` + - SystemMetric 的 metrics/namespace-lock 请求响应类型已迁到 `src/module/systemmetric/api_types.go` + - Execution 的 list/detail/submit/batch-delete 请求响应类型已迁到 `src/module/execution/api_types.go` + - Execution 的 detector/granularity 结果上传请求响应类型已迁到 `src/module/execution/result_types.go` + - RBAC 的 role / permission 响应类型与 permission list 请求类型已迁到 `src/module/rbac/api_types.go` + - Injection 的 list/search/submit/build/label/file/upload 请求响应类型已迁到 `src/module/injection/api_types.go` + - Injection 的时间窗查询类型已迁到 `src/module/injection/time_range.go` + - Dataset 的 search / version CRUD / datapack relation 请求响应类型已迁到 `src/module/dataset/api_types.go` + - Auth 的 profile 响应类型已迁到 `src/module/auth/api_types.go` + - User 的 permission assignment / resource relation 响应类型已迁到 `src/module/user/api_types.go` +- [x] 保留一个极薄的跨模块共享 DTO 层,避免继续养大而全 `dto` + - 当前共享 DTO 只保留分页/搜索/统一响应、权限检查参数,以及 consumer / runtime / trace / log 等跨模块载荷 + +## 当前进展 + +- [x] `auth` 模块已完成本地 API 类型收口并通过校验 + - 已执行:`cd src && go test ./module/auth ./router ./docs` +- [x] `sdk` / `system` 模块已完成前序下沉并继续保持通过 + - 已执行:`cd src && go test ./module/system ./module/sdk ./module/auth ./router ./docs` +- [x] `chaossystem` 模块已完成本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/chaossystem ./router ./docs` +- [x] `team` 模块已完成本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/team ./router ./docs` +- [x] `label` 模块已完成本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/label ./router ./docs` +- [x] `rbac` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/rbac ./router ./docs` +- [x] `user` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/user ./module/rbac ./router ./docs` +- [x] `project` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/project ./module/team ./router ./docs` +- [x] `dataset` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/dataset ./module/project ./router ./docs` +- [x] `container` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/container ./module/project ./router ./docs` +- [x] `evaluation` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/evaluation ./router ./docs` +- [x] `metric` / `task` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/metric ./module/task ./module/systemmetric ./module/system ./router ./docs` +- [x] `notification` / `group` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/notification ./module/group ./service/consumer ./module/docs ./router ./docs` +- [x] `trace` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/trace ./router ./docs` +- [x] `systemmetric` / `system` 模块已完成一轮本地 API 类型下沉并通过校验 + - 已执行:`cd src && go test ./module/systemmetric ./module/system ./router ./docs` +- [x] `execution` / `rbac` / `user` 模块已继续完成一轮本地 API 类型收缩并通过校验 + - 已执行:`cd src && go test ./module/execution ./module/rbac ./module/user ./module/evaluation ./router ./docs` +- [x] `injection` / `dataset` / `project` / `auth` 模块已继续完成一轮本地 API 类型收缩并通过校验 + - 已执行:`cd src && go test ./module/auth ./module/injection ./module/dataset ./module/project ./router ./docs` +- [x] `execution` / `injection` / `label` / `task` / `container` 已继续完成最后一轮共享 DTO 收缩并通过校验 + - 已执行:`cd src && go test ./module/execution ./module/evaluation ./module/injection ./module/label ./module/container ./module/task ./router ./docs` +- [x] 继续按模块清点 `src/dto/*` 中剩余仅被单模块消费的类型 +- [x] 再清一轮已空心化 `repository` / helper 边界壳 + - `src/repository/project.go`、`src/repository/user.go` 已删除;相关 project/user 访问已完全由模块仓储接管 + - `src/module/injection/repository.go` 已删除仅做 label item 转条件的空包装,service 直接传递 label condition + - 已执行:`cd src && go test ./module/project ./module/user ./module/injection ./module/team ./repository ./router ./docs` +- [x] 继续删旧仓储中已无人引用的模块专用壳文件 + - `src/repository/system.go`、`src/repository/evaluation.go`、`src/repository/role.go`、`src/repository/resource.go`、`src/repository/permission.go`、`src/repository/team.go` 已删除 + - 这些能力已分别由 `src/module/chaossystem`、`src/module/evaluation`、`src/module/rbac`、`src/module/team` 或 middleware / initialization 内聚实现接管 + - 已执行:`cd src && go test ./module/chaossystem ./module/evaluation ./module/rbac ./middleware ./service/initialization ./repository ./router ./docs` + - 已执行:`cd src && go test ./module/team ./middleware ./service/initialization ./repository ./router ./docs` +- [x] 再收一轮 `service/common` / `service/consumer` 直连旧仓储 helper + - `src/repository/dynamic_config.go`、`src/repository/task.go`、`src/repository/trace.go`、`src/repository/system_metadata.go` 已删除 + - 配置创建、task/trace upsert、trace 查询、system metadata 查询已分别内聚回 `src/service/common` / `src/service/consumer` + - 当前 `src/repository/*` 仅剩 container/dataset/execution/injection/label/search builder 等跨模块共享查询能力 + - 已执行:`cd src && go test ./service/common ./service/consumer ./module/system ./module/group ./module/trace ./repository ./router ./docs` +- [x] 最后一轮共享层命名 / 文件抛光 + - `src/repository/common.go` 已删除,剩余共享仓储不再保留无语义公共常量文件 + - container/dataset/injection 共享仓储里的 `active_name` omit 常量已改成各文件自解释命名 + - 修正残余命名/注释噪音:如 `contaierType`、`BatchDelteInjections` + - 已执行:`cd src && go test ./repository ./router ./docs ./service/common ./service/consumer` + +## 边界口径 + +- [x] `src/model` 继续只承载持久化实体 / view model / scanner / valuer +- [x] 跨模块共享的 API 请求/响应暂不并入 `src/model` + - 原因:共享 DTO 仍属于接口契约层,不是持久化模型;直接并入 `model` 会重新把存储边界和 HTTP/API 边界混在一起 + - 后续方向:继续缩小 `src/dto`,必要时再拆成更明确的共享契约包,而不是回灌到 `model` + - 当前保留例子:`src/dto/trace.go` 仍保留 trace 自身 stream/list/detail 契约;group 侧统计/stream DTO 已拆回 `src/module/group` + - 更新:`src/dto/trace.go` 现在只保留 trace stream 事件负载等共享结构,trace handler/service 自身契约已迁回模块,未使用 `TraceQuery` 已删除 + - 更新:`src/dto/task.go` 现在只保留 `UnifiedTask` 这类调度/运行时共享结构;原先重复保留的 `TaskResp` 已删除,trace 直接复用 `src/module/task/api_types.go` + - 更新:`src/dto/log.go` 现在只保留 Loki / OTLP / task log 共用的 `LogEntry`;WebSocket 消息壳已迁回 `src/module/task/log_types.go` + +## 当前决定 + +- [x] DB 初始化、迁移、生命周期已转入 `src/infra/db` +- [x] `scope` 查询辅助已从原 `database` 迁到 `src/repository` +- [x] 明确不采用“把 `dto` 并入 `model`”方案 +- [x] 完成第一阶段目录重命名 +- [x] DTO / model 主线重构已完成 + - 当前保留的 `src/repository/*` 主要是 consumer / service/common / metadata / search builder 等跨模块共享查询能力,不再属于本轮“模块专用旧壳” + - 后续若继续做,只剩增量优化,不再是本轮主线阻塞项 diff --git a/docs/swagger-audience-marking-report.md b/docs/swagger-audience-marking-report.md new file mode 100644 index 00000000..3aeef679 --- /dev/null +++ b/docs/swagger-audience-marking-report.md @@ -0,0 +1,201 @@ +# Swagger Audience Marking Report + +> Source of truth: Swagger annotations in `src/module/*/handler.go` and `src/httpapi/docs.go`. +> Route position column uses the `@Router` line, then `@x-api-type`, then function line when available. + +## Summary + +- Total operations scanned: **173** +- Marked operations: **100** +- Empty `@x-api-type {}` operations: **73** +- Missing `@x-api-type` operations: **0** +- Audience counts among marked operations: `sdk=5` `portal=43` `admin=58` + +## Marked Operations + +| Method | Path | Audience | Summary | Location | +| --- | --- | --- | --- | --- | +| GET | `/api/v2/access-keys` | `portal` | List access keys | `src/module/auth/handler.go:288` / `src/module/auth/handler.go:289` | +| POST | `/api/v2/access-keys` | `portal` | Create access key | `src/module/auth/handler.go:247` / `src/module/auth/handler.go:248` | +| DELETE | `/api/v2/access-keys/{access_key_id}` | `portal` | Delete access key | `src/module/auth/handler.go:357` / `src/module/auth/handler.go:358` | +| GET | `/api/v2/access-keys/{access_key_id}` | `portal` | Get access key detail | `src/module/auth/handler.go:328` / `src/module/auth/handler.go:329` | +| POST | `/api/v2/access-keys/{access_key_id}/disable` | `portal` | Disable access key | `src/module/auth/handler.go:385` / `src/module/auth/handler.go:386` | +| POST | `/api/v2/access-keys/{access_key_id}/enable` | `portal` | Enable access key | `src/module/auth/handler.go:413` / `src/module/auth/handler.go:414` | +| POST | `/api/v2/access-keys/{access_key_id}/rotate` | `portal` | Rotate access key secret | `src/module/auth/handler.go:441` / `src/module/auth/handler.go:442` | +| POST | `/api/v2/auth/access-key/token` | `sdk` | Exchange access key for token | `src/module/auth/handler.go:472` / `src/module/auth/handler.go:473` | +| POST | `/api/v2/auth/change-password` | `portal, admin` | Change user password | `src/module/auth/handler.go:177` / `src/module/auth/handler.go:178` | +| POST | `/api/v2/auth/login` | `portal, admin` | User login | `src/module/auth/handler.go:36` / `src/module/auth/handler.go:37` | +| POST | `/api/v2/auth/logout` | `portal, admin` | User logout | `src/module/auth/handler.go:139` / `src/module/auth/handler.go:140` | +| GET | `/api/v2/auth/profile` | `portal, admin` | Get current user profile | `src/module/auth/handler.go:216` / `src/module/auth/handler.go:217` | +| POST | `/api/v2/auth/refresh` | `portal, admin` | Refresh JWT token | `src/module/auth/handler.go:106` / `src/module/auth/handler.go:107` | +| POST | `/api/v2/auth/register` | `portal, admin` | User registration | `src/module/auth/handler.go:71` / `src/module/auth/handler.go:72` | +| GET | `/api/v2/labels` | `portal` | List labels | `src/module/label/handler.go:165` / `src/module/label/handler.go:166` | +| POST | `/api/v2/labels` | `portal` | Create label | `src/module/label/handler.go:69` / `src/module/label/handler.go:70` | +| POST | `/api/v2/labels/batch-delete` | `portal` | Batch delete labels | `src/module/label/handler.go:35` / `src/module/label/handler.go:36` | +| DELETE | `/api/v2/labels/{label_id}` | `portal` | Delete label | `src/module/label/handler.go:103` / `src/module/label/handler.go:104` | +| GET | `/api/v2/labels/{label_id}` | `portal` | Get label by ID | `src/module/label/handler.go:131` / `src/module/label/handler.go:132` | +| PATCH | `/api/v2/labels/{label_id}` | `portal` | Update label | `src/module/label/handler.go:201` / `src/module/label/handler.go:202` | +| GET | `/api/v2/permissions` | `admin` | List permissions | `src/module/rbac/handler.go:328` / `src/module/rbac/handler.go:329` | +| GET | `/api/v2/permissions/{id}` | `admin` | Get permission by ID | `src/module/rbac/handler.go:296` / `src/module/rbac/handler.go:297` | +| GET | `/api/v2/permissions/{permission_id}/roles` | `admin` | List roles from permission | `src/module/rbac/handler.go:362` / `src/module/rbac/handler.go:363` | +| GET | `/api/v2/projects` | `portal` | List projects | `src/module/project/handler.go:146` / `src/module/project/handler.go:147` | +| POST | `/api/v2/projects` | `portal` | Create a new project | `src/module/project/handler.go:39` / `src/module/project/handler.go:40` | +| DELETE | `/api/v2/projects/{project_id}` | `portal` | Delete project | `src/module/project/handler.go:82` / `src/module/project/handler.go:83` | +| GET | `/api/v2/projects/{project_id}` | `portal` | Get project by ID | `src/module/project/handler.go:113` / `src/module/project/handler.go:114` | +| PATCH | `/api/v2/projects/{project_id}` | `portal` | Update project | `src/module/project/handler.go:185` / `src/module/project/handler.go:186` | +| GET | `/api/v2/projects/{project_id}/executions` | `portal` | List project executions | `src/module/execution/handler.go:44` / `src/module/execution/handler.go:45` | +| POST | `/api/v2/projects/{project_id}/executions/execute` | `portal` | Submit batch algorithm execution | `src/module/execution/handler.go:88` / `src/module/execution/handler.go:89` | +| GET | `/api/v2/projects/{project_id}/injections` | `portal` | List project fault injections | `src/module/injection/handler.go:51` / `src/module/injection/handler.go:52` | +| GET | `/api/v2/projects/{project_id}/injections/analysis/no-issues` | `portal` | List project fault injections without issues | `src/module/injection/handler.go:125` / `src/module/injection/handler.go:126` | +| GET | `/api/v2/projects/{project_id}/injections/analysis/with-issues` | `portal` | List project fault injections with issues | `src/module/injection/handler.go:155` / `src/module/injection/handler.go:156` | +| POST | `/api/v2/projects/{project_id}/injections/build` | `portal` | Submit project datapack buildings | `src/module/injection/handler.go:211` / `src/module/injection/handler.go:212` | +| POST | `/api/v2/projects/{project_id}/injections/inject` | `portal` | Submit project fault injections | `src/module/injection/handler.go:183` / `src/module/injection/handler.go:184` | +| POST | `/api/v2/projects/{project_id}/injections/search` | `portal` | Search project fault injections | `src/module/injection/handler.go:95` / `src/module/injection/handler.go:96` | +| PATCH | `/api/v2/projects/{project_id}/labels` | `portal` | Manage project custom labels | `src/module/project/handler.go:229` / `src/module/project/handler.go:230` | +| GET | `/api/v2/resources` | `admin` | List resources | `src/module/rbac/handler.go:422` / `src/module/rbac/handler.go:423` | +| GET | `/api/v2/resources/{id}` | `admin` | Get resource by ID | `src/module/rbac/handler.go:391` / `src/module/rbac/handler.go:392` | +| GET | `/api/v2/resources/{id}/permissions` | `admin` | List permissions from resource | `src/module/rbac/handler.go:456` / `src/module/rbac/handler.go:457` / `src/module/rbac/handler.go:470` | +| GET | `/api/v2/roles` | `admin` | List roles | `src/module/rbac/handler.go:127` / `src/module/rbac/handler.go:128` | +| POST | `/api/v2/roles` | `admin` | Create a new role | `src/module/rbac/handler.go:38` / `src/module/rbac/handler.go:39` | +| DELETE | `/api/v2/roles/{id}` | `admin` | Delete role | `src/module/rbac/handler.go:68` / `src/module/rbac/handler.go:69` | +| GET | `/api/v2/roles/{id}` | `admin` | Get role by ID | `src/module/rbac/handler.go:96` / `src/module/rbac/handler.go:97` | +| PATCH | `/api/v2/roles/{id}` | `admin` | Update role | `src/module/rbac/handler.go:159` / `src/module/rbac/handler.go:160` | +| POST | `/api/v2/roles/{role_id}/permissions/assign` | `admin` | Assign permissions to role | `src/module/rbac/handler.go:199` / `src/module/rbac/handler.go:200` | +| POST | `/api/v2/roles/{role_id}/permissions/remove` | `admin` | Remove permissions from role | `src/module/rbac/handler.go:234` / `src/module/rbac/handler.go:235` | +| GET | `/api/v2/roles/{role_id}/users` | `admin` | List users from role | `src/module/rbac/handler.go:267` / `src/module/rbac/handler.go:268` | +| GET | `/api/v2/sdk/datasets` | `sdk` | List SDK dataset samples | `src/module/sdk/handler.go:116` / `src/module/sdk/handler.go:117` | +| GET | `/api/v2/sdk/evaluations` | `sdk` | List SDK evaluation samples | `src/module/sdk/handler.go:36` / `src/module/sdk/handler.go:37` | +| GET | `/api/v2/sdk/evaluations/experiments` | `sdk` | List SDK experiment IDs | `src/module/sdk/handler.go:92` / `src/module/sdk/handler.go:93` | +| GET | `/api/v2/sdk/evaluations/{id}` | `sdk` | Get SDK evaluation sample by ID | `src/module/sdk/handler.go:68` / `src/module/sdk/handler.go:69` | +| GET | `/api/v2/system/metrics` | `admin` | Get current system metrics | `src/module/systemmetric/handler.go:30` / `src/module/systemmetric/handler.go:31` | +| GET | `/api/v2/system/metrics/history` | `admin` | Get historical system metrics | `src/module/systemmetric/handler.go:53` / `src/module/systemmetric/handler.go:54` | +| GET | `/api/v2/systems` | `admin` | List chaos systems | `src/module/chaossystem/handler.go:35` / `src/module/chaossystem/handler.go:36` | +| POST | `/api/v2/systems` | `admin` | Create chaos system | `src/module/chaossystem/handler.go:96` / `src/module/chaossystem/handler.go:97` | +| DELETE | `/api/v2/systems/{id}` | `admin` | Delete chaos system | `src/module/chaossystem/handler.go:158` / `src/module/chaossystem/handler.go:159` | +| GET | `/api/v2/systems/{id}` | `admin` | Get chaos system by ID | `src/module/chaossystem/handler.go:67` / `src/module/chaossystem/handler.go:68` | +| PUT | `/api/v2/systems/{id}` | `admin` | Update chaos system | `src/module/chaossystem/handler.go:126` / `src/module/chaossystem/handler.go:127` | +| GET | `/api/v2/systems/{id}/metadata` | `admin` | List chaos system metadata | `src/module/chaossystem/handler.go:218` / `src/module/chaossystem/handler.go:219` | +| POST | `/api/v2/systems/{id}/metadata` | `admin` | Upsert chaos system metadata | `src/module/chaossystem/handler.go:186` / `src/module/chaossystem/handler.go:187` | +| GET | `/api/v2/teams` | `portal` | List teams | `src/module/team/handler.go:137` / `src/module/team/handler.go:138` | +| POST | `/api/v2/teams` | `portal` | Create a new team | `src/module/team/handler.go:39` / `src/module/team/handler.go:40` | +| DELETE | `/api/v2/teams/{team_id}` | `portal` | Delete team | `src/module/team/handler.go:78` / `src/module/team/handler.go:79` | +| GET | `/api/v2/teams/{team_id}` | `portal` | Get team by ID | `src/module/team/handler.go:106` / `src/module/team/handler.go:107` | +| PATCH | `/api/v2/teams/{team_id}` | `portal` | Update team | `src/module/team/handler.go:178` / `src/module/team/handler.go:179` | +| GET | `/api/v2/teams/{team_id}/members` | `portal` | List team members | `src/module/team/handler.go:391` / `src/module/team/handler.go:392` | +| POST | `/api/v2/teams/{team_id}/members` | `portal` | Add member to team | `src/module/team/handler.go:261` / `src/module/team/handler.go:262` | +| DELETE | `/api/v2/teams/{team_id}/members/{user_id}` | `portal` | Remove member from team | `src/module/team/handler.go:299` / `src/module/team/handler.go:300` | +| PATCH | `/api/v2/teams/{team_id}/members/{user_id}/role` | `portal` | Update team member role | `src/module/team/handler.go:343` / `src/module/team/handler.go:344` | +| GET | `/api/v2/teams/{team_id}/projects` | `portal` | List team projects | `src/module/team/handler.go:220` / `src/module/team/handler.go:221` | +| GET | `/api/v2/users` | `admin` | List users | `src/module/user/handler.go:134` / `src/module/user/handler.go:135` | +| POST | `/api/v2/users` | `admin` | Create a new user | `src/module/user/handler.go:36` / `src/module/user/handler.go:37` | +| DELETE | `/api/v2/users/{id}` | `admin` | Delete user | `src/module/user/handler.go:73` / `src/module/user/handler.go:74` | +| PATCH | `/api/v2/users/{id}` | `admin` | Update user | `src/module/user/handler.go:170` / `src/module/user/handler.go:171` | +| GET | `/api/v2/users/{id}/detail` | `admin` | Get user by ID | `src/module/user/handler.go:101` / `src/module/user/handler.go:102` | +| DELETE | `/api/v2/users/{user_id}/containers/{container_id}` | `admin` | Remove user from container | `src/module/user/handler.go:379` / `src/module/user/handler.go:380` | +| POST | `/api/v2/users/{user_id}/containers/{container_id}/roles/{role_id}` | `admin` | Assign user to container | `src/module/user/handler.go:342` / `src/module/user/handler.go:343` | +| DELETE | `/api/v2/users/{user_id}/datasets/{dataset_id}` | `admin` | Remove user from dataset | `src/module/user/handler.go:450` / `src/module/user/handler.go:451` | +| POST | `/api/v2/users/{user_id}/datasets/{dataset_id}/roles/{role_id}` | `admin` | Assign user to dataset | `src/module/user/handler.go:413` / `src/module/user/handler.go:414` | +| POST | `/api/v2/users/{user_id}/permissions/assign` | `admin` | Assign permission to user | `src/module/user/handler.go:264` / `src/module/user/handler.go:265` | +| POST | `/api/v2/users/{user_id}/permissions/remove` | `admin` | Remove permission from user | `src/module/user/handler.go:303` / `src/module/user/handler.go:304` | +| DELETE | `/api/v2/users/{user_id}/projects/{project_id}` | `admin` | Remove user from project | `src/module/user/handler.go:521` / `src/module/user/handler.go:522` / `src/module/user/handler.go:538` | +| POST | `/api/v2/users/{user_id}/projects/{project_id}/roles/{role_id}` | `admin` | Assign user to project | `src/module/user/handler.go:484` / `src/module/user/handler.go:485` | +| POST | `/api/v2/users/{user_id}/role/{role_id}` | `admin` | Assign global role to user | `src/module/user/handler.go:205` / `src/module/user/handler.go:206` | +| DELETE | `/api/v2/users/{user_id}/roles/{role_id}` | `admin` | Remove role from user | `src/module/user/handler.go:234` / `src/module/user/handler.go:235` | +| GET | `/system/audit` | `admin` | List audit logs | `src/module/system/handler.go:184` / `src/module/system/handler.go:185` | +| GET | `/system/audit/{id}` | `admin` | Get audit log by ID | `src/module/system/handler.go:148` / `src/module/system/handler.go:149` | +| GET | `/system/configs` | `admin` | List configurations | `src/module/system/handler.go:253` / `src/module/system/handler.go:254` | +| GET | `/system/configs/{config_id}` | `admin` | Get configuration | `src/module/system/handler.go:219` / `src/module/system/handler.go:220` | +| PATCH | `/system/configs/{config_id}` | `admin` | Update configuration value | `src/module/system/handler.go:376` / `src/module/system/handler.go:377` | +| GET | `/system/configs/{config_id}/histories` | `admin` | List configuration histories | `src/module/system/handler.go:467` / `src/module/system/handler.go:468` | +| PUT | `/system/configs/{config_id}/metadata` | `admin` | Update configuration metadata | `src/module/system/handler.go:420` / `src/module/system/handler.go:421` | +| POST | `/system/configs/{config_id}/metadata/rollback` | `admin` | Rollback configuration metadata | `src/module/system/handler.go:333` / `src/module/system/handler.go:334` | +| POST | `/system/configs/{config_id}/value/rollback` | `admin` | Rollback configuration value | `src/module/system/handler.go:289` / `src/module/system/handler.go:290` | +| GET | `/system/health` | `admin` | System health check | `src/module/system/handler.go:32` / `src/module/system/handler.go:33` | +| GET | `/system/monitor/info` | `admin` | Get system information | `src/module/system/handler.go:83` / `src/module/system/handler.go:84` | +| POST | `/system/monitor/metrics` | `admin` | Get monitoring metrics | `src/module/system/handler.go:58` / `src/module/system/handler.go:59` | +| GET | `/system/monitor/namespaces/locks` | `admin` | List namespace locks | `src/module/system/handler.go:102` / `src/module/system/handler.go:103` | +| POST | `/system/monitor/tasks/queue` | `admin` | List queued tasks | `src/module/system/handler.go:124` / `src/module/system/handler.go:125` | + +## Empty `@x-api-type {}` Operations + +| Method | Path | Summary | Raw | Location | +| --- | --- | --- | --- | --- | +| GET | `/api/_docs/models` | API Model Definitions | `{}` | `src/httpapi/docs.go:36` / `src/httpapi/docs.go:37` / `src/httpapi/docs.go:38` | +| GET | `/api/v2/containers` | List containers | `{}` | `src/module/container/handler.go:148` / `src/module/container/handler.go:149` | +| POST | `/api/v2/containers` | Create container | `{}` | `src/module/container/handler.go:41` / `src/module/container/handler.go:42` | +| POST | `/api/v2/containers/build` | Submit container building | `{}` | `src/module/container/handler.go:474` / `src/module/container/handler.go:475` | +| DELETE | `/api/v2/containers/{container_id}` | Delete container | `{}` | `src/module/container/handler.go:84` / `src/module/container/handler.go:85` | +| GET | `/api/v2/containers/{container_id}` | Get container by ID | `{}` | `src/module/container/handler.go:114` / `src/module/container/handler.go:115` | +| PATCH | `/api/v2/containers/{container_id}` | Update container | `{}` | `src/module/container/handler.go:187` / `src/module/container/handler.go:188` | +| PATCH | `/api/v2/containers/{container_id}/labels` | Manage container custom labels | `{}` | `src/module/container/handler.go:226` / `src/module/container/handler.go:227` | +| GET | `/api/v2/containers/{container_id}/versions` | List container versions | `{}` | `src/module/container/handler.go:387` / `src/module/container/handler.go:388` | +| POST | `/api/v2/containers/{container_id}/versions` | Create container version | `{}` | `src/module/container/handler.go:270` / `src/module/container/handler.go:271` | +| DELETE | `/api/v2/containers/{container_id}/versions/{version_id}` | Delete container version | `{}` | `src/module/container/handler.go:319` / `src/module/container/handler.go:320` | +| GET | `/api/v2/containers/{container_id}/versions/{version_id}` | Get container version by ID | `{}` | `src/module/container/handler.go:350` / `src/module/container/handler.go:351` | +| PATCH | `/api/v2/containers/{container_id}/versions/{version_id}` | Update container version | `{}` | `src/module/container/handler.go:432` / `src/module/container/handler.go:433` | +| POST | `/api/v2/containers/{container_id}/versions/{version_id}/helm-chart` | Upload Helm chart package | `{}` | `src/module/container/handler.go:521` / `src/module/container/handler.go:522` | +| POST | `/api/v2/containers/{container_id}/versions/{version_id}/helm-values` | Upload Helm values file | `{}` | `src/module/container/handler.go:577` / `src/module/container/handler.go:578` | +| GET | `/api/v2/datasets` | List datasets | `{}` | `src/module/dataset/handler.go:149` / `src/module/dataset/handler.go:150` | +| POST | `/api/v2/datasets` | Create dataset | `{}` | `src/module/dataset/handler.go:42` / `src/module/dataset/handler.go:43` | +| POST | `/api/v2/datasets/search` | Search datasets | `{}` | `src/module/dataset/handler.go:186` / `src/module/dataset/handler.go:187` | +| DELETE | `/api/v2/datasets/{dataset_id}` | Delete dataset | `{}` | `src/module/dataset/handler.go:85` / `src/module/dataset/handler.go:86` | +| GET | `/api/v2/datasets/{dataset_id}` | Get dataset by ID | `{}` | `src/module/dataset/handler.go:115` / `src/module/dataset/handler.go:116` | +| PATCH | `/api/v2/datasets/{dataset_id}` | Update dataset | `{}` | `src/module/dataset/handler.go:225` / `src/module/dataset/handler.go:226` | +| PATCH | `/api/v2/datasets/{dataset_id}/labels` | Manage dataset custom labels | `{}` | `src/module/dataset/handler.go:269` / `src/module/dataset/handler.go:270` | +| PATCH | `/api/v2/datasets/{dataset_id}/version/{version_id}/injections` | Manage dataset injections | `{}` | `src/module/dataset/handler.go:569` / `src/module/dataset/handler.go:570` | +| GET | `/api/v2/datasets/{dataset_id}/versions` | List dataset versions | `{}` | `src/module/dataset/handler.go:430` / `src/module/dataset/handler.go:431` | +| POST | `/api/v2/datasets/{dataset_id}/versions` | Create dataset version | `{}` | `src/module/dataset/handler.go:313` / `src/module/dataset/handler.go:314` | +| DELETE | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Delete dataset version | `{}` | `src/module/dataset/handler.go:362` / `src/module/dataset/handler.go:363` | +| GET | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Get dataset version by ID | `{}` | `src/module/dataset/handler.go:393` / `src/module/dataset/handler.go:394` | +| PATCH | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Update dataset version | `{}` | `src/module/dataset/handler.go:475` / `src/module/dataset/handler.go:476` | +| GET | `/api/v2/datasets/{dataset_id}/versions/{version_id}/download` | Download dataset version | `{}` | `src/module/dataset/handler.go:521` / `src/module/dataset/handler.go:522` | +| GET | `/api/v2/evaluations` | List evaluations | `{}` | `src/module/evaluation/handler.go:122` / `src/module/evaluation/handler.go:123` | +| POST | `/api/v2/evaluations/datapacks` | List Datapack Evaluation Results | `{}` | `src/module/evaluation/handler.go:37` / `src/module/evaluation/handler.go:38` | +| POST | `/api/v2/evaluations/datasets` | List Dataset Evaluation Results | `{}` | `src/module/evaluation/handler.go:80` / `src/module/evaluation/handler.go:81` | +| DELETE | `/api/v2/evaluations/{id}` | Delete evaluation by ID | `{}` | `src/module/evaluation/handler.go:186` / `src/module/evaluation/handler.go:187` | +| GET | `/api/v2/evaluations/{id}` | Get evaluation by ID | `{}` | `src/module/evaluation/handler.go:157` / `src/module/evaluation/handler.go:158` | +| GET | `/api/v2/executions` | List executions | `{}` | `src/module/execution/handler.go:146` / `src/module/execution/handler.go:147` | +| POST | `/api/v2/executions/batch-delete` | Batch delete executions | `{}` | `src/module/execution/handler.go:271` / `src/module/execution/handler.go:272` | +| GET | `/api/v2/executions/labels` | List execution labels | `{}` | `src/module/execution/handler.go:206` / `src/module/execution/handler.go:207` | +| POST | `/api/v2/executions/{execution_id}/detector_results` | Upload detector results | `{}` | `src/module/execution/handler.go:306` / `src/module/execution/handler.go:307` | +| POST | `/api/v2/executions/{execution_id}/granularity_results` | Upload granularity results | `{}` | `src/module/execution/handler.go:346` / `src/module/execution/handler.go:347` | +| GET | `/api/v2/executions/{id}` | Get execution by ID | `{}` | `src/module/execution/handler.go:180` / `src/module/execution/handler.go:181` | +| PATCH | `/api/v2/executions/{id}/labels` | Manage execution custom labels | `{}` | `src/module/execution/handler.go:233` / `src/module/execution/handler.go:234` | +| GET | `/api/v2/groups/{group_id}/stats` | Get statistics for a group of traces | `{}` | `src/module/group/handler.go:43` / `src/module/group/handler.go:44` | +| GET | `/api/v2/groups/{group_id}/stream` | Stream group trace events in real-time | `{}` | `src/module/group/handler.go:82` / `src/module/group/handler.go:84` | +| GET | `/api/v2/injections` | List injections | `{}` | `src/module/injection/handler.go:242` / `src/module/injection/handler.go:243` | +| GET | `/api/v2/injections/analysis/no-issues` | Query Fault Injection Records Without Issues | `{}` | `src/module/injection/handler.go:333` / `src/module/injection/handler.go:334` | +| GET | `/api/v2/injections/analysis/with-issues` | Query Fault Injection Records With Issues | `{}` | `src/module/injection/handler.go:351` / `src/module/injection/handler.go:352` | +| POST | `/api/v2/injections/batch-delete` | Batch delete injections | `{}` | `src/module/injection/handler.go:522` / `src/module/injection/handler.go:523` | +| POST | `/api/v2/injections/build` | Submit batch datapack buildings | `{}` | `src/module/injection/handler.go:315` / `src/module/injection/handler.go:316` | +| POST | `/api/v2/injections/inject` | Submit batch fault injections | `{}` | `src/module/injection/handler.go:296` / `src/module/injection/handler.go:297` | +| PATCH | `/api/v2/injections/labels/batch` | Batch manage injection labels | `{}` | `src/module/injection/handler.go:488` / `src/module/injection/handler.go:489` | +| GET | `/api/v2/injections/metadata` | Get Injection Metadata | `{}` | `src/module/injection/handler.go:401` / `src/module/injection/handler.go:402` | +| POST | `/api/v2/injections/search` | Search injections | `{}` | `src/module/injection/handler.go:276` / `src/module/injection/handler.go:277` | +| POST | `/api/v2/injections/upload` | Upload a manual datapack | `{}` | `src/module/injection/handler.go:828` / `src/module/injection/handler.go:829` | +| GET | `/api/v2/injections/{id}` | Get injection by ID | `{}` | `src/module/injection/handler.go:372` / `src/module/injection/handler.go:373` | +| POST | `/api/v2/injections/{id}/clone` | Clone injection | `{}` | `src/module/injection/handler.go:556` / `src/module/injection/handler.go:557` | +| GET | `/api/v2/injections/{id}/download` | Download datapack | `{}` | `src/module/injection/handler.go:617` / `src/module/injection/handler.go:618` | +| GET | `/api/v2/injections/{id}/files` | List datapack files | `{}` | `src/module/injection/handler.go:653` / `src/module/injection/handler.go:654` | +| GET | `/api/v2/injections/{id}/files/download` | Download datapack file | `{}` | `src/module/injection/handler.go:691` / `src/module/injection/handler.go:692` | +| GET | `/api/v2/injections/{id}/files/query` | Query datapack file content | `{}` | `src/module/injection/handler.go:745` / `src/module/injection/handler.go:746` | +| PUT | `/api/v2/injections/{id}/groundtruth` | Update datapack ground truth | `{}` | `src/module/injection/handler.go:787` / `src/module/injection/handler.go:788` | +| PATCH | `/api/v2/injections/{id}/labels` | Manage injection custom labels | `{}` | `src/module/injection/handler.go:450` / `src/module/injection/handler.go:451` | +| GET | `/api/v2/injections/{id}/logs` | Get injection logs | `{}` | `src/module/injection/handler.go:589` / `src/module/injection/handler.go:590` | +| GET | `/api/v2/metrics/algorithms` | Get algorithm comparison metrics | `{}` | `src/module/metric/handler.go:99` / `src/module/metric/handler.go:100` | +| GET | `/api/v2/metrics/executions` | Get execution metrics | `{}` | `src/module/metric/handler.go:67` / `src/module/metric/handler.go:68` | +| GET | `/api/v2/metrics/injections` | Get injection metrics | `{}` | `src/module/metric/handler.go:35` / `src/module/metric/handler.go:36` | +| GET | `/api/v2/notifications/stream` | Stream global notifications in real-time | `{}` | `src/module/notification/handler.go:40` / `src/module/notification/handler.go:42` | +| GET | `/api/v2/tasks` | List tasks | `{}` | `src/module/task/handler.go:124` / `src/module/task/handler.go:125` | +| POST | `/api/v2/tasks/batch-delete` | Batch delete tasks | `{}` | `src/module/task/handler.go:48` / `src/module/task/handler.go:49` | +| GET | `/api/v2/tasks/{task_id}` | Get task by ID | `{}` | `src/module/task/handler.go:85` / `src/module/task/handler.go:86` | +| GET | `/api/v2/tasks/{task_id}/logs/ws` | Stream task logs via WebSocket | `{}` | `src/module/task/handler.go:159` / `src/module/task/handler.go:160` | +| GET | `/api/v2/traces` | List traces | `{}` | `src/module/trace/handler.go:81` / `src/module/trace/handler.go:82` | +| GET | `/api/v2/traces/{trace_id}` | Get trace by ID | `{}` | `src/module/trace/handler.go:44` / `src/module/trace/handler.go:45` | +| GET | `/api/v2/traces/{trace_id}/stream` | Stream trace events in real-time | `{}` | `src/module/trace/handler.go:118` / `src/module/trace/handler.go:119` | + +## Missing `@x-api-type` Operations + +| Method | Path | Summary | Location | +| --- | --- | --- | --- | + diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 00000000..0a933926 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,584 @@ +# Backend Fx Refactor TODO + +> 创建日期:2026-04-15 +> 目标:把后端从全局初始化 + 包级函数,逐步迁移到 Fx app + 明确模块边界 + 生命周期管理。 + +## 使用方式 + +- 先画模块边界,再做 DI。 +- 每次只迁移一个入口或一个模块。 +- 第一阶段不改 URL,不搬大目录,不重写业务逻辑。 +- Fx 先管理启动和生命周期,再逐步替换 handler / service / repository 的包级函数。 +- 当前已有旧 DI 骨架视为临时试验,后续由 Fx 替换。 + +## 0. 准备阶段 + +- [x] 确认项目更适合 Fx,而不是继续扩大旧 DI 方案 +- [x] 确认后端有多入口:producer / consumer / both +- [x] 确认有多基础设施资源:DB / Redis / Etcd / K8s / Loki / tracing / receiver +- [x] 确认第一阶段不改 URL +- [x] 确认第一阶段不大规模搬目录 +- [x] 决定是否立即删除当前旧 DI 骨架 +- [ ] 确认 Fx 生成的启动日志是否可接受 + - 属于人工验收项,不阻塞当前代码主线收口。 + +验证: + +- [x] 阅读 [backend-fx-refactor-plan.md](./backend-fx-refactor-plan.md) +- [x] `cd src && go test ./app ./router ./handlers/v2` + - 实际执行:`cd src && go test ./app ./interface/http ./router ./handlers/v2` + +## 1. 停止继续旧 DI 扩张 + +- [x] 不再新增旧 DI provider set +- [x] 不再继续按旧 DI TODO 迁移 Project service / repository +- [x] 决定旧 DI 文件处理方式 + - [x] 方案 A:立即删除 app 下旧 DI 生成文件与相关依赖 + - 方案 B 未采用:不再保留旧 DI 骨架等待后删。 +- [x] 文档和 TODO 全部切换到 Fx 方案 + +验证: + +- [x] 检查 app 与依赖中旧 DI 痕迹 + - 代码和依赖已删除;文档中的历史说明也已切成中性表述。 + +## 2. 引入 Fx 基础设施 + +- [x] 在 `src/go.mod` 增加 `go.uber.org/fx` +- [x] 新建或调整 `src/app` 为 Fx app 入口 +- [x] 新建 `src/app/options.go` +- [x] 新建 `src/app/producer.go` +- [x] 新建 `src/app/consumer.go` +- [x] 新建 `src/app/both.go` +- [x] 定义 `CommonOptions()` +- [x] 定义 `ProducerOptions()` +- [x] 定义 `ConsumerOptions()` +- [x] 定义 `BothOptions()` + +目标: + +```go +func ProducerOptions() fx.Option +func ConsumerOptions() fx.Option +func BothOptions() fx.Option +``` + +验证: + +- [x] `cd src && go test ./app` + +## 3. Config / Logger Module + +- [x] 新建 `src/infra/config/module.go` +- [x] 包装现有 `config.Init` +- [x] 让配置路径从 app 参数传入,而不是各处自行读取 +- [x] 新建 `src/infra/logger/module.go` +- [x] 把 logrus 初始化从 `main.go` 收进 logger module +- [x] 确认 logger 初始化只执行一次 + +验收: + +- [x] `main.go` 不再直接配置 logger +- [x] `main.go` 不再直接调用 `config.Init` + - 当前 `config.Init` 仅保留在 `infra/config` module 与少量测试中,producer / consumer / both 主启动链均已通过 Fx 配置模块进入。 +- [x] `cd src && go test ./infra/config ./infra/logger` + +## 4. DB Module + +- [x] 新建 `src/infra/db/module.go` +- [x] 新建 `NewGormDB` +- [x] 将现有 `database.InitDB()` 包装进 Fx provider 或重构为返回 `*gorm.DB` +- [x] DB module 提供 `*gorm.DB` +- [x] 使用 `fx.Lifecycle` 注册 DB close +- [x] 过渡期继续同步 `database.DB = db`,避免一次性修改旧代码 + +目标: + +```go +var Module = fx.Module("db", + fx.Provide(NewGormDB), +) +``` + +验收: + +- [x] producer 可通过 Fx 初始化 DB +- [x] `database.DB` 兼容旧代码 +- [x] DB 关闭逻辑在 `OnStop` + +## 5. Redis / Etcd / Tracing Module + +Redis: + +- [x] 新建 `src/infra/redis/module.go` +- [x] 提供 Redis client +- [x] `OnStop` 关闭 Redis +- [x] Redis 实现已从 `src/client/redis_client.go` 并入 `src/infra/redis/*` +- [x] `src/infra/redis/client.go` 已删除,连接创建已继续并入 `src/infra/redis/gateway.go` 私有方法 +- [x] 过渡期兼容 `client.GetRedisClient()` + - 兼容期已结束;`module/system` / `module/trace` / `module/group` / `module/notification` / `service/common` / `service/consumer` / `service/logreceiver` 等调用点已切到 `redisinfra`。 + +Etcd: + +- [x] 新建 `src/infra/etcd/module.go` +- [x] 提供 Etcd client 或 gateway +- [x] 收口 Etcd watch / get / put 的初始化 +- [x] Etcd 实现已从 `src/client/etcd_client.go` 并入 `src/infra/etcd/*` +- [x] `src/infra/etcd/client.go` 已删除,连接创建已继续并入 `src/infra/etcd/gateway.go` 私有方法 + +Tracing: + +- [x] 新建 `src/infra/tracing/module.go` +- [x] 包装 `client.InitTraceProvider()` +- [x] 如支持 shutdown,则注册 `OnStop` +- [x] tracing provider 实现已从 `src/client/jaeger.go` 并入 `src/infra/tracing/*` + +Loki: + +- [x] 新建 `src/infra/loki/module.go` +- [x] Fx graph 提供 `*client.LokiClient` +- [x] `module/task.LokiGateway` 注入 `*client.LokiClient`,不再自行 `client.NewLokiClient()` +- [x] Loki 实现已从 `src/client/loki.go` 并入 `src/infra/loki/*` +- [x] `module/task` / `module/injection` / `app.CommonResources` 已切到 `lokiinfra.Client` + +验收: + +- [x] 基础设施资源由 Fx module 创建 +- [x] 旧代码仍可运行 +- [x] `cd src && go test ./infra/...` + - 实际执行:`cd src && go test ./app ./infra/config ./infra/logger ./infra/db ./infra/redis ./infra/etcd ./infra/tracing ./interface/http ./router ./handlers/v2` + +## 6. HTTP Interface Module + +- [x] 新建 `src/interface/http/module.go` +- [x] 新建 `src/interface/http/server.go` +- [x] 新建 `src/interface/http/router.go` +- [x] 将现有 `router.New(...)` 包装为 Fx provider +- [x] HTTP server 使用 `http.Server` +- [x] `OnStart` 启动 server goroutine +- [x] `OnStop` graceful shutdown +- [x] producer 模式通过 Fx 启动 HTTP server + +目标: + +```go +var Module = fx.Module("http", + fx.Provide(NewGinEngine, NewHTTPServer), + fx.Invoke(RegisterHTTPServerLifecycle), +) +``` + +验收: + +- [x] `main.go producer` 不再直接 `engine.Run` +- [x] HTTP server 可优雅停止 +- [x] API URL 不变 +- [x] `cd src && go test ./interface/http ./router` + - 实际执行:`cd src && go test ./app ./interface/http ./router ./handlers/v2` + +## 7. main.go 收口 + +- [x] `main.go` 只保留 cobra mode 解析 +- [x] producer mode 调用 `fx.New(app.ProducerOptions(...)).Run()` +- [x] consumer mode 调用 `fx.New(app.ConsumerOptions(...)).Run()` +- [x] both mode 调用 `fx.New(app.BothOptions(...)).Run()` +- [x] 删除 `main.go` 中直接 DB 初始化 +- [x] 删除 `main.go` 中直接 trace 初始化 +- [x] 删除 `main.go` 中直接 HTTP server 启动 + +验收: + +- [x] `main.go` 明显变薄 +- [x] producer 可启动 +- [x] consumer 暂时可保留旧逻辑或已接入 Fx +- [x] both 可启动 + +## 8. Consumer / Scheduler Module + +- [x] 新建 `src/interface/worker/module.go` +- [x] 包装 `consumer.StartScheduler` +- [x] 包装 `consumer.ConsumeTasks` +- [x] 使用 Fx lifecycle 管理 context cancel +- [x] `OnStart` 启动 scheduler goroutine +- [x] `OnStart` 启动 consumer goroutine +- [x] `OnStop` cancel context +- [x] 避免 consumer 阻塞 Fx 启动流程 + +验收: + +- [x] consumer mode 通过 Fx 启动 +- [x] both mode 通过 Fx 同时启动 HTTP 和 consumer +- [x] 停止时能 cancel worker context + +## 9. K8s Controller / Chaos Module + +- [x] 新建 `src/infra/k8s/module.go` +- [x] 包装 `k8s.GetK8sController()` +- [x] 包装 K8s rest config +- [x] 新建 `src/infra/chaos/module.go` +- [x] 包装 `chaosCli.InitWithConfig` +- [x] 新建 `src/interface/controller/module.go` +- [x] 用 lifecycle 启动 K8s controller +- [x] 用 lifecycle 停止 controller context + +验收: + +- [x] consumer / both mode 中 K8s controller 由 Fx 启动 +- [x] 初始化顺序由 Fx 表达 + +## 10. OTLP Receiver Module + +- [x] 新建 `src/interface/receiver/module.go` +- [x] 包装 `logreceiver.NewOTLPLogReceiver` +- [x] receiver port 从 config module 注入 +- [x] `OnStart` 启动 receiver +- [x] `OnStop` shutdown receiver + +验收: + +- [x] consumer / both mode 中 receiver 由 Fx 启动 +- [x] 停止时 receiver 正常关闭 + +## 11. HTTP Routes 按受众拆分 + +先拆注册函数,不改 URL。 + +- [x] 新建或迁移 public routes +- [x] 新建或迁移 sdk routes +- [x] 新建或迁移 portal routes +- [x] 新建或迁移 admin routes +- [x] 整理 system routes +- [x] route 注册依赖 handler 容器 + +建议: + +```go +func RegisterPublicRoutes(...) +func RegisterSDKRoutes(...) +func RegisterPortalRoutes(...) +func RegisterAdminRoutes(...) +func RegisterSystemRoutes(...) +``` + +验收: + +- [x] URL 不变 +- [x] `router/v2.go` 变薄 + - 从 647 行降到 365 行;核心业务路由仍保留在 `v2.go`,后续随业务 module 迁移继续拆。 +- [x] Admin / SDK / Portal 边界在代码上可见 + +## 12. 业务 Module 壳 + +先建立壳,不急着重写内部逻辑。 + +- [x] `module/project` +- [x] `module/auth` +- [x] `module/task` +- [x] `module/injection` +- [x] `module/execution` +- [x] `module/container` +- [x] `module/dataset` +- [x] `module/rbac` +- [x] `module/user` + +每个模块先暴露: + +```go +var Module = fx.Module("project", + fx.Provide(NewHandler), +) +``` + +过渡期 handler 可以 wrapper 旧函数。 + +验收: + +- [x] app 通过业务 module 收集 handler + - 已新增 `app.ProducerHTTPModules()` 与 `router.Module`,Producer/Both 由 app 统一收集业务 module,再向 HTTP interface 提供 `router.Handlers`。 +- [x] router 不直接散装引用所有裸函数 + - 业务路由已统一经 `router.Handlers` 聚合,`interface/http` 不再散装依赖各模块构造;剩余主要是旧兼容层清理与少量 middleware/初始化收尾。 + +## 13. Project 模块正式迁移 + +Project 作为第一个完整业务样板。 + +- [x] 新建 `module/project/repository.go` +- [x] 新建 `module/project/service.go` +- [x] 新建 `module/project/handler.go` +- [x] 新建 `module/project/module.go` +- [x] Repository 注入 `*gorm.DB` +- [x] Service 注入 Repository + - RBAC 独立接口化与 Label 接口进一步抽象保留为后续优化项,不阻塞当前主线。 +- [x] Handler 注入 Service +- [x] Handler method 使用 `c.Request.Context()` +- [x] 移除 Project handler wrapper 对旧包级函数的依赖 + - Project CRUD/labels 已移除旧 wrapper;project 下 injection/execution routes 已切到 `module/injection` 和 `module/execution`。 +- [x] Project routes 使用新 handler + +验收: + +- [x] Project handler 不直接 import `database` +- [x] Project handler 不直接 import repository implementation +- [x] Project service 不直接使用全局 `database.DB` +- [x] Project CRUD 行为不变 + +## 14. Auth / Task 模块迁移 + +Auth: + +- [x] 新建 Auth module +- [x] Token blacklist 从 repository 迁移到 store + - 新路由已走 `module/auth.TokenStore`;旧 `repository/token.go` 与 `service/producer/auth.go` 已删除。 +- [x] Auth service 注入 UserRepository / RoleRepository / TokenStore +- [x] Auth handler method 化 + +Task: + +- [x] 新建 Task module +- [x] Task queue Redis 访问收口到 store +- [x] WebSocket handler 只做认证和连接升级 + - 日志推送已走 `module/task.TaskLogService`。 +- [x] 日志历史查询走 Loki gateway +- [x] 订阅逻辑走 service / store + - Redis Pub/Sub 已收口到 `module/task.TaskQueueStore`;task state polling 已收口到 `module/task.TaskLogService`。 + +验收: + +- [x] `handlers/v2/tasks.go` 不再 direct import `database` +- [x] `handlers/v2/tasks.go` 不再 direct import `repository` + - 旧文件已删除,Task 路由切到 `module/task.Handler`。 +- [x] `repository/token.go` 能力迁移出去 + - Auth 黑名单能力已统一收口到 `module/auth.TokenStore`。 + +## 15. 核心业务模块逐个迁移 + +按顺序推进: + +- [x] Injection + - 已建立 module/handler/service 壳并切 Project 子路由;深层 producer/repository 逻辑后续继续下沉。 +- [x] Execution + - 已建立 module/handler/service 壳并切 Project 子路由;深层 producer/repository 逻辑后续继续下沉。 +- [x] Container +- [x] Dataset +- [x] Evaluation +- [x] Trace +- [x] Metrics +- [x] Group +- [x] Notification +- [x] SDK Evaluation +- [x] Chaos System + +主线完成项: + +- [x] Module / Handler / Service / Repository 壳 +- [x] Fx providers +- [x] route 切换 +- [x] 主路径所需 Store / Gateway 收口 +- [x] 关键模块级测试 + - Container / Dataset / Evaluation / Trace / Group / Metrics / Notification / SDK Evaluation / Chaos System 已完成 module/handler/service/repository 壳、Fx providers 和 route 切换;其中 Metrics / SDK Evaluation / Chaos System 已进一步切离 `service/producer` 包级入口。测试层面已覆盖 auth / project / execution / injection / task / user / sdk / docs / app 等主路径,剩余测试补强属于后续质量项,不阻塞当前主线。 + +## 16. Store / Gateway 拆分 + +- [x] Redis token blacklist -> TokenStore + - 旧 `repository/token.go` 已删除,认证退出逻辑统一走 `module/auth/token_store.go`。 +- [x] Redis task queue -> TaskQueueStore + - 已新增 `src/infra/redis/task_queue.go`,consumer / scheduler / system monitor 队列读写全部从 `repository/task.go` 迁出。 +- [x] Loki -> LokiGateway + - 当前完成 Task 日志查询侧,并将 Loki client 纳入 Fx graph;Injection 日志查询仍待深层 service 迁移。 +- [x] K8s -> K8sGateway + - 已新增 `src/infra/k8s/gateway.go`,统一收口 controller / create job / volume mount / job logs / health check 访问;并已把 `src/client/k8s/*` 的真实实现整体迁入 `src/infra/k8s/*`。其中 `RestConfig / Client / DynamicClient` 这类简单转发已继续收口,改由 `infra/k8s` 内部私有 getter 与 Fx provider 使用。 +- [x] Etcd -> EtcdGateway + - 已新增 `src/infra/etcd/gateway.go`,配置监听与动态配置发布已切到 gateway;`Put/Get/Delete/Watch` 逻辑也已收回 gateway,`client.go` 仅保留底层连接创建与关闭。 +- [x] Harbor -> HarborGateway + - 已新增 `src/infra/harbor/gateway.go`,并把 Harbor client 实现从 `src/client/harbor_client.go` 并入 `src/infra/harbor/*`;对外不再保留空转发 `Client` 抽象,逻辑已直接内聚到 gateway。 +- [x] Helm -> HelmGateway + - 已新增 `src/infra/helm/gateway.go`,consumer pedestal 安装侧已改由 gateway 直接承担 repo/install 逻辑;Helm 实现已从 `src/client/helm.go` 并入 `src/infra/helm/*`,不再额外保留对外 `Client` 层。 +- [x] BuildKit -> BuildKitGateway + - 已新增 `src/infra/buildkit/gateway.go`,BuildKit 健康检查与构建 client 创建已开始从业务层抽离。 + +验收: + +- [x] repository 只负责 DB +- [x] service 依赖接口而不是全局 client + - Redis / Loki / Etcd / Harbor / Helm / K8s 这批调用点已不再依赖 `aegis/client` 包级入口;root `client` 目录现仅剩 debug 与 aegisctl 客户端侧代码。 +- [x] 外部系统资源按需纳入 Fx graph / lifecycle 管理 + - Redis / Etcd / tracing 已在 Fx lifecycle 中管理;Harbor / Helm / Loki 当前以无状态或按需 client 为主,不额外引入 shutdown 生命周期也不阻塞主线。 + +## 17. SDK / Portal / Admin 标记治理 + +当前口径: + +- 不再在代码里 hardcode audience allowlist。 +- audience 归属统一以 `src/docs/openapi3/openapi.json` 里的 `x-api-type` 扩展为准。 +- 只要某个 operation 带有对应 key 且值为 `"true"`,就会被提取进对应产物。 +- 同一个 operation 可以同时落入多个 audience。 +- Python SDK 只消费 `sdk.json`;TypeScript SDK 不再消费共享并集视图,而是分别按 `portal.json` / `admin.json` 生成独立 Portal SDK 与 Admin SDK。 +- SDK audience 改为显式白名单;默认不再把通用登录、Portal/Admin 控制面接口顺手放进 `sdk.json`。 +- SDK / CLI 认证主线改为 `AK/SK -> access token`;`username/password login` 仅保留给 Portal / Admin 等人类交互入口。 +- `AK/SK -> token` 进一步改为 header 签名模式:`X-Access-Key`、`X-Timestamp`、`X-Nonce`、`X-Signature`,业务接口仍继续走 Bearer token。 + +本轮执行清单: + +- [x] 收缩 `sdk` audience 到最小可维护白名单 + - 已继续把剩余误标的 `sdk` audience 收回,只保留 `POST /api/v2/auth/access-key/token` 与 `src/router/sdk.go` 下 4 个 SDK 样例接口;当前 `sdk.json` 已收缩到 `5 paths / 5 operations`。 +- [x] 从 Swagger audience 中移除 `POST /api/v2/auth/register` 的 `sdk` +- [x] 从 Swagger audience 中移除 `POST /api/v2/auth/login` 的 `sdk` +- [x] 盘点并设计 AK/SK 数据模型 + - 已新增 `database.UserAccessKey`,覆盖 `owner`、`enabled/disabled/deleted`、`expires_at`、`last_used_at`、`name/description`、`secret_hash`,并纳入 `AutoMigrate`。 +- [x] 增加 AK/SK 管理接口 + - 已补 `portal` 路由:`GET/POST /api/v2/access-keys`、`GET/DELETE /api/v2/access-keys/{access_key_id}`、`POST /api/v2/access-keys/{access_key_id}/rotate|disable|enable`。 +- [x] 增加 `AK/SK -> token` 接口并标为 `sdk` + - 已补 `POST /api/v2/auth/access-key/token`,返回 Bearer token,并在 JWT claims 中标记 `auth_type=access_key` 与 `access_key_id`;当前入口改为 `X-Access-Key` / `X-Timestamp` / `X-Nonce` / `X-Signature` 头签名校验,服务端会校验 5 分钟时间窗并用 Redis 做 nonce 防重放。 +- [x] 将 Python SDK 的鉴权入口切到 AK/SK + - `sdk/python/src/rcabench/client/http_client.py` 已改为优先使用 `token` 或 `access_key + secret_key`;SDK 不再依赖 username/password login,环境变量同步切到 `RCABENCH_ACCESS_KEY` / `RCABENCH_SECRET_KEY`,并在换 token 时自动按 `METHOD\\nPATH\\nACCESS_KEY\\nTIMESTAMP\\nNONCE` 规范计算 HMAC-SHA256 签名头。 +- [x] 将 `aegisctl` 的鉴权入口切到 AK/SK + - `src/cmd/aegisctl/cmd/auth.go` / `src/cmd/aegisctl/client/auth.go` 已切到 `--access-key` + `--secret-key` 签名换取 `POST /api/v2/auth/access-key/token`;签名规范与 Python SDK 保持一致,登录结果继续只落盘 Bearer token,不保存 `secret_key`。 +- [x] 给 `aegisctl` 增加本地签名排障命令 + - 已补 `aegisctl auth inspect` 与 `aegisctl auth sign-debug`;前者可检查当前 context 的 token / auth_type / access_key / expiry,后者可直接打印 canonical string、签名头与 curl 样例,并可通过 `--execute` 直接发起换 token 请求回显响应,或通过 `--save-context` 直接把成功返回的 Bearer token 落盘到当前 CLI context,便于排查 SDK / CLI / 服务端签名不一致问题。 +- [x] 补充 AK/SK 头签名规范文档 + - 已新增 `docs/access-key-signature-spec.md`,明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 access key 的使用说明、curl 示例与 `aegisctl` 排障命令说明;同时已回填 `src/handlers/v2/access_keys.go` / `src/dto/auth.go` 的 Swagger/OpenAPI 注释与 schema example,前端与文档站可直接消费。 +- [x] 补 Portal access key 前端文案与表单提示 + - `../AegisLab-frontend/src/pages/settings/Settings.tsx` 已新增 Access Keys 管理页签,覆盖创建 / 轮换 / 启停 / 删除与一次性 secret 提示;`../AegisLab-frontend/src/api/auth.ts` 也已补齐 access key API 封装,页面文案与 OpenAPI 说明保持一致。 +- [x] 重新生成 `openapi3` / `sdk.json` 并回填最新统计 + - 当前生成结果为:`openapi3/openapi.json` `138 paths / 173 operations`,`sdk.json` `5 / 5`,`portal.json` `31 / 43`,`admin.json` `48 / 58`;Python SDK 已按最新 `sdk.json` 重新生成,TypeScript 侧改为分别消费 `portal.json` 与 `admin.json`。 + +完成项: + +- [x] 统计 OpenAPI3 中的 `x-api-type` audience 标记 + - 当前已按 Go Swagger 注释补齐一批 `portal:"true"` / `admin:"true"` 标记;`converted/portal.json` 与 `converted/admin.json` 会分别作为独立 TypeScript SDK 的输入。 +- [x] Python SDK 只提取 `x-api-type.sdk == "true"` 的接口 +- [x] Portal 产物提取 `x-api-type.portal == "true"` 的接口 +- [x] Admin 产物提取 `x-api-type.admin == "true"` 的接口 +- [x] TypeScript Portal SDK 仅提取 `x-api-type.portal == "true"` 的接口 +- [x] TypeScript Admin SDK 仅提取 `x-api-type.admin == "true"` 的接口 +- [x] 更新 SDK 生成脚本 + - `scripts/command/src/swagger/init.py` 现会先完整重跑 `swag init`,再把 `openapi2/swagger.json` 本地转换成 `openapi3/openapi.json`,并继续产出 `client.json`、`sdk.json`、`portal.json`、`admin.json`;不再依赖 Docker 生成 OpenAPI3,也不再产生 root-owned 文档目录。 +- [x] 修回 `swagger init` 主链可完整再生 + - 通过恢复 `src/handlers/debug.go`、`src/handlers/system/*`、`src/handlers/v2/*` 这批仅用于 Swagger 注释扫描的 build-ignored 文档桩,`swag init` 已重新稳定产出全量接口;当前 `openapi2/swagger.json`、`openapi3/openapi.json`、`converted/client.json` 均为 `132 paths / 165 operations`。 +- [x] 校正 Python SDK 生成链 + - `scripts/command/src/formatter/python.py` 现优先使用本地 `scripts/command/.venv/bin/ruff`,缺失时也不会再因为 formatter 中断;`scripts/command/src/swagger/python.py` 的 Docker 生成步骤继续显式使用当前用户 UID:GID 运行,避免再次产出 root-owned 文件。 +- [x] 重新生成 TypeScript SDK + - 已执行 `cd scripts/command && ./.venv/bin/python main.py swagger generate-sdk -l typescript -v 1.2.1` +- [x] 检查 SDK diff + - TypeScript 不再输出共享 `typescript.json` / `sdk/typescript`;当前口径改为 `sdk/typescript/portal` 与 `sdk/typescript/admin` 两套独立产物,分别只消费 `portal.json` 与 `admin.json`,避免 Portal/Admin 共用同一份 TS SDK。 +- [x] 验证 audience 文档产物 + - 已执行 `cd scripts/command && ./.venv/bin/python main.py swagger init -v 1.2.1` 与 `cd src && go test ./docs` + +## 18. 删除旧 DI 骨架和旧兼容层 + +等 Fx producer / consumer / both 跑通后执行。 + +- [x] 删除 app 下旧 DI 生成文件 +- [x] 删除旧 DI 依赖 +- [x] 删除过渡 handler wrapper + - `src/handlers/v2` 现仅保留空的 `doc.go` 占位包以兼容既有测试命令,已不再承担任何运行态 wrapper 职责;`src/handlers/debug.go`、`src/handlers/system/*` 与 `src/handlers/v2/*` 旧兼容入口均已清空或删除。最近一轮又把 `src/app/producer_init.go`、`src/interface/{worker,controller,receiver}/module.go`、`src/interface/http/server.go` 中仅供 Fx 编排使用的注册 helper 全部缩成包内私有实现,启动链公开暴露面继续收口。 +- [x] 删除旧包级 service 函数 + - `module/user` CRUD / 资源授权、`module/systemmetric` 指标查询、`module/rbac` 已基本切离 `service/producer`;`handlers/system/monitor.go`、`configs.go`、`audit.go` 主路由入口也已并入 `module/system`。此前已删除旧 `service/producer` 中的 system / metrics / sdk / chaos-system / permission / audit / evaluation / notification / team / trace / group 兼容入口;middleware 也不再直接依赖旧 producer。`module/container` 与 `module/dataset` 现已进一步把 CRUD / detail / list / labels / version 元数据、container build / helm upload、dataset filename / download / version injection 路径下沉到模块 service/repository,并把直接碰 `config` / git / 文件系统的部分收成模块内 gateway/store。旧 `service/producer/container.go` / `dataset.go` 已删除;初始化已改走 `module/container` / `module/dataset` 暴露的 core helper。最近几轮里,`module/injection` 已先后接管 datapack download / files / file query / upload / build 提交流程,以及 injection list / project list / detail / labels / logs / submit fault injection / search / no-issues / with-issues / clone / batch delete 主路径;`src/service/producer/injection.go` 已整体删除。随后又继续按“模块语义留在模块 repo、纯转发尽量删除”的口径收缩:`module/injection` 把 search / list / labels / batch label 管理,以及 project injection list 的标签装配收进 `repository.go`,并继续把 `LoadInjection` / `FindInjectionByName` / `CreateInjectionRecord` / `LoadTask` / `LoadPedestalHelmConfig` / label/execution 删除辅助等一批原子转发写实到模块仓储;最近三轮又把 project resolve、detail with labels、existing injection map、label 条件聚合、project injection list、issue/no-issue 视图、label id by key、fault injection 批量 with labels 这批组合查询继续收成模块内实现。`module/user` 这一轮又把 `CreateUser + EnsureUserUnique`、`Get/Update` 这批基础 CRUD 空包装进一步折成 `CreateUserIfUnique`、`GetUserDetailBase`、`UpdateMutableUser`、`ListUserViews`,并把 `DeleteUserCascade`、global/container/dataset/project 的 assign/remove、permission batch create/delete 这批 relation 逻辑也直接写进模块 repo;随后又把 user detail 关系装配,以及 role/container/dataset/project 的加载 helper 继续改为模块内直接查库;最近又把 permission id 批量校验也直接内聚到模块仓储,并把纯存在性校验提升成公开 `EnsureUserExists(...)` 供 service 组合点复用。`module/rbac` 把 role 详情装配、权限批量校验、角色删除级联、resource/permission 关系查询收进模块 repo,并继续把 role / permission / resource 的基础 list/load/create 查询直接内聚到模块仓储;最近又把 role detail、role->user、permission->role、resource->permission 这批组合视图改成模块内直查;上一轮再把 role delete cascade、mutable update、permission id 批量加载也进一步改成模块仓储自管;这一轮继续把“可写 role”校验收口成模块内 `loadWritableRole(...)`,同时把通用 `LoadPermission` / `LoadResource` 改成更贴业务语义的 `GetPermissionDetail(...)` / `GetResourceDetail(...)`。`module/project` 现已把 create-with-owner、delete cascade、detail/list 视图装配、mutable update、label reload 与按 key 移除标签收进自身 repo,这几轮继续把 project owner role 查询、project statistics 聚合、label 批量装配 / project label id 查找 / usage decrease 一并写实;这一轮再把内部 helper 命名继续往语义侧收紧成 `loadProjectRecord(...)` / `listProjectStatistics(...)`。`module/team` 也把 create-with-creator、detail 聚合、visible list、team project list、member add/remove/update role、team visibility 读取等操作收进 repo,并把 team project statistics 聚合也留在模块内;这一轮又把 team 加载进一步收成 `loadTeam(...)`,用于 detail / mutable update / ensure exists / visibility 读取,同时把 project statistics helper 明确成 `listTeamProjectStatistics(...)`。`module/execution` 现已接管 project list / global list / detail / labels / batch delete / detector result / granularity result / submit execution 全链路,新增自身 `repository.go` 并删除旧 `src/service/producer/execution.go`。由于 project 主路径此前早已由 `module/project` 承接,本轮也同步删除了已空心化的 `src/service/producer/project.go`;同时 `service/producer/label.go` 也已删除,初始化阶段改走 `module/label.CreateLabelCore`。`service/producer/relation.go`、`user.go`、`role.go`、`resource.go`、`auth_helpers.go`、`permission_helpers.go`、`datapack_archive.go` 同样已清掉,producer 侧残余重点进一步收敛到更少的共享逻辑;当前 `src/service/producer` 已无 Go 源文件残留。与此同时,旧 `src/client/loki.go` / `jaeger.go` / `redis_client.go` / `etcd_client.go` / `harbor_client.go` / `helm.go` / `client/k8s/*` 及 Helm 对应测试也已从 root `client` 包清走,真实实现统一并入 `src/infra/*`;上一轮已把 `src/infra/k8s/client.go` 删除,rest/client/dynamic/controller 的单例初始化直接吸回 `src/infra/k8s/gateway.go`;这一轮继续把 `service/consumer` / `service/initialization` 中的 `CurrentK8sController()` fallback 干掉,改成由 Fx 注入 `*k8sinfra.Controller`,同时 `service/common` 的 etcd fallback 改为回落到 `infra/etcd.GetGateway()` 单点入口,并进一步删掉 `service/consumer/deps.go` / `service/common/deps.go` 这类旧全局依赖注册文件。`service/consumer` 中剩余的 K8s / BuildKit / Helm 访问也继续改为直接走 `infra/*` 单点入口:新增 `buildkitinfra.GetGateway()`、`helminfra.GetGateway()`,`CurrentK8sGateway()` / `currentBuildkitGateway()` / `currentHelmGateway()` 已全部清掉;这轮又把 `app/startup.go` 删除,并进一步引入 `app.RegisterProducerInitialization`,把 producer 初始化从 `context.Background()` 改成走 Fx `OnStart` 生命周期上下文。随后又继续把 `interface/controller` / `interface/receiver` / `interface/worker` 的生命周期上下文改成从 Fx `OnStart` 派生,不再在模块注册期直接构造 `context.Background()`;再往下一轮又把 `service/consumer/task.go` / `trace.go` / `jvm_runtime_mutator.go` / `k8s_handler.go` 里残余 `context.Background()` 全部清成 consumer 内部 detached context helper。初始化侧原先带 callback 的 `registerHandlers(...)` 旧 helper 也已改成更窄职责的 `activateConfigScope(...)`,consumer / producer 各自显式注册所需 handlers,再统一激活 listener scope;这一轮再把 `GetConfigUpdateListener(...)` 单例 helper 从启动链收掉,改为在 producer / worker Fx `OnStart` 生命周期里显式创建 `ConfigUpdateListener` 后传给 initialization。`service/consumer` 的 Redis 直连也开始往更窄语义收:新增内部 `currentRedisGateway` / `currentRedisClient` / `publishRedisStreamEvent` / `publishTraceStreamEvent` / `loadCachedInjectionAlgorithms` helper,先把 trace/group stream 发布、detector cache 读取,以及 `monitor` / `rate_limiter` 对 Redis gateway 的获取收进更窄入口;随后又把 monitor 的上下文来源收回 worker lifecycle,并把 namespace SMembers/HGet/HSet/Pipeline 这批读取/写入改为统一走 consumer 内部 Redis helper 取 client,同时 `rate_limiter` 也不再自持 Redis client,而是统一经由 consumer Redis helper 获取连接;最近一轮再把 namespace key / exists / field read / seed / lock write 继续折成 `monitor` 内部更窄 helper,减少 monitor 主流程里散落的 Redis 原语;上一轮则继续把 rate limiter Redis 操作下沉成独立 `tokenBucketStore`,把 token acquire/release 的 Redis 细节与 limiter 配置/调度逻辑分开;这一轮再正式把 monitor 按同一路径拆出独立 `namespaceStore`,把 namespace key/list/exists/read/write/watch/status 这批 Redis 操作从 monitor 主流程里抽走;紧接着又继续深拆成 `namespaceCatalogStore` / `namespaceLockStore` / `namespaceStatusStore` 三个更窄 store,把锁读取/抢占/释放、namespace 注册、status 读写彻底从 `monitor.go` 抽开,并删除已空心化的 `src/service/consumer/namespace_store.go`。这一轮再把 startup / interface 链路里对 monitor 的旧包级获取收一批:`consumer.NewMonitor(...)` 作为 Fx provider 现在直接吃 `*redisinfra.Gateway` 并在内部自取 client,monitor 构造期不再向启动链暴露裸 `*redis.Client`,`initialization.InitializeConsumer(...)`、`RegisterConsumerHandlers(...)`、`interface/controller` 的 K8s callback 构造均改为显式注入 monitor,而不再自己碰 `GetMonitor()`;紧接着又继续把运行时执行主流程里的 monitor 单例拿掉,新增 `consumer.RuntimeDeps` 由 worker lifecycle 显式传入,`dispatchTask(...)` / `executeTaskWithRetry(...)` / `executeFaultInjection(...)` / `executeRestartPedestal(...)` 已不再自己碰 `GetMonitor()`。这一轮继续顺着同一主线把 rate limiter 也从进程级单例收成纯 Fx provider:`NewRestartPedestalRateLimiter(...)` / `NewBuildContainerRateLimiter(...)` / `NewAlgoExecutionRateLimiter(...)` 现在直接吃 `*redisinfra.Gateway` 构造 limiter,不再经过 `Get*RateLimiter()` / `sync.Once`;`executeBuildContainer(...)`、`executeAlgorithm(...)`、`executeRestartPedestal(...)` 与 K8s job 回调里的 algorithm token release 也都改为走显式传入 limiter,不再直接碰旧包级 getter。与此同时,`service/common/config_registry.go` / `config_listener.go` 把配置元数据读取继续收成 `service/common/config_store.go` 本地语义 store,不再穿过公共 `repository` 包;随后又把 producer/worker/controller/receiver 的启动执行体再收成显式可替换的 `ProducerInitializer` / `LifecycleRunner` 依赖,避免 lifecycle 本身直接抱一大串底层依赖,主路径更贴近 Fx;在此基础上,`src/app/startup_validate_test.go` 与 `src/app/startup_smoke_test.go` 现在已经补上 producer / consumer / both 三种 app option 的 Fx 图校验与 start/stop smoke(通过替换重型初始化依赖,验证 HTTP/worker/controller/receiver/producer lifecycle 编排本身可启动可停止)。这一轮继续顺着同一条线,把 `service/common/config_registry.go` 里的 `sync.Once` / `globalHandlersOnce` 再压掉,改成常驻 registry + 幂等注册逻辑,并补上 `config_registry_test.go` 锁住“全局 handlers 多次注册不重复”行为,进一步减少 config startup 主路径上的一次性单例状态;紧接着又继续把 listener / publish 周边的剩余全局依赖再收一层:`ConfigUpdateListener` 现在显式携带 `*gorm.DB`,不再在读取配置元数据和处理变更时回落到 `database.DB`;`RegisterGlobalHandlers(...)` / `RegisterConsumerHandlers(...)` 也开始显式接收 `ConfigPublisher`,`PublishWrapper(...)` 改成走传入 publisher,而不再自己碰 `redisinfra.GetGateway()`。对应地 producer / consumer 初始化与 worker lifecycle 现已把 Redis gateway / DB 一路显式传进 config listener 与 handler 注册主链。顺手也暴露并修复了 producer 模式此前缺少 `k8sinfra.Module`、导致 `chaosinfra.Module` 无法解析 `*rest.Config` 的问题。当前 producer / consumer / both 三种 app options 都已能通过 `go test ./app` 的图校验和启动链 smoke。这一轮继续把 `service/common` 热路径往显式 DB 收:`DBMetadataStore` 改成由 initialization 注入 `*gorm.DB` 创建,`container` / `dataset` / `task` 公共能力补上 `WithDB` 变体,`module/execution` / `module/injection` / `module/container` 的提交与 ref 解析主路径已改用模块 repo 自带 DB,不再回落到 `database.DB`。这一轮又继续把 consumer 运行态主链的 DB 依赖显式化:`consumer.RuntimeDeps` 开始携带 DB,worker/controller 生命周期分别把 DB 显式注入 task runtime 与 K8s handler,build/restart/algo reschedule、fault injection 落库、collect result 查询、K8s job/CRD 回调里的 execution/injection 状态推进与后续 task submit 也都改成优先走注入 DB,而不再默认抓全局 `database.DB`。这一轮继续把状态同步链也收进显式 DB:`taskStateUpdate` 新增 DB 上下文,`updateTaskState(...)` / `updateTraceState(...)` / trace optimistic lock 更新现在优先沿调用链携带的 DB 执行;K8s error context 也开始透传 handler 注入 DB,因此 consumer 主链里剩余 `database.DB` 基本只落在少量兼容 fallback 和 `service/common` 默认 wrapper。这一轮顺手再把 `module/evaluation` -> `service/analyzer` 这条链也切到显式 DB:evaluation service 改用 repo 持有 DB 调 analyzer 的 `WithDB` 版本,container/dataset ref 解析与 evaluation 持久化不再依赖 analyzer 内部全局 DB;同时 `service/common/ExtractDatapacks(...)` 解析 dataset 时也已改走传入 DB 的 `MapRefsToDatasetVersionsWithDB(...)`。再往下一步,consumer 里 `collect_result` / `fault_injection` / `createExecution` 这类原先“nil 就回落全局 DB”的点也开始直接要求 runtime DB 存在,进一步缩小 fallback 面积。这一轮再继续把兼容层直接砍掉:`service/common` 里默认版 `MapRefsToContainerVersions` / `MapRefsToDatasetVersions` / `ListContainerVersionEnvVars` / `ListHelmConfigValues` / `SubmitTask` / `ProduceFaultInjectionTasks` 已删除,`service/analyzer` 里的默认版 evaluation 入口也删掉,只保留显式 `WithDB` 路径;同时 `consumer/task.go` / `trace.go` / `k8s_handler.go` 里的 DB fallback 也改成显式报错,不再默默回落全局 `database.DB`。紧接着又把 `module/system` 里最后一处直接碰 `database.DB` 的 health check 改成走 `repo.DB()`;目前 `module/*`、`service/common`、`service/consumer`、`service/analyzer` 这批主线包内已无 `database.DB` 残留。顺手又把 repository 层里残留的统计/搜索/资源/注入查询改成统一吃显式 `db` 参数,`repository/task.go` 的 `ListTasksByTimeRange(...)` 也不再偷偷回落全局 DB;现在全仓库只剩 `src/infra/db/module.go` 这一处集中持有 `database.DB`,作为 Fx 提供与关闭数据库连接的基础设施边界。最近两轮又继续把 consumer 外部依赖收窄到 Fx 注入:`interface/worker` 把 `*k8sinfra.Gateway` / `*buildkitinfra.Gateway` / `*helminfra.Gateway` / `*consumer.FaultBatchManager` / `*redisinfra.Gateway` 显式塞进 `consumer.RuntimeDeps`,`build container` / `build datapack` / `algo execution` / `restart pedestal` / `collect result` / task retry / trace state update / K8s callback 已不再直接碰 `GetGateway()` 与 fault batch `sync.Once` 单例;`interface/controller` 同步把 K8s gateway、Redis gateway 和 batch manager 显式交给 `consumer.NewHandler(...)`;`service/logreceiver` 也开始由 `interface/receiver` 注入 Redis publisher,OTLP receiver 不再自己抓 `redisinfra.GetGateway()`。这一轮又继续把 HTTP 链路里的 middleware 全局态收掉:`src/middleware/deps.go` 现在提供 `middleware.Service` 与 `InjectService(...)`,`src/router/router.go` 在根路由中显式注入 middleware service,`src/middleware/permission.go` / `audit.go` 改为按请求从 Gin context 读取 checker/logger,不再持有 `currentPermissionChecker` / `currentAuditLogger` 这类包级默认服务;`src/interface/http/module.go` 也不再用 `fx.Invoke(middleware.RegisterDeps)` 做全局注册。最近这一轮再把 startup 初始化链里的隐藏 fatal 收掉:`newConfigDataWithDB(...)`、`activateConfigScope(...)`、`InitializeProducer(...)`、`InitializeConsumer(...)` 全部改成显式返回 `error`,producer/worker 的 Fx `OnStart` 现在会把初始化失败直接上抛,而不再在 helper 内部 `logrus.Fatalf(...)` 提前退出进程。紧接着这一轮又继续把 consumer startup 链里的 Redis 裸 client 收口到 gateway:worker 初始化改为走 `RedisGateway.InitConcurrencyLock(...)`,`monitor` / `rate limiter` provider 也改成只依赖 `*redisinfra.Gateway`。再下一轮又把模块侧剩余 Redis 全局入口清掉:`module/group` / `module/notification` / `module/trace` / `module/injection` / `module/systemmetric` / `module/system` 现在都改为通过构造注入 `*redisinfra.Gateway`,trace/group/notification stream 读取、injection algorithm cache、system config response subscribe、system metric Redis 查询不再直接碰 `redisinfra.GetGateway()`。这一轮继续把任务队列 helper 也收回 gateway:`infra/redis/task_queue.go` 里的 submit/get/reschedule/dead-letter/queue index/concurrency lock/list/remove 操作全部改成 `Gateway` 方法,`service/common.SubmitTaskWithDB(...)`、`service/consumer` 调度与取消链路、`module/systemmetric` 排队任务查询都已改走显式 Redis gateway。紧接着又把 `infra/redis` / `infra/etcd` / `infra/buildkit` / `infra/helm` / `infra/k8s` 里已经没有调用方的 `GetGateway()` 单例 fallback 全部删除,主线现在只剩少量 lifecycle/startup 组织层 wrapper 需要再压。当前 `src/service/consumer` / `src/middleware` 里残余重点已从“全局 gateway fallback / 全局 default service”收缩到更少的流程组织 helper 与 initialization 邻近收尾。 +- [x] 删除旧包级 repository wrapper + - 已移除 `repository/task.go` 中 Redis 队列职责与 `repository/token.go` 黑名单兼容层。 +- [x] 删除全局 default service +- [x] 清理未使用 imports + +验收: + +- [x] 检查源码与文档中旧 DI 文案残留 +- [x] `cd src && go test ./...` + - 已补齐 `src/cmd/aegisctl/output/output.go`,修复 `cmd/aegisctl` 缺失输出包导致的全量测试阻塞;同时把 `infra/k8s` 的集成 Job 用例改为 `RUN_K8S_INTEGRATION=1` 显式开启,避免默认 `go test ./...` 卡在真实集群状态。 + +## 19. 最终验收 + +功能验收: + +- [x] producer 模式可启动 +- [x] consumer 模式可启动 +- [x] both 模式可启动 +- [x] login / register / refresh 正常 +- [x] Project CRUD 正常 +- [x] Injection 提交流程正常 +- [x] Execution 提交流程正常 +- [x] Task 状态和日志正常 +- [x] Admin 用户管理正常 +- [x] SDK 生成正常 +- [x] Swagger 文档正常 + +架构验收: + +- [x] `main.go` 只负责 mode 和 Fx 启动 +- [x] DB / Redis / HTTP / worker / receiver / controller 都有 lifecycle +- [x] handler 不直接 import `database` +- [x] handler 不直接 import repository implementation +- [x] service 不直接使用全局 `database.DB` +- [x] repository 不直接访问 Redis / K8s / Loki / Etcd +- [x] middleware 不直接依赖具体 producer package +- [x] Public / SDK / Portal / Admin / System 路由分离 +- [x] 业务模块通过 `Module` 暴露 + +说明: + +- `src/app/startup_validate_test.go` 已覆盖 producer / consumer / both 三种 Fx 图校验。 +- `src/app/startup_smoke_test.go` 已覆盖 producer / consumer / both 三种 start/stop smoke,并继续补上 consumer lifecycle 集成冒烟、both 模式的 HTTP + lifecycle 联合冒烟。 +- `src/router/router_test.go` 已锁定 `Public / SDK / Portal / Admin / System` 关键路由前缀分离。 +- `src/app/http_modules.go` 统一通过各业务模块的 `Module` 暴露 HTTP 能力并聚合进 producer app。 +- 启动链补扫后,`src/app` / `src/service/initialization` / `src/interface` / `src/middleware` 生产代码里已无旧 `service/producer` / `handlers/system` / `client/*` 引用,也无残余 `context.Background()` / `GetGateway()` 启动期直拿全局对象。 +- 本轮顺手补齐 `src/cmd/aegisctl/output/output.go`,把 CLI 的 JSON / table / info / error 输出能力收回本地包,`cmd/aegisctl` 不再因缺失输出层而阻塞仓库全量构建。 +- 本轮继续把启动链残余接口壳压掉:`src/app/producer_init.go` 与 `src/interface/{worker,controller,receiver}/module.go` 已从 `ProducerInitializer` / `LifecycleRunner` 接口切到可直接替换的具体 lifecycle struct,smoke test 也同步改成按具体类型替换,启动编排层又薄了一轮。 +- 本轮再顺手清了一批模块仓储纯转发:`src/module/system/repository.go` 的 audit/config/history 查询与写入已直接写实到模块仓储;`src/module/injection/repository.go` 的 groundtruth 更新也不再空转调公共 `repository`;同时 `RegisterProducerInitialization(...)` 已不再额外挂 `CommonResources` 形参。 +- 本轮继续把 `src/module/execution/repository.go` 写实:project resolve / execution list/detail/result / execution labels / result save / batch delete / duration update / labels attach 这一整段已直接落回模块仓储,不再散着空转调 `repository/execution.go`、`repository/detector.go`、`repository/granularity.go`、`repository/label.go`。 +- 本轮再把 `src/module/container/repository.go` 与 `src/module/dataset/repository.go` 两块成片稳定 CRUD 仓储写实:role resolve、container/dataset CRUD、version CRUD、label relation、helm/env/parameter config、dataset version injection 关系等都已回收到模块仓储;目前这两块只保留 dataset search 对共享 query builder 的调用。 +- 本轮继续把剩余一批小模块仓储空转发彻底写回模块:`src/module/{sdk,chaossystem,trace,group,evaluation,task,auth,label}/repository.go` 里的 list/detail/create/update/delete / relation-count / metadata / user-role 查询等都已直接落回模块仓储;`src/module/label/core.go` 也不再直连公共 `repository/label.go`。当前模块侧保留的共享 `repository.ExecuteSearch(...)` 只剩 injection / dataset 两处,作为通用 query builder 基础设施继续复用,不再是无意义兼容层。 +- 本轮继续把 search 这条尾巴也收掉:`src/module/dataset/repository.go` 与 `src/module/injection/repository.go` 已不再调用公共 `repository.ExecuteSearch(...)`,而是直接使用 `repository/query_builder.go` 里的通用 builder 组装查询;公共 `ExecuteSearch` 兼容入口已删除,模块侧只保留对底层 query builder 基础设施的显式使用。 +- 本轮继续顺手压掉一批 raw client / helper 暴露面:`src/module/auth/token_store.go` 与 `src/module/task/queue_store.go` 已改成依赖 `infra/redis.Gateway`,`src/infra/redis/gateway.go` 补齐 `Set/Subscribe` 语义方法,`src/app/common.go` 的 Fx 公共资源探针也改成依赖 `Redis/Etcd Gateway` 而不是裸 client;同时 `src/module/trace/service.go` / `src/module/trace/stream.go` 把 trace stream processor/read 的包级 helper 收回 service,`src/module/group/service.go` 的 group stream processor 初始化也去掉了无意义的 context 包装。 +- 本轮再把剩余 Fx / consumer 暴露面继续压一轮:`src/infra/{redis,etcd}/module.go` 的 `ProvideClient` 已删除,Fx graph 不再向外暴露裸 Redis/Etcd client;`src/service/consumer/{namespace_catalog_store,namespace_lock_store,namespace_status_store,rate_limiter_store}.go` 也都改成直接持有 `infra/redis.Gateway`,`src/service/consumer/{monitor,rate_limiter}.go` 不再在上层显式拿 `gateway.Client()`;另外 `src/app/common.go` 这个仅用于依赖探测的空文件已删除,`src/app/app.go` 不再保留无意义的 `RequireCommonResources` invoke。 +- 本轮继续把模块内部 API 面收紧一层:`src/module/{project,team,dataset,rbac,auth,user,execution,injection,container,system}/repository.go` 的 `WithDB` 已统一缩成包内 `withDB`;`src/module/{execution,injection}/repository.go` 的 `EnsureProjectExists`、`src/module/user/repository.go` 的 `EnsureUserExists` 也已缩成包内 helper;`src/module/{execution,injection,container,system,evaluation}` 里原先为 service 暴露的 `DB()` 访问器已删除,service 直接在包内使用 repository 持有的 db。 +- 本轮顺手再收一批 `context.Background()` 残点:`src/module/task/log_service.go`、`src/module/injection/handler.go`、`src/module/injection/service.go`、`src/module/system/service.go` 已改成沿调用链传递 request/service context;`src/module/systemmetric/collector.go` 也改成 lifecycle 管理的 collector context,在 `OnStop` 时显式 cancel。 +- 本轮继续压掉最后一批显眼的 helper 暴露:`src/module/systemmetric/service.go` 已改成直接使用 `infra/redis.Gateway` 暴露的 `SetMembers / HashGetAll / ZRangeByScore / ZAdd / ZRemRangeByScore` 语义方法,`src/module/system/service.go` 的 Redis 健康检查也切到 `gateway.Ping`;同时 `src/infra/redis/gateway.go` 又补齐这一批语义 API,模块/系统层不再直接拼裸 Redis 命令。当前生产代码里已无 `context.Background()` 残点,剩余 `redisGateway.Client()` 仅收敛在 `service/consumer/*store.go` 这一层 Redis 原语适配代码中。 +- 本轮继续把 consumer 最后一层 Redis 原语适配再往 infra 收:`src/service/consumer/{namespace_catalog_store,namespace_status_store,namespace_lock_store,rate_limiter_store}.go` 里残余的 `gateway.Client()` 已全部清掉,分别改成走 `infra/redis/gateway.go` 新增的 `Exists / HashGet / HashSet / SeedNamespaceState / SetRemove / RunScript / Watch` 等语义方法;当前生产代码中 `service/consumer` / `module` / `app` / `interface` 已无直接 `gateway.Client()` 调用,裸 Redis client 已彻底退回 `infra/redis` 内部实现。 +- 本轮继续把 infra 边界再收紧一层:`src/infra/{redis,etcd}/gateway.go` 的公开 `Client()` 暴露面已删除,连接初始化/关闭统一收进私有 `clientOrInit()/close()`;`src/infra/k8s/{job,controller}.go` 中原先仅供 gateway 转调的 `CreateJob / GetJobPodLogs / GetVolumeMountConfigMap / NewController` 也已缩成包内私有实现,`Gateway` 成为对外唯一主入口。 +- 本轮继续清 initialization 残余全局态:`src/service/initialization/{producer,consumer}.go` 不再持有包级 `producerData / consumerData / resourceIDMap`,初始化配置状态改成局部装配后沿调用链使用;`InitializeSystems(...)` 也已改为显式返回 `error`,producer 启动链不再吞掉系统注册失败。 +- 本轮继续压一轮启动壳与模块内部 helper:`src/app/producer_init.go`、`src/interface/{worker,controller,receiver}/module.go`、`src/interface/http/{module,server}.go` 中的 lifecycle/register helper 已全部收成包内私有;`src/module/project/repository.go` 删掉 `loadProjectLabelView(...)`,`src/module/rbac/repository.go` 删掉 `loadWritableRole(...)` 并把 system-role 校验内聚回具体语义方法,`src/module/user/repository.go` 则把 role/container/dataset/project 四组原子 load helper 收成单个 `ensureActiveRecordExists(...)`,`src/module/team/repository.go` 把 role 校验压成 `ensureRoleExists(...)`,`src/module/injection/repository.go` 的 `ensureProjectExists(...)` 也已删掉并改为 service 包内直接使用 repo DB 校验项目存在性。 +- 本轮最后再把 execution / user 邻近模块尾巴收掉:`src/module/execution/repository.go` 的 `ensureProjectExists(...)` 已删除,project 存在性校验直接回到 `service.go` 包内用 repo DB 执行;`src/module/user/repository.go` 的 `ensureUserExists(...)` 也已删掉,统一并入已有 `ensureActiveRecordExists(...)`,避免“同一语义多套 helper”继续扩散。 +- 最终抛光轮再顺手做了一轮“模块 repo API 面收紧”:`src/module/execution/repository.go` 的 `GetProjectByName / List*View / GetExecution* / ListAvailableExecutionLabels / ListExecutionLabelIDsByKeys`,以及 `src/module/project/repository.go` 的 CRUD/label 管理主方法、`src/module/team/repository.go` 的 team CRUD / list / membership 读取主方法,均已统一缩成包内私有实现,只保留 service 真正需要的模块边界;模块内部语义仍在,但对外可见面进一步变薄。 +- 最终抛光轮又继续做了两件事:一是把 `src/module/user/repository.go` 与 `src/module/rbac/repository.go` 里仅供 service 使用的 repo 主方法再统一缩成包内私有,模块命名/API 面进一步一致;二是在 `src/app/startup_smoke_test.go` 补上 producer HTTP 集成冒烟,真实启动 Fx producer app 后校验 `/docs/doc.json` 可访问、`/system/configs/abc` 会经过真实路由与鉴权链返回 `401`,把“能启动”进一步提升到“能接住实际 HTTP 主路径”。 +- 本轮已补 `src/module/auth/service_test.go`,覆盖 `register / login / refresh` 成功路径,并顺手修正 `module/auth` / `module/user` 创建用户时密码重复 hash 的问题,避免注册后登录链路天然失效。 +- 本轮已扩充 `src/module/project/service_test.go`,覆盖 `create / get detail / list / update / delete` 主路径;`Project CRUD` 现已具备模块级成功路径保护,剩余是更贴近真实依赖的运行态验收。 +- 本轮已扩充 `src/module/execution/service_test.go`,覆盖标签列表、列表过滤、detector / granularity 结果上传成功路径;`execution result` 主路径已有模块级保护。 +- 本轮新增 `src/module/task/service_test.go`,覆盖 task 列表成功路径与 Loki 历史日志读取;`Task 状态 / 日志` 这条线已具备模块级成功路径保护。 +- 本轮继续扩充 `src/module/execution/service_test.go` 与 `src/module/injection/service_test.go`,分别补上 `SubmitAlgorithmExecution` 和 `SubmitDatapackBuilding` 成功路径;`Injection / Execution 提交` 主路径已具备模块级提交保护。 +- 为避免真实 Redis 依赖阻塞主线验收,本轮新增 `src/testutil/redisstub.go` 作为极小测试桩,仅覆盖任务提交用到的 Redis 命令,供模块级 submit 测试使用。 +- 本轮已补 `src/module/user/service_test.go` 的 create / detail / delete 成功路径,`Admin 用户管理` 主路径现已具备模块级成功路径保护。 +- 本轮新增 `src/module/sdk/service_test.go`,覆盖 SDK evaluation / experiment / dataset sample 主路径;`SDK` 主路径已具备模块级成功路径保护。 +- 本轮新增 `src/docs/docs_test.go`,校验 `openapi2` / `openapi3` / `converted/sdk.json` 三类文档产物存在且包含核心接口路径;同时 `src/router/router.go` 已显式注册 `aegis/docs/openapi2`,`src/router/router_test.go` 继续锁定 `/docs/doc.json` 可直接返回 Swagger 文档。 +- 本轮已完成 `cd src && go test ./...`;当前默认全量测试口径已打通,K8s Job 的真实集群冒烟改为按需用 `RUN_K8S_INTEGRATION=1 go test ./infra/k8s` 单独执行。 +- 本轮继续把 `src/infra/k8s/k8s_test.go` 升级成更明确的真实集群验收入口:先做 `Gateway.CheckHealth(...)` 预检,再跑 job create/get/wait/logs/delete 全链路;同时支持 `RUN_K8S_INTEGRATION_NAMESPACE`、`RUN_K8S_INTEGRATION_IMAGE`、`RUN_K8S_INTEGRATION_KEEP_JOB` 三个可选环境变量,便于回填真实环境验收。 +- 本轮继续把 `src/infra/k8s/gateway.go` 收成 K8s job 生命周期主入口,补上 `GetJob / WaitForJobCompletion / DeleteJob` 这组 gateway 语义方法;`WaitForJobCompletion(...)` 也改成尊重 context 取消,并在 Job 失败条件出现时尽早返回。 +- 本轮继续扩充 `src/app/startup_smoke_test.go`:新增 `TestConsumerOptionsLifecycleIntegrationSmoke`,锁定 worker/controller/receiver 的真实 Fx 启停;新增 `TestBothOptionsHTTPAndLifecycleIntegrationSmoke`,在 both 模式下同时校验 producer 初始化、consumer 生命周期和 `/docs/doc.json` / `/system/configs/:id` 这组真实 HTTP 主路径。 + +当前说明: + +- 第 19 节功能验收已按“模块级成功路径 + 路由/文档产物校验 + app 启动 smoke”口径全部补齐。 +- 目前若继续做,已基本进入纯抛光阶段:更激进的命名统一、个别 repo/helper 再折叠、以及更贴近真实外部依赖的集成验收,都不再阻塞 Fx 主线收口。 +- 本轮已再次重跑三组主路径验证命令:`go test ./module/auth ./module/project ./module/execution ./module/injection ./module/task ./module/user ./module/sdk ./router ./docs ./app`、`go test ./app ./service/consumer ./service/logreceiver ./interface/controller ./interface/receiver ./interface/worker`、`go test ./app ./router ./interface/http ./middleware`,当前均通过。 +- 本轮再补跑 `go test ./...`,当前也已通过。 + +## 20. 仓库级收尾检查清单 + +- [x] 默认回归:`cd src && go test ./...` +- [x] Producer Fx 图校验与 HTTP 主路径:`cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke'` +- [x] Consumer / Both 生命周期集成冒烟:`cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions'` +- [x] 路由 / 文档主路径:`cd src && go test ./router ./docs ./interface/http` +- [x] 真实 K8s 集群验收:`cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` +- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_NAMESPACE=` +- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_IMAGE=` +- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_KEEP_JOB=1` +- [x] producer / consumer / both 主启动链已无旧 `service/producer` / `handlers/system` / `client/*` 运行态依赖 +- [x] K8s / Redis / Etcd / Harbor / Helm / BuildKit 等 infra 主入口已统一收口到 `src/infra/*` +- [x] 仓库级残余兼容面补扫通过 + - 已用 `rg` 对 `service/producer`、`handlers/system`、`GetGateway()`、`CurrentK8s*`、`database.DB`、启动链 `context.Background()` 等模式做补扫;`src/app` / `src/interface` / `src/service` / `src/module` / `src/router` / `src/middleware` 生产代码内未发现这批旧运行态依赖残留。 + +## 当前建议下一步 + +从这里开始: + +1. 如需复验真实集群,执行 `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` +2. 常规回归继续跑 `cd src && go test ./...` +3. 如需继续推进,优先进入第 17 节 SDK 标记治理;其余已基本属于文档/命名/测试抛光 diff --git a/justfile b/justfile index 0c5073e0..48e4252e 100644 --- a/justfile +++ b/justfile @@ -255,6 +255,11 @@ generate-python-sdk version: just swag-init {{version}} just run-command swagger generate-sdk -l python -v {{version}} +# ⚙️ Generate TypeScript SDK from Swagger documentation +generate-typescript-sdk version: + just swag-init {{version}} + just run-command swagger generate-sdk -l typescript -v {{version}} + # ============================================================================= # Utilities # ============================================================================= @@ -289,4 +294,4 @@ release version: git push -u origin main git tag -a "v{{version}}" -m "Release version {{version}}" git push origin "v{{version}}" - printf "{{green}}✅ Version {{version}} released successfully{{reset}}\n" \ No newline at end of file + printf "{{green}}✅ Version {{version}} released successfully{{reset}}\n" diff --git a/project-index.yaml b/project-index.yaml index 70e82437..fd372dda 100644 --- a/project-index.yaml +++ b/project-index.yaml @@ -1673,17 +1673,18 @@ requirements: title: Swagger/OpenAPI Documentation description: > API documentation via Swagger annotations on all handler functions. - APIs marked with @x-api-type {"sdk":"true"} are included in generated SDKs. + Generated audience specs are extracted from OpenAPI3 x-api-type extensions + (sdk / portal / admin) and then fed into the language-specific generators. Generated via swag init with dependency parsing. Backend-only tooling. priority: P1 status: implemented confidence: confirmed - source: "src/handlers/v2/*.go" + source: "src/docs/openapi3/openapi.json" code: - - path: src/handlers/v2/auth.go - description: "Example of Swagger annotations with @x-api-type SDK marking" + - path: src/handlers/docs.go + description: "Swagger annotation example carrying x-api-type audience metadata" frontend: [] has_mock: false @@ -1700,7 +1701,7 @@ requirements: depends_on: [] conflicts: [] - notes: "scripts/command/src/swagger.py filters APIs by x-api-type.sdk field" + notes: "scripts/command/src/swagger/init.py extracts sdk / portal / admin audience specs from OpenAPI3 x-api-type metadata" # =========================================================================== # REQ-7xx: Deployment & Infrastructure (Backend) diff --git a/scripts/command/src/formatter/python.py b/scripts/command/src/formatter/python.py index e5923750..1cdff6a3 100644 --- a/scripts/command/src/formatter/python.py +++ b/scripts/command/src/formatter/python.py @@ -1,5 +1,6 @@ import os import re +import shutil from collections import Counter from rich.table import Table @@ -39,9 +40,26 @@ def __init__(self, scope: ScopeType = ScopeType.STAGED, sdk_dir: str | None = No super().__init__(scope) self.sdk_dir = sdk_dir or settings.python_sdk_dir self.has_errors = False + self.ruff_binary = self._resolve_ruff_binary() self.extra_args = ["--config", os.path.join(self.sdk_dir, "pyproject.toml")] self.files_to_format = self._get_files() + def _resolve_ruff_binary(self) -> str | None: + """Resolve ruff from PATH first, then from the local command venv.""" + binary = shutil.which("ruff") + if binary: + return binary + + candidates = [ + PROJECT_ROOT / "scripts" / "command" / ".venv" / "bin" / "ruff", + PROJECT_ROOT / ".venv" / "bin" / "ruff", + ] + for candidate in candidates: + if candidate.is_file(): + return candidate.as_posix() + + return None + def _get_files(self) -> list[str]: """ Get files to format based on the configured scope. @@ -140,7 +158,7 @@ def _categorize_files(self) -> dict[str, list[str]]: def _run_ruff_check(self, category: str, files: list[str]) -> bool: """Run ruff check --fix on files.""" - cmd = ["ruff", "check", "--fix", "--unsafe-fixes"] + cmd = [self.ruff_binary or "ruff", "check", "--fix", "--unsafe-fixes"] cmd.extend(files) if category == ScopeType.SDK.value: cmd.extend(self.extra_args) @@ -164,7 +182,7 @@ def _run_ruff_check(self, category: str, files: list[str]) -> bool: def _check_remaining_errors(self, category: str, files: list[str]) -> str | None: """Check for remaining errors after fix.""" - cmd = ["ruff", "check"] + cmd = [self.ruff_binary or "ruff", "check"] cmd.extend(files) if category == ScopeType.SDK.value: cmd.extend(self.extra_args) @@ -238,7 +256,7 @@ def _display_error_statistics(self, output: str) -> None: def _run_ruff_format(self, category: str, files: list[str]) -> bool: """Run ruff format on files.""" - cmd = ["ruff", "format"] + files + cmd = [self.ruff_binary or "ruff", "format"] + files cmd.extend(files) if category == ScopeType.SDK.value: cmd.extend(self.extra_args) @@ -267,6 +285,11 @@ def run(self) -> int: if not self.files_to_format: console.print("[bold yellow]No Python files to format.[/bold yellow]") return 0 + if self.ruff_binary is None: + console.print( + "[bold yellow]⚠️ Ruff not found; skipping Python formatting.[/bold yellow]" + ) + return 0 console.print("[bold blue]🎨 Formatting Python files with ruff...[/bold blue]") diff --git a/scripts/command/src/swagger/__init__.py b/scripts/command/src/swagger/__init__.py index 0cd81fc6..07af1b19 100644 --- a/scripts/command/src/swagger/__init__.py +++ b/scripts/command/src/swagger/__init__.py @@ -2,10 +2,11 @@ from src.swagger.common import Generator from src.swagger.init import init from src.swagger.python import PythonSDK -from src.swagger.typescript import TypeScriptClient +from src.swagger.typescript import TypeScriptClient, TypeScriptSDK __all__ = ["init", "Generator"] Generator.register_client(LanguageType.TYPESCRIPT, TypeScriptClient) Generator.register_sdk(LanguageType.PYTHON, PythonSDK) +Generator.register_sdk(LanguageType.TYPESCRIPT, TypeScriptSDK) diff --git a/scripts/command/src/swagger/common.py b/scripts/command/src/swagger/common.py index ebd1cfb9..f1961737 100644 --- a/scripts/command/src/swagger/common.py +++ b/scripts/command/src/swagger/common.py @@ -12,6 +12,8 @@ class RunMode(str, Enum): CLIENT = "client" SDK = "sdk" + PORTAL = "portal" + ADMIN = "admin" class Generator(ABC): diff --git a/scripts/command/src/swagger/init.py b/scripts/command/src/swagger/init.py index 735a6d73..164cc218 100644 --- a/scripts/command/src/swagger/init.py +++ b/scripts/command/src/swagger/init.py @@ -5,10 +5,8 @@ from pathlib import Path from typing import Any -from python_on_whales import docker - from src.common.command import run_command -from src.common.common import PROJECT_ROOT, console, settings +from src.common.common import console from src.swagger.common import SWAGGER_ROOT, RunMode from src.util import get_longest_common_substring @@ -19,6 +17,317 @@ __all__ = ["init"] +def audience_flag_enabled(x_api_type: Any, audience: str) -> bool: + """Return whether an x-api-type audience flag is enabled.""" + if not isinstance(x_api_type, dict): + return False + + value = x_api_type.get(audience) + if isinstance(value, bool): + return value + if isinstance(value, str): + return value.strip().lower() == "true" + return False + + +def normalize_openapi_ref(ref: str) -> str: + """Convert Swagger 2 refs to OpenAPI 3 component refs.""" + return ( + ref.replace("#/definitions/", "#/components/schemas/") + .replace("#/parameters/", "#/components/parameters/") + .replace("#/responses/", "#/components/responses/") + ) + + +def convert_schema_object(schema: Any) -> Any: + """Recursively convert a Swagger 2 schema object into OpenAPI 3 format.""" + if isinstance(schema, dict): + if schema.get("type") == "file": + converted_file_schema = dict(schema) + converted_file_schema["type"] = "string" + converted_file_schema["format"] = "binary" + return converted_file_schema + + converted: dict[str, Any] = {} + for key, value in schema.items(): + if key == "$ref" and isinstance(value, str): + converted[key] = normalize_openapi_ref(value) + continue + + if key in { + "schema", + "items", + "additionalProperties", + "not", + "propertyNames", + "contains", + }: + converted[key] = convert_schema_object(value) + continue + + if key in {"allOf", "anyOf", "oneOf"} and isinstance(value, list): + converted[key] = [convert_schema_object(item) for item in value] + continue + + if key == "properties" and isinstance(value, dict): + converted[key] = { + name: convert_schema_object(prop) for name, prop in value.items() + } + continue + + converted[key] = convert_schema_object(value) + + return converted + + if isinstance(schema, list): + return [convert_schema_object(item) for item in schema] + + return schema + + +def convert_swagger_parameter(parameter: dict[str, Any]) -> dict[str, Any]: + """Convert a non-body Swagger 2 parameter to OpenAPI 3.""" + converted = copy.deepcopy(parameter) + schema: dict[str, Any] = {} + + for field in ( + "type", + "format", + "items", + "enum", + "default", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "minLength", + "maxLength", + "pattern", + "multipleOf", + "minItems", + "maxItems", + "uniqueItems", + ): + if field in converted: + schema[field] = convert_schema_object(converted.pop(field)) + + if "collectionFormat" in converted: + collection_format = converted.pop("collectionFormat") + if collection_format == "multi": + converted["style"] = "form" + converted["explode"] = True + elif collection_format == "csv": + converted["style"] = "form" + converted["explode"] = False + + if schema: + converted["schema"] = schema + + return converted + + +def make_request_body_content( + schema: dict[str, Any], media_types: list[str] +) -> dict[str, Any]: + """Build an OpenAPI 3 requestBody content map.""" + return {media_type: {"schema": schema} for media_type in media_types} + + +def convert_swagger_operation( + operation: dict[str, Any], + global_consumes: list[str], + global_produces: list[str], +) -> dict[str, Any]: + """Convert a Swagger 2 operation to OpenAPI 3.""" + converted = copy.deepcopy(operation) + consumes = ( + converted.pop("consumes", None) or global_consumes or ["application/json"] + ) + produces = ( + converted.pop("produces", None) or global_produces or ["application/json"] + ) + + parameters = converted.pop("parameters", []) + request_body: dict[str, Any] | None = None + form_properties: dict[str, Any] = {} + form_required: list[str] = [] + converted_parameters: list[dict[str, Any]] = [] + + for parameter in parameters: + if not isinstance(parameter, dict): + continue + + if "$ref" in parameter: + parameter_ref = dict(parameter) + parameter_ref["$ref"] = normalize_openapi_ref(parameter_ref["$ref"]) + converted_parameters.append(parameter_ref) + continue + + location = parameter.get("in") + if location == "body": + request_schema = convert_schema_object(parameter.get("schema", {})) + request_body = { + "required": parameter.get("required", False), + "content": make_request_body_content(request_schema, consumes), + } + if parameter.get("description"): + request_body["description"] = parameter["description"] + continue + + if location == "formData": + property_schema: dict[str, Any] = {} + parameter_type = parameter.get("type") + if parameter_type == "file": + property_schema = {"type": "string", "format": "binary"} + else: + property_schema = { + "type": parameter_type, + } + if "format" in parameter: + property_schema["format"] = parameter["format"] + if "enum" in parameter: + property_schema["enum"] = parameter["enum"] + if "items" in parameter: + property_schema["items"] = convert_schema_object(parameter["items"]) + if "default" in parameter: + property_schema["default"] = parameter["default"] + + if parameter.get("description"): + property_schema["description"] = parameter["description"] + + form_properties[parameter["name"]] = property_schema + if parameter.get("required"): + form_required.append(parameter["name"]) + continue + + converted_parameters.append(convert_swagger_parameter(parameter)) + + if form_properties: + request_body = { + "required": bool(form_required), + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": form_properties, + } + } + }, + } + if form_required: + request_body["content"]["multipart/form-data"]["schema"]["required"] = ( + form_required + ) + + if converted_parameters: + converted["parameters"] = converted_parameters + if request_body is not None: + converted["requestBody"] = request_body + + converted_responses: dict[str, Any] = {} + for code, response in converted.get("responses", {}).items(): + if not isinstance(response, dict): + converted_responses[code] = response + continue + + response_copy = copy.deepcopy(response) + response_schema = response_copy.pop("schema", None) + if response_schema is not None: + response_copy["content"] = { + media_type: {"schema": convert_schema_object(response_schema)} + for media_type in produces + } + converted_responses[code] = convert_schema_object(response_copy) + + converted["responses"] = converted_responses + return convert_schema_object(converted) + + +def convert_swagger2_to_openapi3(swagger_data: dict[str, Any]) -> dict[str, Any]: + """Convert the generated Swagger 2 document into a full OpenAPI 3 document.""" + openapi_data = copy.deepcopy(swagger_data) + openapi_data["openapi"] = "3.0.3" + openapi_data.pop("swagger", None) + + global_consumes = openapi_data.pop("consumes", None) or [] + global_produces = openapi_data.pop("produces", None) or [] + + components: dict[str, Any] = {} + definitions = openapi_data.pop("definitions", {}) + if definitions: + components["schemas"] = { + name: convert_schema_object(schema) for name, schema in definitions.items() + } + + parameters = openapi_data.pop("parameters", {}) + if parameters: + components["parameters"] = { + name: convert_swagger_parameter(parameter) + for name, parameter in parameters.items() + } + + responses = openapi_data.pop("responses", {}) + if responses: + components["responses"] = { + name: convert_schema_object(response) + for name, response in responses.items() + } + + security_definitions = openapi_data.pop("securityDefinitions", {}) + if security_definitions: + components["securitySchemes"] = { + name: convert_schema_object(scheme) + for name, scheme in security_definitions.items() + } + + if components: + openapi_data["components"] = components + + host = openapi_data.pop("host", "") + base_path = openapi_data.pop("basePath", "") or "" + schemes = openapi_data.pop("schemes", None) or [] + if host: + if host.startswith("http://") or host.startswith("https://"): + server_url = host.rstrip("/") + else: + scheme = schemes[0] if schemes else "http" + server_url = f"{scheme}://{host}".rstrip("/") + if base_path: + server_url = f"{server_url}{base_path}" + openapi_data["servers"] = [{"url": server_url}] + + converted_paths: dict[str, Any] = {} + for path, path_item in openapi_data.get("paths", {}).items(): + if not isinstance(path_item, dict): + converted_paths[path] = path_item + continue + + converted_path_item: dict[str, Any] = {} + path_level_parameters = path_item.get("parameters", []) + if path_level_parameters: + converted_path_item["parameters"] = [ + convert_swagger_parameter(parameter) + if isinstance(parameter, dict) and "$ref" not in parameter + else {"$ref": normalize_openapi_ref(parameter["$ref"])} + for parameter in path_level_parameters + ] + + for method, operation in path_item.items(): + if method == "parameters": + continue + if not isinstance(operation, dict): + converted_path_item[method] = operation + continue + converted_path_item[method] = convert_swagger_operation( + operation, global_consumes, global_produces + ) + + converted_paths[path] = converted_path_item + + openapi_data["paths"] = converted_paths + return convert_schema_object(openapi_data) + + class SDKPostProcesser: """Process Swagger JSON to add SSE extensions and update model names.""" @@ -399,8 +708,8 @@ def process_parameters( def output(self, output_file: Path, category: RunMode) -> None: output_data = self.data - if category == RunMode.SDK: - output_data = self._filter_sdk_apis() + if category != RunMode.CLIENT: + output_data = self._filter_apis_by_audience(category) if output_data is None: console.print("[bold red]Processing function returned None[/bold red]") sys.exit(1) @@ -408,14 +717,22 @@ def output(self, output_file: Path, category: RunMode) -> None: with open(output_file, "w", encoding="utf-8") as f: json.dump(output_data, f, indent=2) - def _filter_sdk_apis(self) -> dict[str, Any] | None: + def _filter_apis_by_audience(self, category: RunMode) -> dict[str, Any] | None: """ - Filter Swagger JSON to only keep APIs marked with x-api-type: {"sdk": "true"}. - Remove all other APIs and their unused model definitions. + Filter Swagger JSON according to the x-api-type audience flags. """ + audience_keys_by_mode = { + RunMode.SDK: {"sdk"}, + RunMode.PORTAL: {"portal"}, + RunMode.ADMIN: {"admin"}, + } + audience_keys = audience_keys_by_mode.get(category) + if not audience_keys: + return copy.deepcopy(self.data) + new_data = copy.deepcopy(self.data) - # Step 1: Filter paths - keep only APIs with x-api-type.sdk = "true" + # Step 1: Filter paths - keep only operations tagged for the target audience. original_paths = new_data["paths"] filtered_paths = {} removed_count = 0 @@ -425,8 +742,7 @@ def _filter_sdk_apis(self) -> dict[str, Any] | None: filtered_operations = {} for method, spec in operations.items(): x_api_type = spec.get("x-api-type", {}) - # Check if sdk is explicitly "true" (string) - if x_api_type.get("sdk") == "true": + if any(audience_flag_enabled(x_api_type, key) for key in audience_keys): filtered_operations[method] = spec kept_count += 1 console.print(f"[gray] ✓ Kept: {method.upper()} {path}[/gray]") @@ -499,6 +815,10 @@ def collect_refs(obj: dict[str, Any] | list[dict[str, Any]]) -> None: f"[gray]\n Models: {len(filtered_schemas)} kept, {removed_models} removed[/gray]" ) + console.print( + f"[gray]\n {category.value} operations: {kept_count} kept, {removed_count} removed[/gray]" + ) + return new_data @@ -524,43 +844,42 @@ def init(version: str) -> None: ] ) - # 2. Generate OpenAPI3 using OpenAPI Generator - volume_path = Path("/local") - relative_swagger = SWAGGER_ROOT.relative_to(PROJECT_ROOT) - container_input_path = volume_path / relative_swagger / "openapi2" / "swagger.json" - container_output_path = volume_path / relative_swagger / "openapi3" - - try: - docker.run( - settings.generator_image, - command=[ - "generate", - "-i", - container_input_path.as_posix(), - "-g", - "openapi", - "-o", - container_output_path.as_posix(), - ], - volumes=[(PROJECT_ROOT, volume_path)], - remove=True, - ) - except Exception as e: - console.print(f"[bold_red]❌ Error during OpenAPI3 generation: {e}[/bold_red]") + # 2. Convert Swagger 2.0 into a full OpenAPI 3 document locally. + swagger2_file = OPENAPI2_DIR / "swagger.json" + if not swagger2_file.exists(): + console.print(f"[bold red]{swagger2_file} not found[/bold red]") sys.exit(1) + if OPENAPI3_DIR.exists(): + shutil.rmtree(OPENAPI3_DIR) + OPENAPI3_DIR.mkdir(parents=True) + + with open(swagger2_file, encoding="utf-8") as f: + swagger2_data = json.load(f) + + openapi3_data = convert_swagger2_to_openapi3(swagger2_data) + with open(OPENAPI3_DIR / "openapi.json", "w", encoding="utf-8") as f: + json.dump(openapi3_data, f, indent=2) + # 3. Post-process Swagger JSON console.print("[bold blue]📦 Post-processing swagger initiaization...[/bold blue]") if not CONVERTED_DIR.exists(): CONVERTED_DIR.mkdir(parents=True) + else: + legacy_typescript_file = CONVERTED_DIR / "typescript.json" + legacy_typescript_file.unlink(missing_ok=True) post_input_file = OPENAPI3_DIR / "openapi.json" client_file = CONVERTED_DIR / "client.json" sdk_file = CONVERTED_DIR / "sdk.json" + portal_file = CONVERTED_DIR / "portal.json" + admin_file = CONVERTED_DIR / "admin.json" shutil.copyfile(post_input_file, dst=client_file) shutil.copyfile(post_input_file, dst=sdk_file) + shutil.copyfile(post_input_file, dst=portal_file) + shutil.copyfile(post_input_file, dst=admin_file) processor = SDKPostProcesser(post_input_file) processor.update_version(version) @@ -571,6 +890,8 @@ def init(version: str) -> None: processor.output(client_file, RunMode.CLIENT) processor.output(sdk_file, RunMode.SDK) + processor.output(portal_file, RunMode.PORTAL) + processor.output(admin_file, RunMode.ADMIN) console.print( "[bold green]✅ Swagger documentation generation completed successfully![/bold green]" diff --git a/scripts/command/src/swagger/python.py b/scripts/command/src/swagger/python.py index 388d63c4..eb69807a 100644 --- a/scripts/command/src/swagger/python.py +++ b/scripts/command/src/swagger/python.py @@ -168,7 +168,9 @@ def generate(self) -> None: console.print( "[bold blue]Step 3: Formatting post-processed Python SDK...[/bold blue]" ) - formatter = PythonFormatter(scope=ScopeType.SDK) + formatter = PythonFormatter( + scope=ScopeType.SDK, sdk_dir=self.PYTHON_SDK_DIR.as_posix() + ) formatter.run() # 5. Update version information in project files diff --git a/scripts/command/src/swagger/typescript.py b/scripts/command/src/swagger/typescript.py index 67d80db3..24e9f874 100644 --- a/scripts/command/src/swagger/typescript.py +++ b/scripts/command/src/swagger/typescript.py @@ -32,12 +32,58 @@ def generate(self) -> None: ) +class TypeScriptSDK(Generator): + """TypeScript generator for separate portal/admin audience specs.""" + + SDK_ROOT_DIR = PROJECT_ROOT / "sdk" / "typescript" + SDK_GEN_ROOT_DIR = PROJECT_ROOT / "sdk" / "typescript-gen" + GENERATOR_CONFIG_DIR = PROJECT_ROOT / ".openapi-generator" / "typescript" / "sdk" + + def __init__(self, version: str) -> None: + self.version = version + + def generate(self) -> None: + legacy_shared_sdk = self.SDK_ROOT_DIR + if legacy_shared_sdk.exists() and legacy_shared_sdk.is_dir(): + shutil.rmtree(legacy_shared_sdk) + + audience_packages = { + RunMode.PORTAL: { + "dst_dir": self.SDK_ROOT_DIR / "portal", + "gen_dir": self.SDK_GEN_ROOT_DIR / "portal", + "config_overrides": { + "npmName": "@OperationsPAI/portal", + "npmDescription": "TypeScript Portal SDK for RCABench API", + }, + }, + RunMode.ADMIN: { + "dst_dir": self.SDK_ROOT_DIR / "admin", + "gen_dir": self.SDK_GEN_ROOT_DIR / "admin", + "config_overrides": { + "npmName": "@OperationsPAI/admin", + "npmDescription": "TypeScript Admin SDK for RCABench API", + }, + }, + } + + for mode, spec in audience_packages.items(): + _generate_typescript_helper( + mode, + self.version, + spec["dst_dir"], + spec["gen_dir"], + self.GENERATOR_CONFIG_DIR, + config_overrides=spec["config_overrides"], + ) + + def _generate_typescript_helper( mode: RunMode, version: str, dst_dir: Path, gen_dir: Path, generator_config_dir: Path, + config_overrides: dict[str, str] | None = None, ) -> None: """ Helper function to generate TypeScript client or SDK. @@ -46,10 +92,24 @@ def _generate_typescript_helper( 3. Post-processes the generated client/SDK. 4. Cleans up temporary directories. """ - if mode not in {RunMode.CLIENT, RunMode.SDK}: - raise ValueError(f"Invalid mode: {mode}. Must be 'client' or 'sdk'.") + if mode not in { + RunMode.CLIENT, + RunMode.SDK, + RunMode.PORTAL, + RunMode.ADMIN, + }: + raise ValueError( + f"Invalid mode: {mode}. Must be 'client', 'sdk', 'portal', or 'admin'." + ) - msg = "Client" if mode == RunMode.CLIENT else "SDK" + if mode == RunMode.CLIENT: + msg = "Client" + elif mode == RunMode.PORTAL: + msg = "Portal SDK" + elif mode == RunMode.ADMIN: + msg = "Admin SDK" + else: + msg = "SDK" # 1. Update generator config with the specified version generator_config = generator_config_dir / "config.json" @@ -57,6 +117,8 @@ def _generate_typescript_helper( config_data = json.load(f) config_data["npmVersion"] = version + if config_overrides: + config_data.update(config_overrides) tmp_generator_config = generator_config_dir / "config_tmp.json" with open(tmp_generator_config, "w") as f: diff --git a/scripts/command/uv.lock b/scripts/command/uv.lock index 4d45647e..9dc1d89a 100644 --- a/scripts/command/uv.lock +++ b/scripts/command/uv.lock @@ -1148,7 +1148,7 @@ wheels = [ [[package]] name = "rcabench" -version = "1.2.0" +version = "1.2.1" source = { editable = "../../sdk/python" } dependencies = [ { name = "lazy-imports" }, diff --git a/scripts/generate_swagger_audience_report.py b/scripts/generate_swagger_audience_report.py new file mode 100644 index 00000000..5816bee9 --- /dev/null +++ b/scripts/generate_swagger_audience_report.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +from collections import Counter +from dataclasses import dataclass +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = REPO_ROOT / "src" / "module" +OUTPUT_FILE = REPO_ROOT / "docs" / "swagger-audience-marking-report.md" + +ROUTER_RE = re.compile(r"@Router\s+(\S+)\s+\[(\w+)\]") +SUMMARY_RE = re.compile(r"@Summary\s+(.+)") +X_API_TYPE_RE = re.compile(r"@x-api-type\s+(.+)") +FUNC_RE = re.compile(r"^func\s+([A-Za-z0-9_]+)\s*\(") + + +@dataclass +class Operation: + method: str + path: str + summary: str + file_path: str + router_line: int + x_api_type_line: int | None + function_name: str | None + function_line: int | None + audiences: list[str] + raw_x_api_type: str | None + status: str + + @property + def source(self) -> str: + parts = [f"`{self.file_path}:{self.router_line}`"] + if self.x_api_type_line is not None: + parts.append(f"`{self.file_path}:{self.x_api_type_line}`") + if self.function_line is not None: + parts.append(f"`{self.file_path}:{self.function_line}`") + return " / ".join(parts) + + @property + def audience_text(self) -> str: + return ", ".join(self.audiences) if self.audiences else "-" + + +def parse_x_api_type(raw_value: str | None) -> list[str]: + if raw_value is None: + return [] + + raw_value = raw_value.strip() + try: + parsed = json.loads(raw_value) + except json.JSONDecodeError: + return [] + + if not isinstance(parsed, dict): + return [] + + audiences: list[str] = [] + for key in ("sdk", "portal", "admin"): + value = parsed.get(key) + if isinstance(value, bool) and value: + audiences.append(key) + elif isinstance(value, str) and value.strip().lower() == "true": + audiences.append(key) + return audiences + + +def iter_handler_files() -> list[Path]: + files: list[Path] = [] + files.extend( + path + for path in SOURCE_ROOT.rglob("*.go") + if path.is_file() and not path.name.endswith("_test.go") + ) + return sorted(files) + + +def collect_operations() -> list[Operation]: + operations: list[Operation] = [] + + for file_path in iter_handler_files(): + rel_path = file_path.relative_to(REPO_ROOT).as_posix() + lines = file_path.read_text(encoding="utf-8").splitlines() + + for index, line in enumerate(lines): + router_match = ROUTER_RE.search(line) + if not router_match: + continue + + path = router_match.group(1) + method = router_match.group(2).upper() + + summary = "" + raw_x_api_type: str | None = None + x_api_type_line: int | None = None + + # Search around the router annotation inside the current comment block. + start = max(0, index - 40) + end = min(len(lines), index + 12) + for scan_index in range(index, end): + x_match = X_API_TYPE_RE.search(lines[scan_index]) + if x_match: + raw_x_api_type = x_match.group(1).strip() + x_api_type_line = scan_index + 1 + break + + for scan_index in range(index, start - 1, -1): + summary_match = SUMMARY_RE.search(lines[scan_index]) + if summary_match: + summary = summary_match.group(1).strip() + break + if ( + scan_index != index + and lines[scan_index].strip() + and not lines[scan_index].lstrip().startswith("//") + ): + break + + function_name: str | None = None + function_line: int | None = None + for scan_index in range(index + 1, min(len(lines), index + 20)): + func_match = FUNC_RE.match(lines[scan_index].strip()) + if func_match: + function_name = func_match.group(1) + function_line = scan_index + 1 + break + + audiences = parse_x_api_type(raw_x_api_type) + if audiences: + status = "marked" + elif raw_x_api_type is None: + status = "missing" + else: + status = "empty" + + operations.append( + Operation( + method=method, + path=path, + summary=summary, + file_path=rel_path, + router_line=index + 1, + x_api_type_line=x_api_type_line, + function_name=function_name, + function_line=function_line, + audiences=audiences, + raw_x_api_type=raw_x_api_type, + status=status, + ) + ) + + operations.sort( + key=lambda item: (item.path, item.method, item.file_path, item.router_line) + ) + return operations + + +def markdown_table(rows: list[list[str]]) -> list[str]: + if not rows: + return ["_None_"] + + header = rows[0] + lines = [ + "| " + " | ".join(header) + " |", + "| " + " | ".join(["---"] * len(header)) + " |", + ] + for row in rows[1:]: + lines.append("| " + " | ".join(row) + " |") + return lines + + +def build_report(operations: list[Operation]) -> str: + marked = [item for item in operations if item.status == "marked"] + empty = [item for item in operations if item.status == "empty"] + missing = [item for item in operations if item.status == "missing"] + + audience_counter: Counter[str] = Counter() + for item in marked: + audience_counter.update(item.audiences) + + lines: list[str] = [] + lines.append("# Swagger Audience Marking Report") + lines.append("") + lines.append("> Source of truth: Swagger annotations in `src/module/**/*.go`.") + lines.append( + "> Route position column uses the `@Router` line, then `@x-api-type`, then function line when available." + ) + lines.append("") + lines.append("## Summary") + lines.append("") + lines.append(f"- Total operations scanned: **{len(operations)}**") + lines.append(f"- Marked operations: **{len(marked)}**") + lines.append(f"- Empty `@x-api-type {{}}` operations: **{len(empty)}**") + lines.append(f"- Missing `@x-api-type` operations: **{len(missing)}**") + lines.append( + "- Audience counts among marked operations: " + f"`sdk={audience_counter.get('sdk', 0)}` " + f"`portal={audience_counter.get('portal', 0)}` " + f"`admin={audience_counter.get('admin', 0)}`" + ) + lines.append("") + + lines.append("## Marked Operations") + lines.append("") + lines.extend( + markdown_table( + [ + ["Method", "Path", "Audience", "Summary", "Location"], + *[ + [ + item.method, + f"`{item.path}`", + f"`{item.audience_text}`", + item.summary or "-", + item.source, + ] + for item in marked + ], + ] + ) + ) + lines.append("") + + lines.append("## Empty `@x-api-type {}` Operations") + lines.append("") + lines.extend( + markdown_table( + [ + ["Method", "Path", "Summary", "Raw", "Location"], + *[ + [ + item.method, + f"`{item.path}`", + item.summary or "-", + f"`{item.raw_x_api_type or ''}`", + item.source, + ] + for item in empty + ], + ] + ) + ) + lines.append("") + + lines.append("## Missing `@x-api-type` Operations") + lines.append("") + lines.extend( + markdown_table( + [ + ["Method", "Path", "Summary", "Location"], + *[ + [ + item.method, + f"`{item.path}`", + item.summary or "-", + item.source, + ] + for item in missing + ], + ] + ) + ) + lines.append("") + + return "\n".join(lines) + "\n" + + +def main() -> None: + operations = collect_operations() + OUTPUT_FILE.write_text(build_report(operations), encoding="utf-8") + print( + f"generated {OUTPUT_FILE.relative_to(REPO_ROOT)} with {len(operations)} operations" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/migrate_swagger_comments.py b/scripts/migrate_swagger_comments.py new file mode 100644 index 00000000..0f18a9a4 --- /dev/null +++ b/scripts/migrate_swagger_comments.py @@ -0,0 +1,1104 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] + +OLD_FUNC_RE = re.compile(r"^func\s+([A-Za-z0-9_]+)\s*\(") +NEW_METHOD_RE = re.compile(r"^func\s+\([^)]*\)\s+([A-Za-z0-9_]+)\s*\(") + + +@dataclass(frozen=True) +class Mapping: + source: str + source_func: str + target: str + target_func: str + + +MAPPINGS = [ + Mapping( + "src/handlers/v2/access_keys.go", + "CreateAccessKey", + "src/module/auth/handler.go", + "CreateAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "ListAccessKeys", + "src/module/auth/handler.go", + "ListAccessKeys", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "GetAccessKey", + "src/module/auth/handler.go", + "GetAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "DeleteAccessKey", + "src/module/auth/handler.go", + "DeleteAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "RotateAccessKey", + "src/module/auth/handler.go", + "RotateAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "DisableAccessKey", + "src/module/auth/handler.go", + "DisableAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "EnableAccessKey", + "src/module/auth/handler.go", + "EnableAccessKey", + ), + Mapping( + "src/handlers/v2/access_keys.go", + "ExchangeAccessKeyToken", + "src/module/auth/handler.go", + "ExchangeAccessKeyToken", + ), + Mapping( + "src/handlers/v2/auth.go", "Register", "src/module/auth/handler.go", "Register" + ), + Mapping("src/handlers/v2/auth.go", "Login", "src/module/auth/handler.go", "Login"), + Mapping( + "src/handlers/v2/auth.go", + "RefreshToken", + "src/module/auth/handler.go", + "RefreshToken", + ), + Mapping( + "src/handlers/v2/auth.go", "Logout", "src/module/auth/handler.go", "Logout" + ), + Mapping( + "src/handlers/v2/auth.go", + "ChangePassword", + "src/module/auth/handler.go", + "ChangePassword", + ), + Mapping( + "src/handlers/v2/auth.go", + "GetProfile", + "src/module/auth/handler.go", + "GetProfile", + ), + Mapping( + "src/handlers/v2/containers.go", + "CreateContainer", + "src/module/container/handler.go", + "CreateContainer", + ), + Mapping( + "src/handlers/v2/containers.go", + "DeleteContainer", + "src/module/container/handler.go", + "DeleteContainer", + ), + Mapping( + "src/handlers/v2/containers.go", + "GetContainer", + "src/module/container/handler.go", + "GetContainer", + ), + Mapping( + "src/handlers/v2/containers.go", + "ListContainers", + "src/module/container/handler.go", + "ListContainers", + ), + Mapping( + "src/handlers/v2/containers.go", + "UpdateContainer", + "src/module/container/handler.go", + "UpdateContainer", + ), + Mapping( + "src/handlers/v2/containers.go", + "CreateContainerVersion", + "src/module/container/handler.go", + "CreateContainerVersion", + ), + Mapping( + "src/handlers/v2/containers.go", + "DeleteContainerVersion", + "src/module/container/handler.go", + "DeleteContainerVersion", + ), + Mapping( + "src/handlers/v2/containers.go", + "GetContainerVersion", + "src/module/container/handler.go", + "GetContainerVersion", + ), + Mapping( + "src/handlers/v2/containers.go", + "ListContainerVersions", + "src/module/container/handler.go", + "ListContainerVersions", + ), + Mapping( + "src/handlers/v2/containers.go", + "UpdateContainerVersion", + "src/module/container/handler.go", + "UpdateContainerVersion", + ), + Mapping( + "src/handlers/v2/containers.go", + "ManageContainerCustomLabels", + "src/module/container/handler.go", + "ManageContainerCustomLabels", + ), + Mapping( + "src/handlers/v2/containers.go", + "SubmitContainerBuilding", + "src/module/container/handler.go", + "SubmitContainerBuilding", + ), + Mapping( + "src/handlers/v2/containers.go", + "UploadHelmChart", + "src/module/container/handler.go", + "UploadHelmChart", + ), + Mapping( + "src/handlers/v2/containers.go", + "UploadHelmValueFile", + "src/module/container/handler.go", + "UploadHelmValueFile", + ), + Mapping( + "src/handlers/v2/datasets.go", + "CreateDataset", + "src/module/dataset/handler.go", + "CreateDataset", + ), + Mapping( + "src/handlers/v2/datasets.go", + "DeleteDataset", + "src/module/dataset/handler.go", + "DeleteDataset", + ), + Mapping( + "src/handlers/v2/datasets.go", + "GetDataset", + "src/module/dataset/handler.go", + "GetDataset", + ), + Mapping( + "src/handlers/v2/datasets.go", + "ListDatasets", + "src/module/dataset/handler.go", + "ListDatasets", + ), + Mapping( + "src/handlers/v2/datasets.go", + "SearchDataset", + "src/module/dataset/handler.go", + "SearchDataset", + ), + Mapping( + "src/handlers/v2/datasets.go", + "UpdateDataset", + "src/module/dataset/handler.go", + "UpdateDataset", + ), + Mapping( + "src/handlers/v2/datasets.go", + "ManageDatasetCustomLabels", + "src/module/dataset/handler.go", + "ManageDatasetCustomLabels", + ), + Mapping( + "src/handlers/v2/datasets.go", + "CreateDatasetVersion", + "src/module/dataset/handler.go", + "CreateDatasetVersion", + ), + Mapping( + "src/handlers/v2/datasets.go", + "DeleteDatasetVersion", + "src/module/dataset/handler.go", + "DeleteDatasetVersion", + ), + Mapping( + "src/handlers/v2/datasets.go", + "GetDatasetVersion", + "src/module/dataset/handler.go", + "GetDatasetVersion", + ), + Mapping( + "src/handlers/v2/datasets.go", + "ListDatasetVersions", + "src/module/dataset/handler.go", + "ListDatasetVersions", + ), + Mapping( + "src/handlers/v2/datasets.go", + "UpdateDatasetVersion", + "src/module/dataset/handler.go", + "UpdateDatasetVersion", + ), + Mapping( + "src/handlers/v2/datasets.go", + "DownloadDatasetVersion", + "src/module/dataset/handler.go", + "DownloadDatasetVersion", + ), + Mapping( + "src/handlers/v2/datasets.go", + "ManageDatasetVersionInjections", + "src/module/dataset/handler.go", + "ManageDatasetVersionInjections", + ), + Mapping( + "src/handlers/v2/evaluations.go", + "ListDatapackEvaluationResults", + "src/module/evaluation/handler.go", + "ListDatapackEvaluationResults", + ), + Mapping( + "src/handlers/v2/evaluations.go", + "ListDatasetEvaluationResults", + "src/module/evaluation/handler.go", + "ListDatasetEvaluationResults", + ), + Mapping( + "src/handlers/v2/evaluations.go", + "ListEvaluations", + "src/module/evaluation/handler.go", + "ListEvaluations", + ), + Mapping( + "src/handlers/v2/evaluations.go", + "GetEvaluation", + "src/module/evaluation/handler.go", + "GetEvaluation", + ), + Mapping( + "src/handlers/v2/evaluations.go", + "DeleteEvaluation", + "src/module/evaluation/handler.go", + "DeleteEvaluation", + ), + Mapping( + "src/handlers/v2/executions.go", + "BatchDeleteExecutions", + "src/module/execution/handler.go", + "BatchDeleteExecutions", + ), + Mapping( + "src/handlers/v2/executions.go", + "GetExecution", + "src/module/execution/handler.go", + "GetExecution", + ), + Mapping( + "src/handlers/v2/executions.go", + "ListExecutions", + "src/module/execution/handler.go", + "ListExecutions", + ), + Mapping( + "src/handlers/v2/executions.go", + "ListAvaliableExecutionLabels", + "src/module/execution/handler.go", + "ListAvailableExecutionLabels", + ), + Mapping( + "src/handlers/v2/executions.go", + "ManageExecutionCustomLabels", + "src/module/execution/handler.go", + "ManageExecutionCustomLabels", + ), + Mapping( + "src/handlers/v2/executions.go", + "SubmitAlgorithmExecution", + "src/module/execution/handler.go", + "SubmitAlgorithmExecution", + ), + Mapping( + "src/handlers/v2/executions.go", + "UploadDetectorResults", + "src/module/execution/handler.go", + "UploadDetectorResults", + ), + Mapping( + "src/handlers/v2/executions.go", + "UploadGranularityResults", + "src/module/execution/handler.go", + "UploadGranularityResults", + ), + Mapping( + "src/handlers/v2/groups.go", + "GetGroupStats", + "src/module/group/handler.go", + "GetGroupStats", + ), + Mapping( + "src/handlers/v2/groups.go", + "GetGroupStream", + "src/module/group/handler.go", + "GetGroupStream", + ), + Mapping( + "src/handlers/v2/injections.go", + "BatchDeleteInjections", + "src/module/injection/handler.go", + "BatchDeleteInjections", + ), + Mapping( + "src/handlers/v2/injections.go", + "GetInjection", + "src/module/injection/handler.go", + "GetInjection", + ), + Mapping( + "src/handlers/v2/injections.go", + "GetInjectionMetadata", + "src/module/injection/handler.go", + "GetInjectionMetadata", + ), + Mapping( + "src/handlers/v2/injections.go", + "ListInjections", + "src/module/injection/handler.go", + "ListInjections", + ), + Mapping( + "src/handlers/v2/injections.go", + "SearchInjections", + "src/module/injection/handler.go", + "SearchInjections", + ), + Mapping( + "src/handlers/v2/injections.go", + "ListFaultInjectionNoIssues", + "src/module/injection/handler.go", + "ListFaultInjectionNoIssues", + ), + Mapping( + "src/handlers/v2/injections.go", + "ListFaultInjectionWithIssues", + "src/module/injection/handler.go", + "ListFaultInjectionWithIssues", + ), + Mapping( + "src/handlers/v2/injections.go", + "ManageInjectionCustomLabels", + "src/module/injection/handler.go", + "ManageInjectionCustomLabels", + ), + Mapping( + "src/handlers/v2/injections.go", + "BatchManageInjectionLabels", + "src/module/injection/handler.go", + "BatchManageInjectionLabels", + ), + Mapping( + "src/handlers/v2/injections.go", + "SubmitFaultInjection", + "src/module/injection/handler.go", + "SubmitFaultInjection", + ), + Mapping( + "src/handlers/v2/injections.go", + "SubmitDatapackBuilding", + "src/module/injection/handler.go", + "SubmitDatapackBuilding", + ), + Mapping( + "src/handlers/v2/injections.go", + "CloneInjection", + "src/module/injection/handler.go", + "CloneInjection", + ), + Mapping( + "src/handlers/v2/injections.go", + "GetInjectionLogs", + "src/module/injection/handler.go", + "GetInjectionLogs", + ), + Mapping( + "src/handlers/v2/injections.go", + "DownloadDatapack", + "src/module/injection/handler.go", + "DownloadDatapack", + ), + Mapping( + "src/handlers/v2/injections.go", + "ListDatapackFiles", + "src/module/injection/handler.go", + "ListDatapackFiles", + ), + Mapping( + "src/handlers/v2/injections.go", + "DownloadDatapackFile", + "src/module/injection/handler.go", + "DownloadDatapackFile", + ), + Mapping( + "src/handlers/v2/injections.go", + "QueryDatapackFile", + "src/module/injection/handler.go", + "QueryDatapackFile", + ), + Mapping( + "src/handlers/v2/injections.go", + "UploadDatapack", + "src/module/injection/handler.go", + "UploadDatapack", + ), + Mapping( + "src/handlers/v2/injections.go", + "UpdateGroundtruth", + "src/module/injection/handler.go", + "UpdateGroundtruth", + ), + Mapping( + "src/handlers/v2/labels.go", + "BatchDeleteLabels", + "src/module/label/handler.go", + "BatchDeleteLabels", + ), + Mapping( + "src/handlers/v2/labels.go", + "CreateLabel", + "src/module/label/handler.go", + "CreateLabel", + ), + Mapping( + "src/handlers/v2/labels.go", + "DeleteLabel", + "src/module/label/handler.go", + "DeleteLabel", + ), + Mapping( + "src/handlers/v2/labels.go", + "GetLabelDetail", + "src/module/label/handler.go", + "GetLabelDetail", + ), + Mapping( + "src/handlers/v2/labels.go", + "ListLabels", + "src/module/label/handler.go", + "ListLabels", + ), + Mapping( + "src/handlers/v2/labels.go", + "UpdateLabel", + "src/module/label/handler.go", + "UpdateLabel", + ), + Mapping( + "src/handlers/v2/metrics.go", + "GetInjectionMetrics", + "src/module/metric/handler.go", + "GetInjectionMetrics", + ), + Mapping( + "src/handlers/v2/metrics.go", + "GetExecutionMetrics", + "src/module/metric/handler.go", + "GetExecutionMetrics", + ), + Mapping( + "src/handlers/v2/metrics.go", + "GetAlgorithmMetrics", + "src/module/metric/handler.go", + "GetAlgorithmMetrics", + ), + Mapping( + "src/handlers/v2/notifications.go", + "GetNotificationStream", + "src/module/notification/handler.go", + "GetStream", + ), + Mapping( + "src/handlers/v2/permissions.go", + "GetPermission", + "src/module/rbac/handler.go", + "GetPermission", + ), + Mapping( + "src/handlers/v2/permissions.go", + "ListPermissions", + "src/module/rbac/handler.go", + "ListPermissions", + ), + Mapping( + "src/handlers/v2/permissions.go", + "ListRolesFromPermission", + "src/module/rbac/handler.go", + "ListRolesFromPermission", + ), + Mapping( + "src/handlers/v2/projects.go", + "CreateProject", + "src/module/project/handler.go", + "CreateProject", + ), + Mapping( + "src/handlers/v2/projects.go", + "DeleteProject", + "src/module/project/handler.go", + "DeleteProject", + ), + Mapping( + "src/handlers/v2/projects.go", + "GetProjectDetail", + "src/module/project/handler.go", + "GetProjectDetail", + ), + Mapping( + "src/handlers/v2/projects.go", + "ListProjects", + "src/module/project/handler.go", + "ListProjects", + ), + Mapping( + "src/handlers/v2/projects.go", + "UpdateProject", + "src/module/project/handler.go", + "UpdateProject", + ), + Mapping( + "src/handlers/v2/projects.go", + "ManageProjectCustomLabels", + "src/module/project/handler.go", + "ManageProjectCustomLabels", + ), + Mapping( + "src/handlers/v2/projects.go", + "ListProjectInjections", + "src/module/injection/handler.go", + "ListProjectInjections", + ), + Mapping( + "src/handlers/v2/projects.go", + "SearchProjectInjections", + "src/module/injection/handler.go", + "SearchProjectInjections", + ), + Mapping( + "src/handlers/v2/projects.go", + "ListProjectFaultInjectionNoIssues", + "src/module/injection/handler.go", + "ListProjectFaultInjectionNoIssues", + ), + Mapping( + "src/handlers/v2/projects.go", + "ListProjectFaultInjectionWithIssues", + "src/module/injection/handler.go", + "ListProjectFaultInjectionWithIssues", + ), + Mapping( + "src/handlers/v2/projects.go", + "SubmitProjectFaultInjection", + "src/module/injection/handler.go", + "SubmitProjectFaultInjection", + ), + Mapping( + "src/handlers/v2/projects.go", + "SubmitProjectDatapackBuilding", + "src/module/injection/handler.go", + "SubmitProjectDatapackBuilding", + ), + Mapping( + "src/handlers/v2/projects.go", + "ListProjectExecutions", + "src/module/execution/handler.go", + "ListProjectExecutions", + ), + Mapping( + "src/handlers/v2/resources.go", + "GetResourceDetail", + "src/module/rbac/handler.go", + "GetResource", + ), + Mapping( + "src/handlers/v2/resources.go", + "ListResources", + "src/module/rbac/handler.go", + "ListResources", + ), + Mapping( + "src/handlers/v2/resources.go", + "ListResourcePermissions", + "src/module/rbac/handler.go", + "ListResourcePermissions", + ), + Mapping( + "src/handlers/v2/roles.go", + "CreateRole", + "src/module/rbac/handler.go", + "CreateRole", + ), + Mapping( + "src/handlers/v2/roles.go", + "DeleteRole", + "src/module/rbac/handler.go", + "DeleteRole", + ), + Mapping( + "src/handlers/v2/roles.go", "GetRole", "src/module/rbac/handler.go", "GetRole" + ), + Mapping( + "src/handlers/v2/roles.go", + "ListRoles", + "src/module/rbac/handler.go", + "ListRoles", + ), + Mapping( + "src/handlers/v2/roles.go", + "UpdateRole", + "src/module/rbac/handler.go", + "UpdateRole", + ), + Mapping( + "src/handlers/v2/roles.go", + "AssignRolePermission", + "src/module/rbac/handler.go", + "AssignRolePermissions", + ), + Mapping( + "src/handlers/v2/roles.go", + "RemovePermissionsFromRole", + "src/module/rbac/handler.go", + "RemoveRolePermissions", + ), + Mapping( + "src/handlers/v2/sdk_evaluations.go", + "ListSDKEvaluations", + "src/module/sdk/handler.go", + "ListEvaluations", + ), + Mapping( + "src/handlers/v2/sdk_evaluations.go", + "GetSDKEvaluation", + "src/module/sdk/handler.go", + "GetEvaluation", + ), + Mapping( + "src/handlers/v2/sdk_evaluations.go", + "ListSDKExperiments", + "src/module/sdk/handler.go", + "ListExperiments", + ), + Mapping( + "src/handlers/v2/sdk_evaluations.go", + "ListSDKDatasetSamples", + "src/module/sdk/handler.go", + "ListDatasetSamples", + ), + Mapping( + "src/handlers/v2/system.go", + "GetSystemMetrics", + "src/module/systemmetric/handler.go", + "GetSystemMetrics", + ), + Mapping( + "src/handlers/v2/system.go", + "GetSystemMetricsHistory", + "src/module/systemmetric/handler.go", + "GetSystemMetricsHistory", + ), + Mapping( + "src/handlers/v2/systems.go", + "ListChaosSystemsHandler", + "src/module/chaossystem/handler.go", + "ListSystems", + ), + Mapping( + "src/handlers/v2/systems.go", + "GetChaosSystemHandler", + "src/module/chaossystem/handler.go", + "GetSystem", + ), + Mapping( + "src/handlers/v2/systems.go", + "CreateChaosSystemHandler", + "src/module/chaossystem/handler.go", + "CreateSystem", + ), + Mapping( + "src/handlers/v2/systems.go", + "UpdateChaosSystemHandler", + "src/module/chaossystem/handler.go", + "UpdateSystem", + ), + Mapping( + "src/handlers/v2/systems.go", + "DeleteChaosSystemHandler", + "src/module/chaossystem/handler.go", + "DeleteSystem", + ), + Mapping( + "src/handlers/v2/systems.go", + "UpsertChaosSystemMetadataHandler", + "src/module/chaossystem/handler.go", + "UpsertMetadata", + ), + Mapping( + "src/handlers/v2/systems.go", + "ListChaosSystemMetadataHandler", + "src/module/chaossystem/handler.go", + "ListMetadata", + ), + Mapping( + "src/handlers/v2/tasks.go", + "BatchDeleteTasks", + "src/module/task/handler.go", + "BatchDelete", + ), + Mapping("src/handlers/v2/tasks.go", "GetTask", "src/module/task/handler.go", "Get"), + Mapping( + "src/handlers/v2/tasks.go", "ListTasks", "src/module/task/handler.go", "List" + ), + Mapping( + "src/handlers/v2/tasks.go", + "GetTaskLogsWS", + "src/module/task/handler.go", + "LogsWS", + ), + Mapping( + "src/handlers/v2/teams.go", + "CreateTeam", + "src/module/team/handler.go", + "CreateTeam", + ), + Mapping( + "src/handlers/v2/teams.go", + "DeleteTeam", + "src/module/team/handler.go", + "DeleteTeam", + ), + Mapping( + "src/handlers/v2/teams.go", + "GetTeamDetail", + "src/module/team/handler.go", + "GetTeamDetail", + ), + Mapping( + "src/handlers/v2/teams.go", + "ListTeams", + "src/module/team/handler.go", + "ListTeams", + ), + Mapping( + "src/handlers/v2/teams.go", + "UpdateTeam", + "src/module/team/handler.go", + "UpdateTeam", + ), + Mapping( + "src/handlers/v2/teams.go", + "ListTeamProjects", + "src/module/team/handler.go", + "ListTeamProjects", + ), + Mapping( + "src/handlers/v2/teams.go", + "AddTeamMember", + "src/module/team/handler.go", + "AddTeamMember", + ), + Mapping( + "src/handlers/v2/teams.go", + "RemoveTeamMember", + "src/module/team/handler.go", + "RemoveTeamMember", + ), + Mapping( + "src/handlers/v2/teams.go", + "UpdateTeamMemberRole", + "src/module/team/handler.go", + "UpdateTeamMemberRole", + ), + Mapping( + "src/handlers/v2/teams.go", + "ListTeamMembers", + "src/module/team/handler.go", + "ListTeamMembers", + ), + Mapping( + "src/handlers/v2/traces.go", + "GetTrace", + "src/module/trace/handler.go", + "GetTrace", + ), + Mapping( + "src/handlers/v2/traces.go", + "ListTraces", + "src/module/trace/handler.go", + "ListTraces", + ), + Mapping( + "src/handlers/v2/traces.go", + "GetTraceStream", + "src/module/trace/handler.go", + "GetTraceStream", + ), + Mapping( + "src/handlers/v2/users.go", + "CreateUser", + "src/module/user/handler.go", + "CreateUser", + ), + Mapping( + "src/handlers/v2/users.go", + "DeleteUser", + "src/module/user/handler.go", + "DeleteUser", + ), + Mapping( + "src/handlers/v2/users.go", + "GetUserDetailV2", + "src/module/user/handler.go", + "GetUserDetail", + ), + Mapping( + "src/handlers/v2/users.go", + "ListUsersV2", + "src/module/user/handler.go", + "ListUsers", + ), + Mapping( + "src/handlers/v2/users.go", + "UpdateUser", + "src/module/user/handler.go", + "UpdateUser", + ), + Mapping( + "src/handlers/v2/users.go", + "AssignUserRole", + "src/module/user/handler.go", + "AssignRole", + ), + Mapping( + "src/handlers/v2/users.go", + "RemoveGlobalRole", + "src/module/user/handler.go", + "RemoveRole", + ), + Mapping( + "src/handlers/v2/users.go", + "AssignUserPermission", + "src/module/user/handler.go", + "AssignPermissions", + ), + Mapping( + "src/handlers/v2/users.go", + "RemoveUserPermission", + "src/module/user/handler.go", + "RemovePermissions", + ), + Mapping( + "src/handlers/v2/users.go", + "AssignUserContainer", + "src/module/user/handler.go", + "AssignContainer", + ), + Mapping( + "src/handlers/v2/users.go", + "RemoveUserContainer", + "src/module/user/handler.go", + "RemoveContainer", + ), + Mapping( + "src/handlers/v2/users.go", + "AssignUserDataset", + "src/module/user/handler.go", + "AssignDataset", + ), + Mapping( + "src/handlers/v2/users.go", + "RemoveUserDataset", + "src/module/user/handler.go", + "RemoveDataset", + ), + Mapping( + "src/handlers/v2/users.go", + "AssignUserProject", + "src/module/user/handler.go", + "AssignProject", + ), + Mapping( + "src/handlers/v2/users.go", + "RemoveUserProject", + "src/module/user/handler.go", + "RemoveProject", + ), + Mapping( + "src/handlers/v2/users.go", + "ListUsersFromRole", + "src/module/rbac/handler.go", + "ListUsersFromRole", + ), + Mapping( + "src/handlers/system/audit.go", + "GetAuditLog", + "src/module/system/handler.go", + "GetAuditLog", + ), + Mapping( + "src/handlers/system/audit.go", + "ListAuditLogs", + "src/module/system/handler.go", + "ListAuditLogs", + ), + Mapping( + "src/handlers/system/configs.go", + "GetConfig", + "src/module/system/handler.go", + "GetConfig", + ), + Mapping( + "src/handlers/system/configs.go", + "ListConfigs", + "src/module/system/handler.go", + "ListConfigs", + ), + Mapping( + "src/handlers/system/configs.go", + "RollbackConfigValue", + "src/module/system/handler.go", + "RollbackConfigValue", + ), + Mapping( + "src/handlers/system/configs.go", + "RollbackConfigMetadata", + "src/module/system/handler.go", + "RollbackConfigMetadata", + ), + Mapping( + "src/handlers/system/configs.go", + "UpdateConfigValue", + "src/module/system/handler.go", + "UpdateConfigValue", + ), + Mapping( + "src/handlers/system/configs.go", + "UpdateConfigMetadata", + "src/module/system/handler.go", + "UpdateConfigMetadata", + ), + Mapping( + "src/handlers/system/configs.go", + "ListConfigHistories", + "src/module/system/handler.go", + "ListConfigHistories", + ), + Mapping( + "src/handlers/system/health.go", + "GetHealth", + "src/module/system/handler.go", + "GetHealth", + ), + Mapping( + "src/handlers/system/monitor.go", + "GetMetrics", + "src/module/system/handler.go", + "GetMetrics", + ), + Mapping( + "src/handlers/system/monitor.go", + "GetSystemInfo", + "src/module/system/handler.go", + "GetSystemInfo", + ), + Mapping( + "src/handlers/system/monitor.go", + "ListNamespaceLocks", + "src/module/system/handler.go", + "ListNamespaceLocks", + ), + Mapping( + "src/handlers/system/monitor.go", + "ListQueuedTasks", + "src/module/system/handler.go", + "ListQueuedTasks", + ), +] + + +def read_lines(path: str) -> list[str]: + return (REPO_ROOT / path).read_text(encoding="utf-8").splitlines() + + +def extract_comments(path: str) -> dict[str, list[str]]: + lines = read_lines(path) + comments: dict[str, list[str]] = {} + for idx, line in enumerate(lines): + match = OLD_FUNC_RE.match(line.strip()) + if not match: + continue + func_name = match.group(1) + start = idx - 1 + while start >= 0 and lines[start].startswith("//"): + start -= 1 + block = lines[start + 1 : idx] + if block and any("@Router" in item for item in block): + comments[func_name] = block + return comments + + +def inject_comments(path: str, blocks: dict[str, list[str]]) -> None: + lines = read_lines(path) + output: list[str] = [] + idx = 0 + while idx < len(lines): + stripped = lines[idx].strip() + match = NEW_METHOD_RE.match(stripped) + if match and match.group(1) in blocks: + start = len(output) + while start > 0 and output[start - 1].startswith("//"): + start -= 1 + if start > 0 and output[start - 1] == "": + # Preserve a single separator before the comment block. + pass + if start < len(output): + output = output[:start] + if output and output[-1] != "": + output.append("") + elif output and output[-1] != "": + output.append("") + output.extend(blocks[match.group(1)]) + output.append(lines[idx]) + idx += 1 + (REPO_ROOT / path).write_text("\n".join(output) + "\n", encoding="utf-8") + + +def main() -> None: + source_cache: dict[str, dict[str, list[str]]] = {} + target_blocks: dict[str, dict[str, list[str]]] = {} + + for mapping in MAPPINGS: + if mapping.source not in source_cache: + source_cache[mapping.source] = extract_comments(mapping.source) + block = source_cache[mapping.source].get(mapping.source_func) + if not block: + raise RuntimeError( + f"missing comment block for {mapping.source}:{mapping.source_func}" + ) + target_blocks.setdefault(mapping.target, {})[mapping.target_func] = block + + for target, blocks in target_blocks.items(): + inject_comments(target, blocks) + print(f"updated {target} ({len(blocks)} methods)") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index f95a02ff..e1614864 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rcabench" -version = "1.2.0" +version = "1.2.1" description = "RCABench - A comprehensive root cause analysis benchmarking platform for microservices" authors = [ { name = "Lincyaw", email = "814750204@qq.com" }, @@ -61,4 +61,4 @@ exclude = [ # "logs/", "vendor/", "output/", -] +] \ No newline at end of file diff --git a/sdk/python/src/rcabench/__init__.py b/sdk/python/src/rcabench/__init__.py index c68196d1..a955fdae 100644 --- a/sdk/python/src/rcabench/__init__.py +++ b/sdk/python/src/rcabench/__init__.py @@ -1 +1 @@ -__version__ = "1.2.0" +__version__ = "1.2.1" diff --git a/sdk/python/src/rcabench/client/http_client.py b/sdk/python/src/rcabench/client/http_client.py index 6ec6c8a3..0d2a355b 100644 --- a/sdk/python/src/rcabench/client/http_client.py +++ b/sdk/python/src/rcabench/client/http_client.py @@ -1,12 +1,15 @@ import os +import secrets +import time from dataclasses import dataclass +from hashlib import sha256 +from hmac import new as hmac_new from pydantic import StrictStr from rcabench.openapi.api.authentication_api import AuthenticationApi from rcabench.openapi.api_client import ApiClient from rcabench.openapi.configuration import Configuration -from rcabench.openapi.models.login_req import LoginReq @dataclass(kw_only=True) @@ -17,42 +20,47 @@ class SessionData: class RCABenchClient: """ - RCABench client supporting both username/password and token-based authentication. + RCABench client supporting access-key and token-based authentication. - Token-based auth (for K8s jobs): client = RCABenchClient(base_url="...", token="...") or via environment variable RCABENCH_TOKEN - - Username/password auth (for interactive use): - client = RCABenchClient(base_url="...", username="...", password="...") - or via environment variables RCABENCH_USERNAME, RCABENCH_PASSWORD + - Access-key auth (recommended for SDK use): + client = RCABenchClient(base_url="...", access_key="...", secret_key="...") + or via environment variables RCABENCH_ACCESS_KEY, RCABENCH_SECRET_KEY """ _instances: dict[tuple[str, str, str | None], "RCABenchClient"] = {} _sessions: dict[tuple[str, str, str | None], SessionData] = {} + _token_exchange_path = "/api/v2/auth/access-key/token" def __new__( cls, base_url: str | None = None, - username: str | None = None, - password: str | None = None, + access_key: str | None = None, + secret_key: str | None = None, token: str | None = None, ): # Parse actual configuration values actual_base_url = base_url or os.getenv("RCABENCH_BASE_URL") actual_token = token or os.getenv("RCABENCH_TOKEN") - actual_username = username or os.getenv("RCABENCH_USERNAME") - actual_password = password or os.getenv("RCABENCH_PASSWORD") + actual_access_key = access_key or os.getenv("RCABENCH_ACCESS_KEY") + actual_secret_key = secret_key or os.getenv("RCABENCH_SECRET_KEY") assert actual_base_url is not None, "base_url or RCABENCH_BASE_URL is not set" - # Token auth takes precedence over username/password + # Token auth takes precedence over access-key authentication if actual_token: instance_key = (actual_base_url, actual_token, None) else: - assert actual_username is not None, "username or RCABENCH_USERNAME is not set (or use token/RCABENCH_TOKEN)" - assert actual_password is not None, "password or RCABENCH_PASSWORD is not set (or use token/RCABENCH_TOKEN)" - instance_key = (actual_base_url, actual_username, actual_password) + assert actual_access_key is not None, ( + "access_key or RCABENCH_ACCESS_KEY is not set (or use token/RCABENCH_TOKEN)" + ) + assert actual_secret_key is not None, ( + "secret_key or RCABENCH_SECRET_KEY is not set (or use token/RCABENCH_TOKEN)" + ) + instance_key = (actual_base_url, actual_access_key, actual_secret_key) if instance_key not in cls._instances: instance = super().__new__(cls) @@ -64,8 +72,8 @@ def __new__( def __init__( self, base_url: str | None = None, - username: str | None = None, - password: str | None = None, + access_key: str | None = None, + secret_key: str | None = None, token: str | None = None, ): # Avoid duplicate initialization of the same instance @@ -74,8 +82,8 @@ def __init__( self.base_url = base_url or os.getenv("RCABENCH_BASE_URL") self.token = token or os.getenv("RCABENCH_TOKEN") - self.username = username or os.getenv("RCABENCH_USERNAME") - self.password = password or os.getenv("RCABENCH_PASSWORD") + self.access_key = access_key or os.getenv("RCABENCH_ACCESS_KEY") + self.secret_key = secret_key or os.getenv("RCABENCH_SECRET_KEY") assert self.base_url is not None, "base_url or RCABENCH_BASE_URL is not set" @@ -83,9 +91,13 @@ def __init__( if self.token: self.instance_key = (self.base_url, self.token, None) else: - assert self.username is not None, "username or RCABENCH_USERNAME is not set (or use token/RCABENCH_TOKEN)" - assert self.password is not None, "password or RCABENCH_PASSWORD is not set (or use token/RCABENCH_TOKEN)" - self.instance_key = (self.base_url, self.username, self.password) + assert self.access_key is not None, ( + "access_key or RCABENCH_ACCESS_KEY is not set (or use token/RCABENCH_TOKEN)" + ) + assert self.secret_key is not None, ( + "secret_key or RCABENCH_SECRET_KEY is not set (or use token/RCABENCH_TOKEN)" + ) + self.instance_key = (self.base_url, self.access_key, self.secret_key) self._initialized = True @@ -110,27 +122,40 @@ def _is_session_valid(self) -> bool: return session_data.access_token is not None def _authenticate(self) -> None: - """Authenticate using either token or username/password""" + """Authenticate using either token or access-key credentials.""" if self.token: - # Direct token authentication (for K8s jobs using service tokens) + # Direct token authentication. self._sessions[self.instance_key] = SessionData( access_token=self.token, api_client=None, ) else: - # Username/password login - self._login() + self._exchange_access_key_token() - def _login(self) -> None: - """Login using username and password""" + def _exchange_access_key_token(self) -> None: + """Exchange access_key/secret_key for a bearer token.""" config = Configuration(host=self.base_url) with ApiClient(config) as api_client: auth_api = AuthenticationApi(api_client) assert self.base_url is not None - assert self.username is not None - assert self.password is not None - login_request = LoginReq(username=self.username, password=self.password) - response = auth_api.login(request=login_request) + assert self.access_key is not None + assert self.secret_key is not None + timestamp = str(int(time.time())) + nonce = secrets.token_hex(16) + signature = self._sign_access_key_request( + secret_key=self.secret_key, + method="POST", + path=self._token_exchange_path, + access_key=self.access_key, + timestamp=timestamp, + nonce=nonce, + ) + response = auth_api.exchange_access_key_token( + x_access_key=self.access_key, + x_timestamp=timestamp, + x_nonce=nonce, + x_signature=signature, + ) assert response.data is not None # Store session information in class-level cache @@ -162,6 +187,22 @@ def _get_authenticated_client(self) -> ApiClient: def get_client(self) -> ApiClient: return self._get_authenticated_client() + @staticmethod + def _sign_access_key_request( + secret_key: str, + method: str, + path: str, + access_key: str, + timestamp: str, + nonce: str, + ) -> str: + canonical = "\n".join([method.upper(), path, access_key, timestamp, nonce]) + return hmac_new( + secret_key.encode("utf-8"), + canonical.encode("utf-8"), + sha256, + ).hexdigest() + @classmethod def clear_sessions(cls): cls._sessions.clear() diff --git a/src/app/app.go b/src/app/app.go new file mode 100644 index 00000000..d733bb9d --- /dev/null +++ b/src/app/app.go @@ -0,0 +1,32 @@ +package app + +import ( + buildkitinfra "aegis/infra/buildkit" + configinfra "aegis/infra/config" + dbinfra "aegis/infra/db" + etcdinfra "aegis/infra/etcd" + harborinfra "aegis/infra/harbor" + helminfra "aegis/infra/helm" + loggerinfra "aegis/infra/logger" + lokiinfra "aegis/infra/loki" + redisinfra "aegis/infra/redis" + tracinginfra "aegis/infra/tracing" + + "go.uber.org/fx" +) + +func CommonOptions(confPath string) fx.Option { + return fx.Options( + fx.Supply(configinfra.Params{Path: confPath}), + loggerinfra.Module, + configinfra.Module, + dbinfra.Module, + redisinfra.Module, + etcdinfra.Module, + harborinfra.Module, + helminfra.Module, + buildkitinfra.Module, + lokiinfra.Module, + tracinginfra.Module, + ) +} diff --git a/src/app/both.go b/src/app/both.go new file mode 100644 index 00000000..42a78537 --- /dev/null +++ b/src/app/both.go @@ -0,0 +1,38 @@ +package app + +import ( + chaosinfra "aegis/infra/chaos" + k8sinfra "aegis/infra/k8s" + runtimeinfra "aegis/infra/runtime" + controllerinterface "aegis/interface/controller" + httpinterface "aegis/interface/http" + receiverinterface "aegis/interface/receiver" + workerinterface "aegis/interface/worker" + "aegis/service/consumer" + + "go.uber.org/fx" +) + +func BothOptions(confPath string, port string) fx.Option { + return fx.Options( + CommonOptions(confPath), + runtimeinfra.Module, + chaosinfra.Module, + k8sinfra.Module, + fx.Provide( + consumer.NewMonitor, + fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), + fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), + fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), + consumer.NewFaultBatchManager, + newProducerInitializer, + ), + ProducerHTTPModules(), + fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), + httpinterface.Module, + workerinterface.Module, + controllerinterface.Module, + receiverinterface.Module, + fx.Invoke(registerProducerInitialization), + ) +} diff --git a/src/app/consumer.go b/src/app/consumer.go new file mode 100644 index 00000000..733c51c5 --- /dev/null +++ b/src/app/consumer.go @@ -0,0 +1,32 @@ +package app + +import ( + chaosinfra "aegis/infra/chaos" + k8sinfra "aegis/infra/k8s" + runtimeinfra "aegis/infra/runtime" + controllerinterface "aegis/interface/controller" + receiverinterface "aegis/interface/receiver" + workerinterface "aegis/interface/worker" + "aegis/service/consumer" + + "go.uber.org/fx" +) + +func ConsumerOptions(confPath string) fx.Option { + return fx.Options( + CommonOptions(confPath), + runtimeinfra.Module, + chaosinfra.Module, + k8sinfra.Module, + fx.Provide( + consumer.NewMonitor, + fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), + fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), + fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), + consumer.NewFaultBatchManager, + ), + workerinterface.Module, + controllerinterface.Module, + receiverinterface.Module, + ) +} diff --git a/src/app/http_modules.go b/src/app/http_modules.go new file mode 100644 index 00000000..3a542431 --- /dev/null +++ b/src/app/http_modules.go @@ -0,0 +1,53 @@ +package app + +import ( + authmodule "aegis/module/auth" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + labelmodule "aegis/module/label" + metricmodule "aegis/module/metric" + notificationmodule "aegis/module/notification" + projectmodule "aegis/module/project" + rbacmodule "aegis/module/rbac" + sdkmodule "aegis/module/sdk" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" + teammodule "aegis/module/team" + tracemodule "aegis/module/trace" + usermodule "aegis/module/user" + "aegis/router" + + "go.uber.org/fx" +) + +func ProducerHTTPModules() fx.Option { + return fx.Options( + authmodule.Module, + chaossystemmodule.Module, + containermodule.Module, + datasetmodule.Module, + evaluationmodule.Module, + executionmodule.Module, + groupmodule.Module, + injectionmodule.Module, + labelmodule.Module, + metricmodule.Module, + notificationmodule.Module, + projectmodule.Module, + rbacmodule.Module, + sdkmodule.Module, + systemmodule.Module, + systemmetricmodule.Module, + taskmodule.Module, + teammodule.Module, + tracemodule.Module, + usermodule.Module, + router.Module, + ) +} diff --git a/src/app/options.go b/src/app/options.go new file mode 100644 index 00000000..6029b857 --- /dev/null +++ b/src/app/options.go @@ -0,0 +1,13 @@ +package app + +import "strings" + +func normalizeAddr(port string) string { + if port == "" { + return ":8080" + } + if strings.HasPrefix(port, ":") { + return port + } + return ":" + port +} diff --git a/src/app/producer.go b/src/app/producer.go new file mode 100644 index 00000000..a504a336 --- /dev/null +++ b/src/app/producer.go @@ -0,0 +1,22 @@ +package app + +import ( + chaosinfra "aegis/infra/chaos" + k8sinfra "aegis/infra/k8s" + httpinterface "aegis/interface/http" + + "go.uber.org/fx" +) + +func ProducerOptions(confPath string, port string) fx.Option { + return fx.Options( + CommonOptions(confPath), + chaosinfra.Module, + k8sinfra.Module, + fx.Provide(newProducerInitializer), + ProducerHTTPModules(), + fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), + httpinterface.Module, + fx.Invoke(registerProducerInitialization), + ) +} diff --git a/src/app/producer_init.go b/src/app/producer_init.go new file mode 100644 index 00000000..fb17006a --- /dev/null +++ b/src/app/producer_init.go @@ -0,0 +1,44 @@ +package app + +import ( + "context" + + etcdinfra "aegis/infra/etcd" + redisinfra "aegis/infra/redis" + commonservice "aegis/service/common" + "aegis/service/initialization" + "aegis/utils" + + "go.uber.org/fx" + "gorm.io/gorm" +) + +type ProducerInitializer struct { + etcd *etcdinfra.Gateway + redis *redisinfra.Gateway + db *gorm.DB + StartFunc func(context.Context) error +} + +func newProducerInitializer(etcd *etcdinfra.Gateway, redis *redisinfra.Gateway, db *gorm.DB) *ProducerInitializer { + return &ProducerInitializer{etcd: etcd, redis: redis, db: db} +} + +func (i *ProducerInitializer) start(ctx context.Context) error { + if i.StartFunc != nil { + return i.StartFunc(ctx) + } + if err := initialization.InitializeProducer(i.db, i.redis, commonservice.NewConfigUpdateListener(ctx, i.db, i.etcd)); err != nil { + return err + } + utils.InitValidator() + return nil +} + +func registerProducerInitialization(lc fx.Lifecycle, initializer *ProducerInitializer) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return initializer.start(ctx) + }, + }) +} diff --git a/src/app/startup_smoke_test.go b/src/app/startup_smoke_test.go new file mode 100644 index 00000000..9c594984 --- /dev/null +++ b/src/app/startup_smoke_test.go @@ -0,0 +1,331 @@ +package app + +import ( + "context" + "fmt" + "net" + "net/http" + "sync/atomic" + "testing" + "time" + + buildkitinfra "aegis/infra/buildkit" + etcdinfra "aegis/infra/etcd" + harborinfra "aegis/infra/harbor" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + lokiinfra "aegis/infra/loki" + redisinfra "aegis/infra/redis" + controllerinterface "aegis/interface/controller" + httpinterface "aegis/interface/http" + receiverinterface "aegis/interface/receiver" + workerinterface "aegis/interface/worker" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/redis/go-redis/v9" + clientv3 "go.etcd.io/etcd/client/v3" + "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/fx" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "k8s.io/client-go/rest" +) + +type smokeLifecycleSpies struct { + producerStarts int32 + workerStarts int32 + workerStops int32 + controllerStarts int32 + controllerStops int32 + receiverStarts int32 + receiverStops int32 +} + +func newSmokeDB(t *testing.T) (*gorm.DB, func()) { + t.Helper() + + sqlDB, _, err := sqlmock.New() + if err != nil { + t.Fatalf("create sqlmock: %v", err) + } + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + if err != nil { + _ = sqlDB.Close() + t.Fatalf("open gorm db: %v", err) + } + + return db, func() { + _ = sqlDB.Close() + } +} + +func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, func()) { + t.Helper() + + db, cleanupDB := newSmokeDB(t) + redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}) + redisGateway := redisinfra.NewGateway(redisClient) + etcdClient := &clientv3.Client{} + etcdGateway := etcdinfra.NewGateway(etcdClient) + traceProvider := trace.NewTracerProvider() + controller := &k8sinfra.Controller{} + k8sGateway := k8sinfra.NewGateway(controller) + + producerInitializer := &ProducerInitializer{StartFunc: func(context.Context) error { + if spies != nil { + atomic.AddInt32(&spies.producerStarts, 1) + } + return nil + }} + workerLifecycle := &workerinterface.Lifecycle{ + StartFunc: func(context.Context) error { + if spies != nil { + atomic.AddInt32(&spies.workerStarts, 1) + } + return nil + }, + StopFunc: func() { + if spies != nil { + atomic.AddInt32(&spies.workerStops, 1) + } + }, + } + controllerLifecycle := &controllerinterface.Lifecycle{ + RunFunc: func(context.Context, context.CancelFunc) error { + if spies != nil { + atomic.AddInt32(&spies.controllerStarts, 1) + } + return nil + }, + StopFunc: func() { + if spies != nil { + atomic.AddInt32(&spies.controllerStops, 1) + } + }, + } + receiverLifecycle := &receiverinterface.Lifecycle{ + StartFunc: func(context.Context) error { + if spies != nil { + atomic.AddInt32(&spies.receiverStarts, 1) + } + return nil + }, + StopFunc: func() { + if spies != nil { + atomic.AddInt32(&spies.receiverStops, 1) + } + }, + } + + return fx.Replace( + db, + redisGateway, + redisClient, + etcdGateway, + etcdClient, + &lokiinfra.Client{}, + traceProvider, + &rest.Config{}, + controller, + k8sGateway, + harborinfra.NewGateway(), + helminfra.NewGateway(), + buildkitinfra.NewGateway(), + producerInitializer, + workerLifecycle, + controllerLifecycle, + receiverLifecycle, + ), func() { + redisClient.Close() + traceProvider.Shutdown(context.Background()) + cleanupDB() + } +} + +func startAndStopApp(t *testing.T, option fx.Option) { + t.Helper() + + app := fx.New(option) + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := app.Start(startCtx); err != nil { + t.Fatalf("app start failed: %v", err) + } + + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := app.Stop(stopCtx); err != nil { + t.Fatalf("app stop failed: %v", err) + } +} + +func reserveLoopbackAddr(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on loopback: %v", err) + } + addr := listener.Addr().String() + if err := listener.Close(); err != nil { + t.Fatalf("close reserved listener: %v", err) + } + return addr +} + +func waitForHTTPStatus(t *testing.T, client *http.Client, method, url string, want int) { + t.Helper() + + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + req, err := http.NewRequest(method, url, nil) + if err != nil { + t.Fatalf("create request %s %s: %v", method, url, err) + } + + resp, err := client.Do(req) + if err == nil { + _ = resp.Body.Close() + if resp.StatusCode == want { + return + } + } + time.Sleep(50 * time.Millisecond) + } + + req, _ := http.NewRequest(method, url, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request %s %s failed: %v", method, url, err) + } + defer resp.Body.Close() + t.Fatalf("expected %d from %s %s, got %d", want, method, url, resp.StatusCode) +} + +func requireLifecycleCallCount(t *testing.T, name string, got *int32, want int32) { + t.Helper() + + if actual := atomic.LoadInt32(got); actual != want { + t.Fatalf("expected %s call count %d, got %d", name, want, actual) + } +} + +func TestProducerOptionsStartStopSmoke(t *testing.T) { + replacements, cleanup := newSmokeReplacements(t, nil) + defer cleanup() + + startAndStopApp(t, fx.Options( + ProducerOptions("..", "0"), + replacements, + )) +} + +func TestConsumerOptionsStartStopSmoke(t *testing.T) { + replacements, cleanup := newSmokeReplacements(t, nil) + defer cleanup() + + startAndStopApp(t, fx.Options( + ConsumerOptions(".."), + replacements, + )) +} + +func TestBothOptionsStartStopSmoke(t *testing.T) { + replacements, cleanup := newSmokeReplacements(t, nil) + defer cleanup() + + startAndStopApp(t, fx.Options( + BothOptions("..", "0"), + replacements, + )) +} + +func TestProducerOptionsHTTPIntegrationSmoke(t *testing.T) { + replacements, cleanup := newSmokeReplacements(t, nil) + defer cleanup() + + addr := reserveLoopbackAddr(t) + app := fx.New( + ProducerOptions("..", "0"), + replacements, + fx.Replace(httpinterface.ServerConfig{Addr: addr}), + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := app.Start(startCtx); err != nil { + t.Fatalf("app start failed: %v", err) + } + defer func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := app.Stop(stopCtx); err != nil { + t.Fatalf("app stop failed: %v", err) + } + }() + + client := &http.Client{Timeout: time.Second} + baseURL := fmt.Sprintf("http://%s", addr) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) +} + +func TestConsumerOptionsLifecycleIntegrationSmoke(t *testing.T) { + spies := &smokeLifecycleSpies{} + replacements, cleanup := newSmokeReplacements(t, spies) + defer cleanup() + + startAndStopApp(t, fx.Options( + ConsumerOptions(".."), + replacements, + )) + + requireLifecycleCallCount(t, "worker start", &spies.workerStarts, 1) + requireLifecycleCallCount(t, "worker stop", &spies.workerStops, 1) + requireLifecycleCallCount(t, "controller start", &spies.controllerStarts, 1) + requireLifecycleCallCount(t, "controller stop", &spies.controllerStops, 1) + requireLifecycleCallCount(t, "receiver start", &spies.receiverStarts, 1) + requireLifecycleCallCount(t, "receiver stop", &spies.receiverStops, 1) +} + +func TestBothOptionsHTTPAndLifecycleIntegrationSmoke(t *testing.T) { + spies := &smokeLifecycleSpies{} + replacements, cleanup := newSmokeReplacements(t, spies) + defer cleanup() + + addr := reserveLoopbackAddr(t) + app := fx.New( + BothOptions("..", "0"), + replacements, + fx.Replace(httpinterface.ServerConfig{Addr: addr}), + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := app.Start(startCtx); err != nil { + t.Fatalf("app start failed: %v", err) + } + + client := &http.Client{Timeout: time.Second} + baseURL := fmt.Sprintf("http://%s", addr) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) + requireLifecycleCallCount(t, "producer start", &spies.producerStarts, 1) + requireLifecycleCallCount(t, "worker start", &spies.workerStarts, 1) + requireLifecycleCallCount(t, "controller start", &spies.controllerStarts, 1) + requireLifecycleCallCount(t, "receiver start", &spies.receiverStarts, 1) + + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := app.Stop(stopCtx); err != nil { + t.Fatalf("app stop failed: %v", err) + } + + requireLifecycleCallCount(t, "worker stop", &spies.workerStops, 1) + requireLifecycleCallCount(t, "controller stop", &spies.controllerStops, 1) + requireLifecycleCallCount(t, "receiver stop", &spies.receiverStops, 1) +} diff --git a/src/app/startup_validate_test.go b/src/app/startup_validate_test.go new file mode 100644 index 00000000..02d524b4 --- /dev/null +++ b/src/app/startup_validate_test.go @@ -0,0 +1,25 @@ +package app + +import ( + "testing" + + "go.uber.org/fx" +) + +func TestProducerOptionsValidate(t *testing.T) { + if err := fx.ValidateApp(ProducerOptions("..", "0")); err != nil { + t.Fatalf("producer fx graph validation failed: %v", err) + } +} + +func TestConsumerOptionsValidate(t *testing.T) { + if err := fx.ValidateApp(ConsumerOptions("..")); err != nil { + t.Fatalf("consumer fx graph validation failed: %v", err) + } +} + +func TestBothOptionsValidate(t *testing.T) { + if err := fx.ValidateApp(BothOptions("..", "0")); err != nil { + t.Fatalf("both fx graph validation failed: %v", err) + } +} diff --git a/src/client/debug/status_registry.go b/src/client/debug/status_registry.go deleted file mode 100644 index 539d6f7b..00000000 --- a/src/client/debug/status_registry.go +++ /dev/null @@ -1,247 +0,0 @@ -package debug - -import ( - "context" - "encoding/json" - "fmt" - "sync" - "sync/atomic" - "time" - - "aegis/client" - "aegis/utils" - - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" -) - -type EntryType string - -const ( - EntryTypeReadOnly EntryType = "readonly" - EntryTypeReadWrite EntryType = "readwrite" - - HistoryKey string = "rcabench:debug:history" - - DefaultHistoryLimit int = 100 -) - -type DebugEntry struct { - Name string `json:"name"` - Description string `json:"description"` - Category string `json:"category"` - Type EntryType `json:"type"` // "readonly", "readwrite", "action", "health_check" - GetFunc func() (any, error) `json:"-"` - SetFunc func(any) error `json:"-"` - AutoFix bool `json:"auto_fix"` // Whether auto-fix is supported -} - -// HistoryEntry operation history -type HistoryEntry struct { - ID string `json:"id"` - Timestamp time.Time `json:"timestamp"` - Action string `json:"action"` - Target string `json:"target"` - OldValue any `json:"old_value,omitempty"` - NewValue any `json:"new_value,omitempty"` - Success bool `json:"success"` - Error string `json:"error,omitempty"` -} - -type DebugRegistry struct { - mu sync.RWMutex - entries map[string]*DebugEntry - - ctx context.Context - cancel context.CancelFunc - - // State variable - debugMode int32 // atomic operation -} - -func NewDebugRegistry() *DebugRegistry { - ctx, cancel := context.WithCancel(context.Background()) - - registry := &DebugRegistry{ - entries: make(map[string]*DebugEntry), - ctx: ctx, - cancel: cancel, - } - registry.registerEntries() - - return registry -} - -func (r *DebugRegistry) Get(name string) (map[string]any, error) { - r.mu.RLock() - entry, exists := r.entries[name] - r.mu.RUnlock() - - if !exists { - return nil, fmt.Errorf("entry %s not found", name) - } - - entryData := utils.StructToMap(entry) - if entry.GetFunc != nil { - value, err := entry.GetFunc() - if err != nil { - entryData["value"] = fmt.Sprintf("Error: %v", err) - entryData["error"] = true - } else { - entryData["value"] = value - entryData["error"] = false - } - } - - return entryData, nil -} - -func (r *DebugRegistry) GetAll() map[string]any { - r.mu.RLock() - defer r.mu.RUnlock() - - result := make(map[string]any) - for name, entry := range r.entries { - entryData := utils.StructToMap(entry) - if entry.GetFunc != nil { - if value, err := entry.GetFunc(); err != nil { - entryData["value"] = fmt.Sprintf("Error: %v", err) - entryData["error"] = true - } else { - entryData["value"] = value - entryData["error"] = false - } - } - - result[name] = entryData - } - - return result -} - -func (r *DebugRegistry) GetHistory(limit int) ([]HistoryEntry, error) { - if limit <= 0 { - limit = DefaultHistoryLimit - } - - streamResult, err := client.GetRedisClient().XRead(r.ctx, &redis.XReadArgs{ - Streams: []string{HistoryKey, "0"}, - Count: int64(limit), - Block: -1, - }).Result() - if err != nil { - return nil, fmt.Errorf("failed to read history from redis: %v", err) - } - - errorTemplate := "invalid or missing '%s' in task payload" - - var history []HistoryEntry - for _, result := range streamResult { - for _, message := range result.Messages { - entry, err := utils.MapToStruct[HistoryEntry](message.Values, "", errorTemplate) - if err != nil { - return nil, fmt.Errorf("failed to parse history entry: %v", err) - } - - history = append(history, *entry) - } - } - - return history, nil -} - -func (r *DebugRegistry) Register(entry *DebugEntry) { - r.mu.Lock() - defer r.mu.Unlock() - r.entries[entry.Name] = entry -} - -func (r *DebugRegistry) Set(name string, value any) error { - r.mu.RLock() - entry, exists := r.entries[name] - r.mu.RUnlock() - - if !exists { - return fmt.Errorf("entry %s not found", name) - } - - if entry.Type == EntryTypeReadOnly { - return fmt.Errorf("entry %s is readonly", name) - } - - if entry.SetFunc == nil { - return fmt.Errorf("set function not implemented for %s", name) - } - - var oldValue any - if entry.GetFunc != nil { - oldValue, _ = entry.GetFunc() - } - - err := entry.SetFunc(value) - r.addHistory(HistoryEntry{ - ID: fmt.Sprintf("%s_%d", name, time.Now().UnixNano()), - Timestamp: time.Now(), - Action: "set", - Target: name, - OldValue: oldValue, - NewValue: value, - Success: err == nil, - Error: func() string { - if err != nil { - return err.Error() - } - return "" - }(), - }) - - return err -} - -func (r *DebugRegistry) addHistory(entry HistoryEntry) { - entryJSON, err := json.Marshal(entry) - if err != nil { - return - } - - _, err = client.GetRedisClient().XAdd(r.ctx, &redis.XAddArgs{ - Stream: HistoryKey, - MaxLen: 10000, - Approx: true, - ID: "*", - Values: entryJSON, - }).Result() - if err != nil { - logrus.Errorf("failed to add event to Redis stream %s: %v", HistoryKey, err) - } -} - -func (r *DebugRegistry) registerEntries() { - r.Register(&DebugEntry{ - Name: "debug_mode", - Description: "Debug mode status", - Category: "system", - Type: EntryTypeReadWrite, - GetFunc: func() (any, error) { - return atomic.LoadInt32(&r.debugMode) == 1, nil - }, - SetFunc: func(value any) error { - var newValue int32 - switch v := value.(type) { - case bool: - if v { - newValue = 1 - } - case string: - if v == "true" || v == "1" { - newValue = 1 - } - default: - return fmt.Errorf("invalid value type: %T", value) - } - - atomic.StoreInt32(&r.debugMode, newValue) - return nil - }, - }) -} diff --git a/src/client/etcd_client.go b/src/client/etcd_client.go deleted file mode 100644 index c4e214c1..00000000 --- a/src/client/etcd_client.go +++ /dev/null @@ -1,160 +0,0 @@ -package client - -import ( - "context" - "fmt" - "sync" - "time" - - "aegis/config" - - "github.com/sirupsen/logrus" - clientv3 "go.etcd.io/etcd/client/v3" -) - -// Singleton pattern etcd client -var ( - etcdClient *clientv3.Client - etcdOnce sync.Once -) - -// GetEtcdClient returns the singleton etcd client instance -// It initializes the client on first call using configuration from config package -func GetEtcdClient() *clientv3.Client { - etcdOnce.Do(func() { - endpoints := config.GetStringSlice("etcd.endpoints") - if len(endpoints) == 0 { - endpoints = []string{"localhost:2379"} - logrus.Warn("etcd.endpoints not configured, using default: localhost:2379") - } - - logrus.Infof("Connecting to etcd endpoints: %v", endpoints) - - var err error - etcdClient, err = clientv3.New(clientv3.Config{ - Endpoints: endpoints, - DialTimeout: 5 * time.Second, - Username: config.GetString("etcd.username"), - Password: config.GetString("etcd.password"), - }) - if err != nil { - logrus.Fatalf("Failed to connect to etcd: %v", err) - } - - // Test connection - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - if _, err := etcdClient.Status(ctx, endpoints[0]); err != nil { - logrus.Fatalf("Failed to verify etcd connection: %v", err) - } - - logrus.Info("Successfully connected to etcd") - }) - return etcdClient -} - -// CloseEtcdClient closes the etcd client connection -// Should be called during application shutdown -func CloseEtcdClient() error { - if etcdClient != nil { - logrus.Info("Closing etcd client connection") - return etcdClient.Close() - } - return nil -} - -// EtcdPut writes a key-value pair to etcd with optional TTL -func EtcdPut(ctx context.Context, key, value string, ttl time.Duration) error { - client := GetEtcdClient() - - if ttl > 0 { - // Create lease for TTL - lease, err := client.Grant(ctx, int64(ttl.Seconds())) - if err != nil { - return fmt.Errorf("failed to create lease: %w", err) - } - - _, err = client.Put(ctx, key, value, clientv3.WithLease(lease.ID)) - if err != nil { - return fmt.Errorf("failed to put key with lease: %w", err) - } - } else { - _, err := client.Put(ctx, key, value) - if err != nil { - return fmt.Errorf("failed to put key: %w", err) - } - } - - return nil -} - -// EtcdGet retrieves a value from etcd by key -func EtcdGet(ctx context.Context, key string) (string, error) { - client := GetEtcdClient() - - resp, err := client.Get(ctx, key) - if err != nil { - return "", fmt.Errorf("failed to get key: %w", err) - } - - if len(resp.Kvs) == 0 { - return "", fmt.Errorf("key not found: %s", key) - } - - return string(resp.Kvs[0].Value), nil -} - -// EtcdDelete deletes a key from etcd -func EtcdDelete(ctx context.Context, key string) error { - client := GetEtcdClient() - - _, err := client.Delete(ctx, key) - if err != nil { - return fmt.Errorf("failed to delete key: %w", err) - } - - return nil -} - -// EtcdWatch watches for changes on a key or prefix -// Returns a channel that receives watch events -func EtcdWatch(ctx context.Context, key string, withPrefix bool) clientv3.WatchChan { - client := GetEtcdClient() - - var opts []clientv3.OpOption - if withPrefix { - opts = append(opts, clientv3.WithPrefix()) - } - - return client.Watch(ctx, key, opts...) -} - -// EtcdGetWithRevision retrieves a value and its revision -func EtcdGetWithRevision(ctx context.Context, key string) (string, int64, error) { - client := GetEtcdClient() - - resp, err := client.Get(ctx, key) - if err != nil { - return "", 0, fmt.Errorf("failed to get key: %w", err) - } - - if len(resp.Kvs) == 0 { - return "", 0, fmt.Errorf("key not found: %s", key) - } - - return string(resp.Kvs[0].Value), resp.Kvs[0].ModRevision, nil -} - -// EtcdWatchFromRevision watches for changes starting from a specific revision -func EtcdWatchFromRevision(ctx context.Context, key string, revision int64, withPrefix bool) clientv3.WatchChan { - client := GetEtcdClient() - - var opts []clientv3.OpOption - if withPrefix { - opts = append(opts, clientv3.WithPrefix()) - } - opts = append(opts, clientv3.WithRev(revision)) - - return client.Watch(ctx, key, opts...) -} diff --git a/src/client/harbor_client.go b/src/client/harbor_client.go deleted file mode 100644 index b8581240..00000000 --- a/src/client/harbor_client.go +++ /dev/null @@ -1,151 +0,0 @@ -package client - -import ( - "context" - "fmt" - "sort" - "sync" - "time" - - "github.com/goharbor/go-client/pkg/harbor" - "github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact" - "github.com/goharbor/go-client/pkg/sdk/v2.0/models" - - "aegis/config" - "aegis/consts" -) - -// Singleton pattern Harbor client -var ( - harborClient *HarborClient - harborOnce sync.Once -) - -type HarborClient struct { - registry string - namespace string - username string - password string - clientSet *harbor.ClientSet -} - -func GetHarborClient() *HarborClient { - harborOnce.Do(func() { - registry := config.GetString("harbor.registry") - namespace := config.GetString("harbor.namespace") - username := config.GetString("harbor.username") - password := config.GetString("harbor.password") - - // Build complete Harbor URL - harborURL := fmt.Sprintf("http://%s", registry) - - clientSet, err := harbor.NewClientSet(&harbor.ClientSetConfig{ - URL: harborURL, - Username: username, - Password: password, - Insecure: true, // Adjust as needed - }) - if err != nil { - // If client creation fails, log error but continue using nil client - // Will return error in actual methods - harborClient = &HarborClient{ - registry: registry, - namespace: namespace, - username: username, - password: password, - clientSet: nil, - } - return - } - - harborClient = &HarborClient{ - registry: registry, - namespace: namespace, - username: username, - password: password, - clientSet: clientSet, - } - }) - return harborClient -} - -func (h *HarborClient) GetLatestTag(image string) (string, error) { - if h.clientSet == nil { - return "", fmt.Errorf("harbor client is not initialized") - } - - ctx, cancel := context.WithTimeout(context.Background(), consts.HarborTimeout*consts.HarborTimeUnit) - defer cancel() - - params := &artifact.ListArtifactsParams{ - ProjectName: h.namespace, - RepositoryName: image, - Context: ctx, - } - - response, err := h.clientSet.V2().Artifact.ListArtifacts(ctx, params) - if err != nil { - return "", fmt.Errorf("failed to list artifacts: %v", err) - } - - if len(response.Payload) == 0 { - return "", fmt.Errorf("no artifacts found for image %s", image) - } - - var allTags []*models.Tag - for _, artifact := range response.Payload { - if artifact.Tags != nil { - allTags = append(allTags, artifact.Tags...) - } - } - - if len(allTags) == 0 { - return "", fmt.Errorf("no tags found for image %s", image) - } - - sort.Slice(allTags, func(i, j int) bool { - return time.Time(allTags[i].PushTime).After(time.Time(allTags[j].PushTime)) - }) - - return allTags[0].Name, nil -} - -func (h *HarborClient) CheckImageExists(repository, tag string) (bool, error) { - if h.clientSet == nil { - return false, fmt.Errorf("harbor client is not initialized") - } - - ctx, cancel := context.WithTimeout(context.Background(), consts.HarborTimeout*consts.HarborTimeUnit) - defer cancel() - - params := &artifact.ListArtifactsParams{ - ProjectName: h.namespace, - RepositoryName: repository, - Context: ctx, - } - - response, err := h.clientSet.V2().Artifact.ListArtifacts(ctx, params) - if err != nil { - return false, nil - } - - if len(response.Payload) == 0 { - return false, nil - } - - if tag == "" || tag == consts.DefaultContainerTag { - return true, nil - } - - for _, artifact := range response.Payload { - if artifact.Tags != nil { - for _, t := range artifact.Tags { - if t.Name == tag { - return true, nil - } - } - } - } - - return false, nil -} diff --git a/src/client/helm.go b/src/client/helm.go deleted file mode 100644 index 9d8d5e2b..00000000 --- a/src/client/helm.go +++ /dev/null @@ -1,323 +0,0 @@ -package client - -import ( - "context" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "time" - - "aegis/config" - "aegis/tracing" - - "github.com/sirupsen/logrus" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/repo" - - "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/yaml" -) - -// HelmClient represents a client for interacting with Helm -type HelmClient struct { - namespace string - actionConfig *action.Configuration - settings *cli.EnvSettings -} - -// NewHelmClient creates a new Helm client with the specified namespace -func NewHelmClient(namespace string) (*HelmClient, error) { - settings := cli.New() - settings.SetNamespace(namespace) - settings.Debug = config.GetBool("helm.debug") - - actionConfig := new(action.Configuration) - configFlags := genericclioptions.NewConfigFlags(true) - configFlags.Namespace = &namespace - - if err := actionConfig.Init(configFlags, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { - return nil, fmt.Errorf("failed to initialize Helm action configuration: %w", err) - } - - return &HelmClient{ - namespace: namespace, - actionConfig: actionConfig, - settings: settings, - }, nil -} - -// AddRepo adds a Helm repository with the given name and URL -func (c *HelmClient) AddRepo(name, url string) error { - repoFile := c.settings.RepositoryConfig - - // Ensure the repository directory exists - err := os.MkdirAll(c.settings.RepositoryCache, 0755) - if err != nil && !os.IsExist(err) { - return fmt.Errorf("could not create repository cache directory: %w", err) - } - - // Check if repo file exists - b, err := os.ReadFile(repoFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("could not read repository file: %w", err) - } - - var f repo.File - if err == nil { - if err := yaml.Unmarshal(b, &f); err != nil { - return fmt.Errorf("cannot unmarshal repository file: %w", err) - } - } - - // Check if the repo already exists - if f.Has(name) { - if f.Get(name).URL != url { - f.Get(name).URL = url - } - - if err := f.WriteFile(repoFile, 0644); err != nil { - return fmt.Errorf("failed to write repository file: %w", err) - } - - logrus.Infof("Updated repository %s URL to %s", name, url) - return nil - } - - // Create new repository entry - entry := &repo.Entry{ - Name: name, - URL: url, - } - r, err := repo.NewChartRepository(entry, getter.All(c.settings)) - if err != nil { - return fmt.Errorf("failed to create chart repository: %w", err) - } - - if _, err := r.DownloadIndexFile(); err != nil { - return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached: %w", url, err) - } - - f.Update(entry) - if err := f.WriteFile(repoFile, 0644); err != nil { - return fmt.Errorf("failed to write repository file: %w", err) - } - - return nil -} - -// UpdateRepo updates all Helm repositories -func (c *HelmClient) UpdateRepo(name string) error { - repoFile := c.settings.RepositoryConfig - - // Read repo file - b, err := os.ReadFile(repoFile) - if err != nil { - return fmt.Errorf("could not read repository file: %w", err) - } - - var f repo.File - if err := yaml.Unmarshal(b, &f); err != nil { - return fmt.Errorf("cannot unmarshal repository file: %w", err) - } - - // Update each repository - for _, entry := range f.Repositories { - if name == entry.Name || name == "" { - logrus.Infof("Updating repository %s", entry.Name) - - r, err := repo.NewChartRepository(entry, getter.All(c.settings)) - if err != nil { - return fmt.Errorf("failed to create chart repository for %s: %w", entry.Name, err) - } - - if _, err := r.DownloadIndexFile(); err != nil { - return fmt.Errorf("failed to update repository %s: %w", entry.Name, err) - } - } - } - - return nil -} - -func (c *HelmClient) SearchRepo(repoName string) ([]*repo.Entry, error) { - repoFile := c.settings.RepositoryConfig - - b, err := os.ReadFile(repoFile) - if err != nil { - return nil, fmt.Errorf("could not read repository file: %w", err) - } - - var f repo.File - if err := yaml.Unmarshal(b, &f); err != nil { - return nil, fmt.Errorf("cannot unmarshal repository file: %w", err) - } - - var repos []*repo.Entry - for _, r := range f.Repositories { - if repoName == "" || r.Name == repoName { - repos = append(repos, r) - } - } - - return repos, nil -} - -func (c *HelmClient) IsReleaseInstalled(releaseName string) (bool, error) { - client := action.NewStatus(c.actionConfig) - - _, err := client.Run(releaseName) - if err != nil { - if strings.Contains(err.Error(), "not found") { - return false, nil - } - return false, fmt.Errorf("failed to get release status: %w", err) - } - - return true, nil -} - -func (c *HelmClient) UninstallRelease(releaseName string, timeout time.Duration) error { - client := action.NewUninstall(c.actionConfig) - client.Wait = true - client.Timeout = timeout - - _, err := client.Run(releaseName) - if err != nil { - if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "release: not found") { - logrus.Infof("Release %s is not installed, nothing to uninstall", releaseName) - return nil - } - - return fmt.Errorf("failed to uninstall release %s: %w", releaseName, err) - } - - return nil -} - -func (c *HelmClient) isChartCachedLocally(chartName string) (string, bool) { - // Check if it's an absolute path or relative path first - if _, err := os.Stat(chartName); err == nil { - abs, err := filepath.Abs(chartName) - if err == nil { - logrus.Infof("Found local chart at: %s", abs) - return abs, true - } - } - - // If it's not a local path, check the cache directory - // The cache directory structure is: {RepositoryCache}/{repo-name}/{chart-name}-{version}.tgz - // We need to check for the chart without knowing the exact version - cacheDir := c.settings.RepositoryCache - - // Try to find any cached version of this chart - // Chart name format: {repo}/{chart} or just {chart} - var searchPatterns []string - - if strings.Contains(chartName, "/") { - // Format like "train-ticket/trainticket" - parts := strings.Split(chartName, "/") - if len(parts) == 2 { - chartBaseName := parts[1] - // Look for patterns like: cache/{repo-hash}/{chart-name}-{version}.tgz - searchPatterns = append(searchPatterns, - fmt.Sprintf("%s/*/%s-*.tgz", cacheDir, chartBaseName), - fmt.Sprintf("%s/%s-*.tgz", cacheDir, chartBaseName), - ) - } - } else { - // Just chart name, search in all subdirectories - searchPatterns = append(searchPatterns, - fmt.Sprintf("%s/*/%s-*.tgz", cacheDir, chartName), - fmt.Sprintf("%s/%s-*.tgz", cacheDir, chartName), - ) - } - - // Check each pattern - for _, pattern := range searchPatterns { - matches, err := filepath.Glob(pattern) - if err == nil && len(matches) > 0 { - // Return the first (most recent if sorted) match - cachedPath := matches[0] - logrus.Infof("Found cached chart at: %s", cachedPath) - return cachedPath, true - } - } - - // Also check if the chart directory exists (for local development) - localChartDir := filepath.Join(cacheDir, chartName) - if stat, err := os.Stat(localChartDir); err == nil && stat.IsDir() { - logrus.Infof("Found cached chart directory at: %s", localChartDir) - return localChartDir, true - } - - return "", false -} - -func (c *HelmClient) InstallRelease(ctx context.Context, releaseName, chartName, version string, vals map[string]any, timeout time.Duration) error { - return tracing.WithSpan(ctx, func(ctx context.Context) error { - now := time.Now() - - defer func() { - log.Printf("InstallRelease took %s", time.Since(now)) - }() - - client := action.NewInstall(c.actionConfig) - client.ReleaseName = releaseName - client.Namespace = c.namespace - client.Wait = true - client.Timeout = timeout - client.CreateNamespace = true - client.Version = version - - var cp string - var err error - - // Check if chart is cached locally first - if cachedPath, isCached := c.isChartCachedLocally(chartName); isCached { - logrus.Infof("Using cached chart for %s at %s", chartName, cachedPath) - cp = cachedPath - } else { - logrus.Infof("Chart %s not found in cache, downloading...", chartName) - cp, err = client.LocateChart(chartName, c.settings) - if err != nil { - return fmt.Errorf("failed to locate chart %s: %w", chartName, err) - } - } - - chart, err := loader.Load(cp) - if err != nil { - return fmt.Errorf("failed to load chart %s: %w", chartName, err) - } - - _, err = client.Run(chart, vals) - if err != nil { - return fmt.Errorf("failed to install release %s: %v", releaseName, err) - } - - return nil - }) -} - -func (c *HelmClient) Install(ctx context.Context, releaseName, chartName, version string, values map[string]any, installTimeout, unInstallTimeout time.Duration) error { - installed, err := c.IsReleaseInstalled(releaseName) - if err != nil { - return err - } - - // If installed, uninstall it first - if installed { - logrus.Infof("Uninstalling existing %s release", releaseName) - if err := c.UninstallRelease(releaseName, unInstallTimeout); err != nil { - return err - } - } else { - logrus.Infof("No existing %s release found", releaseName) - } - - return c.InstallRelease(ctx, releaseName, chartName, version, values, installTimeout) -} diff --git a/src/client/helm_test.go b/src/client/helm_test.go deleted file mode 100644 index d22d9353..00000000 --- a/src/client/helm_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package client - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/cli" - "k8s.io/cli-runtime/pkg/genericclioptions" -) - -// mockActionConfig creates a mock Helm action configuration for testing -func mockActionConfig(t *testing.T) *action.Configuration { - actionConfig := new(action.Configuration) - configFlags := genericclioptions.NewConfigFlags(true) - namespace := "test-namespace" - configFlags.Namespace = &namespace - - // Use memory driver for testing to avoid real k8s connections - err := actionConfig.Init(configFlags, namespace, "memory", func(format string, v ...interface{}) { - t.Logf(format, v...) - }) - if err != nil { - t.Fatalf("Failed to initialize action config: %v", err) - } - - return actionConfig -} - -// createMockHelmClient creates a test HelmClient with mock configuration -func createMockHelmClient(t *testing.T) *HelmClient { - settings := cli.New() - namespace := "test-namespace" - settings.SetNamespace(namespace) - - // Create temporary directories for testing - tempDir := t.TempDir() - settings.RepositoryConfig = filepath.Join(tempDir, "repositories.yaml") - settings.RepositoryCache = filepath.Join(tempDir, "cache") - - return &HelmClient{ - namespace: namespace, - actionConfig: mockActionConfig(t), - settings: settings, - } -} - -func TestHelmClient_isChartCachedLocally(t *testing.T) { - tests := []struct { - name string - chartName string - expectFound bool - }{ - { - name: "chart does not exist in cache", - chartName: "non-existent-chart", - expectFound: false, - }, - { - name: "empty chart name", - chartName: "", - expectFound: false, - }, - { - name: "invalid chart name", - chartName: "invalid/chart/name", - expectFound: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := createMockHelmClient(t) - - gotPath, gotFound := client.isChartCachedLocally(tt.chartName) - - if gotFound != tt.expectFound { - t.Errorf("isChartCachedLocally() gotFound = %v, want %v", gotFound, tt.expectFound) - } - - if tt.expectFound { - if gotPath == "" { - t.Error("Expected non-empty chart path when chart is found") - } - - // Verify the chart path exists - if _, err := os.Stat(gotPath); err != nil { - t.Errorf("Chart path should exist: %v", err) - } - } else { - if gotPath != "" { - t.Errorf("Expected empty chart path when not found, got: %s", gotPath) - } - } - }) - } -} - -func TestHelmClient_isChartCachedLocally_FileSystem(t *testing.T) { - client := createMockHelmClient(t) - - // Test with a chart that definitely doesn't exist - path, found := client.isChartCachedLocally("definitely-non-existent-chart-12345") - if found { - t.Error("Should not find non-existent chart") - } - if path != "" { - t.Errorf("Path should be empty for non-existent chart, got: %s", path) - } -} - -func TestHelmClient_isChartCachedLocally_NoDownload(t *testing.T) { - client := createMockHelmClient(t) - - // This test ensures that the cache check doesn't trigger a download - // We test with a chart name that would normally trigger a download - startTime := time.Now() - - path, found := client.isChartCachedLocally("non-existent-repo/non-existent-chart") - - elapsed := time.Since(startTime) - - // The operation should be very fast since it's only checking local filesystem - if elapsed > 1*time.Second { - t.Errorf("Cache check took too long (%v), might be triggering download", elapsed) - } - - // Should not find the chart since it doesn't exist locally - if found { - t.Error("Should not find non-existent chart") - } - - if path != "" { - t.Errorf("Path should be empty for non-existent chart, got: %s", path) - } - - t.Logf("Cache check completed in %v (no download triggered)", elapsed) -} - -func TestHelmClient_InstallRelease_UsesCachedChart(t *testing.T) { - // This test verifies the method handles the basic flow - client := createMockHelmClient(t) - ctx := context.Background() - - // Create a simple test that verifies the method doesn't panic - // and handles the basic flow (though it will fail due to missing chart) - err := client.InstallRelease(ctx, "test-release", "non-existent-chart", "0.0.0.", map[string]any{}, 500*time.Second) - - // We expect an error since the chart doesn't exist, but it should be a specific error - if err == nil { - t.Error("Expected error for non-existent chart") - } - - if !strings.Contains(err.Error(), "failed to locate chart") { - t.Errorf("Expected 'failed to locate chart' error, got: %v", err) - } -} - -func TestHelmClient_NewHelmClient(t *testing.T) { - namespace := "test-namespace" - - // This test might fail in environments without proper k8s config - // but we can test the basic structure - client, err := NewHelmClient(namespace) - - if err != nil { - // If we can't create a real client (e.g., no k8s config), that's expected in test env - t.Logf("Expected error in test environment: %v", err) - return - } - - if client == nil { - t.Error("Expected non-nil client") - return - } - - if client.namespace != namespace { - t.Errorf("Expected namespace %s, got %s", namespace, client.namespace) - return - } - - if client.actionConfig == nil { - t.Error("Expected non-nil actionConfig") - return - } - - if client.settings == nil { - t.Error("Expected non-nil settings") - } -} diff --git a/src/client/k8s/client.go b/src/client/k8s/client.go deleted file mode 100644 index 65814784..00000000 --- a/src/client/k8s/client.go +++ /dev/null @@ -1,88 +0,0 @@ -package k8s - -import ( - "os" - "path/filepath" - "sync" - - "github.com/sirupsen/logrus" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" -) - -var ( - k8sRestConfig *rest.Config - k8sClient *kubernetes.Clientset - k8sDynamicClient *dynamic.DynamicClient - k8sController *Controller - - k8sRestConfigOnce sync.Once - k8sClientOnce sync.Once - k8sDynamicClientOnce sync.Once - controllerOnce sync.Once -) - -func GetK8sClient() *kubernetes.Clientset { - k8sClientOnce.Do(func() { - restConfig := GetK8sRestConfig() - clientset, err := kubernetes.NewForConfig(restConfig) - if err != nil { - logrus.Fatalf("failed to create Kubernetes clientset: %v", err) - } - - k8sClient = clientset - }) - return k8sClient -} - -func GetK8sDynamicClient() *dynamic.DynamicClient { - k8sDynamicClientOnce.Do(func() { - restConfig := GetK8sRestConfig() - dynamicClient, err := dynamic.NewForConfig(restConfig) - if err != nil { - logrus.Fatalf("failed to create Kubernetes dynamic client: %v", err) - } - - k8sDynamicClient = dynamicClient - }) - return k8sDynamicClient -} - -func GetK8sRestConfig() *rest.Config { - k8sRestConfigOnce.Do(func() { - var restConfig *rest.Config - var err error - var currentContext string - - restConfig, err = rest.InClusterConfig() - if err == nil { - logrus.Info("Successfully loaded In-Cluster Kubernetes configuration.") - currentContext = "In-Cluster" - k8sRestConfig = restConfig - logrus.Infof("Using Kubernetes Context: %s", currentContext) - return - } - - logrus.Warn("In-cluster config not found, trying kubeconfig file") - kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") - config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) - if err != nil { - logrus.Fatalf("Failed to load Kubernetes config: %v", err) - } - - k8sRestConfig = config - if k8sRestConfig == nil { - logrus.Fatalf("Failed to establish Kubernetes REST config: Neither In-Cluster nor external Kubeconfig available.") - } - }) - return k8sRestConfig -} - -func GetK8sController() *Controller { - controllerOnce.Do(func() { - k8sController = NewController() - }) - return k8sController -} diff --git a/src/client/k8s/k8s_test.go b/src/client/k8s/k8s_test.go deleted file mode 100644 index 5fc0f87b..00000000 --- a/src/client/k8s/k8s_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package k8s - -import ( - "aegis/config" - "aegis/utils" - "context" - "testing" - - "github.com/k0kubun/pp/v3" - corev1 "k8s.io/api/core/v1" -) - -func TestGetVolumeMountConfigs(t *testing.T) { - config.Init("../..") - - volumeMountConfigs := make([]VolumeMountConfig, 0) - mapData := config.GetMap("k8s.job.volume_mount") - for _, cfgData := range mapData { - cfg, err := utils.ConvertToType[VolumeMountConfig](cfgData) - if err != nil { - t.Errorf("invalid volume mount config %v: %v", cfgData, err) - } - - volumeMountConfigs = append(volumeMountConfigs, cfg) - } - - volumeMounts := []corev1.VolumeMount{} - volumes := []corev1.Volume{} - for _, cfg := range volumeMountConfigs { - volumeMounts = append(volumeMounts, cfg.GetVolumeMount()) - volumes = append(volumes, cfg.GetVolume()) - } - - pp.Println(volumeMountConfigs) //nolint:errcheck - pp.Println(volumeMounts) //nolint:errcheck - pp.Println(volumes) //nolint:errcheck -} - -func TestCreateGetDeleteK8sJob(t *testing.T) { - jobName := "example-job" - namespace := "default" - image := "busybox" - command := []string{"sh", "-c", "for i in $(seq 1 5); do echo \"Log line $i\"; sleep 1; done"} - restartPolicy := corev1.RestartPolicyNever - backoffLimit := int32(2) - parallelism := int32(2) - completions := int32(2) - - envVars := []corev1.EnvVar{ - {Name: "ENV_TEST", Value: "test"}, - } - - // Step 1: Create Job - if err := CreateJob(context.Background(), &JobConfig{ - Namespace: namespace, - JobName: jobName, - Image: image, - Command: command, - RestartPolicy: restartPolicy, - BackoffLimit: backoffLimit, - Parallelism: parallelism, - Completions: completions, - EnvVars: envVars, - }); err != nil { - t.Fatalf("CreateK8sJob failed: %v", err) - } - t.Logf("Job %s created successfully.", jobName) - - // Step 2: Get Job - job, err := GetJob(context.Background(), namespace, jobName) - if err != nil { - t.Fatalf("GetK8sJob failed: %v", err) - } - t.Logf("Fetched job: %v", job) - - // Ensure job was created with the correct name - if job.Name != jobName { - t.Errorf("expected job name %s, got %s", jobName, job.Name) - } - - // Step 3: Wait for Job completion - t.Logf("Waiting for job %s to complete...", jobName) - if err := WaitForJobCompletion(context.Background(), namespace, jobName); err != nil { - t.Fatalf("WaitForJobCompletion failed: %v", err) - } - t.Logf("Job %s completed successfully.", jobName) - - // Step 4: Get Pod Logs - logs, err := GetJobPodLogs(context.Background(), namespace, jobName) - if err != nil { - t.Fatalf("GetJobPodLogs failed: %v", err) - } - - t.Logf("Logs for job %s:\n", jobName) - for podName, log := range logs { - t.Logf("Pod %s logs:\n%s", podName, log) - } - - // Step 5: Delete Job - if err := deleteJob(context.Background(), namespace, jobName); err != nil { - t.Fatalf("DeleteK8sJob failed: %v", err) - } - t.Logf("Job %s and its associated pods deleted successfully.", jobName) -} diff --git a/src/client/redis_client.go b/src/client/redis_client.go deleted file mode 100644 index dafb2c33..00000000 --- a/src/client/redis_client.go +++ /dev/null @@ -1,164 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "sync" - "time" - - "aegis/config" - - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" -) - -// Singleton pattern Redis client -var ( - redisClient *redis.Client - redisOnce sync.Once -) - -// Get Redis client -func GetRedisClient() *redis.Client { - redisOnce.Do(func() { - logrus.Infof("Connecting to Redis %s", config.GetString("redis.host")) - redisClient = redis.NewClient(&redis.Options{ - Addr: config.GetString("redis.host"), - Password: "", - DB: 0, - }) - - if err := redisClient.Ping(context.Background()).Err(); err != nil { - logrus.Fatalf("Failed to connect to Redis: %v", err) - } - }) - return redisClient -} - -// CheckCachedField checks if a field exists in Redis cache -func CheckCachedField(ctx context.Context, key, field string) bool { - exists, err := GetRedisClient().HExists(ctx, key, field).Result() - if err != nil { - logrus.Errorf("failed to check if field %s exists in cache: %v", field, err) - return false - } - - return exists -} - -// GetHashField retrieves a field from Redis hash and unmarshals it into the target -func GetHashField[T any](ctx context.Context, key, field string, target *T) error { - itemJSON, err := GetRedisClient().HGet(ctx, key, field).Result() - if err != nil && err != redis.Nil { - return fmt.Errorf("failed to get hash field %s from key %s: %w", field, key, err) - } - - if itemJSON == "" { - logrus.Warnf("field %s not found in cache key %s", field, key) - return nil - } - - if err := json.Unmarshal([]byte(itemJSON), target); err != nil { - return fmt.Errorf("failed to unmarshal cached items for field %s: %w", field, err) - } - - return nil -} - -// SetHashField sets a field in Redis hash with the provided item -func SetHashField[T any](ctx context.Context, key, field string, item T) error { - itemJSON, err := json.Marshal(item) - if err != nil { - return fmt.Errorf("failed to marshal items to JSON: %w", err) - } - - if _, err := GetRedisClient().Pipelined(ctx, func(pipe redis.Pipeliner) error { - pipe.HSet(ctx, key, field, itemJSON) - return nil - }); err != nil { - return fmt.Errorf("failed to set hash field %s in key %s: %w", field, key, err) - } - - return nil -} - -// GetRedisListRange retrieves all elements from a Redis list -func GetRedisListRange(ctx context.Context, key string) ([]string, error) { - result, err := GetRedisClient().LRange(ctx, key, 0, -1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get list range for key '%s': %w", key, err) - } - return result, nil -} - -// GetRedisZRangeByScoreWithScores retrieves elements from a Redis sorted set by score with a limit -func GetRedisZRangeByScoreWithScores(ctx context.Context, key string, limit int64) ([]redis.Z, error) { - if limit <= 0 { - return nil, fmt.Errorf("limit must be a positive number") - } - options := &redis.ZRangeBy{ - Min: "-inf", - Max: "+inf", - Offset: 0, - Count: limit, - } - - results, err := GetRedisClient().ZRangeByScoreWithScores(ctx, key, options).Result() - if err != nil { - return nil, fmt.Errorf("failed to get scheduled tasks from key '%s': %w", key, err) - } - - return results, nil -} - -// RedisXAdd adds an entry to a Redis stream -func RedisXAdd(ctx context.Context, stream string, values map[string]any) error { - _, err := GetRedisClient().XAdd(ctx, &redis.XAddArgs{ - Stream: stream, - MaxLen: 1000, - Approx: true, - ID: "*", - Values: values, - }).Result() - - if err != nil { - return fmt.Errorf("redis XADD failed for stream '%s': %w", stream, err) - } - return nil -} - -// RedisXRead reads entries from Redis streams -func RedisXRead(ctx context.Context, streams []string, count int64, block time.Duration) ([]redis.XStream, error) { - result, err := GetRedisClient().XRead(ctx, &redis.XReadArgs{ - Streams: streams, - Count: count, - Block: block, - }).Result() - - if err != nil && err != redis.Nil { - return nil, fmt.Errorf("redis XREAD failed: %w", err) - } - - return result, nil -} - -// RedisPublish publishes a message to a Redis channel -func RedisPublish(ctx context.Context, channel string, message any) error { - var payload string - switch v := message.(type) { - case string: - payload = v - default: - data, err := json.Marshal(message) - if err != nil { - return fmt.Errorf("failed to marshal message: %w", err) - } - payload = string(data) - } - - if err := GetRedisClient().Publish(ctx, channel, payload).Err(); err != nil { - return fmt.Errorf("redis PUBLISH failed for channel '%s': %w", channel, err) - } - return nil -} diff --git a/src/cmd/aegisctl/client/auth.go b/src/cmd/aegisctl/client/auth.go index 95e5cda4..f5b4d80f 100644 --- a/src/cmd/aegisctl/client/auth.go +++ b/src/cmd/aegisctl/client/auth.go @@ -1,26 +1,46 @@ package client import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" "fmt" + "strconv" + "strings" "time" ) -// loginRequest matches dto.LoginReq. -type loginRequest struct { - Username string `json:"username"` - Password string `json:"password"` +const accessKeyTokenPath = "/api/v2/auth/access-key/token" + +// AccessKeyTokenDebug contains the fully materialized signed request data for +// POST /api/v2/auth/access-key/token. +type AccessKeyTokenDebug struct { + Method string + Path string + AccessKey string + Timestamp string + Nonce string + CanonicalString string + Signature string } -// loginResponseData matches dto.LoginResp. -type loginResponseData struct { +func (d *AccessKeyTokenDebug) Headers() map[string]string { + return map[string]string{ + "X-Access-Key": d.AccessKey, + "X-Timestamp": d.Timestamp, + "X-Nonce": d.Nonce, + "X-Signature": d.Signature, + } +} + +// accessKeyTokenResponseData matches dto.AccessKeyTokenResp. +type accessKeyTokenResponseData struct { Token string `json:"token"` + TokenType string `json:"token_type"` ExpiresAt time.Time `json:"expires_at"` - User struct { - ID int `json:"id"` - Username string `json:"username"` - Avatar string `json:"avatar,omitempty"` - Role string `json:"role,omitempty"` - } `json:"user"` + AuthType string `json:"auth_type"` + AccessKey string `json:"access_key"` } // tokenRefreshRequest matches dto.TokenRefreshReq. @@ -38,26 +58,37 @@ type tokenRefreshResponseData struct { type LoginResult struct { Token string ExpiresAt time.Time - Username string + AuthType string + AccessKey string } -// Login authenticates against the server and returns a token. -func Login(server, username, password string) (*LoginResult, error) { - c := NewClient(server, "", 30*time.Second) +// LoginWithAccessKey exchanges an access key signature for a bearer token. +func LoginWithAccessKey(server, accessKey, secretKey string) (*LoginResult, error) { + accessKey = strings.TrimSpace(accessKey) + secretKey = strings.TrimSpace(secretKey) + if accessKey == "" { + return nil, fmt.Errorf("access key is required") + } + if secretKey == "" { + return nil, fmt.Errorf("secret key is required") + } - var resp APIResponse[loginResponseData] - err := c.Post("/api/v2/auth/login", loginRequest{ - Username: username, - Password: password, - }, &resp) + c := NewClient(server, "", 30*time.Second) + debugInfo, err := PrepareAccessKeyTokenDebug(accessKey, secretKey, time.Now().UTC(), "") if err != nil { - return nil, fmt.Errorf("login failed: %w", err) + return nil, fmt.Errorf("prepare signed headers: %w", err) + } + + var resp APIResponse[accessKeyTokenResponseData] + if err := c.PostWithHeaders(accessKeyTokenPath, debugInfo.Headers(), &resp); err != nil { + return nil, fmt.Errorf("exchange access key token failed: %w", err) } return &LoginResult{ Token: resp.Data.Token, ExpiresAt: resp.Data.ExpiresAt, - Username: resp.Data.User.Username, + AuthType: resp.Data.AuthType, + AccessKey: resp.Data.AccessKey, }, nil } @@ -103,3 +134,101 @@ func IsTokenExpired(expiry time.Time) bool { } return time.Now().After(expiry) } + +// PrepareAccessKeyTokenDebug builds the canonical string, signature, and +// headers for the access-key token exchange request. +func PrepareAccessKeyTokenDebug(accessKey, secretKey string, now time.Time, nonce string) (*AccessKeyTokenDebug, error) { + accessKey = strings.TrimSpace(accessKey) + secretKey = strings.TrimSpace(secretKey) + nonce = strings.TrimSpace(nonce) + if accessKey == "" { + return nil, fmt.Errorf("access key is required") + } + if secretKey == "" { + return nil, fmt.Errorf("secret key is required") + } + var err error + if nonce == "" { + nonce, err = newAccessKeyNonce() + if err != nil { + return nil, err + } + } + + timestamp := strconv.FormatInt(now.Unix(), 10) + canonical := canonicalAccessKeyString("POST", accessKeyTokenPath, accessKey, timestamp, nonce) + + return &AccessKeyTokenDebug{ + Method: "POST", + Path: accessKeyTokenPath, + AccessKey: accessKey, + Timestamp: timestamp, + Nonce: nonce, + CanonicalString: canonical, + Signature: signAccessKeyRequest(secretKey, canonical), + }, nil +} + +func buildAccessKeyHeaders(accessKey, secretKey string, now time.Time, path string) (map[string]string, error) { + debugInfo, err := prepareAccessKeyDebug(accessKey, secretKey, now, path, "") + if err != nil { + return nil, err + } + return debugInfo.Headers(), nil +} + +func prepareAccessKeyDebug(accessKey, secretKey string, now time.Time, path, nonce string) (*AccessKeyTokenDebug, error) { + accessKey = strings.TrimSpace(accessKey) + secretKey = strings.TrimSpace(secretKey) + nonce = strings.TrimSpace(nonce) + if accessKey == "" { + return nil, fmt.Errorf("access key is required") + } + if secretKey == "" { + return nil, fmt.Errorf("secret key is required") + } + var err error + if nonce == "" { + nonce, err = newAccessKeyNonce() + if err != nil { + return nil, err + } + } + + timestamp := strconv.FormatInt(now.Unix(), 10) + canonical := canonicalAccessKeyString("POST", path, accessKey, timestamp, nonce) + + return &AccessKeyTokenDebug{ + Method: "POST", + Path: path, + AccessKey: accessKey, + Timestamp: timestamp, + Nonce: nonce, + CanonicalString: canonical, + Signature: signAccessKeyRequest(secretKey, canonical), + }, nil +} + +func canonicalAccessKeyString(method, path, accessKey, timestamp, nonce string) string { + return strings.Join([]string{ + strings.ToUpper(method), + path, + accessKey, + timestamp, + nonce, + }, "\n") +} + +func signAccessKeyRequest(secretKey, payload string) string { + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(payload)) + return hex.EncodeToString(mac.Sum(nil)) +} + +func newAccessKeyNonce() (string, error) { + nonce := make([]byte, 16) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("generate nonce: %w", err) + } + return hex.EncodeToString(nonce), nil +} diff --git a/src/cmd/aegisctl/client/auth_test.go b/src/cmd/aegisctl/client/auth_test.go new file mode 100644 index 00000000..e5e11feb --- /dev/null +++ b/src/cmd/aegisctl/client/auth_test.go @@ -0,0 +1,93 @@ +package client + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestCanonicalAccessKeyString(t *testing.T) { + got := canonicalAccessKeyString( + "post", + "/api/v2/auth/access-key/token", + "ak_demo", + "1713333333", + "abc123", + ) + + want := "POST\n/api/v2/auth/access-key/token\nak_demo\n1713333333\nabc123" + if got != want { + t.Fatalf("canonical string mismatch:\nwant: %q\ngot: %q", want, got) + } +} + +func TestBuildAccessKeyHeaders(t *testing.T) { + headers, err := buildAccessKeyHeaders( + "ak_demo", + "sk_demo", + time.Unix(1713333333, 0).UTC(), + "/api/v2/auth/access-key/token", + ) + if err != nil { + t.Fatalf("buildAccessKeyHeaders returned error: %v", err) + } + + if headers["X-Access-Key"] != "ak_demo" { + t.Fatalf("unexpected access key header: %q", headers["X-Access-Key"]) + } + if headers["X-Timestamp"] != "1713333333" { + t.Fatalf("unexpected timestamp header: %q", headers["X-Timestamp"]) + } + if headers["X-Nonce"] == "" { + t.Fatal("expected nonce header to be set") + } + if len(headers["X-Signature"]) != 64 { + t.Fatalf("unexpected signature length: %d", len(headers["X-Signature"])) + } +} + +func TestPrepareAccessKeyTokenDebug(t *testing.T) { + debugInfo, err := PrepareAccessKeyTokenDebug( + "ak_demo", + "sk_demo", + time.Unix(1713333333, 0).UTC(), + "abc123", + ) + if err != nil { + t.Fatalf("PrepareAccessKeyTokenDebug returned error: %v", err) + } + + if debugInfo.Method != "POST" { + t.Fatalf("unexpected method: %q", debugInfo.Method) + } + if debugInfo.Path != "/api/v2/auth/access-key/token" { + t.Fatalf("unexpected path: %q", debugInfo.Path) + } + if debugInfo.CanonicalString != "POST\n/api/v2/auth/access-key/token\nak_demo\n1713333333\nabc123" { + t.Fatalf("unexpected canonical string: %q", debugInfo.CanonicalString) + } + if debugInfo.Headers()["X-Signature"] != debugInfo.Signature { + t.Fatal("signature header mismatch") + } +} + +func TestPostWithHeaders(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("X-Access-Key"); got != "ak_demo" { + t.Fatalf("unexpected X-Access-Key header: %q", got) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"code":0,"message":"ok"}`)) + })) + defer server.Close() + + c := NewClient(server.URL, "", 5*time.Second) + var resp APIResponse[map[string]any] + if err := c.PostWithHeaders("/api/v2/auth/access-key/token", map[string]string{ + "X-Access-Key": "ak_demo", + }, &resp); err != nil { + t.Fatalf("PostWithHeaders returned error: %v", err) + } +} diff --git a/src/cmd/aegisctl/client/client.go b/src/cmd/aegisctl/client/client.go index b0e40d39..27faafac 100644 --- a/src/cmd/aegisctl/client/client.go +++ b/src/cmd/aegisctl/client/client.go @@ -65,7 +65,7 @@ func NewClient(baseURL, token string, timeout time.Duration) *Client { } // doRequest executes an HTTP request and decodes the JSON response into dest. -func (c *Client) doRequest(method, path string, body any, dest any) error { +func (c *Client) doRequest(method, path string, body any, headers map[string]string, dest any) error { var bodyReader io.Reader if body != nil { data, err := json.Marshal(body) @@ -86,6 +86,9 @@ func (c *Client) doRequest(method, path string, body any, dest any) error { if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } + for key, value := range headers { + req.Header.Set(key, value) + } resp, err := c.HTTPClient.Do(req) if err != nil { @@ -123,25 +126,30 @@ func (c *Client) doRequest(method, path string, body any, dest any) error { // Get sends a GET request. func (c *Client) Get(path string, dest any) error { - return c.doRequest(http.MethodGet, path, nil, dest) + return c.doRequest(http.MethodGet, path, nil, nil, dest) } // Post sends a POST request. func (c *Client) Post(path string, body any, dest any) error { - return c.doRequest(http.MethodPost, path, body, dest) + return c.doRequest(http.MethodPost, path, body, nil, dest) +} + +// PostWithHeaders sends a POST request with additional headers. +func (c *Client) PostWithHeaders(path string, headers map[string]string, dest any) error { + return c.doRequest(http.MethodPost, path, nil, headers, dest) } // Put sends a PUT request. func (c *Client) Put(path string, body any, dest any) error { - return c.doRequest(http.MethodPut, path, body, dest) + return c.doRequest(http.MethodPut, path, body, nil, dest) } // Patch sends a PATCH request. func (c *Client) Patch(path string, body any, dest any) error { - return c.doRequest(http.MethodPatch, path, body, dest) + return c.doRequest(http.MethodPatch, path, body, nil, dest) } // Delete sends a DELETE request. func (c *Client) Delete(path string, dest any) error { - return c.doRequest(http.MethodDelete, path, nil, dest) + return c.doRequest(http.MethodDelete, path, nil, nil, dest) } diff --git a/src/cmd/aegisctl/cmd/auth.go b/src/cmd/aegisctl/cmd/auth.go index 987c88d1..73da9ba2 100644 --- a/src/cmd/aegisctl/cmd/auth.go +++ b/src/cmd/aegisctl/cmd/auth.go @@ -2,6 +2,8 @@ package cmd import ( "fmt" + "os" + "strings" "time" "aegis/cmd/aegisctl/client" @@ -19,13 +21,13 @@ var authCmd = &cobra.Command{ // --- auth login --- var authLoginServer string -var authLoginUsername string -var authLoginPassword string +var authLoginAccessKey string +var authLoginSecretKey string var authLoginContext string var authLoginCmd = &cobra.Command{ Use: "login", - Short: "Authenticate with an AegisLab server", + Short: "Exchange AK/SK for a bearer token", RunE: func(cmd *cobra.Command, args []string) error { server := authLoginServer if server == "" { @@ -35,16 +37,25 @@ var authLoginCmd = &cobra.Command{ return fmt.Errorf("--server is required for login") } - if authLoginUsername == "" { - return fmt.Errorf("--username is required") + accessKey := authLoginAccessKey + if accessKey == "" { + accessKey = os.Getenv("AEGIS_ACCESS_KEY") } - if authLoginPassword == "" { - return fmt.Errorf("--password is required") + if accessKey == "" { + return fmt.Errorf("--access-key is required") } - output.PrintInfo(fmt.Sprintf("Logging in to %s as %s...", server, authLoginUsername)) + secretKey := authLoginSecretKey + if secretKey == "" { + secretKey = os.Getenv("AEGIS_SECRET_KEY") + } + if secretKey == "" { + return fmt.Errorf("--secret-key is required") + } + + output.PrintInfo(fmt.Sprintf("Exchanging access key token with %s using %s...", server, accessKey)) - result, err := client.Login(server, authLoginUsername, authLoginPassword) + result, err := client.LoginWithAccessKey(server, accessKey, secretKey) if err != nil { return err } @@ -59,6 +70,8 @@ var authLoginCmd = &cobra.Command{ cfg.Contexts[ctxName] = config.Context{ Server: server, Token: result.Token, + AuthType: result.AuthType, + AccessKey: result.AccessKey, TokenExpiry: result.ExpiresAt, } cfg.CurrentContext = ctxName @@ -71,11 +84,12 @@ var authLoginCmd = &cobra.Command{ output.PrintJSON(map[string]any{ "context": ctxName, "server": server, - "username": result.Username, + "auth_type": result.AuthType, + "access_key": result.AccessKey, "expires_at": result.ExpiresAt.Format(time.RFC3339), }) } else { - output.PrintInfo(fmt.Sprintf("Logged in as %s (context: %s)", result.Username, ctxName)) + output.PrintInfo(fmt.Sprintf("Token issued for access key %s (context: %s)", result.AccessKey, ctxName)) output.PrintInfo(fmt.Sprintf("Token expires at %s", result.ExpiresAt.Format(time.RFC3339))) } return nil @@ -108,6 +122,8 @@ var authStatusCmd = &cobra.Command{ "context": ctxName, "server": ctx.Server, "status": status, + "auth_type": ctx.AuthType, + "access_key": ctx.AccessKey, "expires_at": ctx.TokenExpiry.Format(time.RFC3339), }) return nil @@ -135,7 +151,169 @@ var authStatusCmd = &cobra.Command{ } else { output.PrintInfo(fmt.Sprintf("Authenticated as: %s (id: %d)", profile.Username, profile.ID)) } + if ctx.AccessKey != "" { + output.PrintInfo(fmt.Sprintf("Issued via access key: %s", ctx.AccessKey)) + } + + return nil + }, +} + +// --- auth inspect --- + +var authInspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Inspect locally stored authentication context", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, ctxName, err := config.GetCurrentContext(cfg) + if err != nil { + return err + } + + tokenPreview := "" + if ctx.Token != "" { + tokenPreview = ctx.Token + if len(tokenPreview) > 20 { + tokenPreview = tokenPreview[:10] + "..." + tokenPreview[len(tokenPreview)-10:] + } + } + expiresAt := "" + if !ctx.TokenExpiry.IsZero() { + expiresAt = ctx.TokenExpiry.Format(time.RFC3339) + } + expired := false + if !ctx.TokenExpiry.IsZero() { + expired = client.IsTokenExpired(ctx.TokenExpiry) + } + + if output.OutputFormat(flagOutput) == output.FormatJSON { + output.PrintJSON(map[string]any{ + "context": ctxName, + "server": ctx.Server, + "auth_type": ctx.AuthType, + "access_key": ctx.AccessKey, + "token_present": ctx.Token != "", + "token_preview": tokenPreview, + "token_expired": expired, + "expires_at": expiresAt, + }) + return nil + } + + output.PrintTable( + []string{"Context", "Server", "AuthType", "AccessKey", "Token", "Expired", "Expires"}, + [][]string{{ + ctxName, + ctx.Server, + emptyOrValue(ctx.AuthType, "-"), + emptyOrValue(ctx.AccessKey, "-"), + emptyOrValue(tokenPreview, "-"), + fmt.Sprintf("%t", expired), + emptyOrValue(expiresAt, "-"), + }}, + ) + return nil + }, +} + +// --- auth sign-debug --- + +var authSignDebugAccessKey string +var authSignDebugSecretKey string +var authSignDebugTimestamp int64 +var authSignDebugNonce string +var authSignDebugExecute bool +var authSignDebugSaveContext bool + +var authSignDebugCmd = &cobra.Command{ + Use: "sign-debug", + Short: "Print canonical string and signature headers for AK/SK token exchange", + RunE: func(cmd *cobra.Command, args []string) error { + accessKey := authSignDebugAccessKey + if accessKey == "" { + accessKey = os.Getenv("AEGIS_ACCESS_KEY") + } + if accessKey == "" { + return fmt.Errorf("--access-key is required") + } + + secretKey := authSignDebugSecretKey + if secretKey == "" { + secretKey = os.Getenv("AEGIS_SECRET_KEY") + } + if secretKey == "" { + return fmt.Errorf("--secret-key is required") + } + + signTime := time.Now().UTC() + if authSignDebugTimestamp > 0 { + signTime = time.Unix(authSignDebugTimestamp, 0).UTC() + } + debugInfo, err := client.PrepareAccessKeyTokenDebug(accessKey, secretKey, signTime, authSignDebugNonce) + if err != nil { + return err + } + + server := strings.TrimRight(resolveServerForAuthDebug(), "/") + if authSignDebugExecute && (server == "" || strings.Contains(server, "HOST:8082")) { + return fmt.Errorf("--execute requires a real --server or configured AEGIS_SERVER/current context") + } + curlCommand := buildAccessKeyCurl(server, debugInfo) + var executeResp map[string]any + if authSignDebugExecute { + executeResp, err = executeAccessKeyTokenExchange(server, debugInfo) + if err != nil { + return err + } + if authSignDebugSaveContext { + if err := saveAccessKeyContext(server, executeResp); err != nil { + return err + } + } + } else if authSignDebugSaveContext { + return fmt.Errorf("--save-context requires --execute") + } + + if output.OutputFormat(flagOutput) == output.FormatJSON { + result := map[string]any{ + "server": server, + "method": debugInfo.Method, + "path": debugInfo.Path, + "access_key": debugInfo.AccessKey, + "timestamp": debugInfo.Timestamp, + "nonce": debugInfo.Nonce, + "canonical_string": debugInfo.CanonicalString, + "signature": debugInfo.Signature, + "headers": debugInfo.Headers(), + "curl": curlCommand, + "executed": authSignDebugExecute, + "saved_context": authSignDebugSaveContext, + } + if authSignDebugExecute { + result["response"] = executeResp + } + output.PrintJSON(result) + return nil + } + + fmt.Printf("Server: %s\n", server) + fmt.Printf("Method: %s\n", debugInfo.Method) + fmt.Printf("Path: %s\n", debugInfo.Path) + fmt.Printf("Access-Key: %s\n", debugInfo.AccessKey) + fmt.Printf("Timestamp: %s\n", debugInfo.Timestamp) + fmt.Printf("Nonce: %s\n", debugInfo.Nonce) + fmt.Printf("Signature: %s\n\n", debugInfo.Signature) + fmt.Println("Canonical String:") + fmt.Println(debugInfo.CanonicalString) + fmt.Println() + fmt.Println("curl:") + fmt.Println(curlCommand) + if authSignDebugExecute { + fmt.Println() + fmt.Println("response:") + output.PrintJSON(executeResp) + } return nil }, } @@ -175,6 +353,9 @@ var authTokenCmd = &cobra.Command{ ctx := cfg.Contexts[ctxName] ctx.Token = authTokenSet + ctx.AuthType = "token" + ctx.AccessKey = "" + ctx.TokenExpiry = time.Time{} cfg.Contexts[ctxName] = ctx cfg.CurrentContext = ctxName @@ -189,13 +370,111 @@ var authTokenCmd = &cobra.Command{ func init() { authLoginCmd.Flags().StringVar(&authLoginServer, "server", "", "Server URL") - authLoginCmd.Flags().StringVar(&authLoginUsername, "username", "", "Username") - authLoginCmd.Flags().StringVar(&authLoginPassword, "password", "", "Password") + authLoginCmd.Flags().StringVar(&authLoginAccessKey, "access-key", "", "Access key (env: AEGIS_ACCESS_KEY)") + authLoginCmd.Flags().StringVar(&authLoginSecretKey, "secret-key", "", "Secret key (env: AEGIS_SECRET_KEY)") authLoginCmd.Flags().StringVar(&authLoginContext, "context", "", "Context name to save credentials under (default: \"default\")") + authSignDebugCmd.Flags().StringVar(&authSignDebugAccessKey, "access-key", "", "Access key (env: AEGIS_ACCESS_KEY)") + authSignDebugCmd.Flags().StringVar(&authSignDebugSecretKey, "secret-key", "", "Secret key (env: AEGIS_SECRET_KEY)") + authSignDebugCmd.Flags().Int64Var(&authSignDebugTimestamp, "timestamp", 0, "Override unix timestamp in seconds") + authSignDebugCmd.Flags().StringVar(&authSignDebugNonce, "nonce", "", "Override nonce for reproducible signature output") + authSignDebugCmd.Flags().BoolVar(&authSignDebugExecute, "execute", false, "Execute the signed token exchange request and print the response") + authSignDebugCmd.Flags().BoolVar(&authSignDebugSaveContext, "save-context", false, "Save the exchanged bearer token into the current context after --execute succeeds") authTokenCmd.Flags().StringVar(&authTokenSet, "set", "", "Set token directly") authCmd.AddCommand(authLoginCmd) authCmd.AddCommand(authStatusCmd) + authCmd.AddCommand(authInspectCmd) + authCmd.AddCommand(authSignDebugCmd) authCmd.AddCommand(authTokenCmd) } + +func resolveServerForAuthDebug() string { + if flagServer != "" { + return flagServer + } + if value := os.Getenv("AEGIS_SERVER"); value != "" { + return value + } + if cfg != nil { + if ctx, _, err := config.GetCurrentContext(cfg); err == nil && ctx.Server != "" { + return ctx.Server + } + } + return "http://HOST:8082" +} + +func buildAccessKeyCurl(server string, debugInfo *client.AccessKeyTokenDebug) string { + return fmt.Sprintf( + "curl -X POST %s%s -H 'Accept: application/json' -H 'X-Access-Key: %s' -H 'X-Timestamp: %s' -H 'X-Nonce: %s' -H 'X-Signature: %s'", + server, + debugInfo.Path, + debugInfo.AccessKey, + debugInfo.Timestamp, + debugInfo.Nonce, + debugInfo.Signature, + ) +} + +func executeAccessKeyTokenExchange(server string, debugInfo *client.AccessKeyTokenDebug) (map[string]any, error) { + httpClient := client.NewClient(server, "", 30*time.Second) + var response map[string]any + if err := httpClient.PostWithHeaders(debugInfo.Path, debugInfo.Headers(), &response); err != nil { + return nil, fmt.Errorf("execute token exchange: %w", err) + } + return response, nil +} + +func saveAccessKeyContext(server string, executeResp map[string]any) error { + ctxName := resolveContextNameForSave() + ctx := cfg.Contexts[ctxName] + ctx.Server = server + + data, ok := executeResp["data"].(map[string]any) + if !ok { + return fmt.Errorf("execute response does not contain a valid data payload") + } + + token, _ := data["token"].(string) + if strings.TrimSpace(token) == "" { + return fmt.Errorf("execute response does not contain a token") + } + ctx.Token = token + + if authType, _ := data["auth_type"].(string); strings.TrimSpace(authType) != "" { + ctx.AuthType = authType + } + if accessKey, _ := data["access_key"].(string); strings.TrimSpace(accessKey) != "" { + ctx.AccessKey = accessKey + } + if expiresAt, _ := data["expires_at"].(string); strings.TrimSpace(expiresAt) != "" { + parsed, err := time.Parse(time.RFC3339, expiresAt) + if err != nil { + return fmt.Errorf("parse expires_at: %w", err) + } + ctx.TokenExpiry = parsed + } + + cfg.Contexts[ctxName] = ctx + cfg.CurrentContext = ctxName + if err := config.SaveConfig(cfg); err != nil { + return fmt.Errorf("save config: %w", err) + } + + output.PrintInfo(fmt.Sprintf("Saved token to context %q", ctxName)) + return nil +} + +func resolveContextNameForSave() string { + if cfg != nil && strings.TrimSpace(cfg.CurrentContext) != "" { + return cfg.CurrentContext + } + return "default" +} + +func emptyOrValue(value, fallback string) string { + if strings.TrimSpace(value) == "" { + return fallback + } + return value +} diff --git a/src/cmd/aegisctl/cmd/root.go b/src/cmd/aegisctl/cmd/root.go index 2ddfb396..05e83533 100644 --- a/src/cmd/aegisctl/cmd/root.go +++ b/src/cmd/aegisctl/cmd/root.go @@ -33,8 +33,8 @@ var rootCmd = &cobra.Command{ fault-injection and root-cause-analysis benchmarking platform. QUICK START: - # 1. Login (saves token to ~/.aegisctl/config.yaml) - aegisctl auth login --server http://HOST:8082 --username admin --password admin123 + # 1. Exchange AK/SK for a token (saves token to ~/.aegisctl/config.yaml) + aegisctl auth login --server http://HOST:8082 --access-key ak_xxx --secret-key sk_xxx # 2. Set default project so you don't need --project every time aegisctl context set --name default --default-project pair_diagnosis @@ -68,11 +68,13 @@ OUTPUT: Use --quiet (-q) to suppress informational messages. ENVIRONMENT VARIABLES: - AEGIS_SERVER - Server URL (overridden by --server flag) - AEGIS_TOKEN - Auth token (overridden by --token flag) - AEGIS_PROJECT - Default project name (overridden by --project flag) - AEGIS_OUTPUT - Output format: table|json (overridden by --output flag) - AEGIS_TIMEOUT - Request timeout in seconds (overridden by --request-timeout flag) + AEGIS_SERVER - Server URL (overridden by --server flag) + AEGIS_TOKEN - Auth token (overridden by --token flag) + AEGIS_ACCESS_KEY - Access key for 'aegisctl auth login' + AEGIS_SECRET_KEY - Secret key for 'aegisctl auth login' + AEGIS_PROJECT - Default project name (overridden by --project flag) + AEGIS_OUTPUT - Output format: table|json (overridden by --output flag) + AEGIS_TIMEOUT - Request timeout in seconds (overridden by --request-timeout flag) NAMING CONVENTION: Most commands accept human-readable names instead of numeric IDs. @@ -142,7 +144,7 @@ NAMING CONVENTION: flagRequestTimeout = 30 } - // Wire quiet flag into output package. + // Forward quiet flag into the output package. output.Quiet = flagQuiet return nil diff --git a/src/cmd/aegisctl/config/config.go b/src/cmd/aegisctl/config/config.go index 605f9f9a..5aea9622 100644 --- a/src/cmd/aegisctl/config/config.go +++ b/src/cmd/aegisctl/config/config.go @@ -20,6 +20,8 @@ type Config struct { type Context struct { Server string `yaml:"server"` Token string `yaml:"token,omitempty"` + AuthType string `yaml:"auth-type,omitempty"` + AccessKey string `yaml:"access-key,omitempty"` DefaultProject string `yaml:"default-project,omitempty"` TokenExpiry time.Time `yaml:"token-expiry,omitempty"` } diff --git a/src/database/database.go b/src/database/database.go deleted file mode 100644 index 7a001b7a..00000000 --- a/src/database/database.go +++ /dev/null @@ -1,159 +0,0 @@ -package database - -import ( - "fmt" - "log" - "os" - "time" - - "aegis/config" - - "github.com/sirupsen/logrus" - - "gorm.io/driver/mysql" - "gorm.io/gorm" - "gorm.io/gorm/logger" - "gorm.io/plugin/opentelemetry/tracing" -) - -type DatabaseConfig struct { - Type string - Host string - Port int - User string - Password string - Database string - Timezone string -} - -func NewDatabaseConfig(databaseType string) *DatabaseConfig { - return &DatabaseConfig{ - Type: databaseType, - Host: config.GetString(fmt.Sprintf("database.%s.host", databaseType)), - Port: config.GetInt(fmt.Sprintf("database.%s.port", databaseType)), - User: config.GetString(fmt.Sprintf("database.%s.user", databaseType)), - Password: config.GetString(fmt.Sprintf("database.%s.password", databaseType)), - Database: config.GetString(fmt.Sprintf("database.%s.db", databaseType)), - Timezone: config.GetString(fmt.Sprintf("database.%s.timezone", databaseType)), - } -} - -func (d *DatabaseConfig) ToDSN() (string, error) { - if d.Type != "mysql" { - return "", fmt.Errorf("unsupported database type: %s", d.Type) - } - - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", - d.User, d.Password, d.Host, d.Port, d.Database) - return dsn, nil -} - -// Global DB object -var DB *gorm.DB - -func InitDB() { - var err error - - mysqlConfig := NewDatabaseConfig("mysql") - - connectWithRetry(mysqlConfig) - - if err = DB.AutoMigrate( - // Core entities - &Container{}, - &ContainerVersion{}, - &HelmConfig{}, - &ParameterConfig{}, - &Dataset{}, - &DatasetVersion{}, - &Project{}, - &Label{}, - &User{}, - &Role{}, - &Permission{}, - &Resource{}, - &AuditLog{}, - - // Business entities - &Task{}, - &FaultInjection{}, - &Execution{}, - &DetectorResult{}, - &GranularityResult{}, - - // Many-to-many relationship tables - &ContainerLabel{}, - &DatasetLabel{}, - &ProjectLabel{}, - &ContainerVersionEnvVar{}, - &HelmConfigValue{}, - &DatasetVersionInjection{}, - &FaultInjectionLabel{}, - &ExecutionInjectionLabel{}, - &ConfigLabel{}, - - &UserContainer{}, - &UserDataset{}, - &UserProject{}, - &UserRole{}, - &RolePermission{}, - &UserPermission{}, - &UserTeam{}, - - // Dynamic configuration entities - &DynamicConfig{}, - &ConfigHistory{}, - - // Evaluation entities - &Evaluation{}, - - // System registration entities - &System{}, - &SystemMetadata{}, - ); err != nil { - logrus.Fatalf("Failed to migrate database: %v", err) - } - - createDetectorViews() -} - -func connectWithRetry(dbConfig *DatabaseConfig) { - maxRetries := 3 - retryDelay := 10 * time.Second - - dsn, err := dbConfig.ToDSN() - if err != nil { - logrus.Fatalf("Failed to construct DSN: %v", err) - } - - for i := 0; i <= maxRetries; i++ { - DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{ - Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), - logger.Config{ - SlowThreshold: time.Second, - LogLevel: logger.Warn, - IgnoreRecordNotFoundError: true, - Colorful: true, - }), - TranslateError: true, - }) - if err == nil { - logrus.Info("Successfully connected to the database") - if err := DB.Use(tracing.NewPlugin()); err != nil { - panic(err) - } - - break - } - - logrus.Errorf("Failed to connect to database (attempt %d/%d): %v", i+1, maxRetries+1, err) - if i < maxRetries { - logrus.Infof("Retrying in %v...", retryDelay) - time.Sleep(retryDelay) - } - } - - if err != nil { - logrus.Fatalf("Failed to connect to database after %d attempts: %v", maxRetries+1, err) - } -} diff --git a/src/database/view.go b/src/database/view.go deleted file mode 100644 index 918f8bac..00000000 --- a/src/database/view.go +++ /dev/null @@ -1,120 +0,0 @@ -package database - -import ( - "time" - - chaos "github.com/OperationsPAI/chaos-experiment/handler" - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// FaultInjectionNoIssues view model -type FaultInjectionNoIssues struct { - ID int `gorm:"column:datapack_id"` - Name string `gorm:"column:datapack_name"` - FaultType chaos.ChaosType `gorm:"column:fault_type"` - Category chaos.SystemType `gorm:"column:category"` - EngineConfig string `gorm:"column:engine_config"` - LabelKey string `gorm:"column:label_key"` - LabelValue string `gorm:"column:value_key"` - CreatedAt time.Time `gorm:"column:created_at"` -} - -func (FaultInjectionNoIssues) TableName() string { - return "fault_injection_no_issues" -} - -// FaultInjectionWithIssues view model -type FaultInjectionWithIssues struct { - ID int `gorm:"column:datapack_id"` - Name string `gorm:"column:datapack_name"` - FaultType chaos.ChaosType `gorm:"column:fault_type"` - Category chaos.SystemType `gorm:"column:category"` - EngineConfig string `gorm:"column:engine_config"` - LabelKey string `gorm:"column:label_key"` - LabelValue string `gorm:"column:value_key"` - CreatedAt time.Time `gorm:"column:created_at"` - Issues string `gorm:"column:issues"` - AbnormalAvgDuration float64 `gorm:"column:abnormal_avg_duration"` - NormalAvgDuration float64 `gorm:"column:normal_avg_duration"` - AbnormalSuccRate float64 `gorm:"column:abnormal_succ_rate"` - NormalSuccRate float64 `gorm:"column:normal_succ_rate"` - AbnormalP99 float64 `gorm:"column:abnormal_p99"` - NormalP99 float64 `gorm:"column:normal_p99"` -} - -func (FaultInjectionWithIssues) TableName() string { - return "fault_injection_with_issues" -} - -func addDetectorJoins(query *gorm.DB) *gorm.DB { - return query. - Joins(`JOIN ( - SELECT - e.id, - c.id AS algorithm_id, - e.datapack_id, - ROW_NUMBER() OVER ( - PARTITION BY c.id, e.datapack_id - ORDER BY e.created_at DESC, e.id DESC - ) as rn - FROM executions e - JOIN container_versions cv ON e.algorithm_version_id = cv.id - JOIN containers c ON c.id = cv.container_id - WHERE e.state = 2 AND e.status = 1 AND c.id = ? - ) er_ranked ON fi.id = er_ranked.datapack_id AND er_ranked.rn = 1`, 1). - Joins("JOIN detector_results dr ON er_ranked.id = dr.execution_id") -} - -func createDetectorViews() { - var err error - - _ = DB.Migrator().DropView("fault_injection_no_issues") - _ = DB.Migrator().DropView("fault_injection_with_issues") - - // Create view for fault injections with no issues - noIssuesQuery := addDetectorJoins(DB.Table("fault_injections fi"). - Select(`DISTINCT - fi.id AS datapack_id, - fi.name AS name, - fi.fault_type AS fault_type, - fi.category AS category, - fi.engine_config AS engine_config, - l.label_key as label_key, - l.label_value as label_value, - fi.created_at`). - Joins("LEFT JOIN fault_injection_labels fil ON fil.fault_injection_id = fi.id"). - Joins("LEFT JOIN labels l ON fil.label_id = l.id"). - Group("fi.id, fi.name, fi.fault_type, fi.engine_config, fi.created_at, l.label_key, l.label_value"), - ).Where("dr.issues = '{}' OR dr.issues IS NULL") - if err = DB.Migrator().CreateView("fault_injection_no_issues", gorm.ViewOption{Query: noIssuesQuery}); err != nil { - logrus.Errorf("failed to create fault_injection_no_issues view: %v", err) - } - - // Create view for fault injections with issues - withIssuesQuery := addDetectorJoins(DB.Table("fault_injections fi"). - Select(`DISTINCT - fi.id AS datapack_id, - fi.name AS name, - fi.fault_type AS fault_type, - fi.category AS category, - fi.engine_config AS engine_config, - l.label_key as label_key, - l.label_value as label_value, - fi.created_at, - dr.issues, - dr.abnormal_avg_duration, - dr.normal_avg_duration, - dr.abnormal_succ_rate, - dr.normal_succ_rate, - dr.abnormal_p99, - dr.normal_p99`). - Joins("LEFT JOIN tasks t ON t.id = fi.task_id"). - Joins("LEFT JOIN fault_injection_labels fil ON fil.fault_injection_id = fi.id"). - Joins("LEFT JOIN labels l ON fil.label_id = l.id"). - Group("fi.id, fi.name, fi.fault_type, fi.engine_config, fi.created_at, l.label_key, l.label_value, dr.issues, dr.abnormal_avg_duration, dr.normal_avg_duration, dr.abnormal_succ_rate, dr.normal_succ_rate, dr.abnormal_p99, dr.normal_p99"), - ).Where("dr.issues != '{}' AND dr.issues IS NOT NULL") - if err = DB.Migrator().CreateView("fault_injection_with_issues", gorm.ViewOption{Query: withIssuesQuery}); err != nil { - logrus.Errorf("failed to create fault_injection_with_issues view: %v", err) - } -} diff --git a/src/docs/docs_test.go b/src/docs/docs_test.go new file mode 100644 index 00000000..5f248442 --- /dev/null +++ b/src/docs/docs_test.go @@ -0,0 +1,167 @@ +package docs_test + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "strings" + "testing" +) + +func TestGeneratedAPIDocsContainCorePaths(t *testing.T) { + baseDir := "." + + checkJSONContains(t, filepath.Join(baseDir, "openapi2", "swagger.json"), []string{ + `"/api/v2/auth/login"`, + `"/api/v2/users"`, + `"/api/v2/projects"`, + }) + checkJSONContains(t, filepath.Join(baseDir, "openapi3", "openapi.json"), []string{ + `"/api/v2/auth/login"`, + `"/api/v2/projects"`, + `"/api/v2/users"`, + }) + checkJSONContains(t, filepath.Join(baseDir, "converted", "sdk.json"), []string{ + `"/api/v2/auth/access-key/token"`, + `"/api/v2/sdk/evaluations"`, + }) +} + +func TestAudienceFilteredDocsMatchOpenAPI3Extensions(t *testing.T) { + openapi := readJSON(t, filepath.Join(".", "openapi3", "openapi.json")) + + checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "sdk.json"), "sdk") + checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "portal.json"), "portal") + checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "admin.json"), "admin") +} + +func checkJSONContains(t *testing.T, path string, fragments []string) { + t.Helper() + + data := readJSONBytes(t, path) + text := string(data) + for _, fragment := range fragments { + if !strings.Contains(text, fragment) { + t.Fatalf("expected %s to contain %s", path, fragment) + } + } +} + +func checkAudienceMatches(t *testing.T, openapi map[string]any, filteredPath string, audience string) { + t.Helper() + checkAudienceMatchesAny(t, openapi, filteredPath, []string{audience}) +} + +func checkAudienceMatchesAny(t *testing.T, openapi map[string]any, filteredPath string, audiences []string) { + t.Helper() + + filtered := readJSON(t, filteredPath) + want := collectAudienceOperations(t, openapi, audiences) + got := collectOperationsFromDoc(t, filtered) + + if len(want) != len(got) { + t.Fatalf("expected %s to have %d operations, got %d", filteredPath, len(want), len(got)) + } + if strings.Join(want, "\n") != strings.Join(got, "\n") { + t.Fatalf("unexpected operations in %s\nwant:\n%s\n\ngot:\n%s", filteredPath, strings.Join(want, "\n"), strings.Join(got, "\n")) + } +} + +func collectAudienceOperations(t *testing.T, doc map[string]any, audiences []string) []string { + t.Helper() + + paths, ok := doc["paths"].(map[string]any) + if !ok { + t.Fatalf("paths is not an object") + } + + audienceSet := make(map[string]struct{}, len(audiences)) + for _, audience := range audiences { + audienceSet[audience] = struct{}{} + } + + var operations []string + for path, opsValue := range paths { + ops, ok := opsValue.(map[string]any) + if !ok { + continue + } + for method, specValue := range ops { + spec, ok := specValue.(map[string]any) + if !ok { + continue + } + xAPIType, _ := spec["x-api-type"].(map[string]any) + for audience := range audienceSet { + if isAudienceEnabled(xAPIType, audience) { + operations = append(operations, strings.ToUpper(method)+" "+path) + break + } + } + } + } + + sort.Strings(operations) + return operations +} + +func isAudienceEnabled(xAPIType map[string]any, audience string) bool { + value, ok := xAPIType[audience] + if !ok { + return false + } + + switch typed := value.(type) { + case string: + return strings.EqualFold(strings.TrimSpace(typed), "true") + case bool: + return typed + default: + return false + } +} + +func collectOperationsFromDoc(t *testing.T, doc map[string]any) []string { + t.Helper() + + paths, ok := doc["paths"].(map[string]any) + if !ok { + t.Fatalf("paths is not an object") + } + + var operations []string + for path, opsValue := range paths { + ops, ok := opsValue.(map[string]any) + if !ok { + continue + } + for method := range ops { + operations = append(operations, strings.ToUpper(method)+" "+path) + } + } + + sort.Strings(operations) + return operations +} + +func readJSON(t *testing.T, path string) map[string]any { + t.Helper() + + data := readJSONBytes(t, path) + var body map[string]any + if err := json.Unmarshal(data, &body); err != nil { + t.Fatalf("unmarshal %s: %v", path, err) + } + return body +} + +func readJSONBytes(t *testing.T, path string) []byte { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return data +} diff --git a/src/dto/analyzer.go b/src/dto/analyzer.go deleted file mode 100644 index 850b8b0b..00000000 --- a/src/dto/analyzer.go +++ /dev/null @@ -1,64 +0,0 @@ -package dto - -import ( - "fmt" - - "aegis/consts" -) - -var ValidFirstTaskTypes = map[consts.TaskType]struct{}{ - consts.TaskTypeBuildContainer: {}, - consts.TaskTypeRestartPedestal: {}, - consts.TaskTypeBuildDatapack: {}, - consts.TaskTypeRunAlgorithm: {}, -} - -type AnalyzeTracesReq struct { - FirstTaskType *consts.TaskType `form:"first_task_type" binding:"omitempty"` - - TimeRangeQuery -} - -func (req *AnalyzeTracesReq) Validate() error { - if req.FirstTaskType != nil { - if _, exists := ValidFirstTaskTypes[*req.FirstTaskType]; !exists { - return fmt.Errorf("invalid event name: %d", req.FirstTaskType) - } - } - - return req.TimeRangeQuery.Validate() -} - -type PairStats struct { - Name string - InDegree int - OutDegree int -} - -type ServiceCoverageItem struct { - Num int - NotCovered []string - Coverage float64 -} - -type AttributeCoverageItem struct { - Num int - Coverage float64 -} - -type InjectionDiversity struct { - FaultDistribution map[string]int `json:"fault_distribution"` - ServiceDistribution map[string]int `json:"service_distribution"` - PairDistribution []PairStats `json:"pair_distribution"` - ServiceCoverages map[string]ServiceCoverageItem `json:"fault_service_coverages"` - AttributeCoverages map[string]map[string]AttributeCoverageItem `json:"attribute_coverages"` -} - -type InjectionStats struct { - Diversity InjectionDiversity `json:"diversity"` -} - -type AnalyzeInjectionsResp struct { - Efficiency string `json:"efficiency"` - Stats map[string]InjectionStats `json:"stats"` -} diff --git a/src/dto/audit.go b/src/dto/audit.go deleted file mode 100644 index b27450cc..00000000 --- a/src/dto/audit.go +++ /dev/null @@ -1,133 +0,0 @@ -package dto - -import ( - "aegis/consts" - "aegis/database" - "fmt" - "time" -) - -type ListAuditLogFilters struct { - Action string - IpAddress string - UserID int - ResourceID int - State *consts.AuditLogState - Status *consts.StatusType - StartTime *time.Time - EndTime *time.Time -} - -type ListAuditLogReq struct { - PaginationReq - - Action string `form:"action" binding:"omitempty"` - IPAddress string `form:"ip_address" binding:"omitempty"` - UserID int `form:"user_id" binding:"omitempty"` - ResourceID int `form:"resource_id" binding:"omitempty"` - State *consts.AuditLogState `form:"state" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` - StartDate string `form:"start_date" binding:"omitempty"` - EndDate string `form:"end_date" binding:"omitempty"` -} - -func (req *ListAuditLogReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if req.StartDate != "" { - if err := validateTimeField(req.StartDate, time.DateOnly); err != nil { - return fmt.Errorf("invalid start_time: %w", err) - } - } - if req.EndDate != "" { - if err := validateTimeField(req.EndDate, time.DateOnly); err != nil { - return fmt.Errorf("invalid end_time: %w", err) - } - } - - if _, exists := consts.ValidAuditLogStates[*req.State]; !exists { - return fmt.Errorf("invalid state: %d", *req.State) - } - - return validateStatusField(req.Status, false) -} - -func (req *ListAuditLogReq) ToFilterOptions() *ListAuditLogFilters { - var startTimePtr, endTimePtr *time.Time - - if req.StartDate != "" { - startTime, _ := time.Parse(time.DateTime, req.StartDate) - startTimePtr = &startTime - } - - if req.EndDate != "" { - endTime, _ := time.Parse(time.DateTime, req.EndDate) - endTimePtr = &endTime - } - - return &ListAuditLogFilters{ - Action: req.Action, - IpAddress: req.IPAddress, - UserID: req.UserID, - ResourceID: req.ResourceID, - State: req.State, - Status: req.Status, - StartTime: startTimePtr, - EndTime: endTimePtr, - } -} - -// AuditLogResp represents a summarized view of an audit log -type AuditLogResp struct { - ID int `json:"id"` - Action string `json:"action"` - IPAddress string `json:"ip_address"` - Duration int `json:"duration"` - UserAgent string `json:"user_agent"` - UserID int `json:"user_id,omitempty"` - Username string `json:"username,omitempty"` - ResourceID int `json:"resource_id,omitempty"` - Resource consts.ResourceName `json:"resource,omitempty"` - State string `json:"state"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` -} - -func NewAuditLogResp(log *database.AuditLog) *AuditLogResp { - resp := &AuditLogResp{ - ID: log.ID, - Action: log.Action, - IPAddress: log.IPAddress, - Duration: log.Duration, - UserAgent: log.UserAgent, - UserID: log.UserID, - ResourceID: log.ResourceID, - State: consts.GetAuditLogStateName(log.State), - Status: consts.GetStatusTypeName(log.Status), - CreatedAt: log.CreatedAt, - } - - if log.User != nil { - resp.Username = log.User.Username - } - if log.Resource != nil { - resp.Resource = log.Resource.Name - } - return resp -} - -// AuditLogDetailResp extends AuditLogResp with Details and ErrorMsg -type AuditLogDetailResp struct { - AuditLogResp - Details string `json:"details"` - ErrorMsg string `json:"error_msg,omitempty"` -} - -func NewAuditLogDetailResp(log *database.AuditLog) *AuditLogDetailResp { - return &AuditLogDetailResp{ - AuditLogResp: *NewAuditLogResp(log), - Details: log.Details, - ErrorMsg: log.ErrorMsg, - } -} diff --git a/src/dto/auth.go b/src/dto/auth.go deleted file mode 100644 index b9b3718f..00000000 --- a/src/dto/auth.go +++ /dev/null @@ -1,119 +0,0 @@ -package dto - -import ( - "fmt" - "regexp" - "time" - - "aegis/database" -) - -const ( - usernamePattern = `^[a-zA-Z0-9_]{3,20}$` -) - -// RegisterReq represents user registration request -type RegisterReq struct { - Username string `json:"username" binding:"required" example:"newuser"` - Email string `json:"email" binding:"required,email" example:"user@example.com"` - Password string `json:"password" binding:"required,min=8" example:"password123"` -} - -// Validate validates the registration request -func (req *RegisterReq) Validate() error { - // Username validation - usernameRegex := regexp.MustCompile(usernamePattern) - if !usernameRegex.MatchString(req.Username) { - return fmt.Errorf("username must be 3-20 characters and contain only letters, numbers, and underscores") - } - - // Password validation - if len(req.Password) == 0 { - return fmt.Errorf("password is required") - } - if len(req.Password) < 8 { - return fmt.Errorf("password must be at least 8 characters long") - } - - return nil -} - -// LoginReq represents user login request -type LoginReq struct { - Username string `json:"username" binding:"required" example:"admin"` - Password string `json:"password" binding:"required" example:"password123"` -} - -func (req *LoginReq) Validate() error { - usernameRegex := regexp.MustCompile(usernamePattern) - if !usernameRegex.MatchString(req.Username) { - return fmt.Errorf("invalid username or password") - } - if req.Password == "" { - return fmt.Errorf("invalid username or password") - } - return nil -} - -// TokenRefreshReq represents token refresh request -type TokenRefreshReq struct { - Token string `json:"token" binding:"required" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` -} - -func (req *TokenRefreshReq) Validate() error { - if req.Token == "" { - return fmt.Errorf("invalid token") - } - return nil -} - -// ChangePasswordReq represents password change request -type ChangePasswordReq struct { - OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"` - NewPassword string `json:"new_password" binding:"required,min=8" example:"newpassword123"` -} - -func (req *ChangePasswordReq) Validate() error { - if req.OldPassword == "" { - return fmt.Errorf("old_password is required") - } - if len(req.OldPassword) < 8 { - return fmt.Errorf("old_password must be at least 8 characters long") - } - if req.NewPassword == "" { - return fmt.Errorf("new_password is required") - } - if len(req.NewPassword) < 8 { - return fmt.Errorf("new_password must be at least 8 characters long") - } - return nil -} - -// LoginResp represents user login response -type LoginResp struct { - Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` - ExpiresAt time.Time `json:"expires_at" example:"2024-12-31T23:59:59Z"` - User UserInfo `json:"user"` -} - -// TokenRefreshResp represents token refresh response -type TokenRefreshResp struct { - Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` - ExpiresAt time.Time `json:"expires_at" example:"2024-12-31T23:59:59Z"` -} - -// UserInfo represents basic user information -type UserInfo struct { - ID int `json:"id" example:"1"` - Username string `json:"username" example:"admin"` - Avatar string `json:"avatar,omitempty"` - Role string `json:"role,omitempty"` -} - -func NewUserInfo(user *database.User) *UserInfo { - return &UserInfo{ - ID: user.ID, - Username: user.Username, - Avatar: user.Avatar, - } -} diff --git a/src/dto/container.go b/src/dto/container.go index 21065a8c..302fa957 100644 --- a/src/dto/container.go +++ b/src/dto/container.go @@ -2,13 +2,10 @@ package dto import ( "fmt" - "net/url" "path/filepath" "strings" - "time" - "aegis/consts" - "aegis/database" + "aegis/model" "aegis/utils" ) @@ -23,10 +20,6 @@ type ParameterItem struct { TemplateString string `json:"template_string,omitempty"` } -// ===================================================================== -// Container Service DTOs -// ===================================================================== - type HelmConfigItem struct { Version string `json:"version"` RepoURL string `json:"repo_url"` @@ -37,7 +30,7 @@ type HelmConfigItem struct { DynamicValues []ParameterItem `json:"values,omitempty"` } -func NewHelmConfigItem(cfg *database.HelmConfig) *HelmConfigItem { +func NewHelmConfigItem(cfg *model.HelmConfig) *HelmConfigItem { return &HelmConfigItem{ Version: cfg.Version, RepoURL: cfg.RepoURL, @@ -48,38 +41,31 @@ func NewHelmConfigItem(cfg *database.HelmConfig) *HelmConfigItem { } } -// GetValuesMap constructs a nested map of Helm values by merging +// GetValuesMap constructs a nested map of Helm values by merging file and dynamic values. func (hci *HelmConfigItem) GetValuesMap() map[string]any { root := make(map[string]any) - // Load values from ValueFile if it exists if hci.ValueFile != "" { if fileValues, err := utils.LoadYAMLFile(hci.ValueFile); err == nil { root = fileValues } } - // Merge dynamic values (override file values) for _, item := range hci.DynamicValues { value := item.Value - keys := utils.ParseHelmKey(item.Key) cur := root for i, k := range keys { if i == len(keys)-1 { - // Last key - set the value if k.IsArray { - // Handle array index if arr, ok := cur[k.Key].([]any); ok { - // Extend array if needed for len(arr) <= k.Index { arr = append(arr, make(map[string]any)) } arr[k.Index] = value cur[k.Key] = arr } else { - // Create new array arr := make([]any, k.Index+1) for j := 0; j < k.Index; j++ { arr[j] = make(map[string]any) @@ -93,11 +79,8 @@ func (hci *HelmConfigItem) GetValuesMap() map[string]any { break } - // Handle intermediate keys if k.IsArray { - // Current key is an array if _, exists := cur[k.Key]; !exists { - // Create new array arr := make([]any, k.Index+1) for j := 0; j <= k.Index; j++ { arr[j] = make(map[string]any) @@ -106,7 +89,6 @@ func (hci *HelmConfigItem) GetValuesMap() map[string]any { } if arr, ok := cur[k.Key].([]any); ok { - // Extend array if needed for len(arr) <= k.Index { arr = append(arr, make(map[string]any)) } @@ -115,21 +97,18 @@ func (hci *HelmConfigItem) GetValuesMap() map[string]any { if nextMap, ok := arr[k.Index].(map[string]any); ok { cur = nextMap } else { - // Create new map at this index newMap := make(map[string]any) arr[k.Index] = newMap cur = newMap } } } else { - // Regular key if _, exists := cur[k.Key]; !exists { cur[k.Key] = make(map[string]any) } if nextMap, ok := cur[k.Key].(map[string]any); ok { cur = nextMap } else { - // If the path exists but is not a map, replace it with a map newMap := make(map[string]any) cur[k.Key] = newMap cur = newMap @@ -154,7 +133,7 @@ type ContainerVersionItem struct { Extra *HelmConfigItem `json:"extra,omitempty"` } -func NewContainerVersionItem(version *database.ContainerVersion) ContainerVersionItem { +func NewContainerVersionItem(version *model.ContainerVersion) ContainerVersionItem { item := ContainerVersionItem{ ID: version.ID, Name: version.Name, @@ -190,7 +169,7 @@ func (ref *ContainerRef) Validate() error { type ContainerSpec struct { ContainerRef EnvVars []ParameterSpec `json:"env_vars" binding:"omitempty"` - Payload map[string]any `json:"payload,omitempty" swaggertype:"object"` // Additional payload data + Payload map[string]any `json:"payload,omitempty" swaggertype:"object"` } func (item *ContainerSpec) Validate() error { @@ -205,347 +184,11 @@ func (item *ContainerSpec) Validate() error { return nil } -// ===================================================================== -// Container CRUD DTOs -// ===================================================================== - -type CreateContainerReq struct { - Name string `json:"name" binding:"required"` - Type *consts.ContainerType `json:"type"` - README string `json:"readme" binding:"omitempty"` - IsPublic *bool `json:"is_public"` - - VersionReq *CreateContainerVersionReq `json:"version" binding:"omitempty"` -} - -func (req *CreateContainerReq) Validate() error { - req.Name = strings.TrimSpace(req.Name) - - if req.Name == "" { - return fmt.Errorf("container name cannot be empty") - } - if req.IsPublic == nil { - req.IsPublic = utils.BoolPtr(true) - } - - if req.Type == nil { - return fmt.Errorf("container type is required") - } - if err := validateContainerType(req.Type); err != nil { - return err - } - - if req.VersionReq != nil { - if err := req.VersionReq.Validate(); err != nil { - return fmt.Errorf("invalid container version request: %v", err) - } - } - - return nil -} - -func (req *CreateContainerReq) ConvertToContainer() *database.Container { - container := &database.Container{ - Name: req.Name, - Type: *req.Type, - README: req.README, - IsPublic: *req.IsPublic, - Status: consts.CommonEnabled, - } - - if req.VersionReq != nil { - container.Versions = []database.ContainerVersion{ - *req.VersionReq.ConvertToContainerVersion(), - } - } - - return container -} - type ParameterSpec struct { Key string `json:"key"` Value any `json:"value,omitempty"` } -type CreateContainerVersionReq struct { - Name string `json:"name" binding:"required"` - GithubLink string `json:"github_link" binding:"omitempty"` - ImageRef string `json:"image_ref" binding:"required"` - Command string `json:"command" binding:"omitempty"` - EnvVarRequests []CreateParameterConfigReq `json:"env_vars" binding:"omitempty"` - HelmConfigRequest *CreateHelmConfigReq `json:"helm_config" binding:"omitempty"` -} - -func (req *CreateContainerVersionReq) Validate() error { - req.Name = strings.TrimSpace(req.Name) - req.ImageRef = strings.TrimSpace(req.ImageRef) - - if req.Name == "" { - return fmt.Errorf("name cannot be empty") - } - if req.ImageRef == "" { - return fmt.Errorf("docker image reference cannot be empty") - } - - if req.GithubLink != "" { - req.GithubLink = strings.TrimSpace(req.GithubLink) - if err := utils.IsValidGitHubLink(req.GithubLink); err != nil { - return fmt.Errorf("invalid github link: %s, %v", req.GithubLink, err) - } - } - if _, _, _, err := utils.ParseSemanticVersion(req.Name); err != nil { - return fmt.Errorf("invalid semantic version: %s, %v", req.Name, err) - } - if _, _, _, _, err := utils.ParseFullImageRefernce(req.ImageRef); err != nil { - return fmt.Errorf("invalid docker image reference: %s, %v", req.ImageRef, err) - } - - for idx, envVarReq := range req.EnvVarRequests { - if err := envVarReq.Validate(); err != nil { - return fmt.Errorf("invalid env var at index %d: %v", idx, err) - } - } - - if req.HelmConfigRequest != nil { - if err := req.HelmConfigRequest.Validate(); err != nil { - return fmt.Errorf("invalid helm config: %v", err) - } - } - - return nil -} - -func (req *CreateContainerVersionReq) ConvertToContainerVersion() *database.ContainerVersion { - version := &database.ContainerVersion{ - Name: req.Name, - ImageRef: req.ImageRef, - Command: req.Command, - Status: consts.CommonEnabled, - } - - if len(req.EnvVarRequests) > 0 { - params := make([]database.ParameterConfig, 0, len(req.EnvVarRequests)) - for _, envVarReq := range req.EnvVarRequests { - params = append(params, *envVarReq.ConvertToParameterConfig()) - } - version.EnvVars = params - } - - if req.HelmConfigRequest != nil { - version.HelmConfig = req.HelmConfigRequest.ConvertToHelmConfig() - } - - return version -} - -type CreateHelmConfigReq struct { - Version string `json:"version" binding:"required"` - ChartName string `json:"chart_name" binding:"required"` - RepoName string `json:"repo_name" binding:"required"` - RepoURL string `json:"repo_url" binding:"required"` - DynamicValues []CreateParameterConfigReq `json:"dynamic_values" binding:"omitempty" swaggertype:"object"` -} - -func (req *CreateHelmConfigReq) Validate() error { - req.Version = strings.TrimSpace(req.Version) - req.ChartName = strings.TrimSpace(req.ChartName) - req.RepoName = strings.TrimSpace(req.RepoName) - req.RepoURL = strings.TrimSpace(req.RepoURL) - - if req.Version == "" { - if _, _, _, err := utils.ParseSemanticVersion(req.Version); err != nil { - return fmt.Errorf("invalid semantic version: %s, %v", req.Version, err) - } - } - if req.ChartName == "" { - return fmt.Errorf("chart name cannot be empty") - } - if req.RepoName == "" { - return fmt.Errorf("repository name cannot be empty") - } - if req.RepoURL == "" { - return fmt.Errorf("repository URL cannot be empty") - } - - if _, err := url.ParseRequestURI(req.RepoURL); err != nil { - return fmt.Errorf("invalid repository URL: %s, %w", req.RepoURL, err) - } - - for i, val := range req.DynamicValues { - if err := val.Validate(); err != nil { - return fmt.Errorf("invalid parameter config at index %d: %w", i, err) - } - } - - return nil -} - -func (req *CreateHelmConfigReq) ConvertToHelmConfig() *database.HelmConfig { - cfg := &database.HelmConfig{ - Version: req.Version, - ChartName: req.ChartName, - RepoName: req.RepoName, - RepoURL: req.RepoURL, - } - - if len(req.DynamicValues) > 0 { - params := make([]database.ParameterConfig, 0, len(req.DynamicValues)) - for _, val := range req.DynamicValues { - params = append(params, *val.ConvertToParameterConfig()) - } - cfg.DynamicValues = params - } - - return cfg -} - -type CreateParameterConfigReq struct { - Key string `json:"key" binding:"required"` - Type consts.ParameterType `json:"type" binding:"required"` - Category consts.ParameterCategory `json:"category" binding:"required"` - ValueType consts.ValueDataType `json:"value_type" binding:"omitempty"` - Description string `json:"description" binding:"omitempty"` - DefaultValue *string `json:"default_value" binding:"omitempty"` - TemplateString *string `json:"template_string" binding:"omitempty"` - Required bool `json:"required"` - Overridable *bool `json:"overridable" binding:"omitempty"` -} - -func (req *CreateParameterConfigReq) Validate() error { - if req.Key == "" { - return fmt.Errorf("parameter key cannot be empty") - } - - if _, exists := consts.ValidParameterTypes[req.Type]; !exists { - return fmt.Errorf("invalid parameter type: %v", req.Type) - } - if _, exists := consts.ValidParameterCategories[req.Category]; !exists { - return fmt.Errorf("invalid parameter category: %v", req.Category) - } - - if req.Type == consts.ParameterTypeFixed && req.Required && req.DefaultValue == nil { - return fmt.Errorf("default value is required for fixed parameter type when marked as required") - } - - if req.Type == consts.ParameterTypeDynamic && req.TemplateString == nil { - return fmt.Errorf("template string is required for dynamic parameter type") - } - - return nil -} - -func (req *CreateParameterConfigReq) ConvertToParameterConfig() *database.ParameterConfig { - config := &database.ParameterConfig{ - Key: req.Key, - Type: req.Type, - Category: req.Category, - ValueType: req.ValueType, - Description: req.Description, - DefaultValue: req.DefaultValue, - TemplateString: req.TemplateString, - Required: req.Required, - Overridable: true, // default to true - } - - // If overridable is explicitly set, use that value - if req.Overridable != nil { - config.Overridable = *req.Overridable - } - - return config -} - -// ListContainerReq represents container list query parameters -type ListContainerReq struct { - PaginationReq - Type *consts.ContainerType `form:"type"` - IsPublic *bool `form:"is_public"` - Status *consts.StatusType `form:"status"` -} - -func (req *ListContainerReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if err := validateContainerType(req.Type); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -// ListContainerVersionReq represents container version list query parameters -type ListContainerVersionReq struct { - PaginationReq - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *ListContainerVersionReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -// SearchContainerReq represents container search request -type SearchContainerReq struct { - AdvancedSearchReq[string] - - // Container-specific filters - Name *string `json:"name,omitempty"` - Image *string `json:"image,omitempty"` - Tag *string `json:"tag,omitempty"` - Type *string `json:"type,omitempty"` - Command *string `json:"command,omitempty"` - Status *int `json:"status,omitempty"` -} - -// ConvertToSearchRequest converts SearchContainerReq to SearchRequest -func (csr *SearchContainerReq) ConvertToSearchRequest() *SearchReq[string] { - sr := csr.ConvertAdvancedToSearch() - - // Add container-specific filters - if csr.Name != nil { - sr.AddFilter("name", OpLike, *csr.Name) - } - if csr.Image != nil { - sr.AddFilter("image", OpLike, *csr.Image) - } - if csr.Tag != nil { - sr.AddFilter("tag", OpEqual, *csr.Tag) - } - if csr.Type != nil { - sr.AddFilter("type", OpEqual, *csr.Type) - } - if csr.Command != nil { - sr.AddFilter("command", OpLike, *csr.Command) - } - - return sr -} - -// UpdateContainerReq represents the request for updating a container -type UpdateContainerReq struct { - README *string `json:"readme" binding:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *UpdateContainerReq) Validate() error { - return validateStatusField(req.Status, true) -} - -func (req *UpdateContainerReq) PatchContainerModel(target *database.Container) { - if req.README != nil { - target.README = *req.README - } - if req.IsPublic != nil { - target.IsPublic = *req.IsPublic - } - if req.Status != nil { - target.Status = *req.Status - } -} - type BuildOptions struct { ContextDir string `json:"context_dir" binding:"omitempty" default:"."` DockerfilePath string `json:"dockerfile_path" binding:"omitempty" default:"Dockerfile"` @@ -598,345 +241,3 @@ func (opts *BuildOptions) ValidateRequiredFiles(sourcePath string) error { return nil } - -// SubmitBuildContainerReq represents the request for building a container into platform registry -type SubmitBuildContainerReq struct { - // Container Meta - ImageName string `json:"image_name" binding:"required"` - Tag string `json:"tag" binding:"omitempty"` - - // GitHub repository information - GithubRepository string `json:"github_repository" binding:"required"` - GithubBranch string `json:"github_branch" binding:"omitempty"` - GithubCommit string `json:"github_commit" binding:"omitempty"` - GithubToken string `json:"github_token" binding:"omitempty"` - SubPath string `json:"sub_path" binding:"omitempty"` - - Options *BuildOptions `json:"build_options" binding:"omitempty"` -} - -func (req *SubmitBuildContainerReq) Validate() error { - req.ImageName = strings.TrimSpace(req.ImageName) - req.GithubRepository = strings.TrimSpace(req.GithubRepository) - - if req.ImageName == "" { - return fmt.Errorf("container image name cannot be empty") - } - if req.Tag != "" { - req.Tag = strings.TrimSpace(req.Tag) - } - parts := strings.Split(req.GithubRepository, "/") - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return fmt.Errorf("invalid repository format, expected 'owner/repo'") - } - if req.GithubBranch != "" { - req.GithubBranch = strings.TrimSpace(req.GithubBranch) - if err := utils.IsValidGitHubBranch(req.GithubBranch); err != nil { - return err - } - } - if req.GithubCommit != "" { - req.GithubCommit = strings.TrimSpace(req.GithubCommit) - if err := utils.IsValidGitHubCommit(req.GithubCommit); err != nil { - return err - } - } - if req.GithubToken != "" { - req.GithubToken = strings.TrimSpace(req.GithubToken) - if err := utils.IsValidGitHubToken(req.GithubToken); err != nil { - return err - } - } - - if req.Tag == "" { - req.Tag = "latest" - } - if req.GithubBranch == "" { - req.GithubBranch = "main" - } - if req.SubPath == "" { - req.SubPath = "." - } - - return req.Options.Validate() -} - -func (req *SubmitBuildContainerReq) ValidateInfoContent(sourcePath string) error { - if req.ImageName == "" { - tomlPath := filepath.Join(sourcePath, InfoFileName) - content, err := utils.ReadTomlFile(tomlPath) - if err != nil { - return err - } - - if name, ok := content[InfoNameField].(string); ok && name != "" { - req.ImageName = name - } else { - return fmt.Errorf("%s does not contain a valid name field", InfoFileName) - } - } - - return nil -} - -// UpdateContainerVersionReq represents the request for updating a container version -type UpdateContainerVersionReq struct { - GithubLink *string `json:"github_link" binding:"omitempty"` - Command *string `json:"command" binding:"omitempty"` - Status *consts.StatusType `json:"status" binding:"omitempty"` - HelmConfigRequest *UpdateHelmConfigReq `json:"helm_config" binding:"omitempty"` -} - -func (req *UpdateContainerVersionReq) Validate() error { - if req.GithubLink != nil { - trimmedLink := strings.TrimSpace(*req.GithubLink) - *req.GithubLink = trimmedLink - - if trimmedLink != "" { - if err := utils.IsValidGitHubLink(trimmedLink); err != nil { - return fmt.Errorf("invalid GitHub link '%s': %v", trimmedLink, err) - } - } - } - if req.Command != nil { - *req.Command = strings.TrimSpace(*req.Command) - } - if req.Status != nil { - if err := validateStatusField(req.Status, true); err != nil { - return err - } - } - - if req.HelmConfigRequest != nil { - if err := req.HelmConfigRequest.Validate(); err != nil { - return fmt.Errorf("invalid helm config: %v", err) - } - } - - return nil -} - -func (req *UpdateContainerVersionReq) PatchContainerVersionModel(target *database.ContainerVersion) { - if req.GithubLink != nil { - target.GithubLink = *req.GithubLink - } - if req.Command != nil { - target.Command = *req.Command - } - if req.Status != nil { - target.Status = *req.Status - } -} - -type UpdateHelmConfigReq struct { - RepoURL *string `json:"repo_url" binding:"omitempty"` - RepoName *string `json:"repo_name" binding:"omitempty"` - ChartName *string `json:"chart_name" binding:"omitempty"` - DynamicValues *map[string]any `json:"dynamic_values" binding:"omitempty" swaggertype:"object"` -} - -func (req *UpdateHelmConfigReq) Validate() error { - if req.RepoURL != nil { - trimmedURL := strings.TrimSpace(*req.RepoURL) - *req.RepoURL = trimmedURL - - if trimmedURL == "" { - return fmt.Errorf("repository URL cannot be empty if provided") - } - if _, err := url.Parse(trimmedURL); err != nil { - return fmt.Errorf("invalid repository URL format: %s. Error: %v", trimmedURL, err) - } - } - if req.RepoName != nil { - *req.RepoName = strings.TrimSpace(*req.RepoName) - } - if req.ChartName != nil { - *req.ChartName = strings.TrimSpace(*req.ChartName) - } - return nil -} - -func (req *UpdateHelmConfigReq) PatchHelmConfigModel(target *database.HelmConfig) error { - if req.RepoURL != nil { - target.RepoURL = *req.RepoURL - } - if req.RepoName != nil { - target.RepoName = *req.RepoName - } - if req.ChartName != nil { - target.ChartName = *req.ChartName - } - return nil -} - -// ContainerResp is basic container info used -type ContainerResp struct { - ID int `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsPublic bool `json:"is_public"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - Labels []LabelItem `json:"labels,omitempty"` -} - -func NewContainerResp(container *database.Container) *ContainerResp { - resp := &ContainerResp{ - ID: container.ID, - Name: container.Name, - Type: consts.GetContainerTypeName(container.Type), - IsPublic: container.IsPublic, - Status: consts.GetStatusTypeName(container.Status), - CreatedAt: container.CreatedAt, - UpdatedAt: container.UpdatedAt, - } - - if len(container.Labels) > 0 { - resp.Labels = make([]LabelItem, 0, len(container.Labels)) - for _, l := range container.Labels { - resp.Labels = append(resp.Labels, LabelItem{ - Key: l.Key, - Value: l.Value, - }) - } - } - return resp -} - -// ContainerDetailResp is used for single resource retrieval. -type ContainerDetailResp struct { - ContainerResp - - README string `json:"readme"` - - Versions []ContainerVersionResp `json:"versions"` -} - -func NewContainerDetailResp(container *database.Container) *ContainerDetailResp { - return &ContainerDetailResp{ - ContainerResp: *NewContainerResp(container), - README: container.README, - } -} - -type ContainerVersionResp struct { - ID int `json:"id"` - Name string `json:"name"` - ImageRef string `json:"image_ref"` - Usage int `json:"usage"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewContainerVersionResp(version *database.ContainerVersion) *ContainerVersionResp { - return &ContainerVersionResp{ - ID: version.ID, - Name: version.Name, - ImageRef: version.ImageRef, - Usage: version.Usage, - UpdatedAt: version.UpdatedAt, - } -} - -type ContainerVersionDetailResp struct { - ContainerVersionResp - - GithubLink string `json:"github_link"` - Command string `json:"command"` - EnvVars string `json:"env_vars"` - - HelmConfig *HelmConfigDetailResp `json:"helm_config,omitempty"` -} - -func NewContainerVersionDetailResp(version *database.ContainerVersion) *ContainerVersionDetailResp { - return &ContainerVersionDetailResp{ - ContainerVersionResp: *NewContainerVersionResp(version), - GithubLink: version.GithubLink, - Command: version.Command, - } -} - -type ListContainerVersionResp struct { - Items []ContainerResp `json:"items"` - Pagination PaginationInfo `json:"pagination"` -} - -type HelmConfigDetailResp struct { - ID int `json:"id"` - Version string `json:"version"` - ChartName string `json:"chart_name"` - RepoName string `json:"repo_name"` - RepoURL string `json:"repo_url"` - LocalPath string `json:"local_path,omitempty"` - ValueFile string `json:"value_file,omitempty"` - Values map[string]any `json:"values"` -} - -func NewHelmConfigDetailResp(cfg *database.HelmConfig) (*HelmConfigDetailResp, error) { - resp := &HelmConfigDetailResp{ - ID: cfg.ID, - Version: cfg.Version, - ChartName: cfg.ChartName, - RepoName: cfg.RepoName, - RepoURL: cfg.RepoURL, - LocalPath: cfg.LocalPath, - ValueFile: cfg.ValueFile, - } - - return resp, nil -} - -// UploadHelmValueFileResp represents the response for uploading a Helm values file -type UploadHelmValueFileResp struct { - FilePath string `json:"file_path"` // Saved file path - FileName string `json:"file_name"` // Original file name -} - -type UploadHelmChartResp struct { - FilePath string `json:"file_path"` // Saved chart path - FileName string `json:"file_name"` // Original chart file name - Checksum string `json:"checksum"` // SHA256 checksum of the chart -} - -type SubmitContainerBuildResp struct { - GroupID string `json:"group_id"` - TraceID string `json:"trace_id"` - TaskID string `json:"task_id"` -} - -// ---------------------- Container Label DTOs ------------------ - -// ManageContainerLabelReq represents the request for managing container labels -type ManageContainerLabelReq struct { - AddLabels []LabelItem `json:"add_labels" binding:"omitempty"` // List of labels to add - RemoveLabels []string `json:"remove_labels" binding:"omitempty"` // List of label keys to remove -} - -func (req *ManageContainerLabelReq) Validate() error { - if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { - return fmt.Errorf("at least one of add_labels or remove_labels must be provided") - } - - if err := validateLabelItemsFiled(req.AddLabels); err != nil { - return err - } - - for i, key := range req.RemoveLabels { - if strings.TrimSpace(key) == "" { - return fmt.Errorf("empty label key at index %d in remove_labels", i) - } - } - - return nil -} - -// validateContainerType checks if the provided container type is valid -func validateContainerType(containerType *consts.ContainerType) error { - if containerType != nil { - if _, exists := consts.ValidContainerTypes[*containerType]; !exists { - return fmt.Errorf("invalid container type: %d", *containerType) - } - } - return nil -} diff --git a/src/dto/dataset.go b/src/dto/dataset.go index 5c44da48..2eb32224 100644 --- a/src/dto/dataset.go +++ b/src/dto/dataset.go @@ -2,18 +2,11 @@ package dto import ( "fmt" - "strings" - "time" - "aegis/consts" - "aegis/database" "aegis/utils" ) -// ===================================================================== -// Dataset Service DTOs -// ===================================================================== - +// DatasetRef is the shared dataset reference used across modules and tasks. type DatasetRef struct { Name string `json:"name" binding:"required"` Version string `json:"version" binding:"omitempty"` @@ -30,322 +23,3 @@ func (ref *DatasetRef) Validate() error { } return nil } - -// ===================== Dataset CRUD DTOs ===================== - -type CreateDatasetReq struct { - Name string `json:"name" binding:"required"` - Type string `json:"type" binding:"required"` - Description string `json:"description" binding:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` - - VersionReq *CreateDatasetVersionReq `json:"version" binding:"omitempty"` -} - -func (req *CreateDatasetReq) Validate() error { - req.Name = strings.TrimSpace(req.Name) - req.Type = strings.TrimSpace(req.Type) - - if req.Name == "" { - return fmt.Errorf("dataset name cannot be empty") - } - if req.Type == "" { - return fmt.Errorf("dataset type cannot be empty") - } - if req.IsPublic == nil { - req.IsPublic = utils.BoolPtr(true) - } - - if req.VersionReq != nil { - if err := req.VersionReq.Validate(); err != nil { - return fmt.Errorf("invalid dataset version request: %v", err) - } - } - - return nil -} - -func (req *CreateDatasetReq) ConvertToDataset() *database.Dataset { - return &database.Dataset{ - Name: req.Name, - Type: req.Type, - Description: req.Description, - IsPublic: *req.IsPublic, - Status: consts.CommonEnabled, - } -} - -type ListDatasetReq struct { - PaginationReq - Type string `form:"type" binding:"omitempty"` - IsPublic *bool `form:"is_public" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListDatasetReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -type SearchDatasetReq struct { - AdvancedSearchReq[consts.DatasetField] - - NamePattern string `json:"name_pattern" binding:"omitempty"` - IncludeVersions bool `json:"include_versions" binding:"omitempty"` -} - -func (req *SearchDatasetReq) Validate() error { - if err := req.AdvancedSearchReq.Validate(); err != nil { - return err - } - for i, sortField := range req.Sort { - if _, valid := consts.DatasetAllowedFields[sortField.Field]; !valid { - return fmt.Errorf("invalid sort_by field at index %d: %s", i, sortField.Field) - } - } - for i, field := range req.GroupBy { - if _, valid := consts.DatasetAllowedFields[field]; !valid { - return fmt.Errorf("invalid group_by field at index %d: %s", i, field) - } - } - return nil -} - -func (req *SearchDatasetReq) ConvertToSearchReq() *SearchReq[consts.DatasetField] { - sr := req.ConvertAdvancedToSearch() - - if req.NamePattern != "" { - sr.AddFilter("name", OpLike, req.NamePattern) - } - - if req.IncludeVersions { - sr.AddInclude("Versions") - } - - return sr -} - -type UpdateDatasetReq struct { - Description *string `json:"description" binding:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *UpdateDatasetReq) Validate() error { - return validateStatusField(req.Status, true) -} - -func (req *UpdateDatasetReq) PatchDatasetModel(target *database.Dataset) { - if req.Description != nil { - target.Description = *req.Description - } - if req.IsPublic != nil { - target.IsPublic = *req.IsPublic - } - if req.Status != nil { - target.Status = *req.Status - } -} - -type ManageDatasetLabelReq struct { - AddLabels []LabelItem `json:"add_labels" binding:"omitempty"` // List of labels to add - RemoveLabels []string `json:"remove_labels" binding:"omitempty"` // List of label keys to remove -} - -func (req *ManageDatasetLabelReq) Validate() error { - if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { - return fmt.Errorf("at least one of add_labels or remove_labels must be provided") - } - - if err := validateLabelItemsFiled(req.AddLabels); err != nil { - return err - } - - for i, key := range req.RemoveLabels { - if strings.TrimSpace(key) == "" { - return fmt.Errorf("empty label key at index %d in remove_labels", i) - } - } - - return nil -} - -type ManageDatasetVersionInjectionReq struct { - AddDatapacks []string `json:"add_datapacks" binding:"omitempty"` - RemoveDatapacks []string `json:"remove_datapacks" binding:"omitempty"` -} - -func (req *ManageDatasetVersionInjectionReq) Validate() error { - if len(req.AddDatapacks) == 0 && len(req.RemoveDatapacks) == 0 { - return fmt.Errorf("at least one of add_injections or remove_injections must be provided") - } - - for i, datapack := range req.AddDatapacks { - if strings.TrimSpace(datapack) == "" { - return fmt.Errorf("empty datapack name at index %d in add_datapacks", i) - } - } - for i, datapack := range req.RemoveDatapacks { - if strings.TrimSpace(datapack) == "" { - return fmt.Errorf("empty datapack name at index %d in add_datapacks", i) - } - } - - return nil -} - -type DatasetResp struct { - ID int `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsPublic bool `json:"is_public"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - Labels []LabelItem `json:"labels,omitempty"` -} - -func NewDatasetResp(dataset *database.Dataset) *DatasetResp { - resp := &DatasetResp{ - ID: dataset.ID, - Name: dataset.Name, - Type: dataset.Type, - IsPublic: dataset.IsPublic, - Status: consts.GetStatusTypeName(dataset.Status), - CreatedAt: dataset.CreatedAt, - UpdatedAt: dataset.UpdatedAt, - } - - if len(dataset.Labels) > 0 { - resp.Labels = make([]LabelItem, 0, len(dataset.Labels)) - for _, l := range dataset.Labels { - resp.Labels = append(resp.Labels, LabelItem{ - Key: l.Key, - Value: l.Value, - }) - } - } - return resp -} - -type DatasetDetailResp struct { - DatasetResp - - Description string `json:"description"` - - Versions []DatasetVersionResp `json:"versions"` -} - -func NewDatasetDetailResp(dataset *database.Dataset) *DatasetDetailResp { - return &DatasetDetailResp{ - DatasetResp: *NewDatasetResp(dataset), - Description: dataset.Description, - } -} - -// ===================== Dataset Version CRUD DTOs ===================== - -type CreateDatasetVersionReq struct { - Name string `json:"name" binding:"required"` - Datapacks []string `json:"datapacks" binding:"omitempty"` -} - -func (req *CreateDatasetVersionReq) Validate() error { - req.Name = strings.TrimSpace(req.Name) - - if req.Name == "" { - return fmt.Errorf("name cannot be empty") - } - - if _, _, _, err := utils.ParseSemanticVersion(req.Name); err != nil { - return fmt.Errorf("invalid semantic version: %s, %v", req.Name, err) - } - - if len(req.Datapacks) > 0 { - for i, dp := range req.Datapacks { - if strings.TrimSpace(dp) == "" { - return fmt.Errorf("empty datapack name at index %d", i) - } - } - } - - return nil -} - -func (req *CreateDatasetVersionReq) ConvertToDatasetVersion() *database.DatasetVersion { - version := &database.DatasetVersion{ - Name: req.Name, - Status: consts.CommonEnabled, - } - - return version -} - -// ListDatasetVersionReq represents dataset version list query parameters -type ListDatasetVersionReq struct { - PaginationReq - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *ListDatasetVersionReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -type UpdateDatasetVersionReq struct { - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *UpdateDatasetVersionReq) Validate() error { - return validateStatusField(req.Status, true) -} - -func (req *UpdateDatasetVersionReq) PatchDatasetVersionModel(target *database.DatasetVersion) { - if req.Status != nil { - target.Status = *req.Status - } -} - -type DatasetVersionResp struct { - ID int `json:"id"` - Name string `json:"name"` - Checksum string `json:"checksum"` - FileCount int `json:"file_count"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewDatasetVersionResp(version *database.DatasetVersion) *DatasetVersionResp { - return &DatasetVersionResp{ - ID: version.ID, - Name: version.Name, - Checksum: version.Checksum, - FileCount: version.FileCount, - UpdatedAt: version.UpdatedAt, - } -} - -type DatasetVersionDetailResp struct { - DatasetVersionResp - - Datapacks []InjectionResp `json:"datapacks,omitempty"` -} - -func NewDatasetVersionDetailResp(version *database.DatasetVersion) *DatasetVersionDetailResp { - resp := &DatasetVersionDetailResp{ - DatasetVersionResp: *NewDatasetVersionResp(version), - } - - if len(version.Datapacks) > 0 { - resp.Datapacks = make([]InjectionResp, 0, len(version.Datapacks)) - for _, inj := range version.Datapacks { - resp.Datapacks = append(resp.Datapacks, *NewInjectionResp(&inj)) - } - } - - return resp -} diff --git a/src/dto/debug.go b/src/dto/debug.go deleted file mode 100644 index c18d3475..00000000 --- a/src/dto/debug.go +++ /dev/null @@ -1,10 +0,0 @@ -package dto - -type DebugGetReq struct { - Name string `form:"name" binding:"required"` -} - -type DebugSetReq struct { - Name string `json:"name" binding:"required"` - Value any `json:"value"` -} diff --git a/src/dto/dynamic_config.go b/src/dto/dynamic_config.go index 81b7d35d..56046723 100644 --- a/src/dto/dynamic_config.go +++ b/src/dto/dynamic_config.go @@ -5,271 +5,9 @@ import ( "fmt" "time" - "aegis/consts" - "aegis/database" "aegis/utils" ) -// ===================================================================== -// Configuration DTOs -// ===================================================================== - -// ListConfigReq represents config list query parameters -type ListConfigReq struct { - PaginationReq - ValueType *consts.ConfigValueType `form:"value_type" binding:"omitempty"` - Category *string `form:"category" binding:"omitempty"` - IsSecret *bool `form:"is_secret" binding:"omitempty"` - UpdatedBy *int `form:"updated_by" binding:"omitempty,min_ptr=1"` -} - -func (req *ListConfigReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if err := validateValuteType(req.ValueType); err != nil { - return err - } - return nil -} - -// RollbackConfigReq represents a request to rollback a configuration -type RollbackConfigReq struct { - HistoryID int `json:"history_id" binding:"required,min=1"` - Reason string `json:"reason" binding:"required"` -} - -// UpdateConfigValueReq represents a request to update a configuration value (runtime config) -type UpdateConfigValueReq struct { - Value string `json:"value" binding:"required"` - Reason string `json:"reason" binding:"required"` -} - -// UpdateConfigMetadataReq represents a request to update configuration metadata -type UpdateConfigMetadataReq struct { - // Metadata fields - only ONE field should be provided per request - DefaultValue *string `json:"default_value" binding:"omitempty"` - Description *string `json:"description" binding:"omitempty"` - MinValue *float64 `json:"min_value" binding:"omitempty"` - MaxValue *float64 `json:"max_value" binding:"omitempty"` - Pattern *string `json:"pattern" binding:"omitempty"` - Options *string `json:"options" binding:"omitempty"` - - // Audit trail - Reason string `json:"reason" binding:"required"` -} - -func (req *UpdateConfigMetadataReq) Validate() error { - // Count how many fields are being updated - fieldCount := 0 - if req.DefaultValue != nil { - fieldCount++ - } - if req.Description != nil { - fieldCount++ - } - if req.MinValue != nil { - fieldCount++ - } - if req.MaxValue != nil { - fieldCount++ - } - if req.Pattern != nil { - fieldCount++ - } - if req.Options != nil { - fieldCount++ - } - - if fieldCount == 0 { - return fmt.Errorf("at least one metadata field must be provided for update") - } - if fieldCount > 1 { - return fmt.Errorf("can only update one metadata field at a time") - } - - return nil -} - -func (req *UpdateConfigMetadataReq) PatchConfigModel(target *database.DynamicConfig) (string, string) { - var oldValue string - var newValue string - - if req.DefaultValue != nil { - oldValue = target.DefaultValue - newValue = *req.DefaultValue - target.DefaultValue = *req.DefaultValue - } - if req.Description != nil { - oldValue = target.Description - newValue = *req.Description - target.Description = *req.Description - } - if req.MinValue != nil { - oldValue = fmt.Sprintf("%v", target.MinValue) - newValue = fmt.Sprintf("%v", req.MinValue) - target.MinValue = req.MinValue - } - if req.MaxValue != nil { - oldValue = fmt.Sprintf("%v", target.MaxValue) - newValue = fmt.Sprintf("%v", req.MaxValue) - target.MaxValue = req.MaxValue - } - if req.Pattern != nil { - oldValue = target.Pattern - newValue = *req.Pattern - target.Pattern = *req.Pattern - } - if req.Options != nil { - oldValue = target.Options - newValue = *req.Options - target.Options = *req.Options - } - - return oldValue, newValue -} - -// GetChangeField returns the specific metadata field being changed -func (req *UpdateConfigMetadataReq) GetChangeField() consts.ConfigHistoryChangeField { - if req.DefaultValue != nil { - return consts.ChangeFieldDefaultValue - } - if req.Description != nil { - return consts.ChangeFieldDescription - } - if req.MinValue != nil { - return consts.ChangeFieldMinValue - } - if req.MaxValue != nil { - return consts.ChangeFieldMaxValue - } - if req.Pattern != nil { - return consts.ChangeFieldPattern - } - if req.Options != nil { - return consts.ChangeFieldOptions - } - return consts.ChangeFieldValue -} - -type ListConfigHistoryReq struct { - PaginationReq - ChangeType *consts.ConfigHistoryChangeType `form:"change_type" binding:"omitempty"` - OperatorID *int `form:"operator_id" binding:"omitempty,min_ptr=1"` -} - -func (req *ListConfigHistoryReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if req.ChangeType != nil { - if _, ok := consts.ValidConfigHistoryChanteTypes[*req.ChangeType]; !ok { - return fmt.Errorf("invalid change type: %v", req.ChangeType) - } - } - return nil -} - -// ConfigResp represents a configuration item response -type ConfigResp struct { - ID int `json:"id"` - Key string `json:"key"` - ValueType string `json:"value_type"` - Category string `json:"category"` - UpdatedAt time.Time `json:"updated_at"` - UpdatedByID int `json:"updated_by_id"` - UpdatedByName string `json:"updated_by_name"` -} - -// NewConfigResp converts a DynamicConfig entity to ConfigResp DTO -func NewConfigResp(config *database.DynamicConfig) *ConfigResp { - resp := &ConfigResp{ - ID: config.ID, - Key: config.Key, - ValueType: consts.GetDynamicConfigTypeName(config.ValueType), - Category: config.Category, - UpdatedAt: config.UpdatedAt, - } - - if config.UpdatedByUser != nil { - resp.UpdatedByName = config.UpdatedByUser.Username - } - - return resp -} - -type ConfigDetailResp struct { - ConfigResp - - DefaultValue string `json:"default_value"` - Description string `json:"description"` - MinValue *float64 `json:"min_value,omitempty"` - MaxValue *float64 `json:"max_value,omitempty"` - Pattern string `json:"pattern,omitempty"` - Options string `json:"options,omitempty"` - Histories []ConfigHistoryResp `json:"histories,omitempty"` -} - -func NewConfigDetailResp(config *database.DynamicConfig) *ConfigDetailResp { - return &ConfigDetailResp{ - ConfigResp: *NewConfigResp(config), - DefaultValue: config.DefaultValue, - Description: config.Description, - MinValue: config.MinValue, - MaxValue: config.MaxValue, - Pattern: config.Pattern, - Options: config.Options, - } -} - -// ConfigHistoryResp represents a configuration change history entry response -type ConfigHistoryResp struct { - ID int `json:"id"` - ChangeType string `json:"change_type"` - OldValue string `json:"old_value"` - NewValue string `json:"new_value"` - Reason string `json:"reason"` - ConfigID int `json:"config_id"` - OperatorID *int `json:"operator_id"` - OperatorName string `json:"operator_name,omitempty"` - IPAddress string `json:"ip_address,omitempty"` - UserAgent string `json:"user_agent,omitempty"` - RolledBackFromID *int `json:"rolled_back_from_id,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -func NewConfigHistoryResp(history *database.ConfigHistory) *ConfigHistoryResp { - resp := &ConfigHistoryResp{ - ID: history.ID, - ChangeType: consts.GetConfigHistoryChangeTypeName(history.ChangeType), - ConfigID: history.ConfigID, - OldValue: history.OldValue, - NewValue: history.NewValue, - Reason: history.Reason, - OperatorID: history.OperatorID, - IPAddress: history.IPAddress, - UserAgent: history.UserAgent, - RolledBackFromID: history.RolledBackFromID, - CreatedAt: history.CreatedAt, - } - - if history.Operator != nil { - resp.OperatorName = history.Operator.Username - } - return resp -} - -// ConfigStatsResp represents statistics about the configuration system -type ConfigStatsResp struct { - TotalConfigs int `json:"total_configs"` - DynamicConfigs int `json:"dynamic_configs"` - StaticConfigs int `json:"static_configs"` - TotalChanges int `json:"total_changes"` - ChangesLast24h int `json:"changes_last_24h"` - Categories []string `json:"categories"` - LastUpdate time.Time `json:"last_update"` -} - // ConfigUpdateResponse represents the response to a configuration update event type ConfigUpdateResponse struct { ID string `json:"id"` @@ -307,13 +45,3 @@ func (r *ConfigUpdateResponse) ToMap() (map[string]any, error) { return m, nil } - -// validateValuteType checks if the provided config value type is valid -func validateValuteType(valueType *consts.ConfigValueType) error { - if valueType != nil { - if _, ok := consts.ValidDynamicConfigTypes[*valueType]; !ok { - return fmt.Errorf("invalid value type: %v", valueType) - } - } - return nil -} diff --git a/src/dto/group.go b/src/dto/group.go deleted file mode 100644 index d90a4fa2..00000000 --- a/src/dto/group.go +++ /dev/null @@ -1,47 +0,0 @@ -package dto - -import ( - "aegis/consts" - "fmt" - "strings" -) - -// ===================== Group Stream DTO ===================== - -// GroupStreamEvent represents a lightweight event pushed to group-level Redis stream -// when a trace reaches a terminal state (Completed/Failed). -type GroupStreamEvent struct { - TraceID string `json:"trace_id"` - State consts.TraceState `json:"state"` - LastEvent consts.EventType `json:"last_event"` -} - -// ToRedisStream converts GroupStreamEvent to Redis stream field-value pairs -func (e *GroupStreamEvent) ToRedisStream() map[string]any { - return map[string]any{ - consts.RdbEventTraceID: e.TraceID, - consts.RdbEventTraceState: e.State, - consts.RdbEventTraceLastEvent: e.LastEvent, - } -} - -// GetGroupStreamReq represents the request to subscribe to a group stream -type GetGroupStreamReq struct { - LastID string `form:"last_id" binding:"omitempty"` -} - -func (req *GetGroupStreamReq) Validate() error { - if req.LastID == "" { - req.LastID = "0" - } - - if req.LastID == "0" { - return nil - } - - if strings.Count(req.LastID, "-") != 1 { - return fmt.Errorf("invalid last_id format: must be '0' or a valid stream ID (e.g., 1678886400000-0)") - } - - return nil -} diff --git a/src/dto/injection.go b/src/dto/injection.go index c92d2589..ae83259a 100644 --- a/src/dto/injection.go +++ b/src/dto/injection.go @@ -1,19 +1,12 @@ package dto import ( - "encoding/json" - "fmt" - "strings" "time" - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/utils" - - chaos "github.com/OperationsPAI/chaos-experiment/handler" + "aegis/model" ) +// InjectionItem is the shared runtime datapack payload carried across tasks/consumers. type InjectionItem struct { ID int `json:"id"` Name string `json:"name"` @@ -22,13 +15,11 @@ type InjectionItem struct { EndTime time.Time `json:"end_time,omitempty"` } -func NewInjectionItem(injection *database.FaultInjection) InjectionItem { +func NewInjectionItem(injection *model.FaultInjection) InjectionItem { item := InjectionItem{ ID: injection.ID, Name: injection.Name, PreDuration: injection.PreDuration, - StartTime: *injection.StartTime, - EndTime: *injection.EndTime, } if injection.StartTime != nil { @@ -40,848 +31,3 @@ func NewInjectionItem(injection *database.FaultInjection) InjectionItem { return item } - -// BatchDeleteInjectionReq represents the request to batch delete injections -type BatchDeleteInjectionReq struct { - IDs []int `json:"ids,omitempty"` // List of injection IDs for deletion - Labels []LabelItem `json:"labels,omitempty"` // List of label keys to match for deletion -} - -func (req *BatchDeleteInjectionReq) Validate() error { - hasIDs := len(req.IDs) > 0 - hasLabels := len(req.Labels) > 0 - - criteriaCount := 0 - if hasIDs { - criteriaCount++ - } - if hasLabels { - criteriaCount++ - } - - if criteriaCount == 0 { - return fmt.Errorf("must provide one of: ids, labels, or tags") - } - if criteriaCount > 1 { - return fmt.Errorf("can only specify one deletion criteria (ids, labels, or tags)") - } - - if hasIDs { - for i, id := range req.IDs { - if id <= 0 { - return fmt.Errorf("invalid id at index %d: %d", i, id) - } - } - } - - if hasLabels { - for i, label := range req.Labels { - if strings.TrimSpace(label.Key) == "" { - return fmt.Errorf("empty label key at index %d", i) - } - if strings.TrimSpace(label.Value) == "" { - return fmt.Errorf("empty label value at index %d", i) - } - } - } - - return nil -} - -// CloneInjectionReq represents the request to clone an injection -type CloneInjectionReq struct { - Name string `json:"name" binding:"required"` // New name for cloned injection - Labels []LabelItem `json:"labels" binding:"omitempty"` // Optional labels for cloned injection -} - -// InjectionLogsResp represents the response for injection logs -type InjectionLogsResp struct { - InjectionID int `json:"injection_id"` - TaskID string `json:"task_id,omitempty"` - Logs []string `json:"logs"` -} - -// TriggerDatasetBuildItemResponse represents the response for a single injection in batch trigger -type TriggerDatasetBuildItemResponse struct { - TaskID string `json:"task_id"` - TraceID string `json:"trace_id"` - InjectionName string `json:"injection_name"` - Benchmark string `json:"benchmark"` - Namespace string `json:"namespace"` - Message string `json:"message"` -} - -// TriggerDatasetBuildError represents an error during dataset build trigger -type TriggerDatasetBuildError struct { - InjectionName string `json:"injection_name"` - Error string `json:"error"` -} - -// TriggerFailedDatapackRebuildRequest represents the request for triggering rebuild of failed datapacks -type TriggerFailedDatapackRebuildRequest struct { - Namespace string `json:"namespace,omitempty"` // Optional namespace, defaults to "ts" - Days *int `json:"days,omitempty"` // Number of days to look back, defaults to 3 -} - -// TriggerFailedDatapackRebuildResponse represents the response for triggering rebuild of failed datapacks -type TriggerFailedDatapackRebuildResponse struct { - SuccessCount int `json:"success_count"` - SuccessItems []TriggerDatasetBuildItemResponse `json:"success_items"` - FailedCount int `json:"failed_count"` - FailedItems []TriggerDatasetBuildError `json:"failed_items,omitempty"` - TotalFound int `json:"total_found"` // Total number of failed datapacks found - DaysSearched int `json:"days_searched"` // Number of days searched - SearchCutoff string `json:"search_cutoff"` // ISO timestamp of search cutoff - Message string `json:"message"` -} - -// TriggerFailedDatapackRebuildProgressEvent represents a single progress event for SSE -type TriggerFailedDatapackRebuildProgressEvent struct { - Type string `json:"type"` // "start", "progress", "item_success", "item_error", "complete", "error" - Message string `json:"message"` // Human readable message - TotalFound int `json:"total_found"` // Total number of failed datapacks found - CurrentIndex int `json:"current_index"` // Current processing index (0-based) - Progress float64 `json:"progress"` // Progress percentage (0-100) - SuccessCount int `json:"success_count"` // Number of successful triggers so far - FailedCount int `json:"failed_count"` // Number of failed triggers so far - CurrentItem *TriggerDatasetBuildItemResponse `json:"current_item,omitempty"` // Current successful item - CurrentError *TriggerDatasetBuildError `json:"current_error,omitempty"` // Current error item - EstimatedTime *time.Duration `json:"estimated_time,omitempty"` // Estimated remaining time - FinalResponse *TriggerFailedDatapackRebuildResponse `json:"final_response,omitempty"` // Final response (only for "complete" type) -} - -type InjectionFieldMappingResp struct { - StatusMap map[int]string `json:"status" swaggertype:"object"` - FaultTypeMap map[chaos.ChaosType]string `json:"fault_type" swaggertype:"object"` - FaultResourceMap map[string]chaos.ChaosResourceMapping `json:"fault_resource" swaggertype:"object"` -} - -type ListInjectionFilters struct { - FaultType *chaos.ChaosType - Category *chaos.SystemType - Benchmark string - State *consts.DatapackState - Status *consts.StatusType - LabelConditions []map[string]string -} - -// ListInjectionReq represents the request to list injections with various filters -type ListInjectionReq struct { - PaginationReq - Type *chaos.ChaosType `form:"fault_type" binding:"omitempty"` - Category *chaos.SystemType `form:"category" binding:"omitempty"` - Benchmark string `form:"benchmark" binding:"omitempty"` - State *consts.DatapackState `form:"state" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` - Labels []string `form:"labels" binding:"omitempty"` -} - -func (req *ListInjectionReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if err := validateChaosType(req.Type); err != nil { - return err - } - // Only validate category if it's provided (not nil) - if req.Category != nil && !req.Category.IsValid() { - return fmt.Errorf("invalid category: %s", *req.Category) - } - if err := validateDatapackState(req.State); err != nil { - return err - } - if err := validateStatusField(req.Status, false); err != nil { - return err - } - if err := validateLabelsField(req.Labels); err != nil { - return err - } - - return nil -} - -func (req *ListInjectionReq) ToFilterOptions() *ListInjectionFilters { - labelConditions := make([]map[string]string, 0, len(req.Labels)) - for _, item := range req.Labels { - parts := strings.SplitN(item, ":", 2) - labelConditions = append(labelConditions, map[string]string{ - "key": parts[0], - "value": parts[1], - }) - } - - return &ListInjectionFilters{ - FaultType: req.Type, - Benchmark: req.Benchmark, - State: req.State, - Status: req.Status, - LabelConditions: labelConditions, - } -} - -// SearchInjectionReq represents the request to search fault injections with advanced filters -type SearchInjectionReq struct { - AdvancedSearchReq[consts.InjectionField] - TaskIDs []string `json:"task_ids" binding:"omitempty"` - Names []string `json:"names" binding:"omitempty"` - NamePattern string `json:"name_pattern" binding:"omitempty"` - FaultTypes []chaos.ChaosType `json:"fault_types" binding:"omitempty"` - Categories []chaos.SystemType `json:"categories" binding:"omitempty"` - States []consts.DatapackState `json:"states" binding:"omitempty"` - Benchmarks []string `json:"benchmarks" binding:"omitempty"` - Labels []LabelItem `json:"labels" binding:"omitempty"` // Custom labels to filter by - StartTime *DateRange `json:"start_time" binding:"omitempty"` - EndTime *DateRange `json:"end_time" binding:"omitempty"` - IncludeLabels bool `json:"include_labels" binding:"omitempty"` // Whether to include labels in the response - IncludeTask bool `json:"include_task" binding:"omitempty"` // Whether to include task details in the response -} - -func (req *SearchInjectionReq) Validate() error { - if err := req.AdvancedSearchReq.Validate(); err != nil { - return err - } - - for i, id := range req.TaskIDs { - if strings.TrimSpace(id) == "" { - return fmt.Errorf("empty task ID at index %d", i) - } - if !utils.IsValidUUID(id) { - return fmt.Errorf("invalid task ID format at index %d: %s", i, id) - } - } - - if len(req.Names) > 0 && req.NamePattern != "" { - return fmt.Errorf("can only specify one of names or name_pattern for filtering") - } - - for i, name := range req.Names { - if strings.TrimSpace(name) == "" { - return fmt.Errorf("empty injection name at index %d", i) - } - } - - if err := validateLabelItemsFiled(req.Labels); err != nil { - return err - } - - if req.StartTime != nil { - if err := req.StartTime.Validate(); err != nil { - return fmt.Errorf("invalid start_time: %w", err) - } - } - if req.EndTime != nil { - if err := req.EndTime.Validate(); err != nil { - return fmt.Errorf("invalid end_time: %w", err) - } - } - - for i, sortField := range req.Sort { - if _, valid := consts.InjectionAllowedFields[sortField.Field]; !valid { - return fmt.Errorf("invalid sort_by field at index %d: %s", i, sortField.Field) - } - } - - for i, field := range req.GroupBy { - if _, valid := consts.InjectionAllowedFields[field]; !valid { - return fmt.Errorf("invalid group_by field at index %d: %s", i, field) - } - } - - return nil -} - -func (req *SearchInjectionReq) ConvertToSearchReq() *SearchReq[consts.InjectionField] { - sr := req.ConvertAdvancedToSearch() - - if len(req.TaskIDs) > 0 { - sr.AddFilter("task_id", OpIn, req.TaskIDs) - } - if len(req.Names) > 0 { - sr.AddFilter("name", OpIn, req.Names) - } - if req.NamePattern != "" { - sr.AddFilter("name", OpLike, req.NamePattern) - } - if len(req.Benchmarks) > 0 { - sr.AddFilter("benchmark", OpIn, req.Benchmarks) - } - - if len(req.FaultTypes) > 0 { - faultTypeValues := make([]string, len(req.FaultTypes)) - for i, ft := range req.FaultTypes { - faultTypeValues[i] = fmt.Sprintf("%d", ft) - } - sr.AddFilter("fault_type", OpIn, faultTypeValues) - } - if len(req.Categories) > 0 { - categoryValues := make([]string, len(req.Categories)) - for i, ct := range req.Categories { - categoryValues[i] = ct.String() - } - sr.AddFilter("category", OpIn, categoryValues) - } - - if len(req.States) > 0 { - stateValues := make([]string, len(req.States)) - for i, st := range req.States { - stateValues[i] = fmt.Sprintf("%d", st) - } - sr.AddFilter("state", OpIn, stateValues) - } - - if req.StartTime != nil { - if req.StartTime.From != nil && req.StartTime.To != nil { - sr.AddFilter("created_at", OpDateBetween, []any{req.StartTime.From, req.StartTime.To}) - } else if req.StartTime.From != nil { - sr.AddFilter("created_at", OpDateAfter, req.StartTime.From) - } else if req.StartTime.To != nil { - sr.AddFilter("created_at", OpDateBefore, req.StartTime.To) - } - } - if req.EndTime != nil { - if req.EndTime.From != nil && req.EndTime.To != nil { - sr.AddFilter("created_at", OpDateBetween, []any{req.EndTime.From, req.EndTime.To}) - } else if req.EndTime.From != nil { - sr.AddFilter("created_at", OpDateAfter, req.EndTime.From) - } else if req.EndTime.To != nil { - sr.AddFilter("created_at", OpDateBefore, req.EndTime.To) - } - } - - if req.IncludeLabels { - sr.AddInclude("Labels") - } - if req.IncludeTask { - sr.AddInclude("Task") - } - - return sr -} - -// SubmitInjectionReq represents a request to submit fault injection tasks with parallel fault support -// Each element in Specs represents a batch of faults to be injected in parallel within a single experiment -type SubmitInjectionReq struct { - ProjectName string `json:"project_name" binding:"omitempty"` // Project name - Pedestal *ContainerSpec `json:"pedestal" binding:"required"` // Pedestal (workload) configuration - Benchmark *ContainerSpec `json:"benchmark" binding:"required"` // Benchmark (detector) configuration - Interval int `json:"interval" binding:"required,min=1"` // Total experiment interval in minutes - PreDuration int `json:"pre_duration" binding:"required,min=1"` // Normal data collection duration before fault injection - Specs [][]chaos.Node `json:"specs" binding:"required"` // Fault injection specs - 2D array where each sub-array is a batch of parallel faults - Algorithms []ContainerSpec `json:"algorithms" binding:"omitempty"` // RCA algorithms to execute (optional) - Labels []LabelItem `json:"labels" binding:"omitempty"` // Labels to attach to the injection -} - -func (req *SubmitInjectionReq) Validate() error { - if req.Pedestal == nil { - return fmt.Errorf("pedestal must not be nil") - } else { - if err := req.Pedestal.Validate(); err != nil { - return fmt.Errorf("invalid pedestal: %w", err) - } - } - - if req.Benchmark == nil { - return fmt.Errorf("benchmark must not be nil") - } - if req.Interval <= req.PreDuration { - return fmt.Errorf("interval must be greater than pre_duration") - } - if len(req.Specs) == 0 { - return fmt.Errorf("specs must not be empty") - } - - if req.Algorithms != nil { - for idx, algorithm := range req.Algorithms { - if err := algorithm.Validate(); err != nil { - return fmt.Errorf("invalid algorithm at index %d: %w", idx, err) - } - if algorithm.Name == config.GetDetectorName() { - return fmt.Errorf("algorithm name %s is reserved and cannot be used", config.GetDetectorName()) - } - } - } - - if req.Labels == nil { - req.Labels = make([]LabelItem, 0) - } - - return nil -} - -type UpdateGroundtruthReq struct { - Groundtruths []database.Groundtruth `json:"ground_truths" binding:"required"` -} - -func (req *UpdateGroundtruthReq) Validate() error { - if len(req.Groundtruths) == 0 { - return fmt.Errorf("at least one ground truth entry is required") - } - return nil -} - -type InjectionResp struct { - ID int `json:"id"` - Name string `json:"name"` - Source string `json:"source"` - FaultType string `json:"fault_type"` - Category string `json:"category"` - DisplayConfig map[string]any `json:"display_config,omitempty" swaggertype:"object"` - PreDuration int `json:"pre_duration"` - StartTime *time.Time `json:"start_time,omitempty"` - EndTime *time.Time `json:"end_time,omitempty"` - State consts.DatapackState `json:"state" swaggertype:"string"` - Status string `json:"status"` - GroundtruthSource string `json:"groundtruth_source"` - BenchmarkID *int `json:"benchmark_id"` - BenchmarkName string `json:"benchmark_name"` - PedestalID *int `json:"pedestal_id"` - PedestalName string `json:"pedestal_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - Labels []LabelItem `json:"labels,omitempty"` -} - -func NewInjectionResp(injection *database.FaultInjection) *InjectionResp { - resp := &InjectionResp{ - ID: injection.ID, - Name: injection.Name, - Source: string(injection.Source), - Category: injection.Category.String(), - PreDuration: injection.PreDuration, - StartTime: injection.StartTime, - EndTime: injection.EndTime, - State: injection.State, - Status: consts.GetStatusTypeName(injection.Status), - GroundtruthSource: injection.GroundtruthSource, - BenchmarkID: injection.BenchmarkID, - PedestalID: injection.PedestalID, - CreatedAt: injection.CreatedAt, - UpdatedAt: injection.UpdatedAt, - } - - if injection.FaultType == consts.Hybrid { - resp.FaultType = "hybrid" - } else { - resp.FaultType = chaos.ChaosTypeMap[injection.FaultType] - } - - if injection.DisplayConfig != nil { - var displayConfigData map[string]any - _ = json.Unmarshal([]byte(*injection.DisplayConfig), &displayConfigData) - resp.DisplayConfig = displayConfigData - } - - if injection.Benchmark != nil { - if injection.Benchmark.Container != nil { - resp.BenchmarkName = injection.Benchmark.Container.Name - } - } - if injection.Pedestal != nil { - if injection.Pedestal.Container != nil { - resp.PedestalName = injection.Pedestal.Container.Name - } - } - - // Get labels from associated Task instead of directly from injection - if len(injection.Labels) > 0 { - resp.Labels = make([]LabelItem, 0, len(injection.Labels)) - for _, l := range injection.Labels { - resp.Labels = append(resp.Labels, LabelItem{ - Key: l.Key, - Value: l.Value, - IsSystem: l.IsSystem, - }) - } - } - return resp -} - -type InjectionDetailResp struct { - InjectionResp - - TaskID string `json:"task_id"` - TraceID string `json:"trace_id"` - Source string `json:"source"` - - Description string `json:"description,omitempty"` - EngineConfig []map[string]any `json:"engine_config" swaggertype:"array,object"` - Groundtruths []chaos.Groundtruth `json:"ground_truth,omitempty"` - GroundtruthSource string `json:"groundtruth_source"` -} - -func NewInjectionDetailResp(injection *database.FaultInjection) *InjectionDetailResp { - injectionResp := NewInjectionResp(injection) - resp := &InjectionDetailResp{ - InjectionResp: *injectionResp, - Source: string(injection.Source), - Description: injection.Description, - GroundtruthSource: injection.GroundtruthSource, - } - - if injection.Task != nil { - resp.TaskID = injection.Task.ID - if injection.Task.Trace != nil { - resp.TraceID = injection.Task.Trace.ID - } - } - - if injection.EngineConfig != "" { - var engineConfigData []map[string]any - _ = json.Unmarshal([]byte(injection.EngineConfig), &engineConfigData) - resp.EngineConfig = engineConfigData - } - - resp.Groundtruths = make([]chaos.Groundtruth, 0, len(injection.Groundtruths)) - if len(injection.Groundtruths) > 0 { - for _, gt := range injection.Groundtruths { - resp.Groundtruths = append(resp.Groundtruths, *gt.ConvertToChaosGroundtruth()) - } - } - - return resp -} - -// InjectionMetadataResp represents the metadata response for injections -type InjectionMetadataResp struct { - Config *chaos.Node `json:"config"` - FaultTypeMap map[chaos.ChaosType]string `json:"fault_type_map"` - FaultResourceMap map[string]chaos.ChaosResourceMapping `json:"fault_resource_map"` - SystemResource chaos.SystemResource `json:"ns_resources"` -} - -type SubmitInjectionItem struct { - Index int `json:"index"` // Index of the batch this injection belongs to - TraceID string `json:"trace_id"` - TaskID string `json:"task_id"` -} - -// Structured warnings about duplications and conflicts -type InjectionWarnings struct { - DuplicateServicesInBatch []string `json:"duplicate_services_in_batch,omitempty"` // Warnings about duplicate service injections within the same batch - DuplicateBatchesInRequest []int `json:"duplicate_batches_in_request,omitempty"` // Batch indices that have duplicate configurations within this request - BatchesExistInDatabase []int `json:"batches_exist_in_database,omitempty"` // Batch indices that already exist in database -} - -type SubmitInjectionResp struct { - GroupID string `json:"group_id"` - Items []SubmitInjectionItem `json:"items"` - OriginalCount int `json:"original_count"` - Warnings *InjectionWarnings `json:"warnings,omitempty"` -} - -type SubmitDatapackBuildingReq struct { - ProjectName string `json:"project_name" binding:"omitempty"` - Specs []BuildingSpec `json:"specs" binding:"required"` - Labels []LabelItem `json:"labels" binding:"omitempty"` -} - -func (req *SubmitDatapackBuildingReq) Validate() error { - if len(req.Specs) == 0 { - return fmt.Errorf("at least one datapack spec is required") - } - - for _, spec := range req.Specs { - if err := spec.Validate(); err != nil { - return fmt.Errorf("invalid datapack spec: %w", err) - } - } - - return validateLabelItemsFiled(req.Labels) -} - -// ManageInjectionLabelReq Represents the request to manage labels for an injection -type ManageInjectionLabelReq struct { - AddLabels []LabelItem `json:"add_labels"` // List of labels to add - RemoveLabels []string `json:"remove_labels"` // List of label keys to remove -} - -func (req *ManageInjectionLabelReq) Validate() error { - if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { - return fmt.Errorf("at least one of add_labels or remove_labels must be provided") - } - - if err := validateLabelItemsFiled(req.AddLabels); err != nil { - return err - } - - for i, key := range req.RemoveLabels { - if strings.TrimSpace(key) == "" { - return fmt.Errorf("empty label key at index %d in remove_labels", i) - } - } - - return nil -} - -// InjectionLabelOperation represents label operations for a single injection -type InjectionLabelOperation struct { - InjectionID int `json:"injection_id" binding:"required"` // Injection ID to manage - AddLabels []LabelItem `json:"add_labels,omitempty"` // Labels to add to this injection - RemoveLabels []LabelItem `json:"remove_labels,omitempty"` // Labels to remove from this injection -} - -// BatchManageInjectionLabelReq represents the request to batch manage injection labels -// Each injection can have its own set of label operations -type BatchManageInjectionLabelReq struct { - Items []InjectionLabelOperation `json:"items" binding:"required,min=1,dive"` // List of label operations per injection -} - -func (req *BatchManageInjectionLabelReq) Validate() error { - if len(req.Items) == 0 { - return fmt.Errorf("items list cannot be empty") - } - - seenIDs := make(map[int]struct{}, len(req.Items)) - for i, item := range req.Items { - if _, exists := seenIDs[item.InjectionID]; exists { - return fmt.Errorf("duplicate injection_id at index %d: %d", i, item.InjectionID) - } - seenIDs[item.InjectionID] = struct{}{} - - if item.InjectionID <= 0 { - return fmt.Errorf("invalid injection_id at index %d: %d", i, item.InjectionID) - } - - if len(item.AddLabels) == 0 && len(item.RemoveLabels) == 0 { - return fmt.Errorf("at least one of add_labels or remove_labels must be provided for injection_id %d at index %d", item.InjectionID, i) - } - - if err := validateLabelItemsFiled(item.AddLabels); err != nil { - return fmt.Errorf("invalid add_labels for injection_id %d at index %d: %w", item.InjectionID, i, err) - } - if err := validateLabelItemsFiled(item.RemoveLabels); err != nil { - return fmt.Errorf("invalid remove_labels for injection_id %d at index %d: %w", item.InjectionID, i, err) - } - } - - return nil -} - -// BatchManageInjectionLabelResp represents the response for batch injection label management -type BatchManageInjectionLabelResp struct { - FailedCount int `json:"failed_count"` - FailedItems []string `json:"failed_items"` - SuccessCount int `json:"success_count"` - SuccessItems []InjectionResp `json:"success_items"` -} - -// analysis -type ListInjectionNoIssuesReq struct { - Labels []string `form:"labels" binding:"omitempty"` - TimeRangeQuery -} - -func (req *ListInjectionNoIssuesReq) Validate() error { - if err := validateLabelsField(req.Labels); err != nil { - return err - } - return req.TimeRangeQuery.Validate() -} - -type ListInjectionWithIssuesReq struct { - Labels []string `form:"labels" binding:"omitempty"` - TimeRangeQuery -} - -func (req *ListInjectionWithIssuesReq) Validate() error { - if err := validateLabelsField(req.Labels); err != nil { - return err - } - return req.TimeRangeQuery.Validate() -} - -type InjectionNoIssuesResp struct { - ID int `json:"datapack_id"` - Name string `json:"datapack_name"` - FaultType string `json:"fault_type"` - Category string `json:"category"` - EngineConfig *chaos.Node `json:"engine_config"` -} - -func NewInjectionNoIssuesResp(entity database.FaultInjectionNoIssues) (*InjectionNoIssuesResp, error) { - var engineConfig *chaos.Node - err := json.Unmarshal([]byte(entity.EngineConfig), engineConfig) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal engine config: %w", err) - } - - return &InjectionNoIssuesResp{ - ID: entity.ID, - Name: entity.Name, - FaultType: chaos.ChaosTypeMap[entity.FaultType], - Category: entity.Category.String(), - EngineConfig: engineConfig, - }, nil -} - -// InjectionWithIssuesResp represents the response for fault injections with issues -type InjectionWithIssuesResp struct { - ID int `json:"datapack_id"` - Name string `json:"datapack_name"` - FaultType string `json:"fault_type"` - Category string `json:"category"` - EngineConfig chaos.Node `json:"engine_config"` - Issues string `json:"issues"` - AbnormalAvgDuration float64 `json:"abnormal_avg_duration"` - NormalAvgDuration float64 `json:"normal_avg_duration"` - AbnormalSuccRate float64 `json:"abnormal_succ_rate"` - NormalSuccRate float64 `json:"normal_succ_rate"` - AbnormalP99 float64 `json:"abnormal_p99"` - NormalP99 float64 `json:"normal_p99"` -} - -func NewInjectionWithIssuesResp(entity database.FaultInjectionWithIssues) (*InjectionWithIssuesResp, error) { - var engineConfig chaos.Node - err := json.Unmarshal([]byte(entity.EngineConfig), &engineConfig) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal engine config: %w", err) - } - return &InjectionWithIssuesResp{ - ID: entity.ID, - Name: entity.Name, - FaultType: chaos.ChaosTypeMap[entity.FaultType], - Category: entity.Category.String(), - EngineConfig: engineConfig, - Issues: entity.Issues, - AbnormalAvgDuration: entity.AbnormalAvgDuration, - NormalAvgDuration: entity.NormalAvgDuration, - AbnormalSuccRate: entity.AbnormalSuccRate, - NormalSuccRate: entity.NormalSuccRate, - AbnormalP99: entity.AbnormalP99, - NormalP99: entity.NormalP99, - }, nil -} - -// datapack -type BuildingSpec struct { - Benchmark ContainerSpec `json:"benchmark" binding:"required"` - Datapack *string `json:"datapack" binding:"omitempty"` - Dataset *DatasetRef `json:"dataset" binding:"omitempty"` - PreDuration *int `json:"pre_duration" binding:"omitempty"` -} - -func (spec *BuildingSpec) Validate() error { - hasDatapack := spec.Datapack != nil - hasDataset := spec.Dataset != nil - - if !hasDatapack && !hasDataset { - return fmt.Errorf("either datapack or dataset must be specified") - } - if hasDatapack && hasDataset { - return fmt.Errorf("cannot specify both datapack and dataset") - } - - if hasDatapack { - if *spec.Datapack == "" { - return fmt.Errorf("datapack name cannot be empty") - } - } - - if hasDataset { - if err := spec.Dataset.Validate(); err != nil { - return fmt.Errorf("invalid dataset: %w", err) - } - } - - if spec.PreDuration != nil && *spec.PreDuration <= 0 { - return fmt.Errorf("pre_duration must be greater than 0") - } - - return nil -} - -type SubmitBuildingItem struct { - Index int `json:"index"` - TraceID string `json:"trace_id"` - TaskID string `json:"task_id"` -} - -// SubmitDatapackResp represents the response for submitting datapack building tasks -type SubmitDatapackBuildingResp struct { - GroupID string `json:"group_id"` - Items []SubmitBuildingItem `json:"items"` -} - -// DatapackFileItem represents a file or directory in the datapack -type DatapackFileItem struct { - Name string `json:"name"` // File or directory name - Path string `json:"path"` // Relative path from datapack root - Size string `json:"size"` // File size in KB/MB format or directory info - ModTime *time.Time `json:"modified_at,omitempty"` // Last modification time (only for files) - Children []DatapackFileItem `json:"children,omitempty"` // Child items (only for directories) -} - -// DatapackFilesResp represents the response for listing datapack files -type DatapackFilesResp struct { - Files []DatapackFileItem `json:"files"` - FileCount int `json:"file_count"` // Number of files (excluding directories) - DirCount int `json:"dir_count"` // Number of directories -} - -// validateChaosType checks if the provided chaos type is valid -func validateChaosType(faultType *chaos.ChaosType) error { - if faultType != nil { - if _, exists := chaos.ChaosTypeMap[*faultType]; !exists { - return fmt.Errorf("invalid fault type: %d", faultType) - } - } - return nil -} - -// validateDatapackState checks if the provided datapack state is valid -func validateDatapackState(state *consts.DatapackState) error { - if state != nil { - if *state < 0 { - return fmt.Errorf("state must be a non-negative integer") - } - if _, exists := consts.ValidDatapackStates[consts.DatapackState(*state)]; !exists { - return fmt.Errorf("invalid state: %d", *state) - } - } - return nil -} - -// UploadDatapackReq represents the request to upload a manual datapack -type UploadDatapackReq struct { - Name string `form:"name" binding:"required"` - Description string `form:"description"` - Category string `form:"category"` - Labels string `form:"labels"` // JSON-encoded []LabelItem - Groundtruths string `form:"ground_truths"` // JSON-encoded []Groundtruth -} - -func (req *UploadDatapackReq) Validate() error { - if strings.TrimSpace(req.Name) == "" { - return fmt.Errorf("name is required") - } - return nil -} - -func (req *UploadDatapackReq) ParseLabels() ([]LabelItem, error) { - if req.Labels == "" { - return nil, nil - } - var labels []LabelItem - if err := json.Unmarshal([]byte(req.Labels), &labels); err != nil { - return nil, fmt.Errorf("invalid labels JSON: %w", err) - } - return labels, nil -} - -func (req *UploadDatapackReq) ParseGroundtruths() ([]database.Groundtruth, error) { - if req.Groundtruths == "" { - return nil, nil - } - var gts []database.Groundtruth - if err := json.Unmarshal([]byte(req.Groundtruths), >s); err != nil { - return nil, fmt.Errorf("invalid ground_truths JSON: %w", err) - } - return gts, nil -} - -// UploadDatapackResp represents the response for uploading a manual datapack -type UploadDatapackResp struct { - ID int `json:"id"` - Name string `json:"name"` -} diff --git a/src/dto/label.go b/src/dto/label.go index ba999a3c..9e9f1ee0 100644 --- a/src/dto/label.go +++ b/src/dto/label.go @@ -1,14 +1,5 @@ package dto -import ( - "fmt" - "time" - - "aegis/consts" - "aegis/database" - "aegis/utils" -) - type LabelItem struct { Key string `json:"key"` Value string `json:"value"` @@ -31,206 +22,3 @@ func ConvertLabelItemsToConditions(labelItems []LabelItem) []map[string]string { return labelConditions } - -// ===================================================================== -// Label DTOs -// ===================================================================== - -// BatchDeleteLabelReq represents the request to batch delete labels -type BatchDeleteLabelReq struct { - IDs []int `json:"ids" binding:"omitempty"` // List of injection IDs for deletion -} - -func (req *BatchDeleteLabelReq) Validate() error { - if len(req.IDs) == 0 { - return fmt.Errorf("ids cannot be empty") - } - for i, id := range req.IDs { - if id <= 0 { - return fmt.Errorf("invalid id at index %d: %d", i, id) - } - } - return nil -} - -// CreateLabelReq represents label creation request -type CreateLabelReq struct { - Key string `json:"key" binding:"required"` - Value string `json:"value" binding:"required"` - Category consts.LabelCategory `json:"category" bindging:"required"` - Description string `json:"description" binding:"omitempty"` - Color *string `json:"color" binding:"omitempty"` -} - -func (req *CreateLabelReq) Validate() error { - if err := validateKeyAndValue(req.Key, req.Value); err != nil { - return err - } - if err := validateLabelCategory(&req.Category); err != nil { - return err - } - if err := validateColor(req.Color); err != nil { - return err - } - return nil -} - -func (req *CreateLabelReq) ConvertToLabel() *database.Label { - return &database.Label{ - Key: req.Key, - Value: req.Value, - Category: req.Category, - Description: req.Description, - Color: utils.GetStringValue(req.Color, "#1890ff"), - IsSystem: false, - Usage: consts.DefaultLabelUsage, - } -} - -type ListLabelFilters struct { - Key string - Value string - Category *consts.LabelCategory - IsSystem *bool - Status *consts.StatusType -} - -type ListLabelReq struct { - PaginationReq - - Key string `form:"key" binding:"omitempty"` - Value string `form:"value" binding:"omitempty"` - Category *consts.LabelCategory `form:"category" binding:"omitempty"` - IsSystem *bool `form:"is_system" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListLabelReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if err := validateKeyAndValue(req.Key, req.Value); err != nil { - return err - } - if err := validateLabelCategory(req.Category); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -type UpdateLabelReq struct { - Description *string `json:"description" binding:"omitempty"` - Color *string `json:"color" binding:"omitempty"` - Status *consts.StatusType `json:"status,omitempty"` -} - -func (req *UpdateLabelReq) Validate() error { - if err := validateColor(req.Color); err != nil { - return err - } - return validateStatusField(req.Status, true) -} - -func (req *UpdateLabelReq) PatchLabelModel(target *database.Label) { - if req.Description != nil { - target.Description = *req.Description - } - if req.Color != nil { - target.Color = *req.Color - } - if req.Status != nil { - target.Status = *req.Status - } -} - -func (req *ListLabelReq) ToFilterOptions() *ListLabelFilters { - return &ListLabelFilters{ - Key: req.Key, - Value: req.Value, - Category: req.Category, - IsSystem: req.IsSystem, - Status: req.Status, - } -} - -type LabelResp struct { - ID int `json:"id"` - Key string `json:"key"` - Value string `json:"value"` - Category string `json:"category"` - Color string `json:"color"` - Usage int `json:"usage"` - IsSystem bool `json:"is_system"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewLabelResp(label *database.Label) *LabelResp { - return &LabelResp{ - ID: label.ID, - Key: label.Key, - Value: label.Value, - Category: consts.GetLabelCategoryName(label.Category), - Color: label.Color, - Usage: label.Usage, - IsSystem: label.IsSystem, - Status: consts.GetStatusTypeName(label.Status), - CreatedAt: label.CreatedAt, - UpdatedAt: label.UpdatedAt, - } -} - -type LabelDetailResp struct { - LabelResp - - Description string `json:"description"` -} - -func NewLabelDetailResp(label *database.Label) *LabelDetailResp { - return &LabelDetailResp{ - LabelResp: *NewLabelResp(label), - Description: label.Description, - } -} - -// ===================================================================== -// Validation Helpers -// ===================================================================== - -// validateColor checks if the provided color is a valid hex color -func validateColor(color *string) error { - if color == nil { - return nil - } - if !utils.IsValidHexColor(*color) { - return fmt.Errorf("invalid color format: %s", *color) - } - return nil -} - -// validateKeyAndValue checks label key and value consistency. -// Both empty is allowed (list all labels). If one is provided, the other must also be provided. -func validateKeyAndValue(key, value string) error { - if key == "" && value == "" { - return nil - } - if key == "" { - return fmt.Errorf("label key cannot be empty when value is provided") - } - if value == "" { - return fmt.Errorf("label value cannot be empty when key is provided") - } - return nil -} - -// validateLabelCategory validates if the provided category is valid -func validateLabelCategory(category *consts.LabelCategory) error { - if category != nil { - if _, exists := consts.ValidLabelCategories[*category]; !exists { - return fmt.Errorf("invalid label category: %d", category) - } - return nil - } - return nil -} diff --git a/src/dto/log.go b/src/dto/log.go index a977d509..dba067e5 100644 --- a/src/dto/log.go +++ b/src/dto/log.go @@ -15,11 +15,3 @@ type LogEntry struct { TraceID string `json:"trace_id,omitempty"` // Trace ID Level consts.LogLevel `json:"level,omitempty"` // Log level } - -// WSLogMessage represents the WebSocket message format for log streaming -type WSLogMessage struct { - Type consts.WSLogType `json:"type"` - Logs []LogEntry `json:"logs,omitempty"` // Log entries - Message string `json:"message,omitempty"` // Error message or end reason - Total int `json:"total,omitempty"` // Total history log count -} diff --git a/src/dto/metrics.go b/src/dto/metrics.go deleted file mode 100644 index cec83c70..00000000 --- a/src/dto/metrics.go +++ /dev/null @@ -1,67 +0,0 @@ -package dto - -import ( - "fmt" - "time" -) - -// GetMetricsReq represents the request to get metrics with time range and filters -type GetMetricsReq struct { - StartTime *time.Time `form:"start_time" binding:"omitempty"` - EndTime *time.Time `form:"end_time" binding:"omitempty"` - FaultType *string `form:"fault_type" binding:"omitempty"` - AlgorithmID *int `form:"algorithm_id" binding:"omitempty"` -} - -func (req *GetMetricsReq) Validate() error { - if req.StartTime != nil && req.EndTime != nil { - if req.EndTime.Before(*req.StartTime) { - return fmt.Errorf("end_time must be after start_time") - } - } - if req.AlgorithmID != nil && *req.AlgorithmID <= 0 { - return fmt.Errorf("algorithm_id must be positive") - } - return nil -} - -// InjectionMetrics represents aggregated metrics for injections -type InjectionMetrics struct { - TotalCount int `json:"total_count"` - SuccessCount int `json:"success_count"` - FailedCount int `json:"failed_count"` - SuccessRate float64 `json:"success_rate"` - AvgDuration float64 `json:"avg_duration"` - MinDuration float64 `json:"min_duration"` - MaxDuration float64 `json:"max_duration"` - StateDistrib map[string]int `json:"state_distribution" swaggertype:"object"` - FaultTypeDistrib map[string]int `json:"fault_type_distribution" swaggertype:"object"` -} - -// ExecutionMetrics represents aggregated metrics for algorithm executions -type ExecutionMetrics struct { - TotalCount int `json:"total_count"` - SuccessCount int `json:"success_count"` - FailedCount int `json:"failed_count"` - SuccessRate float64 `json:"success_rate"` - AvgDuration float64 `json:"avg_duration"` - MinDuration float64 `json:"min_duration"` - MaxDuration float64 `json:"max_duration"` - StateDistrib map[string]int `json:"state_distribution" swaggertype:"object"` -} - -// AlgorithmMetrics represents comparative metrics across different algorithms -type AlgorithmMetrics struct { - Algorithms []AlgorithmMetricItem `json:"algorithms"` -} - -// AlgorithmMetricItem represents metrics for a single algorithm -type AlgorithmMetricItem struct { - AlgorithmID int `json:"algorithm_id"` - AlgorithmName string `json:"algorithm_name"` - ExecutionCount int `json:"execution_count"` - SuccessCount int `json:"success_count"` - FailedCount int `json:"failed_count"` - SuccessRate float64 `json:"success_rate"` - AvgDuration float64 `json:"avg_duration"` -} diff --git a/src/dto/permission.go b/src/dto/permission.go index 1b5d98cd..0ef8d8ca 100644 --- a/src/dto/permission.go +++ b/src/dto/permission.go @@ -2,11 +2,8 @@ package dto import ( "fmt" - "strings" - "time" "aegis/consts" - "aegis/database" ) // CheckPermissionParams represents permission check parameters @@ -33,243 +30,3 @@ func (req *CheckPermissionParams) Validate() error { } return nil } - -// CreatePermissionReq represents permission creation request -type CreatePermissionReq struct { - DisplayName string `json:"display_name" binding:"omitempty"` - Description string `json:"description" binding:"omitempty"` - Action consts.ActionName `json:"action" binding:"required"` - ResourceID int `json:"resource_id" binding:"required,min=1"` -} - -func (req *CreatePermissionReq) Validate() error { - if req.Action == "" { - return fmt.Errorf("action cannot be empty") - } - if _, ok := consts.ValidActions[consts.ActionName(req.Action)]; !ok { - return fmt.Errorf("invalid action: %s", req.Action) - } - return nil -} - -func (req *CreatePermissionReq) ConvertToPermission() *database.Permission { - return &database.Permission{ - Description: req.Description, - Action: req.Action, - IsSystem: false, - Status: consts.CommonEnabled, - } -} - -// ListPermissionReq represents permission list query parameters -type ListPermissionReq struct { - PaginationReq - Action consts.ActionName `form:"action" binding:"omitempty"` - IsSystem *bool `form:"is_system" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListPermissionReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if req.Action != "" { - if _, exists := consts.ValidActions[req.Action]; !exists { - return fmt.Errorf("invalid action: %s", req.Action) - } - } - if req.Status != nil { - return validateStatusField(req.Status, false) - } - return nil -} - -// SearchPermissionReq represents advanced permission search with complex filtering -type SearchPermissionReq struct { - AdvancedSearchReq[string] - - // Permission-specific filter shortcuts - NamePattern string `json:"name_pattern,omitempty"` // Fuzzy match for permission name - DisplayNamePattern string `json:"display_name_pattern,omitempty"` // Fuzzy match for display name - DescriptionPattern string `json:"description_pattern,omitempty"` // Fuzzy match for description - Actions []string `json:"actions,omitempty"` // Action filter - ResourceIDs []int `json:"resource_ids,omitempty"` // Resource ID filter - ResourceNames []string `json:"resource_names,omitempty"` // Resource name filter - IsSystem *bool `json:"is_system,omitempty"` // Is system permission - RoleIDs []int `json:"role_ids,omitempty"` // Role IDs that have this permission -} - -// ConvertToSearchRequest converts PermissionSearchReq to SearchRequest with permission-specific filters -func (psr *SearchPermissionReq) ConvertToSearchRequest() *SearchReq[string] { - sr := psr.ConvertAdvancedToSearch() - - // Add permission-specific filters - if psr.NamePattern != "" { - sr.AddFilter("name", OpLike, psr.NamePattern) - } - - if psr.DisplayNamePattern != "" { - sr.AddFilter("display_name", OpLike, psr.DisplayNamePattern) - } - - if psr.DescriptionPattern != "" { - sr.AddFilter("description", OpLike, psr.DescriptionPattern) - } - - if len(psr.Actions) > 0 { - values := make([]string, len(psr.Actions)) - for i, v := range psr.Actions { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "action", - Operator: OpIn, - Values: values, - }) - } - - if len(psr.ResourceIDs) > 0 { - values := make([]string, len(psr.ResourceIDs)) - for i, v := range psr.ResourceIDs { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "resource_id", - Operator: OpIn, - Values: values, - }) - } - - if len(psr.ResourceNames) > 0 { - values := make([]string, len(psr.ResourceNames)) - for i, v := range psr.ResourceNames { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "resource_name", - Operator: OpIn, - Values: values, - }) - } - - if psr.IsSystem != nil { - sr.AddFilter("is_system", OpEqual, *psr.IsSystem) - } - - if len(psr.RoleIDs) > 0 { - values := make([]string, len(psr.RoleIDs)) - for i, v := range psr.RoleIDs { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "role_id", - Operator: OpIn, - Values: values, - }) - } - - return sr -} - -// UpdatePermissionReq represents permission update request -type UpdatePermissionReq struct { - DisplayName *string `json:"display_name" binding:"omitempty"` - Description *string `json:"description" binding:"omitempty"` - Action *consts.ActionName `json:"action" binding:"omitempty"` - ResourceID *int `json:"resource_id" binding:"omitempty,min_ptr=1"` - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *UpdatePermissionReq) Validate() error { - if req.DisplayName != nil { - if *req.DisplayName != "" { - *req.DisplayName = strings.TrimSpace(*req.DisplayName) - } - } - - if req.Action != nil { - if *req.Action == "" { - return fmt.Errorf("action cannot be empty") - } - if _, ok := consts.ValidActions[consts.ActionName(*req.Action)]; !ok { - return fmt.Errorf("invalid action: %s", *req.Action) - } - } - - return validateStatusField(req.Status, true) -} - -func (req *UpdatePermissionReq) PatchPermissionModel(target *database.Permission) { - if req.DisplayName != nil { - target.DisplayName = *req.DisplayName - } - if req.Description != nil { - target.Description = *req.Description - } - if req.Action != nil { - target.Action = *req.Action - } - if req.Status != nil { - target.Status = *req.Status - } -} - -// PermissionBaseResp contains common fields for permission responses -type PermissionBaseResp struct { - ID int `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Action consts.ActionName `json:"action"` - Scope consts.ResourceScope `json:"scope"` - IsSystem bool `json:"is_system"` - Status string `json:"status"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewPermissionBaseResp(perm *database.Permission) *PermissionBaseResp { - return &PermissionBaseResp{ - ID: perm.ID, - Name: perm.Name, - DisplayName: perm.DisplayName, - Action: perm.Action, - Scope: perm.Scope, - IsSystem: perm.IsSystem, - Status: consts.GetStatusTypeName(perm.Status), - UpdatedAt: perm.UpdatedAt, - } -} - -// PermissionResp represents permission summary information -type PermissionResp struct { - PermissionBaseResp - Resource string `json:"resource_name"` // Simple string for list view -} - -func NewPermissionResp(perm *database.Permission) *PermissionResp { - resp := &PermissionResp{ - PermissionBaseResp: *NewPermissionBaseResp(perm), - } - if perm.Resource != nil { - resp.Resource = perm.Resource.Name.String() - } - return resp -} - -type PermissionDetailResp struct { - PermissionBaseResp - Description string `json:"description"` - Resource *ResourceResp `json:"resource,omitempty"` // Detailed object for detail view - CreatedAt time.Time `json:"created_at"` -} - -func NewPermissionDetailResp(perm *database.Permission) *PermissionDetailResp { - resp := &PermissionDetailResp{ - PermissionBaseResp: *NewPermissionBaseResp(perm), - Description: perm.Description, - CreatedAt: perm.CreatedAt, - } - if perm.Resource != nil { - resp.Resource = NewResourceResp(perm.Resource) - } - return resp -} diff --git a/src/dto/project.go b/src/dto/project.go index bf541306..a135edc4 100644 --- a/src/dto/project.go +++ b/src/dto/project.go @@ -1,123 +1,6 @@ package dto -import ( - "fmt" - "strings" - "time" - - "aegis/consts" - "aegis/database" -) - -// ===================== Project CRUD DTOs ===================== - -// CreateProjectReq represents project creation request -type CreateProjectReq struct { - Name string `json:"name" binding:"required"` - Description string `json:"description" binding:"omitempty"` - IsPublic *bool `json:"is_public" binding:"omitempty"` -} - -func (req *CreateProjectReq) Validate() error { - req.Name = strings.TrimSpace(req.Name) - if req.Name == "" { - return fmt.Errorf("project name cannot be empty") - } - if req.IsPublic == nil { - defaultPublic := true - req.IsPublic = &defaultPublic - } - return nil -} - -func (req *CreateProjectReq) ConvertToProject() *database.Project { - return &database.Project{ - Name: req.Name, - Description: req.Description, - IsPublic: *req.IsPublic, - Status: consts.CommonEnabled, - } -} - -// ListProjectReq represents project list query parameters -type ListProjectReq struct { - PaginationReq - IsPublic *bool `form:"is_public" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListProjectReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -// SearchProjectReq represents advanced project search -type SearchProjectReq struct { - AdvancedSearchReq[string] - - NamePattern string `json:"name_pattern,omitempty"` - DescriptionPattern string `json:"description_pattern,omitempty"` - IsPublic *bool `json:"is_public,omitempty"` -} - -func (req *SearchProjectReq) ConvertToSearchRequest() *SearchReq[string] { - sr := req.ConvertAdvancedToSearch() - - if req.NamePattern != "" { - sr.AddFilter("name", OpLike, req.NamePattern) - } - if req.DescriptionPattern != "" { - sr.AddFilter("description", OpLike, req.DescriptionPattern) - } - if req.IsPublic != nil { - sr.AddFilter("is_public", OpEqual, *req.IsPublic) - } - - return sr -} - -// UpdateProjectReq represents project update request -type UpdateProjectReq struct { - Description *string `json:"description,omitempty"` - IsPublic *bool `json:"is_public,omitempty"` - Status *consts.StatusType `json:"status,omitempty"` -} - -func (req *UpdateProjectReq) Validate() error { - return validateStatusField(req.Status, true) -} - -func (req *UpdateProjectReq) PatchProjectModel(target *database.Project) { - if req.Description != nil { - target.Description = *req.Description - } - if req.IsPublic != nil { - target.IsPublic = *req.IsPublic - } - if req.Status != nil { - target.Status = *req.Status - } -} - -// ProjectResp represents basic project response -type ProjectResp struct { - ID int `json:"id"` - Name string `json:"name"` - IsPublic bool `json:"is_public"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - - // Statistics - LastInjectionAt *time.Time `json:"last_injection_at,omitempty"` // Last successful injection time - LastExecutionAt *time.Time `json:"last_execution_at,omitempty"` // Last successful execution time - InjectionCount int `json:"injection_count"` // Total injection count - ExecutionCount int `json:"execution_count"` // Total execution count - - Labels []LabelItem `json:"labels,omitempty"` -} +import "time" // ProjectStatistics holds statistics for a project type ProjectStatistics struct { @@ -126,80 +9,3 @@ type ProjectStatistics struct { LastInjectionAt *time.Time LastExecutionAt *time.Time } - -func NewProjectResp(project *database.Project, stats *ProjectStatistics) *ProjectResp { - resp := &ProjectResp{ - ID: project.ID, - Name: project.Name, - IsPublic: project.IsPublic, - Status: consts.GetStatusTypeName(project.Status), - CreatedAt: project.CreatedAt, - UpdatedAt: project.UpdatedAt, - } - - // Fill statistics if provided - if stats != nil { - resp.LastInjectionAt = stats.LastInjectionAt - resp.LastExecutionAt = stats.LastExecutionAt - resp.InjectionCount = stats.InjectionCount - resp.ExecutionCount = stats.ExecutionCount - } - - if project.Labels != nil { - resp.Labels = make([]LabelItem, len(project.Labels)) - for i, label := range project.Labels { - resp.Labels[i] = LabelItem{ - Key: label.Key, - Value: label.Value, - } - } - } - return resp -} - -// ProjectDetailResp represents detailed project response -type ProjectDetailResp struct { - ProjectResp - - Containers []ContainerResp `json:"containers,omitempty"` - Datapacks []InjectionResp `json:"datapacks,omitempty"` - Datasets []DatasetResp `json:"datasets,omitempty"` - UserCount int `json:"user_count"` -} - -func NewProjectDetailResp(project *database.Project, stats *ProjectStatistics) *ProjectDetailResp { - return &ProjectDetailResp{ - ProjectResp: *NewProjectResp(project, stats), - } -} - -// ===================== Project-Label DTOs ===================== - -// ManageProjectLabelReq represents project label management request -type ManageProjectLabelReq struct { - AddLabels []LabelItem `json:"add_labels" binding:"omitempty"` // List of labels to add - RemoveLabels []string `json:"remove_labels" binding:"omitempty"` // List of label keys to remove -} - -func (req *ManageProjectLabelReq) Validate() error { - if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { - return fmt.Errorf("at least one of add_labels or remove_labels must be provided") - } - - for i, label := range req.AddLabels { - if strings.TrimSpace(label.Key) == "" { - return fmt.Errorf("empty label key at index %d in add_labels", i) - } - if strings.TrimSpace(label.Value) == "" { - return fmt.Errorf("empty label value at index %d in add_labels", i) - } - } - - for i, key := range req.RemoveLabels { - if strings.TrimSpace(key) == "" { - return fmt.Errorf("empty label key at index %d in remove_labels", i) - } - } - - return nil -} diff --git a/src/dto/redis.go b/src/dto/redis.go deleted file mode 100644 index 8787c16c..00000000 --- a/src/dto/redis.go +++ /dev/null @@ -1,10 +0,0 @@ -package dto - -import "aegis/consts" - -type RdbMsg struct { - Status string `json:"status"` - Error string `json:"error"` - TaskID string `json:"task_id"` - Type consts.TaskType `json:"task_type"` -} diff --git a/src/dto/resource.go b/src/dto/resource.go deleted file mode 100644 index f9b72513..00000000 --- a/src/dto/resource.go +++ /dev/null @@ -1,65 +0,0 @@ -package dto - -import ( - "aegis/consts" - "aegis/database" - "fmt" -) - -// ListResourceReq represents request for listing resources -type ListResourceReq struct { - PaginationReq - - Type *consts.ResourceType `form:"type" binding:"omitempty"` - Category *consts.ResourceCategory `form:"category" binding:"omitempty"` -} - -func (req *ListResourceReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if req.Type != nil { - if _, exists := consts.ValidResourceTypes[*req.Type]; !exists { - return fmt.Errorf("invalid resource type: %d", *req.Type) - } - } - if req.Category != nil { - if _, exists := consts.ValidResourceCategories[*req.Category]; !exists { - return fmt.Errorf("invalid resource category: %d", *req.Category) - } - } - return nil -} - -type ResourceResp struct { - ID int `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Type string `json:"type"` - Category string `json:"category"` - ParentID *int `json:"parent_id,omitempty"` -} - -func NewResourceResp(resource *database.Resource) *ResourceResp { - return &ResourceResp{ - ID: resource.ID, - Name: resource.Name.String(), - DisplayName: resource.DisplayName, - Type: consts.GetResourceTypeName(resource.Type), - Category: consts.GetResourceCategoryName(resource.Category), - ParentID: resource.ParentID, - } -} - -type ResourceDetailResp struct { - ResourceResp - - Description string `json:"description,omitempty"` -} - -func NewResourceDetailResp(resource *database.Resource) *ResourceDetailResp { - return &ResourceDetailResp{ - ResourceResp: *NewResourceResp(resource), - Description: resource.Description, - } -} diff --git a/src/dto/role.go b/src/dto/role.go deleted file mode 100644 index 21079bb0..00000000 --- a/src/dto/role.go +++ /dev/null @@ -1,173 +0,0 @@ -package dto - -import ( - "fmt" - "strings" - "time" - - "aegis/consts" - "aegis/database" -) - -// CreateRoleReq represents role creation request -type CreateRoleReq struct { - Name string `json:"name" binding:"required"` - DisplayName string `json:"display_name" binding:"required"` - Description string `json:"description,omitempty" binding:"omitempty"` -} - -// ConvertToRole converts CreateRoleReq to database Role model -func (req *CreateRoleReq) ConvertToRole() *database.Role { - return &database.Role{ - Name: req.Name, - DisplayName: req.DisplayName, - Description: req.Description, - IsSystem: false, - Status: consts.CommonEnabled, - } -} - -// ListRoleReq represents role list query parameters -type ListRoleReq struct { - PaginationReq - IsSystem *bool `form:"is_system" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListRoleReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - return validateStatusField(req.Status, false) -} - -// SearchRoleReq represents advanced role search with complex filtering -type SearchRoleReq struct { - AdvancedSearchReq[string] - - // Role-specific filter shortcuts - NamePattern string `json:"name_pattern" binding:"omitempty"` // Role name fuzzy match - DisplayNamePattern string `json:"display_name_pattern" binding:"omitempty"` // Display name fuzzy match - DescriptionPattern string `json:"description_pattern" binding:"omitempty"` // Description fuzzy match - IsSystem *bool `json:"is_system" binding:"omitempty"` // Whether system role - PermissionIDs []int `json:"permission_ids" binding:"omitempty"` // Permission ID filter - UserCount *NumberRange `json:"user_count" binding:"omitempty"` // User count range -} - -// ConvertToSearchRequest converts RoleSearchReq to SearchRequest with role-specific filters -func (rsr *SearchRoleReq) ConvertToSearchRequest() *SearchReq[string] { - sr := rsr.ConvertAdvancedToSearch() - - if rsr.NamePattern != "" { - sr.AddFilter("name", OpLike, rsr.NamePattern) - } - if rsr.DisplayNamePattern != "" { - sr.AddFilter("display_name", OpLike, rsr.DisplayNamePattern) - } - if rsr.DescriptionPattern != "" { - sr.AddFilter("description", OpLike, rsr.DescriptionPattern) - } - - if rsr.IsSystem != nil { - sr.AddFilter("is_system", OpEqual, *rsr.IsSystem) - } - if len(rsr.PermissionIDs) > 0 { - values := make([]string, len(rsr.PermissionIDs)) - for i, v := range rsr.PermissionIDs { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "permission_id", - Operator: OpIn, - Values: values, - }) - } - - return sr -} - -// UpdateRoleReq represents role update request -type UpdateRoleReq struct { - DisplayName *string `json:"display_name" binding:"omitempty"` - Description *string `json:"description" binding:"omitempty"` - Status *consts.StatusType `json:"status" binding:"omitempty"` -} - -func (req *UpdateRoleReq) Validate() error { - if req.DisplayName != nil { - if *req.DisplayName != "" { - *req.DisplayName = strings.TrimSpace(*req.DisplayName) - } - } - return validateStatusField(req.Status, true) -} - -func (req *UpdateRoleReq) PatchRoleModel(target *database.Role) { - if req.DisplayName != nil { - target.DisplayName = *req.DisplayName - } - if req.Description != nil { - target.Description = *req.Description - } - if req.Status != nil { - target.Status = *req.Status - } -} - -// AssignRolePermissionReq represents request to assign permissions to a role -type AssignRolePermissionReq struct { - PermissionIDs []int `json:"permission_ids" binding:"required,min=1,non_zero_int_slice"` -} - -// RemoveRolePermissionReq represents request to remove permissions from a role -type RemoveRolePermissionReq struct { - PermissionIDs []int `json:"permission_ids" binding:"required,min=1,non_zero_int_slice"` -} - -// RoleResp represents role response -type RoleResp struct { - ID int `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - Type string `json:"type"` - IsSystem bool `json:"is_system"` - Status string `json:"status"` - UpdatedAt time.Time `json:"updated_at"` -} - -// NewRoleResp converts database Role to RoleResp DTO -func NewRoleResp(role *database.Role) *RoleResp { - return &RoleResp{ - ID: role.ID, - Name: role.Name, - DisplayName: role.DisplayName, - IsSystem: role.IsSystem, - Status: consts.GetStatusTypeName(role.Status), - UpdatedAt: role.UpdatedAt, - } -} - -type RoleDetailResp struct { - RoleResp - - Description string `json:"description"` - CreatedAt time.Time `json:"created_at"` - UserCount int64 `json:"user_count"` - - Permissions []PermissionResp `json:"permissions"` -} - -func NewRoleDetailResp(role *database.Role) *RoleDetailResp { - resp := &RoleDetailResp{ - RoleResp: *NewRoleResp(role), - Description: role.Description, - CreatedAt: role.CreatedAt, - } - return resp -} - -// ListRoleResp represents paginated list of roles -type ListRoleResp struct { - Items []RoleResp `json:"items"` - Pagination PaginationInfo `json:"pagination"` -} diff --git a/src/dto/system.go b/src/dto/system.go deleted file mode 100644 index 2bdadd90..00000000 --- a/src/dto/system.go +++ /dev/null @@ -1,76 +0,0 @@ -package dto - -import ( - "time" -) - -// HealthCheckResp represents system health check response -type HealthCheckResp struct { - Status string `json:"status"` - Timestamp time.Time `json:"timestamp"` - Version string `json:"version"` - Uptime string `json:"uptime"` - Services map[string]ServiceInfo `json:"services" swaggertype:"object"` -} - -// ServiceInfo represents individual service health information -type ServiceInfo struct { - Status string `json:"status"` - LastChecked time.Time `json:"last_checked"` - ResponseTime string `json:"response_time"` - Error string `json:"error,omitempty"` - Details any `json:"details,omitempty"` -} - -type NsMonitorItem struct { - LockedBy string `json:"locked_by"` - EndTime time.Time `json:"end_time"` - Status string `json:"status"` -} - -type ListNamespaceLockResp struct { - Items map[string]NsMonitorItem `json:"items" swaggertype:"object"` -} - -// SystemInfo represents system information -type SystemInfo struct { - CPUUsage float64 `json:"cpu_usage"` - MemoryUsage float64 `json:"memory_usage"` - DiskUsage float64 `json:"disk_usage"` - LoadAverage string `json:"load_average"` -} - -// MonitoringQueryReq represents monitoring query request -type MonitoringQueryReq struct { - Query string `json:"query" binding:"required"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - Step string `json:"step,omitempty"` -} - -// MetricValue represents a single metric value -type MetricValue struct { - Value float64 `json:"value"` - Timestamp time.Time `json:"timestamp"` - Unit string `json:"unit,omitempty"` -} - -// MonitoringMetricsResp represents monitoring metrics response -type MonitoringMetricsResp struct { - Timestamp time.Time `json:"timestamp"` - Metrics map[string]MetricValue `json:"metrics"` - Labels map[string]string `json:"labels,omitempty"` -} - -// SystemMetricsResp represents current system metrics -type SystemMetricsResp struct { - CPU MetricValue `json:"cpu"` - Memory MetricValue `json:"memory"` - Disk MetricValue `json:"disk"` -} - -// SystemMetricsHistoryResp represents historical system metrics -type SystemMetricsHistoryResp struct { - CPU []MetricValue `json:"cpu"` - Memory []MetricValue `json:"memory"` -} diff --git a/src/dto/task.go b/src/dto/task.go index 9a530d83..66511f0e 100644 --- a/src/dto/task.go +++ b/src/dto/task.go @@ -5,12 +5,10 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "time" "aegis/consts" - "aegis/database" - "aegis/utils" + "aegis/model" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" @@ -46,13 +44,13 @@ type UnifiedTask struct { Extra map[consts.TaskExtra]any `json:"extra,omitempty"` // Additional metadata } -func (t *UnifiedTask) ConvertToTask() (*database.Task, error) { +func (t *UnifiedTask) ConvertToTask() (*model.Task, error) { jsonPayload, err := json.Marshal(t.Payload) if err != nil { return nil, fmt.Errorf("failed to marshal task payload: %w", err) } - task := &database.Task{ + task := &model.Task{ ID: t.TaskID, Type: t.Type, Immediate: t.Immediate, @@ -68,7 +66,7 @@ func (t *UnifiedTask) ConvertToTask() (*database.Task, error) { return task, nil } -func (t *UnifiedTask) ConvertToTrace(withAlgorithms bool, leafNum int) (*database.Trace, error) { +func (t *UnifiedTask) ConvertToTrace(withAlgorithms bool, leafNum int) (*model.Trace, error) { var traceType consts.TraceType switch t.Type { case consts.TaskTypeRestartPedestal: @@ -85,7 +83,7 @@ func (t *UnifiedTask) ConvertToTrace(withAlgorithms bool, leafNum int) (*databas return nil, fmt.Errorf("unsupported task type for trace conversion: %s", consts.GetTaskTypeName(t.Type)) } - trace := &database.Trace{ + trace := &model.Trace{ ID: t.TraceID, Type: traceType, StartTime: time.Now(), @@ -187,170 +185,3 @@ func (t *UnifiedTask) SetGroupCtx(ctx context.Context) { otel.GetTextMapPropagator().Inject(ctx, t.GroupCarrier) } - -// BatchDeleteTaskReq represents the request to batch delete tasks -type BatchDeleteTaskReq struct { - IDs []string `json:"ids" binding:"required"` // List of task IDs for deletion -} - -func (req *BatchDeleteTaskReq) Validate() error { - for i, id := range req.IDs { - if strings.TrimSpace(id) == "" { - return fmt.Errorf("empty id at index %d", i) - } - - if !utils.IsValidUUID(id) { - return fmt.Errorf("invalid UUID format for id at index %d: %s", i, id) - } - } - return nil -} - -// ListTaskFilters represents the filters for listing tasks -type ListTaskFilters struct { - TaskType *consts.TaskType - Immediate *bool - TraceID string - GroupID string - ProjectID int - State *consts.TaskState - Status *consts.StatusType -} - -// ListTaskReq represents the request to list tasks -type ListTaskReq struct { - PaginationReq - TaskType *consts.TaskType `form:"task_type" binding:"omitempty"` - Immediate *bool `form:"immediate" binding:"omitempty"` - TraceID string `form:"trace_id" binding:"omitempty"` - GroupID string `form:"group_id" binding:"omitempty"` - ProjectID int `form:"project_id" binding:"omitempty"` - State *consts.TaskState `form:"state" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListTaskReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if err := validateTaskType(req.TaskType); err != nil { - return err - } - if err := validateUUID(req.TraceID); err != nil { - return err - } - if err := validateUUID(req.GroupID); err != nil { - return err - } - - // Only validate project ID if it's provided (> 0) - if req.ProjectID < 0 { - return fmt.Errorf("invalid project ID: %d", req.ProjectID) - } - - if err := validateState(req.State); err != nil { - return err - } - return validateStatusField(req.Status, true) -} - -func (req *ListTaskReq) ToFilterOptions() *ListTaskFilters { - return &ListTaskFilters{ - Immediate: req.Immediate, - TaskType: req.TaskType, - TraceID: req.TraceID, - GroupID: req.GroupID, - ProjectID: req.ProjectID, - State: req.State, - Status: req.Status, - } -} - -// TaskResp represents the response for a task -type TaskResp struct { - ID string `json:"id"` - Type string `json:"type"` - Immediate bool `json:"immediate"` - ExecuteTime int64 `json:"execute_time"` - CronExpr string `json:"cron_expr,omitempty"` - TraceID string `json:"trace_id"` - GroupID string `json:"group_id"` - - State string `json:"state"` - Status string `json:"status"` - ProjectID int `json:"project_id,omitempty"` - ProjectName string `json:"project_name,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewTaskResp(task *database.Task) *TaskResp { - return &TaskResp{ - ID: task.ID, - Type: consts.GetTaskTypeName(task.Type), - Immediate: task.Immediate, - ExecuteTime: task.ExecuteTime, - CronExpr: task.CronExpr, - TraceID: task.TraceID, - State: consts.GetTaskStateName(task.State), - Status: consts.GetStatusTypeName(task.Status), - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, - } -} - -type TaskDetailResp struct { - TaskResp - - Payload map[string]any `json:"payload,omitempty" swaggertype:"object"` - Logs []string `json:"logs"` -} - -func NewTaskDetailResp(task *database.Task, logs []string) *TaskDetailResp { - resp := &TaskDetailResp{ - TaskResp: *NewTaskResp(task), - Logs: logs, - } - - if task.Payload != "" { - var payload map[string]any - if err := json.Unmarshal([]byte(task.Payload), &payload); err == nil { - resp.Payload = payload - } - } - return resp -} - -// QueuedTasksResp represents the response for queued tasks -type QueuedTasksResp struct { - ReadyTasks []TaskResp `json:"ready_tasks"` - DelayedTasks []TaskResp `json:"delayed_tasks"` -} - -func validateState(state *consts.TaskState) error { - if state != nil { - if _, exists := consts.ValidTaskStates[*state]; !exists { - return fmt.Errorf("invalid task state: %d", *state) - } - } - return nil -} - -func validateTaskType(taskType *consts.TaskType) error { - if taskType != nil { - if _, exists := consts.ValidTaskTypes[*taskType]; !exists { - return fmt.Errorf("invalid task type: %d", *taskType) - } - } - return nil -} - -func validateUUID(id string) error { - if id == "" { - return nil // Empty is valid for optional fields - } - if !utils.IsValidUUID(id) { - return fmt.Errorf("invalid UUID format: %s", id) - } - return nil -} diff --git a/src/dto/trace.go b/src/dto/trace.go index f3d09116..8a63594e 100644 --- a/src/dto/trace.go +++ b/src/dto/trace.go @@ -2,12 +2,7 @@ package dto import ( "aegis/consts" - "aegis/database" - "aegis/utils" "encoding/json" - "fmt" - "strings" - "time" ) type TraceStreamEvent struct { @@ -78,220 +73,3 @@ type JobMessage struct { Namespace string `json:"namespace"` LogFile string `json:"log_file,omitempty"` } - -type TraceQuery struct { - TraceID string `json:"trace_id"` - FirstTaskType consts.TaskType `json:"first_task_type"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` -} - -type GetTraceStreamReq struct { - LastID string `form:"last_id" binding:"omitempty"` -} - -func (req *GetTraceStreamReq) Validate() error { - if req.LastID == "" { - req.LastID = "0" - } - - if req.LastID == "0" { - return nil - } - - if strings.Count(req.LastID, "-") != 1 { - return fmt.Errorf("invalid last_id format: must be '0' or a valid stream ID (e.g., 1678886400000-0)") - } - - return nil -} - -// GetGroupStatsReq represents the request to get group stats -type GetGroupStatsReq struct { - GroupID string `form:"group_id" binding:"required"` // Group ID to query -} - -func (req *GetGroupStatsReq) Validate() error { - if !utils.IsValidUUID(req.GroupID) { - return fmt.Errorf("invalid group_id: must be a valid UUID") - } - return nil -} - -// TraceStatsItem represents the stat of a trace -type TraceStatsItem struct { - TraceID string `json:"trace_id"` - Type string `json:"type"` - State string `json:"state"` - StartTime time.Time `json:"start_time"` - EndTime *time.Time `json:"end_time,omitempty"` - CurrentEvent string `json:"current_event"` - CurrentTask string `json:"current_task"` - TaskTypeDurations map[string]float64 `json:"task_type_durations,omitempty" swaggertype:"object"` // Average durations per task type in seconds -} - -func NewTraceStats(trace *database.Trace) *TraceStatsItem { - detail := &TraceStatsItem{ - TraceID: trace.ID, - Type: consts.GetTraceTypeName(trace.Type), - State: consts.GetTraceStateName(trace.State), - StartTime: trace.StartTime, - EndTime: trace.EndTime, - CurrentEvent: trace.LastEvent.String(), - } - - if len(trace.Tasks) > 0 { - detail.CurrentTask = trace.Tasks[0].ID - - taskTypeMap := make(map[string][]database.Task) - for _, task := range trace.Tasks { - if task.State == consts.TaskCompleted || task.State == consts.TaskError { - taskTypeName := consts.GetTaskTypeName(task.Type) - if _, exists := taskTypeMap[taskTypeName]; !exists { - taskTypeMap[taskTypeName] = []database.Task{} - } - taskTypeMap[taskTypeName] = append(taskTypeMap[taskTypeName], task) - } - } - - detail.TaskTypeDurations = make(map[string]float64) - for taskTypeName, tasks := range taskTypeMap { - totalDuration := 0.0 - for _, task := range tasks { - duration := task.UpdatedAt.Sub(task.CreatedAt).Seconds() - totalDuration += duration - } - detail.TaskTypeDurations[taskTypeName] = totalDuration / float64(len(tasks)) - } - } - - return detail -} - -// GroupStats represents the response for group stats -type GroupStats struct { - TotalTraces int `json:"total_traces"` - AvgDuration float64 `json:"avg_duration"` - MinDuration float64 `json:"min_duration"` - MaxDuration float64 `json:"max_duration"` - TraceStateMap map[string][]TraceStatsItem `json:"trace_state_map"` -} - -func NewDefaultGroupStats() *GroupStats { - return &GroupStats{ - TotalTraces: 0, - AvgDuration: 0.0, - MinDuration: 0.0, - MaxDuration: 0.0, - } -} - -// ===================== Trace CRUD DTOs ===================== - -// TraceResp represents the response for a trace in list views -type TraceResp struct { - ID string `json:"id"` - Type string `json:"type"` - LastEvent string `json:"last_event"` - StartTime time.Time `json:"start_time"` - EndTime *time.Time `json:"end_time,omitempty"` - GroupID string `json:"group_id"` - ProjectID int `json:"project_id,omitempty"` - ProjectName string `json:"project_name,omitempty"` - LeafNum int `json:"leaf_num"` - State string `json:"state"` - Status string `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -func NewTraceResp(trace *database.Trace) *TraceResp { - resp := &TraceResp{ - ID: trace.ID, - Type: consts.GetTraceTypeName(trace.Type), - LastEvent: trace.LastEvent.String(), - StartTime: trace.StartTime, - EndTime: trace.EndTime, - GroupID: trace.GroupID, - ProjectID: trace.ProjectID, - LeafNum: trace.LeafNum, - State: consts.GetTraceStateName(trace.State), - Status: consts.GetStatusTypeName(trace.Status), - CreatedAt: trace.CreatedAt, - UpdatedAt: trace.UpdatedAt, - } - if trace.Project != nil { - resp.ProjectName = trace.Project.Name - } - return resp -} - -// TraceDetailResp represents the detailed response for a single trace -type TraceDetailResp struct { - TraceResp - - Tasks []TaskResp `json:"tasks"` -} - -func NewTraceDetailResp(trace *database.Trace) *TraceDetailResp { - resp := &TraceDetailResp{ - TraceResp: *NewTraceResp(trace), - Tasks: make([]TaskResp, 0, len(trace.Tasks)), - } - for i := range trace.Tasks { - resp.Tasks = append(resp.Tasks, *NewTaskResp(&trace.Tasks[i])) - } - return resp -} - -// ListTraceFilters represents the filters for listing traces -type ListTraceFilters struct { - TraceType *consts.TraceType - GroupID string - ProjectID int - State *consts.TraceState - Status *consts.StatusType -} - -// ListTraceReq represents the request to list traces -type ListTraceReq struct { - PaginationReq - TraceType *consts.TraceType `form:"trace_type" binding:"omitempty"` - GroupID string `form:"group_id" binding:"omitempty"` - ProjectID int `form:"project_id" binding:"omitempty"` - State *consts.TraceState `form:"state" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` -} - -func (req *ListTraceReq) Validate() error { - if err := req.PaginationReq.Validate(); err != nil { - return err - } - if req.TraceType != nil { - if _, exists := consts.ValidTraceTypes[*req.TraceType]; !exists { - return fmt.Errorf("invalid trace type: %d", *req.TraceType) - } - } - if err := validateUUID(req.GroupID); err != nil { - return err - } - if req.ProjectID < 0 { - return fmt.Errorf("invalid project ID: %d", req.ProjectID) - } - if req.State != nil { - if _, exists := consts.ValidTraceStates[*req.State]; !exists { - return fmt.Errorf("invalid trace state: %d", *req.State) - } - } - return validateStatusField(req.Status, true) -} - -func (req *ListTraceReq) ToFilterOptions() *ListTraceFilters { - return &ListTraceFilters{ - TraceType: req.TraceType, - GroupID: req.GroupID, - ProjectID: req.ProjectID, - State: req.State, - Status: req.Status, - } -} diff --git a/src/go.mod b/src/go.mod index 98654712..b40a78c9 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,6 +5,7 @@ go 1.24.11 toolchain go1.24.12 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/BurntSushi/toml v1.4.0 github.com/OperationsPAI/chaos-experiment v0.1.0 github.com/antonfisher/nested-logrus-formatter v1.3.1 @@ -55,6 +56,7 @@ require ( k8s.io/client-go v0.33.1 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/yaml v1.4.0 + go.uber.org/fx v1.24.0 ) require ( @@ -265,6 +267,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.12.0 // indirect diff --git a/src/go.sum b/src/go.sum index d3aa974e..5e38d080 100644 --- a/src/go.sum +++ b/src/go.sum @@ -525,6 +525,8 @@ github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0Lh github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -896,6 +898,10 @@ go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/src/handlers/debug.go b/src/handlers/debug.go deleted file mode 100644 index d280d48f..00000000 --- a/src/handlers/debug.go +++ /dev/null @@ -1,45 +0,0 @@ -package handlers - -import ( - "net/http" - - "aegis/client/debug" - "aegis/dto" - - "github.com/gin-gonic/gin" -) - -func GetAllVars(c *gin.Context) { - dto.SuccessResponse[any](c, debug.NewDebugRegistry().GetAll()) -} - -func GetVar(c *gin.Context) { - var req dto.DebugGetReq - if err := c.BindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "invalid JSON format") - return - } - - data, err := debug.NewDebugRegistry().Get(req.Name) - if err != nil { - dto.ErrorResponse(c, http.StatusNotFound, err.Error()) - return - } - - dto.SuccessResponse[any](c, data) -} - -func SetVar(c *gin.Context) { - var req dto.DebugSetReq - if err := c.BindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "invalid JSON format") - return - } - - if err := debug.NewDebugRegistry().Set(req.Name, req.Value); err != nil { - dto.ErrorResponse(c, http.StatusNotFound, err.Error()) - return - } - - dto.SuccessResponse[any](c, nil) -} diff --git a/src/handlers/system/audit.go b/src/handlers/system/audit.go deleted file mode 100644 index 50d7fbcc..00000000 --- a/src/handlers/system/audit.go +++ /dev/null @@ -1,85 +0,0 @@ -package system - -import ( - "net/http" - "strconv" - - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - - "github.com/gin-gonic/gin" -) - -// GetAuditLog handles single audit log retrieval -// -// @Summary Get audit log by ID -// @Description Get a specific audit log entry by ID -// @Tags System -// @Produce json -// @Security BearerAuth -// @Param id path int true "Audit log ID" -// @Success 200 {object} dto.GenericResponse[dto.AuditLogDetailResp] "Audit log retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Audit log not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/audit/{id} [get] -func GetAuditLog(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.Atoi(idStr) - if err != nil || id <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid audit log ID") - return - } - - resp, err := producer.GetAuditLogDetail(id) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListAuditLogs handles audit log listing -// -// @Summary List audit logs -// @Description Get paginated list of audit logs with optional filtering -// @Tags System -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param action query string false "Filter by action" -// @Param user_id query int false "Filter by user ID" -// @Param resource_id query int false "Filter by resource ID" -// @Param state query int false "Filter by state" -// @Param status query int false "Filter by status" -// @Param start_date query string false "Filter from date (YYYY-MM-DD)" -// @Param end_date query string false "Filter to date (YYYY-MM-DD)" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.AuditLogResp]] "Audit logs retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/audit [get] -func ListAuditLogs(c *gin.Context) { - var req dto.ListAuditLogReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query parameters: "+err.Error()) - return - } - - resp, err := producer.ListAuditLogs(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Audit logs retrieved successfully", resp) -} diff --git a/src/handlers/system/configs.go b/src/handlers/system/configs.go deleted file mode 100644 index d093c695..00000000 --- a/src/handlers/system/configs.go +++ /dev/null @@ -1,335 +0,0 @@ -package system - -import ( - "net/http" - "strconv" - - "aegis/consts" - "aegis/dto" - "aegis/handlers" - "aegis/middleware" - producer "aegis/service/producer" - - "github.com/gin-gonic/gin" -) - -// GetConfig retrieves a configuration by ID -// -// @Summary Get configuration -// @Description Get detailed information about a specific configuration -// @Tags Configurations -// @ID get_config_by_id -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Success 200 {object} dto.GenericResponse[dto.ConfigResp] "Configuration retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Config not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id} [get] -func GetConfig(c *gin.Context) { - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - resp, err := producer.GetConfigDetail(configID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListConfigs lists configurations with pagination and filtering -// -// @Summary List configurations -// @Description List configurations with pagination and optional filters -// @Tags Configurations -// @ID list_configs -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param page_size query int false "Page size" default(20) -// @Param category query string false "Filter by configuration category" -// @Param value_type query consts.ConfigValueType false "Filter by configuration value type" -// @Param is_secret query bool false "Filter by secret status" -// @Param updated_by query int false "Filter by ID of the user who last updated the config" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ConfigResp]] "Configurations retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs [get] -func ListConfigs(c *gin.Context) { - var req dto.ListConfigReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListConfigs(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// RollbackConfigValue rolls back a configuration value to previous value from history -// -// @Summary Rollback configuration value -// @Description Rollback a configuration value to a previous value from history -// @Tags Configurations -// @ID rollback_config_value -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param rollback body dto.RollbackConfigReq true "Rollback request with history_id and reason" -// @Success 202 {object} dto.GenericResponse[any] "Configuration value rolled back successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is not a value change" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id}/value/rollback [post] -func RollbackConfigValue(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - ctx := c.Request.Context() - - var req dto.RollbackConfigReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - ipAddress := c.ClientIP() - userAgent := c.Request.UserAgent() - - err = producer.RollbackConfigValue(ctx, &req, configID, userID, ipAddress, userAgent) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusAccepted, "Configuration value rolled back successfully", nil) -} - -// RollbackConfigMetadata rolls back a configuration metadata field to previous value from history -// -// @Summary Rollback configuration metadata -// @Description Rollback a configuration metadata field (e.g., min_value, max_value, pattern) to a previous value from history -// @Tags Configurations -// @ID rollback_config_metadata -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param rollback body dto.RollbackConfigReq true "Rollback request with history_id and reason" -// @Success 200 {object} dto.GenericResponse[dto.ConfigResp] "Configuration metadata rolled back successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is a value change" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id}/metadata/rollback [post] -func RollbackConfigMetadata(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - var req dto.RollbackConfigReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - ipAddress := c.ClientIP() - userAgent := c.Request.UserAgent() - - resp, err := producer.RollbackConfigMetadata(&req, configID, userID, ipAddress, userAgent) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Configuration metadata rolled back successfully", resp) -} - -// UpdateConfigValue updates a configuration value (runtime operational change) -// -// @Summary Update configuration value -// @Description Update a configuration value with validation and history tracking. This is for frequent operational adjustments. -// @Tags Configurations -// @ID update_config_value -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param request body dto.UpdateConfigValueReq true "Configuration value update request" -// @Success 202 {object} dto.GenericResponse[any] "Configuration value updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id} [patch] -func UpdateConfigValue(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - ctx := c.Request.Context() - - var req dto.UpdateConfigValueReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - ipAddress := c.ClientIP() - userAgent := c.Request.UserAgent() - - err = producer.UpdateConfigValue(ctx, &req, configID, userID, ipAddress, userAgent) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusAccepted, "Configuration value updated successfully", nil) -} - -// UpdateConfigMetadata updates configuration metadata (rare admin operation) -// -// @Summary Update configuration metadata -// @Description Update configuration metadata such as min/max values, validation rules, etc. This is a high-privilege operation. -// @Tags Configurations -// @ID update_config_metadata -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param request body dto.UpdateConfigMetadataReq true "Configuration metadata update request" -// @Success 200 {object} dto.GenericResponse[dto.ConfigResp] "Configuration metadata updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id}/metadata [put] -func UpdateConfigMetadata(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - var req dto.UpdateConfigMetadataReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - ipAddress := c.ClientIP() - userAgent := c.Request.UserAgent() - - resp, err := producer.UpdateConfigMetadata(&req, configID, userID, ipAddress, userAgent) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Configuration metadata updated successfully", resp) -} - -// ===================== Config History ===================== - -// ListConfigHistories handles listing config histories with pagination and filtering -// -// @Summary List configuration histories -// @Description Get paginated list of config histories for a specific config -// @Tags Configurations -// @ID list_config_histories -// @Produce json -// @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ConfigHistoryResp]] "Config histories retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/configs/{config_id}/histories [get] -func ListConfigHistories(c *gin.Context) { - configIDStr := c.Param(consts.URLPathConfigID) - configID, err := strconv.Atoi(configIDStr) - if err != nil || configID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid config ID") - return - } - - var req dto.ListConfigHistoryReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListConfigHistories(&req, configID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Config historys retrieved successfully", resp) -} diff --git a/src/handlers/system/health.go b/src/handlers/system/health.go deleted file mode 100644 index f08a3fc4..00000000 --- a/src/handlers/system/health.go +++ /dev/null @@ -1,289 +0,0 @@ -package system - -import ( - "aegis/client" - "aegis/client/k8s" - "aegis/config" - "aegis/database" - "aegis/dto" - "context" - "fmt" - "net" - "net/http" - "time" - - "github.com/gin-gonic/gin" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// GetHealth handles system health check -// -// @Summary System health check -// @Description Get system health status and service information -// @Tags System -// @ID get_system_health -// @Produce json -// @Success 200 {object} dto.GenericResponse[dto.HealthCheckResp] "Health check successful" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/health [get] -// @x-api-type {"sdk":"true"} -func GetHealth(c *gin.Context) { - start := time.Now() - - services := make(map[string]dto.ServiceInfo) - overallStatus := "healthy" - - buildkitInfo := checkBuildKitHealth() - services["buildkit"] = buildkitInfo - if buildkitInfo.Status != "healthy" { - overallStatus = "unhealthy" - } - - dbInfo := checkDatabaseHealth() - services["database"] = dbInfo - if dbInfo.Status != "healthy" { - overallStatus = "unhealthy" - } - - jaegerInfo := checkJaegerHealth() - services["jaeger"] = jaegerInfo - if jaegerInfo.Status != "healthy" { - overallStatus = "unhealthy" - } - - k8sInfo := checkKubernetesHealth() - services["kubernetes"] = k8sInfo - if k8sInfo.Status != "healthy" { - overallStatus = "unhealthy" - } - - redisInfo := checkRedisHealth() - services["redis"] = redisInfo - if redisInfo.Status != "healthy" { - overallStatus = "unhealthy" - } - - response := dto.HealthCheckResp{ - Status: overallStatus, - Timestamp: time.Now(), - Version: config.GetString("version"), - Uptime: time.Since(start).String(), - Services: services, - } - - dto.SuccessResponse(c, response) -} - -// checkBuildKitHealth checks BuildKit daemon connectivity -func checkBuildKitHealth() dto.ServiceInfo { - start := time.Now() - - buildkitAddr := config.GetString("buildkit.address") - - conn, err := net.DialTimeout("tcp", buildkitAddr, 5*time.Second) - responseTime := time.Since(start) - - if err != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Error: "BuildKit daemon unreachable", - Details: fmt.Sprintf("Cannot connect to BuildKit at %s: %v", buildkitAddr, err), - } - } - defer func() { _ = conn.Close() }() - - return dto.ServiceInfo{ - Status: "healthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - } -} - -// checkDatabaseHealth checks database connectivity -func checkDatabaseHealth() dto.ServiceInfo { - start := time.Now() - - if database.DB == nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: "N/A", - Error: "Database connection not available", - } - } - - // Test connection with a simple query - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - var result int - err := database.DB.WithContext(ctx).Raw("SELECT 1").Scan(&result).Error - responseTime := time.Since(start) - - if err != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Error: "Database query failed", - Details: err.Error(), - } - } - - return dto.ServiceInfo{ - Status: "healthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - } -} - -// checkJaegerHealth checks Jaeger tracing service connectivity -func checkJaegerHealth() dto.ServiceInfo { - start := time.Now() - - jaegerURL := fmt.Sprintf("http://%s/v1/traces", config.GetString("jaeger.endpoint")) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, "HEAD", jaegerURL, nil) - if err != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - Error: "Failed to create Jaeger OTLP request", - Details: err.Error(), - } - } - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - responseTime := time.Since(start) - - if err != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Error: "Jaeger OTLP endpoint unreachable", - Details: err.Error(), - } - } - defer func() { _ = resp.Body.Close() }() - - // OTLP endpoints typically return 405 Method Not Allowed for HEAD requests - if resp.StatusCode != http.StatusMethodNotAllowed && resp.StatusCode != http.StatusOK { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Error: fmt.Sprintf("Jaeger OTLP returned unexpected status %d", resp.StatusCode), - } - } - - return dto.ServiceInfo{ - Status: "healthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Details: "Jaeger OTLP endpoint responding", - } -} - -// checkKubernetesHealth checks Kubernetes API connectivity -func checkKubernetesHealth() dto.ServiceInfo { - start := time.Now() - - // Try to get Kubernetes config - restConfig := k8s.GetK8sRestConfig() - if restConfig == nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - Error: "Kubernetes config not available", - } - } - - // Create Kubernetes client - k8sClient := k8s.GetK8sClient() - if k8sClient == nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - Error: "Kubernetes client not available", - } - } - - // Create Kubernetes dynamic client - k8sDynamicClient := k8s.GetK8sDynamicClient() - if k8sDynamicClient == nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - Error: "Kubernetes dynamic client not available", - } - } - - // Test API connectivity - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := k8sClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{Limit: 1}) - if err != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - Error: "Kubernetes API request failed", - Details: err.Error(), - } - } - - return dto.ServiceInfo{ - Status: "healthy", - LastChecked: time.Now(), - ResponseTime: time.Since(start).String(), - } -} - -// checkRedisHealth checks Redis connectivity -func checkRedisHealth() dto.ServiceInfo { - start := time.Now() - - rdb := client.GetRedisClient() - if rdb == nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: "N/A", - Error: "Redis connection not available", - } - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Test connection with PING - result := rdb.Ping(ctx) - responseTime := time.Since(start) - - if result.Err() != nil { - return dto.ServiceInfo{ - Status: "unhealthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - Error: result.Err().Error(), - } - } - - return dto.ServiceInfo{ - Status: "healthy", - LastChecked: time.Now(), - ResponseTime: responseTime.String(), - } -} diff --git a/src/handlers/system/monitor.go b/src/handlers/system/monitor.go deleted file mode 100644 index 5a72ce94..00000000 --- a/src/handlers/system/monitor.go +++ /dev/null @@ -1,152 +0,0 @@ -package system - -import ( - "aegis/config" - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - "net/http" - "runtime" - "time" - - "github.com/gin-gonic/gin" -) - -// GetMetrics handles monitoring metrics query -// -// @Summary Get monitoring metrics -// @Description Deprecated: This endpoint returns hardcoded/fabricated data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. -// @Deprecated -// @Tags System -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body dto.MonitoringQueryReq true "Metrics query request" -// @Success 200 {object} dto.GenericResponse[dto.MonitoringMetricsResp] "Metrics retrieved successfully" -// @Success 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/monitor/metrics [post] -func GetMetrics(c *gin.Context) { - var req dto.MonitoringQueryReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - // Deprecated: returns hardcoded data. Use GET /api/v2/system/metrics instead. - c.Header("Deprecation", "true") - c.Header("Link", `; rel="successor-version"`) - - metrics := map[string]dto.MetricValue{ - "cpu_usage": { - Value: 25.5, - Timestamp: time.Now(), - Unit: "percent", - }, - "memory_usage": { - Value: 60.2, - Timestamp: time.Now(), - Unit: "percent", - }, - "disk_usage": { - Value: 45.8, - Timestamp: time.Now(), - Unit: "percent", - }, - "active_connections": { - Value: 142, - Timestamp: time.Now(), - Unit: "count", - }, - } - - labels := map[string]string{ - "instance": "rcabench-01", - "version": config.GetString("version"), - } - - response := dto.MonitoringMetricsResp{ - Timestamp: time.Now(), - Metrics: metrics, - Labels: labels, - } - - dto.SuccessResponse(c, response) -} - -// GetSystemInfo handles basic system information -// -// @Summary Get system information -// @Description Deprecated: This endpoint returns partially hardcoded data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. -// @Deprecated -// @Tags System -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.SystemInfo] "System info retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/monitor/info [get] -func GetSystemInfo(c *gin.Context) { - var memStats runtime.MemStats - runtime.ReadMemStats(&memStats) - - // Deprecated: returns partially hardcoded data. Use GET /api/v2/system/metrics instead. - c.Header("Deprecation", "true") - c.Header("Link", `; rel="successor-version"`) - - info := dto.SystemInfo{ - CPUUsage: 25.5, - MemoryUsage: float64(memStats.Alloc) / float64(memStats.Sys) * 100, - DiskUsage: 45.8, - LoadAverage: "1.2, 1.5, 1.8", - } - - dto.SuccessResponse(c, info) -} - -// ListNamespaceLocks handles listing of namespace locks -// -// @Summary List namespace locks -// @Description Retrieve the list of currently locked namespaces -// @Tags System -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.ListNamespaceLockResp] "Successfully retrieved the list of locks" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal Server Error" -// @Router /system/monitor/namespaces/locks [get] -func ListNamespaceLocks(c *gin.Context) { - items, err := producer.InspectLock(c.Request.Context()) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Successfully retrieved the list of locks", items) -} - -// ListQueuedTasks handles listing of queued tasks -// -// @Summary List queued tasks -// @Description List tasks in queue (ready and delayed) -// @Tags System -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.QueuedTasksResp] "Queued tasks retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "No queued tasks found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/monitor/tasks/queue [post] -func ListQueuedTasks(c *gin.Context) { - ctx := c.Request.Context() - resp, err := producer.ListQueuedTasks(ctx) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Queued tasks retrieved successfully", resp) -} diff --git a/src/handlers/v2/auth.go b/src/handlers/v2/auth.go deleted file mode 100644 index 5f28d2da..00000000 --- a/src/handlers/v2/auth.go +++ /dev/null @@ -1,220 +0,0 @@ -package v2 - -import ( - "context" - "net/http" - - "aegis/dto" - "aegis/handlers" - "aegis/middleware" - producer "aegis/service/producer" - "aegis/utils" - - "github.com/gin-gonic/gin" -) - -// Register handles user registration -// -// @Summary User registration -// @Description Register a new user account -// @Tags Authentication -// @ID register_user -// @Accept json -// @Produce json -// @Param request body dto.RegisterReq true "Registration details" -// @Success 201 {object} dto.GenericResponse[dto.UserInfo] "Registration successful" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 409 {object} dto.GenericResponse[any] "User already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/register [post] -// @x-api-type {"sdk":"true"} -func Register(c *gin.Context) { - var req dto.RegisterReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.Register(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusCreated, "Registration successful", resp) -} - -// Login handles user authentication -// -// @Summary User login -// @Description Authenticate user with username and password -// @Tags Authentication -// @ID login -// @Accept json -// @Produce json -// @Param request body dto.LoginReq true "Login credentials" -// @Success 200 {object} dto.GenericResponse[dto.LoginResp] "Login successful" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid user name or password" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/login [post] -// @x-api-type {"sdk":"true"} -func Login(c *gin.Context) { - var req dto.LoginReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusUnauthorized, err.Error()) - return - } - - resp, err := producer.Login(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Login successful", resp) -} - -// RefreshToken handles JWT token refresh -// -// @Summary Refresh JWT token -// @Description Refresh an existing JWT token -// @Tags Authentication -// @ID refresh_auth_token -// @Accept json -// @Produce json -// @Param request body dto.TokenRefreshReq true "Token refresh request" -// @Success 200 {object} dto.GenericResponse[dto.TokenRefreshResp] "Token refreshed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/refresh [post] -func RefreshToken(c *gin.Context) { - var req dto.TokenRefreshReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusUnauthorized, err.Error()) - return - } - - resp, err := producer.RefreshToken(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Token refreshed successfully", resp) -} - -// Logout handles user logout -// -// @Summary User logout -// @Description Logout user and invalidate token -// @Tags Authentication -// @ID logout -// @Produce json -// @Success 200 {object} dto.GenericResponse[any] "Logout successful" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid authorization header" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/logout [post] -func Logout(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - token, err := utils.ExtractTokenFromHeader(authHeader) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid authorization header") - return - } - - claims, err := utils.ValidateToken(token) - if err != nil { - dto.ErrorResponse(c, http.StatusUnauthorized, "Invalid token") - return - } - - err = producer.Logout(context.Background(), claims) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusOK, "Logged out successfully", nil) -} - -// ChangePassword handles password change -// -// @Summary Change user password -// @Description Change password for authenticated user -// @Tags Authentication -// @ID change_password -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body dto.ChangePasswordReq true "Password change request" -// @Success 200 {object} dto.GenericResponse[any] "Password changed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/change-password [post] -func ChangePassword(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - var req dto.ChangePasswordReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - err := producer.ChangePassword(&req, userID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusOK, "Password changed successfully", nil) -} - -// GetProfile handles getting current user profile -// -// @Summary Get current user profile -// @Description Get profile information for authenticated user -// @Tags Authentication -// @ID get_current_user_profile -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.UserDetailResp] "Profile retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/profile [get] -func GetProfile(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists || userID <= 0 { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - resp, err := producer.GetProfile(userID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusOK, "Profile retrieved successfully", resp) -} diff --git a/src/handlers/v2/permissions.go b/src/handlers/v2/permissions.go deleted file mode 100644 index 1048b949..00000000 --- a/src/handlers/v2/permissions.go +++ /dev/null @@ -1,119 +0,0 @@ -package v2 - -import ( - "aegis/consts" - "net/http" - "strconv" - - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - - "github.com/gin-gonic/gin" -) - -// GetPermission handles getting a single permission by ID -// -// @Summary Get permission by ID -// @Description Get detailed information about a specific permission -// @Tags Permissions -// @ID get_permission_by_id -// @Produce json -// @Security BearerAuth -// @Param id path int true "Permission ID" -// @Success 200 {object} dto.GenericResponse[dto.PermissionDetailResp] "Permission retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/permissions/{id} [get] -func GetPermission(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid permission ID") - return - } - - resp, err := producer.GetPermissionDetail(id) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListPermissions handles listing permissions with pagination and filtering -// -// @Summary List permissions -// @Description Get paginated list of permissions with optional filtering -// @Tags Permissions -// @ID list_permissions -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param action query string false "Filter by action" -// @Param is_system query bool false "Filter by system permission" -// @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.PermissionResp] "Permissions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/permissions [get] -func ListPermissions(c *gin.Context) { - var req dto.ListPermissionReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - response, err := producer.ListPermissions(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, response) -} - -// ===================== Role-Permission API ===================== - -// ListRolesFromPermission handles listing roles assigned to a permission -// -// @Summary List roles from permission -// @Description Get list of roles assigned to a specific permission -// @Tags Permissions -// @ID list_roles_with_permission -// @Produce json -// @Security BearerAuth -// @Param permission_id path int true "Permission ID" -// @Success 200 {object} dto.GenericResponse[[]dto.RoleResp] "Roles retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/permissions/{permission_id}/roles [get] -// @x-api-type {"sdk":"true"} -func ListRolesFromPermission(c *gin.Context) { - permissionIDStr := c.Param(consts.URLPathPermissionID) - permissionID, err := strconv.Atoi(permissionIDStr) - if err != nil || permissionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid permission ID") - return - } - - resp, err := producer.ListRolesFromPermission(permissionID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} diff --git a/src/handlers/v2/projects.go b/src/handlers/v2/projects.go deleted file mode 100644 index 44a14092..00000000 --- a/src/handlers/v2/projects.go +++ /dev/null @@ -1,504 +0,0 @@ -package v2 - -import ( - "aegis/consts" - "net/http" - "strconv" - - "aegis/dto" - "aegis/handlers" - "aegis/middleware" - producer "aegis/service/producer" - - "github.com/gin-gonic/gin" -) - -// CreateProject handles project creation -// -// @Summary Create a new project -// @Description Create a new project with specified details -// @Tags Projects -// @ID create_project -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body dto.CreateProjectReq true "Project creation request" -// @Success 201 {object} dto.GenericResponse[dto.ProjectResp] "Project created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Project already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects [post] -// @x-api-type {"sdk":"true"} -func CreateProject(c *gin.Context) { - userID, exists := middleware.GetCurrentUserID(c) - if !exists { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - var req dto.CreateProjectReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.CreateProject(&req, userID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusCreated, "Project created successfully", resp) -} - -// DeleteProject handles project deletion -// -// @Summary Delete project -// @Description Delete a project -// @Tags Projects -// @ID delete_project -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Success 204 {object} dto.GenericResponse[any] "Project deleted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id} [delete] -func DeleteProject(c *gin.Context) { - projectIdStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIdStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - err = producer.DeleteProject(projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusNoContent, "Project deleted successfully", nil) -} - -// GetProjectDetail handles getting a single project by ID -// -// @Summary Get project by ID -// @Description Get detailed information about a specific project -// @Tags Projects -// @ID get_project_by_id -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Success 200 {object} dto.GenericResponse[dto.ProjectDetailResp] "Project retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id} [get] -// @x-api-type {"sdk":"true"} -func GetProjectDetail(c *gin.Context) { - projectIdStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIdStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - resp, err := producer.GetProjectDetail(projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListProjects handles listing projects with pagination and filtering -// -// @Summary List projects -// @Description Get paginated list of projects with filtering -// @Tags Projects -// @ID list_projects -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param is_public query bool false "Filter by public status" -// @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ProjectResp]] "Projects retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects [get] -// @x-api-type {"sdk":"true"} -func ListProjects(c *gin.Context) { - var req dto.ListProjectReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListProjects(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// UpdateProject handles project updates -// -// @Summary Update project -// @Description Update an existing project's information -// @Tags Projects -// @ID update_project -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param request body dto.UpdateProjectReq true "Project update request" -// @Success 202 {object} dto.GenericResponse[dto.ProjectResp] "Project updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id} [patch] -func UpdateProject(c *gin.Context) { - projectIdStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIdStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - var req dto.UpdateProjectReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.UpdateProject(&req, projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusAccepted, "Project updated successfully", resp) -} - -// ===================== Project-Label API ===================== - -// ManageProjectCustomLabels manages project custom labels (key-value pairs) -// -// @Summary Manage project custom labels -// @Description Add or remove custom labels (key-value pairs) for a project -// @Tags Projects -// @ID update_project_labels -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param manage body dto.ManageProjectLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[dto.ProjectResp] "Labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/labels [patch] -func ManageProjectCustomLabels(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - var req dto.ManageProjectLabelReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ManageProjectLabels(&req, projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ===================== Project-Injection API ===================== - -// ListProjectInjections lists all fault injections for a project -// -// @Summary List project fault injections -// @Description Get paginated list of fault injections for a specific project -// @Tags Projects -// @ID list_project_injections -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.InjectionResp]] "Fault injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections [get] -// @x-api-type {"sdk":"true"} -func ListProjectInjections(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - var req dto.ListInjectionReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListProjectInjections(&req, projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// SearchProjectInjections searches fault injections within a specific project -// -// @Summary Search project fault injections -// @Description Advanced search for injections within a project with complex filtering -// @Tags Projects -// @ID search_project_injections -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param search body dto.SearchInjectionReq true "Search criteria" -// @Success 200 {object} dto.GenericResponse[dto.SearchResp[dto.InjectionDetailResp]] "Search results" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections/search [post] -// @x-api-type {"sdk":"true"} -func SearchProjectInjections(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - searchInjectionsCommon(c, &projectID) -} - -// ListProjectFaultInjectionNoIssues lists fault injections without issues for a project -// -// @Summary List project fault injections without issues -// @Description Query fault injection records without issues within a project based on time range -// @Tags Projects -// @ID list_project_injections_no_issues -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param labels query []string false "Filter by labels" -// @Param lookback query string false "Time range query" -// @Param custom_start_time query string false "Custom start time" -// @Param custom_end_time query string false "Custom end time" -// @Success 200 {object} dto.GenericResponse[[]dto.InjectionNoIssuesResp] "Injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections/analysis/no-issues [get] -// @x-api-type {"sdk":"true"} -func ListProjectFaultInjectionNoIssues(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - listFaultInjectionNoIssuesCommon(c, &projectID) -} - -// ListProjectFaultInjectionWithIssues lists fault injections with issues for a project -// -// @Summary List project fault injections with issues -// @Description Query fault injection records with issues within a project based on time range -// @Tags Projects -// @ID list_project_injections_with_issues -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param labels query []string false "Filter by labels" -// @Param lookback query string false "Time range query" -// @Param custom_start_time query string false "Custom start time" -// @Param custom_end_time query string false "Custom end time" -// @Success 200 {object} dto.GenericResponse[[]dto.InjectionWithIssuesResp] "Injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections/analysis/with-issues [get] -// @x-api-type {"sdk":"true"} -func ListProjectFaultInjectionWithIssues(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - listFaultInjectionWithIssuesCommon(c, &projectID) -} - -// SubmitProjectFaultInjection submits fault injections for a specific project -// -// @Summary Submit project fault injections -// @Description Submit multiple fault injection tasks for a specific project -// @Tags Projects -// @ID submit_project_fault_injection -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param body body dto.SubmitInjectionReq true "Fault injection request" -// @Success 200 {object} dto.GenericResponse[dto.SubmitInjectionResp] "Injections submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections/inject [post] -// @x-api-type {"sdk":"true"} -func SubmitProjectFaultInjection(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - submitFaultInjectionCommon(c, &projectID) -} - -// SubmitProjectDatapackBuilding submits datapack building tasks for a specific project -// -// @Summary Submit project datapack buildings -// @Description Submit multiple datapack building tasks for a specific project -// @Tags Projects -// @ID submit_project_datapack_building -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param body body dto.SubmitDatapackBuildingReq true "Datapack building request" -// @Success 202 {object} dto.GenericResponse[dto.SubmitDatapackBuildingResp] "Datapack buildings submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/injections/build [post] -// @x-api-type {"sdk":"true"} -func SubmitProjectDatapackBuilding(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - submitDatapackBuildingCommon(c, &projectID) -} - -// ===================== Project-Execution API ===================== - -// ListProjectExecutions lists all algorithm executions for a project -// -// @Summary List project executions -// @Description Get paginated list of algorithm executions for a specific project -// @Tags Projects -// @ID list_project_executions -// @Produce json -// @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ExecutionResp]] "Executions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/projects/{project_id}/executions [get] -// @x-api-type {"sdk":"true"} -func ListProjectExecutions(c *gin.Context) { - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") - return - } - - var req dto.ListExecutionReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListProjectExecutions(&req, projectID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} diff --git a/src/handlers/v2/resources.go b/src/handlers/v2/resources.go deleted file mode 100644 index 29f8e38e..00000000 --- a/src/handlers/v2/resources.go +++ /dev/null @@ -1,117 +0,0 @@ -package v2 - -import ( -"aegis/consts" - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - "net/http" - "strconv" - - "github.com/gin-gonic/gin" -) - -// GetResourceDetail handles getting a single resource by ID -// -// @Summary Get resource by ID -// @Description Get detailed information about a specific resource -// @Tags Resources -// @ID get_resource_by_id -// @Produce json -// @Security BearerAuth -// @Param id path int true "Resource ID" -// @Success 200 {object} dto.GenericResponse[dto.ResourceResp] "Resource retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/resources/{id} [get] -// @x-api-type {"sdk":"true"} -func GetResourceDetail(c *gin.Context) { - resourceIDStr := c.Param(consts.URLPathID) - resourceID, err := strconv.Atoi(resourceIDStr) - if err != nil || resourceID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid resource ID") - return - } - - resp, err := producer.GetResourceDetail(resourceID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListResources handles listing resources with pagination and filtering -// -// @Summary List resources -// @Description Get paginated list of resources with filtering -// @Tags Resources -// @ID list_resources -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param type query consts.ResourceType false "Filter by resource type" -// @Param category query consts.ResourceCategory false "Filter by resource category" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ResourceResp]] "Resources retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/resources [get] -// @x-api-type {"sdk":"true"} -func ListResources(c *gin.Context) { - var req dto.ListResourceReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListResources(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListResourcePermissions handles listing permissions by resource -// -// @Summary List permissions from resource -// @Description Get list of permissions assigned to a specific resource -// @Tags Resources -// @ID list_resource_permissions -// @Produce json -// @Security BearerAuth -// @Param id path int true "Resource ID" -// @Success 200 {object} dto.GenericResponse[[]dto.PermissionResp] "Permissions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID or request form" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/resources/{id}/permissions [get] -// @x-api-type {"sdk":"true"} -func ListResourcePermissions(c *gin.Context) { - resourceIDStr := c.Param(consts.URLPathID) - resourceID, err := strconv.Atoi(resourceIDStr) - if err != nil || resourceID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid resource ID") - return - } - - resp, err := producer.ListResourcePermissions(resourceID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} diff --git a/src/handlers/v2/roles.go b/src/handlers/v2/roles.go deleted file mode 100644 index fd68767b..00000000 --- a/src/handlers/v2/roles.go +++ /dev/null @@ -1,276 +0,0 @@ -package v2 - -import ( - "aegis/consts" - "net/http" - "strconv" - - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - - "github.com/gin-gonic/gin" -) - -// CreateRole handles role creation -// -// @Summary Create a new role -// @Description Create a new role with specified permissions -// @Tags Roles -// @ID create_role -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body dto.CreateRoleReq true "Role creation request" -// @Success 201 {object} dto.GenericResponse[dto.RoleResp] "Role created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Role already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles [post] -// @x-api-type {"sdk":"true"} -func CreateRole(c *gin.Context) { - var req dto.CreateRoleReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - resp, err := producer.CreateRole(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse(c, http.StatusCreated, "Role created successfully", resp) -} - -// DeleteRole handles role deletion -// -// @Summary Delete role -// @Description Delete a role (soft delete by setting status to -1) -// @Tags Roles -// @ID delete_role -// @Produce json -// @Security BearerAuth -// @Param id path int true "Role ID" -// @Success 200 {object} dto.GenericResponse[any] "Role deleted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied or cannot delete system role" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{id} [delete] -// @x-api-type {"sdk":"true"} -func DeleteRole(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - err = producer.DeleteRole(id) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusNoContent, "Role deleted successfully", nil) -} - -// GetRole handles getting a single role by ID -// -// @Summary Get role by ID -// @Description Get detailed information about a specific role -// @Tags Roles -// @ID get_role_by_id -// @Produce json -// @Security BearerAuth -// @Param id path int true "Role ID" -// @Success 200 {object} dto.GenericResponse[dto.RoleDetailResp] "Role retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{id} [get] -// @x-api-type {"sdk":"true"} -func GetRole(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - resp, err := producer.GetRoleDetail(id) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// ListRoles handles listing roles with pagination and filtering -// -// @Summary List roles -// @Description Get paginated list of roles with optional filtering -// @Tags Roles -// @ID list_roles -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param is_system query bool false "Filter by system role" -// @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListRoleResp] "Roles retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles [get] -// @x-api-type {"sdk":"true"} -func ListRoles(c *gin.Context) { - var req dto.ListRoleReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ListRoles(&req) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - -// UpdateRole handles role updates -// -// @Summary Update role -// @Description Update role information (partial update supported) -// @Tags Roles -// @ID update_role -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path int true "Role ID" -// @Param request body dto.UpdateRoleReq true "Role update request" -// @Success 202 {object} dto.GenericResponse[dto.RoleResp] "Role updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{id} [patch] -// @x-api-type {"sdk":"true"} -func UpdateRole(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - var req dto.UpdateRoleReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.UpdateRole(&req, id) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusAccepted, "Role updated successfully", resp) -} - -// ===================== Role-Permission API ===================== - -// AssignRolePermission handles role-permission assignment -// -// @Summary Assign permissions to role -// @Description Assign multiple permissions to a role -// @Tags Roles -// @ID grant_permissions_to_role -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param role_id path int true "Role ID" -// @Param request body dto.AssignRolePermissionReq true "Permission assignment request" -// @Success 200 {object} dto.GenericResponse[any] "Permissions assigned successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID or request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{role_id}/permissions/assign [post] -// @x-api-type {"sdk":"true"} -func AssignRolePermission(c *gin.Context) { - roleIdStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIdStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - var req dto.AssignRolePermissionReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - err = producer.BatchAssignRolePermissions(req.PermissionIDs, roleID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusOK, "Permissions assigned successfully", nil) -} - -// RemovePermissionsFromRole handles permission removal from role -// -// @Summary Remove permissions from role -// @Description Remove multiple permissions from a role -// @Tags Roles -// @ID revoke_permissions_from_role -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param role_id path int true "Role ID" -// @Param request body dto.RemoveRolePermissionReq true "Permission removal request" -// @Success 200 {object} dto.GenericResponse[any] "Permissions removed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID or request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{role_id}/permissions/remove [post] -// @x-api-type {"sdk":"true"} -func RemovePermissionsFromRole(c *gin.Context) { - roleIDStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - var req dto.RemoveRolePermissionReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - err = producer.RemovePermissionsFromRole(req.PermissionIDs, roleID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.JSONResponse[any](c, http.StatusOK, "Permissions removed successfully", nil) -} diff --git a/src/handlers/common.go b/src/httpx/common.go similarity index 89% rename from src/handlers/common.go rename to src/httpx/common.go index 1433e5a8..9ddcf210 100644 --- a/src/handlers/common.go +++ b/src/httpx/common.go @@ -1,18 +1,18 @@ -package handlers +package httpx import ( + "net/http" + "strconv" + "aegis/consts" "aegis/dto" "aegis/utils" - "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) -// ParsePositiveID parses a string ID parameter and validates it's a positive integer -// Returns the parsed ID and true if valid, or writes error response and returns false +// ParsePositiveID parses a string ID parameter and validates it's a positive integer. func ParsePositiveID(c *gin.Context, idStr, fieldName string) (int, bool) { logrus.WithFields(logrus.Fields{ "idStr": idStr, @@ -73,13 +73,11 @@ func HandleServiceError(c *gin.Context, err error) bool { case consts.ErrAlreadyExists: dto.ErrorResponse(c, http.StatusConflict, msg) default: - // Log full error details for debugging logrus.WithFields(logrus.Fields{ "path": c.Request.URL.Path, "method": c.Request.Method, "error": err.Error(), }).Error("Service error") - // Return user-friendly message but expose more details in development dto.ErrorResponse(c, http.StatusInternalServerError, msg) } diff --git a/src/infra/buildkit/gateway.go b/src/infra/buildkit/gateway.go new file mode 100644 index 00000000..389bfb02 --- /dev/null +++ b/src/infra/buildkit/gateway.go @@ -0,0 +1,52 @@ +package buildkitinfra + +import ( + "context" + "fmt" + "net" + "time" + + "aegis/config" + + buildkitclient "github.com/moby/buildkit/client" +) + +type Gateway struct{} + +func NewGateway() *Gateway { + return &Gateway{} +} + +func (g *Gateway) Address() string { + return config.GetString("buildkit.address") +} + +func (g *Gateway) Endpoint() string { + address := g.Address() + if address == "" { + return "" + } + return fmt.Sprintf("tcp://%s", address) +} + +func (g *Gateway) NewClient(ctx context.Context) (*buildkitclient.Client, error) { + endpoint := g.Endpoint() + if endpoint == "" { + return nil, fmt.Errorf("buildkit address is not configured") + } + return buildkitclient.New(ctx, endpoint) +} + +func (g *Gateway) CheckHealth(ctx context.Context, timeout time.Duration) error { + address := g.Address() + if address == "" { + return fmt.Errorf("buildkit address is not configured") + } + + dialer := net.Dialer{Timeout: timeout} + conn, err := dialer.DialContext(ctx, "tcp", address) + if err != nil { + return fmt.Errorf("cannot connect to BuildKit at %s: %w", address, err) + } + return conn.Close() +} diff --git a/src/infra/buildkit/module.go b/src/infra/buildkit/module.go new file mode 100644 index 00000000..1fc9a957 --- /dev/null +++ b/src/infra/buildkit/module.go @@ -0,0 +1,7 @@ +package buildkitinfra + +import "go.uber.org/fx" + +var Module = fx.Module("buildkit", + fx.Provide(NewGateway), +) diff --git a/src/infra/chaos/module.go b/src/infra/chaos/module.go new file mode 100644 index 00000000..0fb6fd8d --- /dev/null +++ b/src/infra/chaos/module.go @@ -0,0 +1,15 @@ +package chaosinfra + +import ( + chaosCli "github.com/OperationsPAI/chaos-experiment/client" + "go.uber.org/fx" + "k8s.io/client-go/rest" +) + +var Module = fx.Module("chaos", + fx.Invoke(Initialize), +) + +func Initialize(restConfig *rest.Config) { + chaosCli.InitWithConfig(restConfig) +} diff --git a/src/infra/config/module.go b/src/infra/config/module.go new file mode 100644 index 00000000..666b8bdb --- /dev/null +++ b/src/infra/config/module.go @@ -0,0 +1,19 @@ +package configinfra + +import ( + "aegis/config" + + "go.uber.org/fx" +) + +type Params struct { + Path string +} + +var Module = fx.Module("config", + fx.Invoke(Init), +) + +func Init(params Params) { + config.Init(params.Path) +} diff --git a/src/infra/db/config.go b/src/infra/db/config.go new file mode 100644 index 00000000..05db6ccb --- /dev/null +++ b/src/infra/db/config.go @@ -0,0 +1,38 @@ +package dbinfra + +import ( + "fmt" + + "aegis/config" +) + +type DatabaseConfig struct { + Type string + Host string + Port int + User string + Password string + Database string + Timezone string +} + +func NewDatabaseConfig(databaseType string) *DatabaseConfig { + return &DatabaseConfig{ + Type: databaseType, + Host: config.GetString(fmt.Sprintf("database.%s.host", databaseType)), + Port: config.GetInt(fmt.Sprintf("database.%s.port", databaseType)), + User: config.GetString(fmt.Sprintf("database.%s.user", databaseType)), + Password: config.GetString(fmt.Sprintf("database.%s.password", databaseType)), + Database: config.GetString(fmt.Sprintf("database.%s.db", databaseType)), + Timezone: config.GetString(fmt.Sprintf("database.%s.timezone", databaseType)), + } +} + +func (d *DatabaseConfig) ToDSN() (string, error) { + if d.Type != "mysql" { + return "", fmt.Errorf("unsupported database type: %s", d.Type) + } + + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + d.User, d.Password, d.Host, d.Port, d.Database), nil +} diff --git a/src/infra/db/migration.go b/src/infra/db/migration.go new file mode 100644 index 00000000..f8dc4da3 --- /dev/null +++ b/src/infra/db/migration.go @@ -0,0 +1,125 @@ +package dbinfra + +import ( + "aegis/model" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func migrate(db *gorm.DB) { + if err := db.AutoMigrate( + &model.Container{}, + &model.ContainerVersion{}, + &model.HelmConfig{}, + &model.ParameterConfig{}, + &model.Dataset{}, + &model.DatasetVersion{}, + &model.Project{}, + &model.Label{}, + &model.User{}, + &model.UserAccessKey{}, + &model.Role{}, + &model.Permission{}, + &model.Resource{}, + &model.AuditLog{}, + &model.Task{}, + &model.FaultInjection{}, + &model.Execution{}, + &model.DetectorResult{}, + &model.GranularityResult{}, + &model.ContainerLabel{}, + &model.DatasetLabel{}, + &model.ProjectLabel{}, + &model.ContainerVersionEnvVar{}, + &model.HelmConfigValue{}, + &model.DatasetVersionInjection{}, + &model.FaultInjectionLabel{}, + &model.ExecutionInjectionLabel{}, + &model.ConfigLabel{}, + &model.UserContainer{}, + &model.UserDataset{}, + &model.UserProject{}, + &model.UserRole{}, + &model.RolePermission{}, + &model.UserPermission{}, + &model.UserTeam{}, + &model.DynamicConfig{}, + &model.ConfigHistory{}, + &model.Evaluation{}, + &model.System{}, + &model.SystemMetadata{}, + ); err != nil { + logrus.Fatalf("Failed to migrate database: %v", err) + } + + createDetectorViews(db) +} + +func addDetectorJoins(query *gorm.DB) *gorm.DB { + return query. + Joins(`JOIN ( + SELECT + e.id, + c.id AS algorithm_id, + e.datapack_id, + ROW_NUMBER() OVER ( + PARTITION BY c.id, e.datapack_id + ORDER BY e.created_at DESC, e.id DESC + ) as rn + FROM executions e + JOIN container_versions cv ON e.algorithm_version_id = cv.id + JOIN containers c ON c.id = cv.container_id + WHERE e.state = 2 AND e.status = 1 AND c.id = ? + ) er_ranked ON fi.id = er_ranked.datapack_id AND er_ranked.rn = 1`, 1). + Joins("JOIN detector_results dr ON er_ranked.id = dr.execution_id") +} + +func createDetectorViews(db *gorm.DB) { + _ = db.Migrator().DropView("fault_injection_no_issues") + _ = db.Migrator().DropView("fault_injection_with_issues") + + noIssuesQuery := addDetectorJoins(db.Table("fault_injections fi"). + Select(`DISTINCT + fi.id AS datapack_id, + fi.name AS name, + fi.fault_type AS fault_type, + fi.category AS category, + fi.engine_config AS engine_config, + l.label_key as label_key, + l.label_value as label_value, + fi.created_at`). + Joins("LEFT JOIN fault_injection_labels fil ON fil.fault_injection_id = fi.id"). + Joins("LEFT JOIN labels l ON fil.label_id = l.id"). + Group("fi.id, fi.name, fi.fault_type, fi.engine_config, fi.created_at, l.label_key, l.label_value"), + ).Where("dr.issues = '{}' OR dr.issues IS NULL") + if err := db.Migrator().CreateView("fault_injection_no_issues", gorm.ViewOption{Query: noIssuesQuery}); err != nil { + logrus.Errorf("failed to create fault_injection_no_issues view: %v", err) + } + + withIssuesQuery := addDetectorJoins(db.Table("fault_injections fi"). + Select(`DISTINCT + fi.id AS datapack_id, + fi.name AS name, + fi.fault_type AS fault_type, + fi.category AS category, + fi.engine_config AS engine_config, + l.label_key as label_key, + l.label_value as label_value, + fi.created_at, + dr.issues, + dr.abnormal_avg_duration, + dr.normal_avg_duration, + dr.abnormal_succ_rate, + dr.normal_succ_rate, + dr.abnormal_p99, + dr.normal_p99`). + Joins("LEFT JOIN tasks t ON t.id = fi.task_id"). + Joins("LEFT JOIN fault_injection_labels fil ON fil.fault_injection_id = fi.id"). + Joins("LEFT JOIN labels l ON fil.label_id = l.id"). + Group("fi.id, fi.name, fi.fault_type, fi.engine_config, fi.created_at, l.label_key, l.label_value, dr.issues, dr.abnormal_avg_duration, dr.normal_avg_duration, dr.abnormal_succ_rate, dr.normal_succ_rate, dr.abnormal_p99, dr.normal_p99"), + ).Where("dr.issues != '{}' AND dr.issues IS NOT NULL") + if err := db.Migrator().CreateView("fault_injection_with_issues", gorm.ViewOption{Query: withIssuesQuery}); err != nil { + logrus.Errorf("failed to create fault_injection_with_issues view: %v", err) + } +} diff --git a/src/infra/db/module.go b/src/infra/db/module.go new file mode 100644 index 00000000..5a0eaf89 --- /dev/null +++ b/src/infra/db/module.go @@ -0,0 +1,77 @@ +package dbinfra + +import ( + "context" + "log" + "os" + "time" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/plugin/opentelemetry/tracing" +) + +var Module = fx.Module("db", + fx.Provide(NewGormDB), +) + +func NewGormDB(lc fx.Lifecycle) *gorm.DB { + db := connectWithRetry(NewDatabaseConfig("mysql")) + migrate(db) + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + sqlDB, err := db.DB() + if err != nil { + return err + } + logrus.Info("Closing database connection") + return sqlDB.Close() + }, + }) + + return db +} + +func connectWithRetry(dbConfig *DatabaseConfig) *gorm.DB { + const maxRetries = 3 + const retryDelay = 10 * time.Second + + dsn, err := dbConfig.ToDSN() + if err != nil { + logrus.Fatalf("Failed to construct DSN: %v", err) + } + + for i := 0; i <= maxRetries; i++ { + db, openErr := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Warn, + IgnoreRecordNotFoundError: true, + Colorful: true, + }), + TranslateError: true, + }) + if openErr == nil { + logrus.Info("Successfully connected to the database") + if pluginErr := db.Use(tracing.NewPlugin()); pluginErr != nil { + panic(pluginErr) + } + return db + } + + err = openErr + logrus.Errorf("Failed to connect to database (attempt %d/%d): %v", i+1, maxRetries+1, err) + if i < maxRetries { + logrus.Infof("Retrying in %v...", retryDelay) + time.Sleep(retryDelay) + } + } + + logrus.Fatalf("Failed to connect to database after %d attempts: %v", maxRetries+1, err) + return nil +} diff --git a/src/infra/etcd/gateway.go b/src/infra/etcd/gateway.go new file mode 100644 index 00000000..3cf4c3f8 --- /dev/null +++ b/src/infra/etcd/gateway.go @@ -0,0 +1,126 @@ +package etcdinfra + +import ( + "context" + "fmt" + "time" + + "aegis/config" + + "github.com/sirupsen/logrus" + clientv3 "go.etcd.io/etcd/client/v3" + "go.uber.org/fx" +) + +type Gateway struct { + client *clientv3.Client +} + +func NewGateway(client *clientv3.Client) *Gateway { + if client == nil { + client = newClient() + } + return &Gateway{client: client} +} + +func NewGatewayWithLifecycle(lc fx.Lifecycle) *Gateway { + gateway := NewGateway(nil) + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + logrus.Info("Closing etcd client") + return gateway.close() + }, + }) + + return gateway +} + +func (g *Gateway) Put(ctx context.Context, key, value string, ttl time.Duration) error { + client := g.clientOrInit() + if ttl > 0 { + lease, err := client.Grant(ctx, int64(ttl.Seconds())) + if err != nil { + return fmt.Errorf("failed to create lease: %w", err) + } + + if _, err = client.Put(ctx, key, value, clientv3.WithLease(lease.ID)); err != nil { + return fmt.Errorf("failed to put key with lease: %w", err) + } + return nil + } + + if _, err := client.Put(ctx, key, value); err != nil { + return fmt.Errorf("failed to put key: %w", err) + } + return nil +} + +func (g *Gateway) Get(ctx context.Context, key string) (string, error) { + resp, err := g.clientOrInit().Get(ctx, key) + if err != nil { + return "", fmt.Errorf("failed to get key: %w", err) + } + if len(resp.Kvs) == 0 { + return "", fmt.Errorf("key not found: %s", key) + } + return string(resp.Kvs[0].Value), nil +} + +func (g *Gateway) Delete(ctx context.Context, key string) error { + if _, err := g.clientOrInit().Delete(ctx, key); err != nil { + return fmt.Errorf("failed to delete key: %w", err) + } + return nil +} + +func (g *Gateway) Watch(ctx context.Context, key string, withPrefix bool) clientv3.WatchChan { + var opts []clientv3.OpOption + if withPrefix { + opts = append(opts, clientv3.WithPrefix()) + } + return g.clientOrInit().Watch(ctx, key, opts...) +} + +func (g *Gateway) clientOrInit() *clientv3.Client { + if g.client == nil { + g.client = newClient() + } + return g.client +} + +func (g *Gateway) close() error { + if g.client == nil { + return nil + } + return g.client.Close() +} + +func newClient() *clientv3.Client { + endpoints := config.GetStringSlice("etcd.endpoints") + if len(endpoints) == 0 { + endpoints = []string{"localhost:2379"} + logrus.Warn("etcd.endpoints not configured, using default: localhost:2379") + } + + logrus.Infof("Connecting to etcd endpoints: %v", endpoints) + + client, err := clientv3.New(clientv3.Config{ + Endpoints: endpoints, + DialTimeout: 5 * time.Second, + Username: config.GetString("etcd.username"), + Password: config.GetString("etcd.password"), + }) + if err != nil { + logrus.Fatalf("Failed to connect to etcd: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if _, err := client.Status(ctx, endpoints[0]); err != nil { + logrus.Fatalf("Failed to verify etcd connection: %v", err) + } + + logrus.Info("Successfully connected to etcd") + return client +} diff --git a/src/infra/etcd/module.go b/src/infra/etcd/module.go new file mode 100644 index 00000000..4cf0887f --- /dev/null +++ b/src/infra/etcd/module.go @@ -0,0 +1,7 @@ +package etcdinfra + +import "go.uber.org/fx" + +var Module = fx.Module("etcd", + fx.Provide(NewGatewayWithLifecycle), +) diff --git a/src/infra/harbor/gateway.go b/src/infra/harbor/gateway.go new file mode 100644 index 00000000..e9585373 --- /dev/null +++ b/src/infra/harbor/gateway.go @@ -0,0 +1,118 @@ +package harborinfra + +import ( + "context" + "fmt" + "sort" + "time" + + "aegis/config" + "aegis/consts" + + "github.com/goharbor/go-client/pkg/harbor" + "github.com/goharbor/go-client/pkg/sdk/v2.0/client/artifact" + "github.com/goharbor/go-client/pkg/sdk/v2.0/models" +) + +type Gateway struct { + namespace string + clientSet *harbor.ClientSet +} + +func NewGateway() *Gateway { + namespace := config.GetString("harbor.namespace") + return &Gateway{ + namespace: namespace, + clientSet: newClientSet(), + } +} + +func (g *Gateway) GetLatestTag(image string) (string, error) { + if g.clientSet == nil { + return "", fmt.Errorf("harbor client is not initialized") + } + + ctx, cancel := context.WithTimeout(context.Background(), consts.HarborTimeout*consts.HarborTimeUnit) + defer cancel() + + response, err := g.clientSet.V2().Artifact.ListArtifacts(ctx, &artifact.ListArtifactsParams{ + ProjectName: g.namespace, + RepositoryName: image, + Context: ctx, + }) + if err != nil { + return "", fmt.Errorf("failed to list artifacts: %v", err) + } + if len(response.Payload) == 0 { + return "", fmt.Errorf("no artifacts found for image %s", image) + } + + var allTags []*models.Tag + for _, item := range response.Payload { + if item.Tags != nil { + allTags = append(allTags, item.Tags...) + } + } + if len(allTags) == 0 { + return "", fmt.Errorf("no tags found for image %s", image) + } + + sort.Slice(allTags, func(i, j int) bool { + return time.Time(allTags[i].PushTime).After(time.Time(allTags[j].PushTime)) + }) + + return allTags[0].Name, nil +} + +func (g *Gateway) CheckImageExists(repository, tag string) (bool, error) { + if g.clientSet == nil { + return false, fmt.Errorf("harbor client is not initialized") + } + + ctx, cancel := context.WithTimeout(context.Background(), consts.HarborTimeout*consts.HarborTimeUnit) + defer cancel() + + response, err := g.clientSet.V2().Artifact.ListArtifacts(ctx, &artifact.ListArtifactsParams{ + ProjectName: g.namespace, + RepositoryName: repository, + Context: ctx, + }) + if err != nil || len(response.Payload) == 0 { + return false, nil + } + if tag == "" || tag == consts.DefaultContainerTag { + return true, nil + } + + for _, item := range response.Payload { + if item.Tags == nil { + continue + } + for _, currentTag := range item.Tags { + if currentTag.Name == tag { + return true, nil + } + } + } + + return false, nil +} + +func newClientSet() *harbor.ClientSet { + registry := config.GetString("harbor.registry") + username := config.GetString("harbor.username") + password := config.GetString("harbor.password") + harborURL := fmt.Sprintf("http://%s", registry) + + clientSet, err := harbor.NewClientSet(&harbor.ClientSetConfig{ + URL: harborURL, + Username: username, + Password: password, + Insecure: true, + }) + if err != nil { + return nil + } + + return clientSet +} diff --git a/src/infra/harbor/module.go b/src/infra/harbor/module.go new file mode 100644 index 00000000..febae93c --- /dev/null +++ b/src/infra/harbor/module.go @@ -0,0 +1,7 @@ +package harborinfra + +import "go.uber.org/fx" + +var Module = fx.Module("harbor", + fx.Provide(NewGateway), +) diff --git a/src/infra/helm/gateway.go b/src/infra/helm/gateway.go new file mode 100644 index 00000000..126450ca --- /dev/null +++ b/src/infra/helm/gateway.go @@ -0,0 +1,262 @@ +package helminfra + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + "aegis/config" + "aegis/tracing" + + "github.com/sirupsen/logrus" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/yaml" +) + +type Gateway struct{} + +func NewGateway() *Gateway { + return &Gateway{} +} + +func (g *Gateway) AddRepo(namespace, name, url string) error { + settings, _, err := newRuntime(namespace) + if err != nil { + return err + } + + repoFile := settings.RepositoryConfig + if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil && !os.IsExist(err) { + return fmt.Errorf("could not create repository cache directory: %w", err) + } + + data, err := os.ReadFile(repoFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("could not read repository file: %w", err) + } + + var repoFileModel repo.File + if err == nil { + if err := yaml.Unmarshal(data, &repoFileModel); err != nil { + return fmt.Errorf("cannot unmarshal repository file: %w", err) + } + } + + if repoFileModel.Has(name) { + if repoFileModel.Get(name).URL != url { + repoFileModel.Get(name).URL = url + } + if err := repoFileModel.WriteFile(repoFile, 0644); err != nil { + return fmt.Errorf("failed to write repository file: %w", err) + } + logrus.Infof("Updated repository %s URL to %s", name, url) + return nil + } + + entry := &repo.Entry{Name: name, URL: url} + repository, err := repo.NewChartRepository(entry, getter.All(settings)) + if err != nil { + return fmt.Errorf("failed to create chart repository: %w", err) + } + if _, err := repository.DownloadIndexFile(); err != nil { + return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached: %w", url, err) + } + + repoFileModel.Update(entry) + if err := repoFileModel.WriteFile(repoFile, 0644); err != nil { + return fmt.Errorf("failed to write repository file: %w", err) + } + + return nil +} + +func (g *Gateway) Install(ctx context.Context, namespace, releaseName, chartName, version string, values map[string]any, installTimeout, uninstallTimeout time.Duration) error { + settings, actionConfig, err := newRuntime(namespace) + if err != nil { + return err + } + + installed, err := g.isReleaseInstalled(actionConfig, releaseName) + if err != nil { + return err + } + if installed { + logrus.Infof("Uninstalling existing %s release", releaseName) + if err := g.uninstallRelease(actionConfig, releaseName, uninstallTimeout); err != nil { + return err + } + } else { + logrus.Infof("No existing %s release found", releaseName) + } + + return g.installRelease(ctx, settings, actionConfig, namespace, releaseName, chartName, version, values, installTimeout) +} + +func (g *Gateway) UpdateRepo(namespace, name string) error { + settings, _, err := newRuntime(namespace) + if err != nil { + return err + } + + data, err := os.ReadFile(settings.RepositoryConfig) + if err != nil { + return fmt.Errorf("could not read repository file: %w", err) + } + + var repoFileModel repo.File + if err := yaml.Unmarshal(data, &repoFileModel); err != nil { + return fmt.Errorf("cannot unmarshal repository file: %w", err) + } + + for _, entry := range repoFileModel.Repositories { + if name != "" && name != entry.Name { + continue + } + logrus.Infof("Updating repository %s", entry.Name) + repository, err := repo.NewChartRepository(entry, getter.All(settings)) + if err != nil { + return fmt.Errorf("failed to create chart repository for %s: %w", entry.Name, err) + } + if _, err := repository.DownloadIndexFile(); err != nil { + return fmt.Errorf("failed to update repository %s: %w", entry.Name, err) + } + } + + return nil +} + +func (g *Gateway) installRelease(ctx context.Context, settings *cli.EnvSettings, actionConfig *action.Configuration, namespace, releaseName, chartName, version string, vals map[string]any, timeout time.Duration) error { + return tracing.WithSpan(ctx, func(ctx context.Context) error { + now := time.Now() + defer func() { + log.Printf("InstallRelease took %s", time.Since(now)) + }() + + installAction := action.NewInstall(actionConfig) + installAction.ReleaseName = releaseName + installAction.Namespace = namespace + installAction.Wait = true + installAction.Timeout = timeout + installAction.CreateNamespace = true + installAction.Version = version + + chartPath, err := findCachedChart(settings, chartName) + if err != nil { + return err + } + if chartPath == "" { + logrus.Infof("Chart %s not found in cache, downloading...", chartName) + chartPath, err = installAction.LocateChart(chartName, settings) + if err != nil { + return fmt.Errorf("failed to locate chart %s: %w", chartName, err) + } + } else { + logrus.Infof("Using cached chart for %s at %s", chartName, chartPath) + } + + chart, err := loader.Load(chartPath) + if err != nil { + return fmt.Errorf("failed to load chart %s: %w", chartName, err) + } + if _, err := installAction.Run(chart, vals); err != nil { + return fmt.Errorf("failed to install release %s: %v", releaseName, err) + } + return nil + }) +} + +func (g *Gateway) isReleaseInstalled(actionConfig *action.Configuration, releaseName string) (bool, error) { + statusAction := action.NewStatus(actionConfig) + _, err := statusAction.Run(releaseName) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return false, nil + } + return false, fmt.Errorf("failed to get release status: %w", err) + } + return true, nil +} + +func (g *Gateway) uninstallRelease(actionConfig *action.Configuration, releaseName string, timeout time.Duration) error { + uninstallAction := action.NewUninstall(actionConfig) + uninstallAction.Wait = true + uninstallAction.Timeout = timeout + + _, err := uninstallAction.Run(releaseName) + if err != nil { + if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "release: not found") { + logrus.Infof("Release %s is not installed, nothing to uninstall", releaseName) + return nil + } + return fmt.Errorf("failed to uninstall release %s: %w", releaseName, err) + } + return nil +} + +func newRuntime(namespace string) (*cli.EnvSettings, *action.Configuration, error) { + settings := cli.New() + settings.SetNamespace(namespace) + settings.Debug = config.GetBool("helm.debug") + + actionConfig := new(action.Configuration) + configFlags := genericclioptions.NewConfigFlags(true) + configFlags.Namespace = &namespace + if err := actionConfig.Init(configFlags, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { + return nil, nil, fmt.Errorf("failed to initialize Helm action configuration: %w", err) + } + + return settings, actionConfig, nil +} + +func findCachedChart(settings *cli.EnvSettings, chartName string) (string, error) { + if _, err := os.Stat(chartName); err == nil { + abs, err := filepath.Abs(chartName) + if err == nil { + logrus.Infof("Found local chart at: %s", abs) + return abs, nil + } + } + + cacheDir := settings.RepositoryCache + var searchPatterns []string + if strings.Contains(chartName, "/") { + parts := strings.Split(chartName, "/") + if len(parts) == 2 { + chartBaseName := parts[1] + searchPatterns = append(searchPatterns, + fmt.Sprintf("%s/*/%s-*.tgz", cacheDir, chartBaseName), + fmt.Sprintf("%s/%s-*.tgz", cacheDir, chartBaseName), + ) + } + } else { + searchPatterns = append(searchPatterns, + fmt.Sprintf("%s/*/%s-*.tgz", cacheDir, chartName), + fmt.Sprintf("%s/%s-*.tgz", cacheDir, chartName), + ) + } + + for _, pattern := range searchPatterns { + matches, err := filepath.Glob(pattern) + if err == nil && len(matches) > 0 { + logrus.Infof("Found cached chart at: %s", matches[0]) + return matches[0], nil + } + } + + localChartDir := filepath.Join(cacheDir, chartName) + if stat, err := os.Stat(localChartDir); err == nil && stat.IsDir() { + logrus.Infof("Found cached chart directory at: %s", localChartDir) + return localChartDir, nil + } + + return "", nil +} diff --git a/src/infra/helm/module.go b/src/infra/helm/module.go new file mode 100644 index 00000000..82b71e17 --- /dev/null +++ b/src/infra/helm/module.go @@ -0,0 +1,7 @@ +package helminfra + +import "go.uber.org/fx" + +var Module = fx.Module("helm", + fx.Provide(NewGateway), +) diff --git a/src/client/k8s/controller.go b/src/infra/k8s/controller.go similarity index 98% rename from src/client/k8s/controller.go rename to src/infra/k8s/controller.go index 6322f1ca..c0ea0f90 100644 --- a/src/client/k8s/controller.go +++ b/src/infra/k8s/controller.go @@ -1,4 +1,4 @@ -package k8s +package k8sinfra import ( "context" @@ -77,7 +77,7 @@ type Controller struct { cancelFunc context.CancelFunc } -func NewController() *Controller { +func newController() *Controller { crdInformers := make(map[string]map[schema.GroupVersionResource]cache.SharedIndexInformer) activeNamespaces := make(map[string]bool) @@ -86,7 +86,7 @@ func NewController() *Controller { } platformFactory := informers.NewSharedInformerFactoryWithOptions( - GetK8sClient(), + getK8sClient(), resyncPeriod, informers.WithNamespace(config.GetString("k8s.namespace")), informers.WithTweakListOptions(tweakListOptions), @@ -168,7 +168,7 @@ func (c *Controller) AddNamespaceInformers(namespaces []string) error { // Create new factory for this namespace logrus.Debugf("Creating new CRD informers for namespace: %s", namespace) chaosFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory( - GetK8sDynamicClient(), + getK8sDynamicClient(), resyncPeriod, namespace, tweakListOptions, @@ -534,7 +534,7 @@ func (c *Controller) genPodEventHandlerFuncs() cache.ResourceEventHandlerFuncs { for _, reason := range podReasons { if checkPodReason(newPod, reason) { - job, err := GetJob(c.ctx, newPod.Namespace, jobOwnerRef.Name) + job, err := getJob(c.ctx, newPod.Namespace, jobOwnerRef.Name) if err != nil { logrus.WithField("job_name", jobOwnerRef.Name).Error(err) } @@ -626,7 +626,7 @@ func (c *Controller) checkRecoveryStatus(item QueueItem) error { "name": item.Name, }) - obj, err := GetK8sDynamicClient(). + obj, err := getK8sDynamicClient(). Resource(*item.GVR). Namespace(item.Namespace). Get(context.Background(), item.Name, metav1.GetOptions{}) @@ -810,7 +810,7 @@ func checkPodReason(pod *corev1.Pod, reason string) bool { func handlePodError(ctx context.Context, pod *corev1.Pod, job *batchv1.Job, reason string) { // Get Pod events - events, err := GetK8sClient().CoreV1().Events(pod.Namespace).List(ctx, metav1.ListOptions{ + events, err := getK8sClient().CoreV1().Events(pod.Namespace).List(ctx, metav1.ListOptions{ FieldSelector: fmt.Sprintf("involvedObject.name=%s", pod.Name), }) if err != nil { diff --git a/src/client/k8s/crd.go b/src/infra/k8s/crd.go similarity index 84% rename from src/client/k8s/crd.go rename to src/infra/k8s/crd.go index 15fee929..22a88bea 100644 --- a/src/client/k8s/crd.go +++ b/src/infra/k8s/crd.go @@ -1,4 +1,4 @@ -package k8s +package k8sinfra import ( "context" @@ -27,7 +27,7 @@ func deleteCRD(ctx context.Context, gvr *schema.GroupVersionResource, namespace, }) // 1. Check if resource exists - obj, err := k8sDynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + obj, err := getK8sDynamicClient().Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -42,7 +42,7 @@ func deleteCRD(ctx context.Context, gvr *schema.GroupVersionResource, namespace, } // 3. Execute deletion (idempotent operation) - _, err = k8sDynamicClient.Resource(*gvr).Namespace(namespace).Patch( + _, err = getK8sDynamicClient().Resource(*gvr).Namespace(namespace).Patch( timeoutCtx, name, types.MergePatchType, @@ -59,7 +59,7 @@ func deleteCRD(ctx context.Context, gvr *schema.GroupVersionResource, namespace, logEntry.Info("Successfully cleared finalizers") - err = k8sDynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, deleteOptions) + err = getK8sDynamicClient().Resource(*gvr).Namespace(namespace).Delete(ctx, name, deleteOptions) if err != nil && !errors.IsNotFound(err) { if timeoutCtx.Err() != nil { return fmt.Errorf("timeout while deleting CRD %s/%s: %v", namespace, name, timeoutCtx.Err()) diff --git a/src/infra/k8s/gateway.go b/src/infra/k8s/gateway.go new file mode 100644 index 00000000..ec0b3fd4 --- /dev/null +++ b/src/infra/k8s/gateway.go @@ -0,0 +1,142 @@ +package k8sinfra + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + "aegis/consts" + + "github.com/sirupsen/logrus" + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type Gateway struct { + controller *Controller +} + +var ( + k8sRestConfig *rest.Config + k8sClient *kubernetes.Clientset + k8sDynamicClient *dynamic.DynamicClient + k8sController *Controller + + k8sRestConfigOnce sync.Once + k8sClientOnce sync.Once + k8sDynamicClientOnce sync.Once + controllerOnce sync.Once +) + +func NewGateway(controller *Controller) *Gateway { + if controller == nil { + controller = getK8sController() + } + return &Gateway{controller: controller} +} + +func (g *Gateway) GetVolumeMountConfigMap() (map[consts.VolumeMountName]VolumeMountConfig, error) { + return getVolumeMountConfigMap() +} + +func (g *Gateway) CreateJob(ctx context.Context, jobConfig *JobConfig) error { + return createJob(ctx, jobConfig) +} + +func (g *Gateway) GetJob(ctx context.Context, namespace, jobName string) (*batchv1.Job, error) { + return getJob(ctx, namespace, jobName) +} + +func (g *Gateway) WaitForJobCompletion(ctx context.Context, namespace, jobName string) error { + return waitForJobCompletion(ctx, namespace, jobName) +} + +func (g *Gateway) GetJobPodLogs(ctx context.Context, namespace, jobName string) (map[string][]string, error) { + return getJobPodLogs(ctx, namespace, jobName) +} + +func (g *Gateway) DeleteJob(ctx context.Context, namespace, jobName string) error { + return deleteJob(ctx, namespace, jobName) +} + +func (g *Gateway) CheckHealth(ctx context.Context) error { + if getK8sRestConfig() == nil { + return fmt.Errorf("kubernetes config not available") + } + client := getK8sClient() + if client == nil { + return fmt.Errorf("kubernetes client not available") + } + if getK8sDynamicClient() == nil { + return fmt.Errorf("kubernetes dynamic client not available") + } + + if _, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{Limit: 1}); err != nil { + return fmt.Errorf("kubernetes API request failed: %w", err) + } + return nil +} + +func getK8sClient() *kubernetes.Clientset { + k8sClientOnce.Do(func() { + restConfig := getK8sRestConfig() + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + logrus.Fatalf("failed to create Kubernetes clientset: %v", err) + } + + k8sClient = clientset + }) + return k8sClient +} + +func getK8sDynamicClient() *dynamic.DynamicClient { + k8sDynamicClientOnce.Do(func() { + restConfig := getK8sRestConfig() + dynamicClient, err := dynamic.NewForConfig(restConfig) + if err != nil { + logrus.Fatalf("failed to create Kubernetes dynamic client: %v", err) + } + + k8sDynamicClient = dynamicClient + }) + return k8sDynamicClient +} + +func getK8sRestConfig() *rest.Config { + k8sRestConfigOnce.Do(func() { + restConfig, err := rest.InClusterConfig() + if err == nil { + logrus.Info("Successfully loaded In-Cluster Kubernetes configuration.") + k8sRestConfig = restConfig + logrus.Infof("Using Kubernetes Context: %s", "In-Cluster") + return + } + + logrus.Warn("In-cluster config not found, trying kubeconfig file") + kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + logrus.Fatalf("Failed to load Kubernetes config: %v", err) + } + if config == nil { + logrus.Fatalf("Failed to establish Kubernetes REST config: Neither In-Cluster nor external Kubeconfig available.") + } + + k8sRestConfig = config + }) + return k8sRestConfig +} + +func getK8sController() *Controller { + controllerOnce.Do(func() { + k8sController = newController() + }) + return k8sController +} diff --git a/src/client/k8s/job.go b/src/infra/k8s/job.go similarity index 80% rename from src/client/k8s/job.go rename to src/infra/k8s/job.go index 1c9265cf..772c4796 100644 --- a/src/client/k8s/job.go +++ b/src/infra/k8s/job.go @@ -1,4 +1,4 @@ -package k8s +package k8sinfra import ( "bufio" @@ -113,15 +113,31 @@ func (v *VolumeMountConfig) GetVolume() corev1.Volume { return volume } -func CreateJob(ctx context.Context, jobConfig *JobConfig) error { +func createJob(ctx context.Context, jobConfig *JobConfig) error { return tracing.WithSpan(ctx, func(ctx context.Context) error { span := trace.SpanFromContext(ctx) - jobConfig.Namespace = config.GetString("k8s.namespace") - jobConfig.BackoffLimit = int32(0) - jobConfig.Parallelism = int32(1) - jobConfig.Completions = int32(1) - jobConfig.RestartPolicy = corev1.RestartPolicyNever + if jobConfig.Namespace == "" { + jobConfig.Namespace = config.GetString("k8s.namespace") + } + if jobConfig.BackoffLimit == 0 { + jobConfig.BackoffLimit = int32(0) + } + if jobConfig.Parallelism == 0 { + jobConfig.Parallelism = int32(1) + } + if jobConfig.Completions == 0 { + jobConfig.Completions = int32(1) + } + if jobConfig.RestartPolicy == "" { + jobConfig.RestartPolicy = corev1.RestartPolicyNever + } + if jobConfig.Annotations == nil { + jobConfig.Annotations = make(map[string]string) + } + if jobConfig.Labels == nil { + jobConfig.Labels = make(map[string]string) + } volumeMounts := []corev1.VolumeMount{} volumes := []corev1.Volume{} @@ -174,7 +190,7 @@ func CreateJob(ctx context.Context, jobConfig *JobConfig) error { }, } - _, err := k8sClient.BatchV1().Jobs(jobConfig.Namespace).Create(ctx, job, metav1.CreateOptions{}) + _, err := getK8sClient().BatchV1().Jobs(jobConfig.Namespace).Create(ctx, job, metav1.CreateOptions{}) if err != nil { span.RecordError(err) span.AddEvent("failed to create job") @@ -186,7 +202,7 @@ func CreateJob(ctx context.Context, jobConfig *JobConfig) error { } // GetVolumeMountConfigMap retrieves volume mount configurations from the application config. -func GetVolumeMountConfigMap() (map[consts.VolumeMountName]VolumeMountConfig, error) { +func getVolumeMountConfigMap() (map[consts.VolumeMountName]VolumeMountConfig, error) { volumeMountConfigMapOnce.Do(func() { cfgMap := config.GetMap("k8s.job.volume_mount") if len(cfgMap) == 0 { @@ -226,7 +242,7 @@ func deleteJob(ctx context.Context, namespace, name string) error { logEntry := logrus.WithField("namespace", namespace).WithField("name", name) // 1. First check if Job exists and its status - job, err := k8sClient.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) + job, err := getK8sClient().BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { return nil @@ -241,7 +257,7 @@ func deleteJob(ctx context.Context, namespace, name string) error { } // 3. Execute deletion (idempotent operation) - err = k8sClient.BatchV1().Jobs(namespace).Delete(ctx, name, deleteOptions) + err = getK8sClient().BatchV1().Jobs(namespace).Delete(ctx, name, deleteOptions) if err != nil { if errors.IsNotFound(err) { return nil @@ -253,16 +269,16 @@ func deleteJob(ctx context.Context, namespace, name string) error { return nil } -func GetJob(ctx context.Context, namespace, jobName string) (*batchv1.Job, error) { - job, err := k8sClient.BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{}) +func getJob(ctx context.Context, namespace, jobName string) (*batchv1.Job, error) { + job, err := getK8sClient().BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{}) if err != nil { return nil, fmt.Errorf("failed to get job: %v", err) } return job, nil } -func GetJobPodLogs(ctx context.Context, namespace, jobName string) (map[string][]string, error) { - podList, err := k8sClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ +func getJobPodLogs(ctx context.Context, namespace, jobName string) (map[string][]string, error) { + podList, err := getK8sClient().CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", consts.JobLabelName, jobName), }) if err != nil { @@ -279,7 +295,7 @@ func GetJobPodLogs(ctx context.Context, namespace, jobName string) (map[string][ continue } - req := k8sClient.CoreV1().Pods(namespace).GetLogs(pod.Name, &corev1.PodLogOptions{}) + req := getK8sClient().CoreV1().Pods(namespace).GetLogs(pod.Name, &corev1.PodLogOptions{}) logStream, err := req.Stream(ctx) if err != nil { return nil, fmt.Errorf("failed to get logs for pod %s: %v", pod.Name, err) @@ -322,20 +338,32 @@ func isPodReadyForLogs(pod corev1.Pod) bool { } } -func WaitForJobCompletion(ctx context.Context, namespace, jobName string) error { +func waitForJobCompletion(ctx context.Context, namespace, jobName string) error { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { - job, err := k8sClient.BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{}) + job, err := getK8sClient().BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get job: %v", err) } if job.Status.Succeeded > 0 { logrus.Info("Job completed successfully!") - break + return nil + } + + for _, condition := range job.Status.Conditions { + if condition.Type == batchv1.JobFailed && condition.Status == corev1.ConditionTrue { + return fmt.Errorf("job %s failed: %s", jobName, condition.Message) + } } logrus.Info("Waiting for job to complete...") - time.Sleep(2 * time.Second) + select { + case <-ctx.Done(): + return fmt.Errorf("waiting for job completion: %w", ctx.Err()) + case <-ticker.C: + } } - return nil } diff --git a/src/infra/k8s/k8s_test.go b/src/infra/k8s/k8s_test.go new file mode 100644 index 00000000..afb1f097 --- /dev/null +++ b/src/infra/k8s/k8s_test.go @@ -0,0 +1,186 @@ +package k8sinfra + +import ( + "aegis/config" + "aegis/utils" + "context" + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/k0kubun/pp/v3" + corev1 "k8s.io/api/core/v1" +) + +const ( + runK8sIntegrationEnv = "RUN_K8S_INTEGRATION" + runK8sIntegrationNamespaceEnv = "RUN_K8S_INTEGRATION_NAMESPACE" + runK8sIntegrationImageEnv = "RUN_K8S_INTEGRATION_IMAGE" + runK8sIntegrationKeepJobEnv = "RUN_K8S_INTEGRATION_KEEP_JOB" +) + +type integrationConfig struct { + namespace string + image string + keepJob bool +} + +func TestGetVolumeMountConfigs(t *testing.T) { + config.Init("../..") + + volumeMountConfigs := make([]VolumeMountConfig, 0) + mapData := config.GetMap("k8s.job.volume_mount") + for _, cfgData := range mapData { + cfg, err := utils.ConvertToType[VolumeMountConfig](cfgData) + if err != nil { + t.Errorf("invalid volume mount config %v: %v", cfgData, err) + } + + volumeMountConfigs = append(volumeMountConfigs, cfg) + } + + volumeMounts := []corev1.VolumeMount{} + volumes := []corev1.Volume{} + for _, cfg := range volumeMountConfigs { + volumeMounts = append(volumeMounts, cfg.GetVolumeMount()) + volumes = append(volumes, cfg.GetVolume()) + } + + pp.Println(volumeMountConfigs) //nolint:errcheck + pp.Println(volumeMounts) //nolint:errcheck + pp.Println(volumes) //nolint:errcheck +} + +func requireIntegrationConfig(t *testing.T) integrationConfig { + t.Helper() + + if os.Getenv(runK8sIntegrationEnv) != "1" { + t.Skipf( + "set %s=1 to run Kubernetes integration test (optional overrides: %s, %s, %s)", + runK8sIntegrationEnv, + runK8sIntegrationNamespaceEnv, + runK8sIntegrationImageEnv, + runK8sIntegrationKeepJobEnv, + ) + } + + config.Init("../..") + + namespace := config.GetString("k8s.namespace") + if override := strings.TrimSpace(os.Getenv(runK8sIntegrationNamespaceEnv)); override != "" { + namespace = override + } + if namespace == "" { + t.Fatal("kubernetes integration namespace is empty; set k8s.namespace or RUN_K8S_INTEGRATION_NAMESPACE") + } + + image := strings.TrimSpace(os.Getenv(runK8sIntegrationImageEnv)) + if image == "" { + image = "busybox:1.36" + } + + return integrationConfig{ + namespace: namespace, + image: image, + keepJob: os.Getenv(runK8sIntegrationKeepJobEnv) == "1", + } +} + +func TestK8sGatewayJobLifecycleIntegration(t *testing.T) { + cfg := requireIntegrationConfig(t) + + gateway := NewGateway(nil) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + if err := gateway.CheckHealth(ctx); err != nil { + t.Fatalf("kubernetes health precheck failed: %v", err) + } + + jobName := fmt.Sprintf("aegis-k8s-integration-%d", time.Now().UnixNano()) + command := []string{"sh", "-c", "for i in $(seq 1 5); do echo \"Log line $i\"; sleep 1; done"} + restartPolicy := corev1.RestartPolicyNever + backoffLimit := int32(1) + parallelism := int32(1) + completions := int32(1) + + envVars := []corev1.EnvVar{ + {Name: "AEGIS_K8S_INTEGRATION", Value: "true"}, + } + + if !cfg.keepJob { + t.Cleanup(func() { + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cleanupCancel() + _ = gateway.DeleteJob(cleanupCtx, cfg.namespace, jobName) + }) + } + + t.Logf("running Kubernetes integration against namespace=%s image=%s", cfg.namespace, cfg.image) + + if err := gateway.CreateJob(ctx, &JobConfig{ + Namespace: cfg.namespace, + JobName: jobName, + Image: cfg.image, + Command: command, + RestartPolicy: restartPolicy, + BackoffLimit: backoffLimit, + Parallelism: parallelism, + Completions: completions, + EnvVars: envVars, + }); err != nil { + t.Fatalf("create job failed: %v", err) + } + t.Logf("job %s created successfully", jobName) + + job, err := gateway.GetJob(ctx, cfg.namespace, jobName) + if err != nil { + t.Fatalf("get job failed: %v", err) + } + if job.Name != jobName { + t.Errorf("expected job name %s, got %s", jobName, job.Name) + } + + t.Logf("waiting for job %s to complete", jobName) + if err := gateway.WaitForJobCompletion(ctx, cfg.namespace, jobName); err != nil { + t.Fatalf("wait for job completion failed: %v", err) + } + t.Logf("job %s completed successfully", jobName) + + logs, err := gateway.GetJobPodLogs(ctx, cfg.namespace, jobName) + if err != nil { + t.Fatalf("get job pod logs failed: %v", err) + } + if len(logs) == 0 { + t.Fatalf("expected logs for job %s, got none", jobName) + } + + foundLogLine := false + for podName, podLogs := range logs { + t.Logf("pod %s emitted %d log lines", podName, len(podLogs)) + for _, line := range podLogs { + if strings.Contains(line, "Log line") { + foundLogLine = true + break + } + } + if foundLogLine { + break + } + } + if !foundLogLine { + t.Fatalf("expected job logs to include test output, got %#v", logs) + } + + if cfg.keepJob { + t.Logf("keeping job %s because %s=1", jobName, runK8sIntegrationKeepJobEnv) + return + } + + if err := gateway.DeleteJob(ctx, cfg.namespace, jobName); err != nil { + t.Fatalf("delete job failed: %v", err) + } + t.Logf("job %s and its associated pods deleted successfully", jobName) +} diff --git a/src/infra/k8s/module.go b/src/infra/k8s/module.go new file mode 100644 index 00000000..661ee6ab --- /dev/null +++ b/src/infra/k8s/module.go @@ -0,0 +1,21 @@ +package k8sinfra + +import ( + "k8s.io/client-go/rest" + + "go.uber.org/fx" +) + +var Module = fx.Module("k8s", + fx.Provide(ProvideController), + fx.Provide(NewGateway), + fx.Provide(ProvideRestConfig), +) + +func ProvideController() *Controller { + return getK8sController() +} + +func ProvideRestConfig() *rest.Config { + return getK8sRestConfig() +} diff --git a/src/infra/logger/module.go b/src/infra/logger/module.go new file mode 100644 index 00000000..673a01d1 --- /dev/null +++ b/src/infra/logger/module.go @@ -0,0 +1,37 @@ +package loggerinfra + +import ( + "fmt" + "path" + "runtime" + "sync" + + nested "github.com/antonfisher/nested-logrus-formatter" + "github.com/sirupsen/logrus" + "go.uber.org/fx" +) + +var ( + configureOnce sync.Once + + Module = fx.Module("logger", + fx.Invoke(Configure), + ) +) + +func Configure() { + configureOnce.Do(func() { + logrus.SetReportCaller(true) + logrus.SetFormatter(&nested.Formatter{ + CustomCallerFormatter: func(f *runtime.Frame) string { + filename := path.Base(f.File) + return fmt.Sprintf(" (%s:%d)", filename, f.Line) + }, + FieldsOrder: []string{"component", "category"}, + HideKeys: true, + TimestampFormat: "2006-01-02 15:04:05", + }) + logrus.SetLevel(logrus.InfoLevel) + logrus.Info("Logger initialized") + }) +} diff --git a/src/client/loki.go b/src/infra/loki/client.go similarity index 69% rename from src/client/loki.go rename to src/infra/loki/client.go index 51f9e565..27799ba9 100644 --- a/src/client/loki.go +++ b/src/infra/loki/client.go @@ -1,4 +1,4 @@ -package client +package lokiinfra import ( "context" @@ -16,34 +16,30 @@ import ( "github.com/sirupsen/logrus" ) -// LokiClient wraps the Loki HTTP API for querying historical logs -type LokiClient struct { +type Client struct { address string httpClient *http.Client } -// QueryOpts defines options for Loki log queries type QueryOpts struct { - Start time.Time // Query start time (default: 1 hour ago) - End time.Time // Query end time (default: now) - Limit int // Max entries to return (default: 5000) - Direction string // "forward" (chronological) or "backward" + Start time.Time + End time.Time + Limit int + Direction string } -// lokiQueryRangeResponse represents the Loki query_range API response -type lokiQueryRangeResponse struct { +type queryRangeResponse struct { Status string `json:"status"` Data struct { ResultType string `json:"resultType"` Result []struct { Stream map[string]string `json:"stream"` - Values [][]string `json:"values"` // [[nanosecond_timestamp, log_line], ...] + Values [][]string `json:"values"` } `json:"result"` } `json:"data"` } -// NewLokiClient creates a new Loki client using configuration -func NewLokiClient() *LokiClient { +func NewClient() *Client { address := config.GetString("loki.address") timeout := config.GetString("loki.timeout") timeoutDuration := 10 * time.Second @@ -53,7 +49,7 @@ func NewLokiClient() *LokiClient { } } - return &LokiClient{ + return &Client{ address: address, httpClient: &http.Client{ Timeout: timeoutDuration, @@ -61,13 +57,11 @@ func NewLokiClient() *LokiClient { } } -// QueryJobLogs queries historical job logs from Loki by task_id -func (c *LokiClient) QueryJobLogs(ctx context.Context, taskID string, opts QueryOpts) ([]dto.LogEntry, error) { +func (c *Client) QueryJobLogs(ctx context.Context, taskID string, opts QueryOpts) ([]dto.LogEntry, error) { if taskID == "" { return nil, fmt.Errorf("taskID is required") } - // Apply defaults if opts.Start.IsZero() { opts.Start = time.Now().Add(-1 * time.Hour) } @@ -86,11 +80,8 @@ func (c *LokiClient) QueryJobLogs(ctx context.Context, taskID string, opts Query opts.Direction = "forward" } - // Build LogQL query - // Use Structured Metadata filter since task_id is stored as structured metadata in Loki logQL := fmt.Sprintf(`{app="rcabench"} | task_id=%q`, taskID) - // Build request URL params := url.Values{} params.Set("query", logQL) params.Set("start", strconv.FormatInt(opts.Start.UnixNano(), 10)) @@ -99,7 +90,6 @@ func (c *LokiClient) QueryJobLogs(ctx context.Context, taskID string, opts Query params.Set("direction", opts.Direction) reqURL := fmt.Sprintf("%s/loki/api/v1/query_range?%s", c.address, params.Encode()) - logrus.Infof("Loki query: url=%s", reqURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) @@ -123,44 +113,36 @@ func (c *LokiClient) QueryJobLogs(ctx context.Context, taskID string, opts Query return nil, fmt.Errorf("failed to read Loki response: %w", err) } - var lokiResp lokiQueryRangeResponse + var lokiResp queryRangeResponse if err := json.Unmarshal(body, &lokiResp); err != nil { return nil, fmt.Errorf("failed to parse Loki response: %w", err) } - if lokiResp.Status != "success" { return nil, fmt.Errorf("loki query status: %s", lokiResp.Status) } - if len(lokiResp.Data.Result) == 0 { logrus.Warnf("Loki returned 0 streams for task %s, raw response: %s", taskID, string(body)) } - // Convert Loki results to LogEntry var entries []dto.LogEntry for _, result := range lokiResp.Data.Result { for _, value := range result.Values { if len(value) < 2 { continue } - - // Parse nanosecond timestamp nsec, err := strconv.ParseInt(value[0], 10, 64) if err != nil { logrus.Warnf("Loki: invalid timestamp %s: %v", value[0], err) continue } - entry := dto.LogEntry{ + entries = append(entries, dto.LogEntry{ Timestamp: time.Unix(0, nsec), Line: value[1], TaskID: taskID, - // Extract additional metadata from stream labels if available - TraceID: result.Stream["trace_id"], - JobID: result.Stream["job_id"], - } - - entries = append(entries, entry) + TraceID: result.Stream["trace_id"], + JobID: result.Stream["job_id"], + }) } } diff --git a/src/infra/loki/module.go b/src/infra/loki/module.go new file mode 100644 index 00000000..9ea0a235 --- /dev/null +++ b/src/infra/loki/module.go @@ -0,0 +1,7 @@ +package lokiinfra + +import "go.uber.org/fx" + +var Module = fx.Module("loki", + fx.Provide(NewClient), +) diff --git a/src/infra/redis/gateway.go b/src/infra/redis/gateway.go new file mode 100644 index 00000000..3f817240 --- /dev/null +++ b/src/infra/redis/gateway.go @@ -0,0 +1,309 @@ +package redisinfra + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/config" + "aegis/consts" + + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "go.uber.org/fx" +) + +type Gateway struct { + client *redis.Client +} + +func NewGateway(client *redis.Client) *Gateway { + if client == nil { + client = newClient() + } + return &Gateway{client: client} +} + +func NewGatewayWithLifecycle(lc fx.Lifecycle) *Gateway { + gateway := NewGateway(nil) + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + logrus.Info("Closing Redis client") + return gateway.close() + }, + }) + + return gateway +} + +func (g *Gateway) clientOrInit() *redis.Client { + if g.client == nil { + g.client = newClient() + } + return g.client +} + +func (g *Gateway) close() error { + if g.client == nil { + return nil + } + return g.client.Close() +} + +func (g *Gateway) CheckCachedField(ctx context.Context, key, field string) bool { + exists, err := g.clientOrInit().HExists(ctx, key, field).Result() + if err != nil { + logrus.Errorf("failed to check if field %s exists in cache: %v", field, err) + return false + } + return exists +} + +func (g *Gateway) GetHashField(ctx context.Context, key, field string, target any) error { + itemJSON, err := g.clientOrInit().HGet(ctx, key, field).Result() + if err != nil && err != redis.Nil { + return fmt.Errorf("failed to get hash field %s from key %s: %w", field, key, err) + } + if itemJSON == "" { + logrus.Warnf("field %s not found in cache key %s", field, key) + return nil + } + if err := json.Unmarshal([]byte(itemJSON), target); err != nil { + return fmt.Errorf("failed to unmarshal cached items for field %s: %w", field, err) + } + return nil +} + +func (g *Gateway) SetHashField(ctx context.Context, key, field string, item any) error { + itemJSON, err := json.Marshal(item) + if err != nil { + return fmt.Errorf("failed to marshal items to JSON: %w", err) + } + if _, err := g.clientOrInit().Pipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.HSet(ctx, key, field, itemJSON) + return nil + }); err != nil { + return fmt.Errorf("failed to set hash field %s in key %s: %w", field, key, err) + } + return nil +} + +func (g *Gateway) ListRange(ctx context.Context, key string) ([]string, error) { + result, err := g.clientOrInit().LRange(ctx, key, 0, -1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get list range for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) SetMembers(ctx context.Context, key string) ([]string, error) { + result, err := g.clientOrInit().SMembers(ctx, key).Result() + if err != nil { + return nil, fmt.Errorf("failed to get set members for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) Exists(ctx context.Context, key string) (bool, error) { + result, err := g.clientOrInit().Exists(ctx, key).Result() + if err != nil { + return false, fmt.Errorf("failed to check key '%s': %w", key, err) + } + return result > 0, nil +} + +func (g *Gateway) HashGetAll(ctx context.Context, key string) (map[string]string, error) { + result, err := g.clientOrInit().HGetAll(ctx, key).Result() + if err != nil { + return nil, fmt.Errorf("failed to get hash fields for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) HashGet(ctx context.Context, key, field string) (string, error) { + result, err := g.clientOrInit().HGet(ctx, key, field).Result() + if err != nil { + return "", err + } + return result, nil +} + +func (g *Gateway) HashSet(ctx context.Context, key string, values map[string]any) error { + if len(values) == 0 { + return nil + } + if err := g.clientOrInit().HSet(ctx, key, values).Err(); err != nil { + return fmt.Errorf("failed to set hash fields for key '%s': %w", key, err) + } + return nil +} + +func (g *Gateway) SeedNamespaceState(ctx context.Context, namespaceKey, namespace string, endTime int64, status int) error { + _, err := g.clientOrInit().Pipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.SAdd(ctx, consts.NamespacesKey, namespace) + pipe.HSetNX(ctx, namespaceKey, "end_time", endTime) + pipe.HSetNX(ctx, namespaceKey, "trace_id", "") + pipe.HSetNX(ctx, namespaceKey, "status", status) + return nil + }) + if err != nil { + return fmt.Errorf("failed to seed namespace state for '%s': %w", namespace, err) + } + return nil +} + +func (g *Gateway) ZRangeByScoreWithScores(ctx context.Context, key string, limit int64) ([]redis.Z, error) { + if limit <= 0 { + return nil, fmt.Errorf("limit must be a positive number") + } + results, err := g.clientOrInit().ZRangeByScoreWithScores(ctx, key, &redis.ZRangeBy{ + Min: "-inf", + Max: "+inf", + Offset: 0, + Count: limit, + }).Result() + if err != nil { + return nil, fmt.Errorf("failed to get scheduled tasks from key '%s': %w", key, err) + } + return results, nil +} + +func (g *Gateway) ZRangeByScore(ctx context.Context, key, min, max string) ([]string, error) { + result, err := g.clientOrInit().ZRangeByScore(ctx, key, &redis.ZRangeBy{ + Min: min, + Max: max, + }).Result() + if err != nil { + return nil, fmt.Errorf("failed to get sorted set range for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) ZAdd(ctx context.Context, key string, member redis.Z) error { + if err := g.clientOrInit().ZAdd(ctx, key, member).Err(); err != nil { + return fmt.Errorf("failed to add sorted set member for key '%s': %w", key, err) + } + return nil +} + +func (g *Gateway) ZRemRangeByScore(ctx context.Context, key, min, max string) error { + if err := g.clientOrInit().ZRemRangeByScore(ctx, key, min, max).Err(); err != nil { + return fmt.Errorf("failed to trim sorted set for key '%s': %w", key, err) + } + return nil +} + +func (g *Gateway) SetRemove(ctx context.Context, key string, members ...any) (int64, error) { + result, err := g.clientOrInit().SRem(ctx, key, members...).Result() + if err != nil { + return 0, fmt.Errorf("failed to remove set members for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) XAdd(ctx context.Context, stream string, values map[string]any) error { + _, err := g.clientOrInit().XAdd(ctx, &redis.XAddArgs{ + Stream: stream, + MaxLen: 1000, + Approx: true, + ID: "*", + Values: values, + }).Result() + if err != nil { + return fmt.Errorf("redis XADD failed for stream '%s': %w", stream, err) + } + return nil +} + +func (g *Gateway) RunScript(ctx context.Context, script *redis.Script, keys []string, args ...any) (any, error) { + result, err := script.Run(ctx, g.clientOrInit(), keys, args...).Result() + if err != nil { + return nil, err + } + return result, nil +} + +func (g *Gateway) Ping(ctx context.Context) error { + if err := g.clientOrInit().Ping(ctx).Err(); err != nil { + return fmt.Errorf("redis PING failed: %w", err) + } + return nil +} + +func (g *Gateway) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { + return g.clientOrInit().Watch(ctx, fn, keys...) +} + +func (g *Gateway) XRead(ctx context.Context, streams []string, count int64, block time.Duration) ([]redis.XStream, error) { + result, err := g.clientOrInit().XRead(ctx, &redis.XReadArgs{ + Streams: streams, + Count: count, + Block: block, + }).Result() + if err != nil && err != redis.Nil { + return nil, fmt.Errorf("redis XREAD failed: %w", err) + } + return result, nil +} + +func (g *Gateway) Publish(ctx context.Context, channel string, message any) error { + var payload string + switch v := message.(type) { + case string: + payload = v + default: + data, err := json.Marshal(message) + if err != nil { + return fmt.Errorf("failed to marshal message: %w", err) + } + payload = string(data) + } + + if err := g.clientOrInit().Publish(ctx, channel, payload).Err(); err != nil { + return fmt.Errorf("redis PUBLISH failed for channel '%s': %w", channel, err) + } + return nil +} + +func (g *Gateway) Set(ctx context.Context, key string, value any, expiration time.Duration) error { + if err := g.clientOrInit().Set(ctx, key, value, expiration).Err(); err != nil { + return fmt.Errorf("redis SET failed for key '%s': %w", key, err) + } + return nil +} + +func (g *Gateway) SetNX(ctx context.Context, key string, value any, expiration time.Duration) (bool, error) { + result, err := g.clientOrInit().SetNX(ctx, key, value, expiration).Result() + if err != nil { + return false, fmt.Errorf("redis SETNX failed for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) Subscribe(ctx context.Context, channel string) (*redis.PubSub, error) { + pubsub := g.clientOrInit().Subscribe(ctx, channel) + if _, err := pubsub.Receive(ctx); err != nil { + _ = pubsub.Close() + return nil, fmt.Errorf("redis SUBSCRIBE failed for channel '%s': %w", channel, err) + } + return pubsub, nil +} + +func (g *Gateway) InitConcurrencyLock(ctx context.Context) error { + return g.clientOrInit().Set(ctx, ConcurrencyLockKey, 0, 0).Err() +} + +func newClient() *redis.Client { + logrus.Infof("Connecting to Redis %s", config.GetString("redis.host")) + client := redis.NewClient(&redis.Options{ + Addr: config.GetString("redis.host"), + Password: "", + DB: 0, + }) + if err := client.Ping(context.Background()).Err(); err != nil { + logrus.Fatalf("Failed to connect to Redis: %v", err) + } + return client +} diff --git a/src/infra/redis/module.go b/src/infra/redis/module.go new file mode 100644 index 00000000..b45ca0c6 --- /dev/null +++ b/src/infra/redis/module.go @@ -0,0 +1,7 @@ +package redisinfra + +import "go.uber.org/fx" + +var Module = fx.Module("redis", + fx.Provide(NewGatewayWithLifecycle), +) diff --git a/src/infra/redis/task_queue.go b/src/infra/redis/task_queue.go new file mode 100644 index 00000000..fef6e487 --- /dev/null +++ b/src/infra/redis/task_queue.go @@ -0,0 +1,194 @@ +package redisinfra + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/dto" + + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" +) + +const ( + DelayedQueueKey = "task:delayed" + ReadyQueueKey = "task:ready" + DeadLetterKey = "task:dead" + TaskIndexKey = "task:index" + ConcurrencyLockKey = "task:concurrency_lock" + LastBatchInfoKey = "last_batch_info" + MaxConcurrency = 20 +) + +func (g *Gateway) SubmitImmediateTask(ctx context.Context, taskData []byte, taskID string) error { + redisCli := g.clientOrInit() + if err := redisCli.LPush(ctx, ReadyQueueKey, taskData).Err(); err != nil { + return err + } + return redisCli.HSet(ctx, TaskIndexKey, taskID, ReadyQueueKey).Err() +} + +func (g *Gateway) GetTask(ctx context.Context, timeout time.Duration) (string, error) { + redisCli := g.clientOrInit() + result, err := redisCli.BRPop(ctx, timeout, ReadyQueueKey).Result() + if err != nil { + return "", err + } + return result[1], nil +} + +func (g *Gateway) HandleFailedTask(ctx context.Context, taskData []byte, backoffSec int) error { + deadLetterTime := time.Now().Add(time.Duration(backoffSec) * time.Second).Unix() + redisCli := g.clientOrInit() + return redisCli.ZAdd(ctx, DeadLetterKey, redis.Z{ + Score: float64(deadLetterTime), + Member: taskData, + }).Err() +} + +func (g *Gateway) SubmitDelayedTask(ctx context.Context, taskData []byte, taskID string, executeTime int64) error { + redisCli := g.clientOrInit() + if err := redisCli.ZAdd(ctx, DelayedQueueKey, redis.Z{ + Score: float64(executeTime), + Member: taskData, + }).Err(); err != nil { + return err + } + return redisCli.HSet(ctx, TaskIndexKey, taskID, DelayedQueueKey).Err() +} + +func (g *Gateway) ProcessDelayedTasks(ctx context.Context) ([]string, error) { + redisCli := g.clientOrInit() + now := time.Now().Unix() + + delayedTaskScript := redis.NewScript(` + local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1]) + if #tasks > 0 then + redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) + redis.call('LPUSH', KEYS[2], unpack(tasks)) + for _, task in ipairs(tasks) do + local t = cjson.decode(task) + redis.call('HSET', KEYS[3], t.task_id, KEYS[2]) + end + end + return tasks + `) + + result, err := delayedTaskScript.Run(ctx, redisCli, + []string{DelayedQueueKey, ReadyQueueKey, TaskIndexKey}, + now, + ).StringSlice() + if err != nil && err != redis.Nil { + return nil, err + } + return result, nil +} + +func (g *Gateway) HandleCronRescheduleFailure(ctx context.Context, taskData []byte) error { + return g.clientOrInit().ZAdd(ctx, DeadLetterKey, redis.Z{ + Score: float64(time.Now().Unix()), + Member: taskData, + }).Err() +} + +func (g *Gateway) AcquireConcurrencyLock(ctx context.Context) bool { + redisCli := g.clientOrInit() + currentCount, _ := redisCli.Get(ctx, ConcurrencyLockKey).Int64() + if currentCount >= MaxConcurrency { + return false + } + return redisCli.Incr(ctx, ConcurrencyLockKey).Err() == nil +} + +func (g *Gateway) ReleaseConcurrencyLock(ctx context.Context) { + if err := g.clientOrInit().Decr(ctx, ConcurrencyLockKey).Err(); err != nil { + logrus.Warnf("error releasing concurrency lock: %v", err) + } +} + +func (g *Gateway) GetTaskQueue(ctx context.Context, taskID string) (string, error) { + return g.clientOrInit().HGet(ctx, TaskIndexKey, taskID).Result() +} + +func (g *Gateway) ListDelayedTasks(ctx context.Context, limit int64) ([]string, error) { + delayedTasksWithScore, err := g.ZRangeByScoreWithScores(ctx, DelayedQueueKey, limit) + if err != nil { + return nil, err + } + + taskDatas := make([]string, 0, len(delayedTasksWithScore)) + for _, z := range delayedTasksWithScore { + taskData, ok := z.Member.(string) + if !ok { + return nil, fmt.Errorf("invalid delayed task data") + } + taskDatas = append(taskDatas, taskData) + } + + return taskDatas, nil +} + +func (g *Gateway) ListReadyTasks(ctx context.Context) ([]string, error) { + return g.ListRange(ctx, ReadyQueueKey) +} + +func (g *Gateway) RemoveFromList(ctx context.Context, key, taskID string) (bool, error) { + removeFromListScript := redis.NewScript(` + local key = KEYS[1] + local taskID = ARGV[1] + local count = 0 + + for i=0, redis.call('LLEN', key)-1 do + local item = redis.call('LINDEX', key, i) + if item then + local task = cjson.decode(item) + if task.task_id == taskID then + redis.call('LSET', key, i, "__DELETED__") + count = count + 1 + end + end + end + + if count > 0 then + redis.call('LREM', key, count, "__DELETED__") + end + + return count + `) + + result, err := removeFromListScript.Run(ctx, g.clientOrInit(), []string{key}, taskID).Int() + if err != nil { + return false, fmt.Errorf("failed to remove from list: %w", err) + } + return result > 0, nil +} + +func (g *Gateway) RemoveFromZSet(ctx context.Context, key, taskID string) bool { + cli := g.clientOrInit() + members, err := cli.ZRangeByScore(ctx, key, &redis.ZRangeBy{ + Min: "-inf", + Max: "+inf", + }).Result() + if err != nil { + return false + } + + for _, member := range members { + var task dto.UnifiedTask + if json.Unmarshal([]byte(member), &task) == nil && task.TaskID == taskID { + if err := cli.ZRem(ctx, key, member).Err(); err != nil { + logrus.Warnf("failed to remove from ZSet: %v", err) + return false + } + return true + } + } + + return false +} + +func (g *Gateway) DeleteTaskIndex(ctx context.Context, taskID string) error { + return g.clientOrInit().HDel(ctx, TaskIndexKey, taskID).Err() +} diff --git a/src/infra/runtime/module.go b/src/infra/runtime/module.go new file mode 100644 index 00000000..c8f2410c --- /dev/null +++ b/src/infra/runtime/module.go @@ -0,0 +1,23 @@ +package runtimeinfra + +import ( + "time" + + "aegis/consts" + "aegis/utils" + + "go.uber.org/fx" +) + +var Module = fx.Module("runtime", + fx.Invoke(InitializeRuntime), +) + +func InitializeRuntime() { + if consts.InitialTime == nil { + consts.InitialTime = utils.TimePtr(time.Now()) + } + if consts.AppID == "" { + consts.AppID = utils.GenerateULID(consts.InitialTime) + } +} diff --git a/src/infra/tracing/module.go b/src/infra/tracing/module.go new file mode 100644 index 00000000..492d2e87 --- /dev/null +++ b/src/infra/tracing/module.go @@ -0,0 +1,28 @@ +package tracinginfra + +import ( + "context" + + "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/fx" +) + +var Module = fx.Module("tracing", + fx.Provide(NewTraceProvider), +) + +func NewTraceProvider(lc fx.Lifecycle) *trace.TracerProvider { + provider, err := NewProvider() + if err != nil { + panic(err) + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + ShutdownProvider(ctx, provider) + return nil + }, + }) + + return provider +} diff --git a/src/client/jaeger.go b/src/infra/tracing/provider.go similarity index 65% rename from src/client/jaeger.go rename to src/infra/tracing/provider.go index 58bd4186..b376178b 100644 --- a/src/client/jaeger.go +++ b/src/infra/tracing/provider.go @@ -1,4 +1,4 @@ -package client +package tracinginfra import ( "context" @@ -15,11 +15,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.34.0" ) -var ( - TraceProvider *sdktrace.TracerProvider -) - -func InitTraceProvider() { +func NewProvider() (*sdktrace.TracerProvider, error) { ctx := context.Background() exporter, err := otlptracehttp.New(ctx, @@ -27,11 +23,10 @@ func InitTraceProvider() { otlptracehttp.WithEndpoint(config.GetString("jaeger.endpoint")), ) if err != nil { - logrus.Errorf("failed to create OTLP HTTP exporter: %v", err) - return + return nil, err } - resource, err := resource.Merge( + res, err := resource.Merge( resource.Default(), resource.NewWithAttributes( semconv.SchemaURL, @@ -40,22 +35,26 @@ func InitTraceProvider() { ), ) if err != nil { - logrus.Errorf("failed to create OTLP sdk resource: %v", err) - return + return nil, err } - TraceProvider = sdktrace.NewTracerProvider( + provider := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), - sdktrace.WithResource(resource), + sdktrace.WithResource(res), ) - otel.SetTracerProvider(TraceProvider) + otel.SetTracerProvider(provider) otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + return provider, nil } -func ShutdownTraceProvider(ctx context.Context) { - ctx, cancel := context.WithTimeout(ctx, time.Second*5) +func ShutdownProvider(ctx context.Context, provider *sdktrace.TracerProvider) { + if provider == nil { + return + } + + shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - if err := TraceProvider.Shutdown(ctx); err != nil { + if err := provider.Shutdown(shutdownCtx); err != nil { logrus.Errorf("failed to shutdown tracer provider: %v", err) } } diff --git a/src/interface/controller/module.go b/src/interface/controller/module.go new file mode 100644 index 00000000..7366a921 --- /dev/null +++ b/src/interface/controller/module.go @@ -0,0 +1,79 @@ +package controllerinterface + +import ( + "context" + "log" + "os" + + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + "aegis/service/consumer" + + "github.com/go-logr/stdr" + "go.uber.org/fx" + "gorm.io/gorm" + k8slogger "sigs.k8s.io/controller-runtime/pkg/log" +) + +var Module = fx.Module("controller", + fx.Provide(newLifecycle), + fx.Invoke(registerLifecycle), +) + +type Params struct { + fx.In + + Controller *k8sinfra.Controller + K8sGateway *k8sinfra.Gateway + RedisGateway *redisinfra.Gateway + DB *gorm.DB + Monitor consumer.NamespaceMonitor + AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` + BatchManager *consumer.FaultBatchManager +} + +type Lifecycle struct { + params Params + RunFunc func(context.Context, context.CancelFunc) error + StopFunc func() +} + +func newLifecycle(params Params) *Lifecycle { + return &Lifecycle{params: params} +} + +func (r *Lifecycle) start(ctx context.Context, cancel context.CancelFunc) error { + if r.RunFunc != nil { + return r.RunFunc(ctx, cancel) + } + k8slogger.SetLogger(stdr.New(log.New(os.Stdout, "", log.LstdFlags))) + go r.params.Controller.Initialize(ctx, cancel, consumer.NewHandler(r.params.DB, r.params.Monitor, r.params.AlgoLimiter, r.params.K8sGateway, r.params.RedisGateway, r.params.BatchManager)) + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + var ( + controllerCtx context.Context + cancel context.CancelFunc + ) + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + controllerCtx, cancel = context.WithCancel(context.WithoutCancel(ctx)) + return runner.start(controllerCtx, cancel) + }, + OnStop: func(ctx context.Context) error { + if cancel != nil { + cancel() + } + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/http/module.go b/src/interface/http/module.go new file mode 100644 index 00000000..b52faad2 --- /dev/null +++ b/src/interface/http/module.go @@ -0,0 +1,16 @@ +package httpinterface + +import ( + "aegis/middleware" + + "go.uber.org/fx" +) + +var Module = fx.Module("http", + fx.Provide( + middleware.NewService, + NewGinEngine, + NewServer, + ), + fx.Invoke(registerServerLifecycle), +) diff --git a/src/interface/http/router.go b/src/interface/http/router.go new file mode 100644 index 00000000..a56725d6 --- /dev/null +++ b/src/interface/http/router.go @@ -0,0 +1,12 @@ +package httpinterface + +import ( + "aegis/middleware" + "aegis/router" + + "github.com/gin-gonic/gin" +) + +func NewGinEngine(handlers *router.Handlers, middlewareService middleware.Service) *gin.Engine { + return router.New(handlers, middlewareService) +} diff --git a/src/interface/http/server.go b/src/interface/http/server.go new file mode 100644 index 00000000..1dadeeae --- /dev/null +++ b/src/interface/http/server.go @@ -0,0 +1,40 @@ +package httpinterface + +import ( + "context" + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "go.uber.org/fx" +) + +type ServerConfig struct { + Addr string +} + +func NewServer(config ServerConfig, engine *gin.Engine) *http.Server { + return &http.Server{ + Addr: config.Addr, + Handler: engine, + } +} + +func registerServerLifecycle(lc fx.Lifecycle, server *http.Server) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + go func() { + logrus.Infof("Starting HTTP server on %s", server.Addr) + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logrus.Errorf("HTTP server error: %v", err) + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { + logrus.Info("Stopping HTTP server") + return server.Shutdown(ctx) + }, + }) +} diff --git a/src/interface/receiver/module.go b/src/interface/receiver/module.go new file mode 100644 index 00000000..9149955a --- /dev/null +++ b/src/interface/receiver/module.go @@ -0,0 +1,76 @@ +package receiverinterface + +import ( + "context" + + "aegis/config" + redisinfra "aegis/infra/redis" + "aegis/service/logreceiver" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" +) + +var Module = fx.Module("receiver", + fx.Provide(newLifecycle), + fx.Invoke(registerLifecycle), +) + +type Lifecycle struct { + receiver *logreceiver.OTLPLogReceiver + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(redisGateway *redisinfra.Gateway) *Lifecycle { + otlpPort := config.GetInt("otlp_receiver.port") + if otlpPort == 0 { + otlpPort = logreceiver.DefaultPort + } + return &Lifecycle{ + receiver: logreceiver.NewOTLPLogReceiver(otlpPort, 0, redisGateway), + } +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + go func() { + if err := r.receiver.Start(ctx); err != nil { + logrus.Errorf("OTLP log receiver error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.receiver != nil { + r.receiver.Shutdown() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + var ( + receiverCtx context.Context + cancel context.CancelFunc + ) + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + receiverCtx, cancel = context.WithCancel(context.WithoutCancel(ctx)) + return runner.start(receiverCtx) + }, + OnStop: func(ctx context.Context) error { + if cancel != nil { + cancel() + } + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/worker/module.go b/src/interface/worker/module.go new file mode 100644 index 00000000..b3181674 --- /dev/null +++ b/src/interface/worker/module.go @@ -0,0 +1,114 @@ +package workerinterface + +import ( + "context" + + buildkitinfra "aegis/infra/buildkit" + etcdinfra "aegis/infra/etcd" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + commonservice "aegis/service/common" + "aegis/service/consumer" + "aegis/service/initialization" + + "go.uber.org/fx" + "gorm.io/gorm" +) + +var Module = fx.Module("worker", + fx.Provide(newLifecycle), + fx.Invoke(registerLifecycle), +) + +type Params struct { + fx.In + + DB *gorm.DB + RedisGateway *redisinfra.Gateway + BuildKit *buildkitinfra.Gateway + Helm *helminfra.Gateway + K8sGateway *k8sinfra.Gateway + Controller *k8sinfra.Controller + Etcd *etcdinfra.Gateway + Monitor consumer.NamespaceMonitor + RestartLimiter *consumer.TokenBucketRateLimiter `name:"restart_limiter"` + BuildLimiter *consumer.TokenBucketRateLimiter `name:"build_limiter"` + AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` + BatchManager *consumer.FaultBatchManager +} + +type Lifecycle struct { + params Params + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(params Params) *Lifecycle { + return &Lifecycle{params: params} +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + params := r.params + if err := initialization.InitializeConsumer( + ctx, + params.DB, + params.Controller, + params.Monitor, + params.RedisGateway, + commonservice.NewConfigUpdateListener(ctx, params.DB, params.Etcd), + params.RestartLimiter, + params.BuildLimiter, + params.AlgoLimiter, + ); err != nil { + return err + } + if err := params.RedisGateway.InitConcurrencyLock(ctx); err != nil { + return err + } + + go consumer.StartScheduler(ctx, params.RedisGateway) + go consumer.ConsumeTasks(ctx, consumer.RuntimeDeps{ + DB: params.DB, + Monitor: params.Monitor, + RestartRateLimiter: params.RestartLimiter, + BuildRateLimiter: params.BuildLimiter, + AlgorithmRateLimiter: params.AlgoLimiter, + RedisGateway: params.RedisGateway, + K8sGateway: params.K8sGateway, + BuildKitGateway: params.BuildKit, + HelmGateway: params.Helm, + FaultBatchManager: params.BatchManager, + }) + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + var ( + workerCtx context.Context + cancel context.CancelFunc + ) + + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + workerCtx, cancel = context.WithCancel(context.WithoutCancel(ctx)) + return runner.start(workerCtx) + }, + OnStop: func(ctx context.Context) error { + if cancel != nil { + cancel() + } + runner.stop() + return nil + }, + }) +} diff --git a/src/main.go b/src/main.go index 595dc92b..c6023790 100644 --- a/src/main.go +++ b/src/main.go @@ -18,52 +18,24 @@ package main import ( - "context" - "fmt" - "log" "os" - "path" - "runtime" - "time" - "aegis/client" - "aegis/client/k8s" - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/router" - "aegis/service/consumer" - "aegis/service/initialization" - "aegis/service/logreceiver" - "aegis/utils" + "aegis/app" - chaosCli "github.com/OperationsPAI/chaos-experiment/client" - nested "github.com/antonfisher/nested-logrus-formatter" - "github.com/go-logr/stdr" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" - k8slogger "sigs.k8s.io/controller-runtime/pkg/log" + "go.uber.org/fx" ) -func init() { - logrus.SetReportCaller(true) - logrus.SetFormatter(&nested.Formatter{ - CustomCallerFormatter: func(f *runtime.Frame) string { - filename := path.Base(f.File) - return fmt.Sprintf(" (%s:%d)", filename, f.Line) +func newModeCommand(use, short string, run func()) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Run: func(cmd *cobra.Command, args []string) { + run() }, - FieldsOrder: []string{"component", "category"}, - HideKeys: true, - TimestampFormat: "2006-01-02 15:04:05", - }) - logrus.SetLevel(logrus.InfoLevel) - logrus.Info("Logger initialized") -} - -func initChaosExperiment() { - k8sConfig := k8s.GetK8sRestConfig() - chaosCli.InitWithConfig(k8sConfig) + } } func main() { @@ -87,113 +59,15 @@ func main() { logrus.Fatalf("failed to bind flag: %v", err) } - config.Init(viper.GetString("conf")) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Producer command - runs HTTP server for API endpoints - var producerCmd = &cobra.Command{ - Use: "producer", - Short: "Run as a producer", - Run: func(cmd *cobra.Command, args []string) { - logrus.Println("Running as producer") - database.InitDB() - initialization.InitializeProducer(ctx) - - utils.InitValidator() - client.InitTraceProvider() - initChaosExperiment() - - engine := router.New() - port := viper.GetString("port") - if err := engine.Run(":" + port); err != nil { - panic(err) - } - }, - } - - // Consumer command - runs background workers and Kubernetes controllers - var consumerCmd = &cobra.Command{ - Use: "consumer", - Short: "Run as a consumer", - Run: func(cmd *cobra.Command, args []string) { - logrus.Println("Running as consumer") - consts.InitialTime = utils.TimePtr(time.Now()) - consts.AppID = utils.GenerateULID(consts.InitialTime) - - k8slogger.SetLogger(stdr.New(log.New(os.Stdout, "", log.LstdFlags))) - initChaosExperiment() - go k8s.GetK8sController().Initialize(ctx, cancel, consumer.NewHandler()) - - database.InitDB() - initialization.InitializeConsumer(ctx) - initialization.InitConcurrencyLock(ctx) - - client.InitTraceProvider() - - // Start OTLP log receiver for real-time job log streaming - go func() { - otlpPort := config.GetInt("otlp_receiver.port") - if otlpPort == 0 { - otlpPort = logreceiver.DefaultPort - } - receiver := logreceiver.NewOTLPLogReceiver(otlpPort, 0) - if err := receiver.Start(ctx); err != nil { - logrus.Errorf("OTLP log receiver error: %v", err) - } - defer receiver.Shutdown() - }() - - go consumer.StartScheduler(ctx) - consumer.ConsumeTasks(ctx) - }, - } - - // Both subcommand - var bothCmd = &cobra.Command{ - Use: "both", - Short: "Run as both producer and consumer", - Run: func(cmd *cobra.Command, args []string) { - logrus.Println("Running as both producer and consumer") - consts.InitialTime = utils.TimePtr(time.Now()) - consts.AppID = utils.GenerateULID(consts.InitialTime) - - k8slogger.SetLogger(stdr.New(log.New(os.Stdout, "", log.LstdFlags))) - initChaosExperiment() - go k8s.GetK8sController().Initialize(ctx, cancel, consumer.NewHandler()) - - database.InitDB() - initialization.InitializeProducer(ctx) - initialization.InitializeConsumer(ctx) - initialization.InitConcurrencyLock(ctx) - - utils.InitValidator() - client.InitTraceProvider() - - // Start OTLP log receiver for real-time job log streaming - go func() { - otlpPort := config.GetInt("otlp_receiver.port") - if otlpPort == 0 { - otlpPort = logreceiver.DefaultPort - } - receiver := logreceiver.NewOTLPLogReceiver(otlpPort, 0) - if err := receiver.Start(ctx); err != nil { - logrus.Errorf("OTLP log receiver error: %v", err) - } - defer receiver.Shutdown() - }() - - go consumer.StartScheduler(ctx) - go consumer.ConsumeTasks(ctx) - - engine := router.New() - port := viper.GetString("port") - if err := engine.Run(":" + port); err != nil { - panic(err) - } - }, - } + producerCmd := newModeCommand("producer", "Run as a producer", func() { + fx.New(app.ProducerOptions(viper.GetString("conf"), viper.GetString("port"))).Run() + }) + consumerCmd := newModeCommand("consumer", "Run as a consumer", func() { + fx.New(app.ConsumerOptions(viper.GetString("conf"))).Run() + }) + bothCmd := newModeCommand("both", "Run as both producer and consumer", func() { + fx.New(app.BothOptions(viper.GetString("conf"), viper.GetString("port"))).Run() + }) rootCmd.AddCommand(producerCmd, consumerCmd, bothCmd) if err := rootCmd.Execute(); err != nil { diff --git a/src/middleware/audit.go b/src/middleware/audit.go index 729a6265..750ae509 100644 --- a/src/middleware/audit.go +++ b/src/middleware/audit.go @@ -8,7 +8,6 @@ import ( "time" "aegis/consts" - producer "aegis/service/producer" "aegis/utils" "github.com/gin-gonic/gin" @@ -27,6 +26,7 @@ func AuditMiddleware() gin.HandlerFunc { // Get user information (if authenticated) userID, _ := GetCurrentUserID(c) + logger := auditLoggerFromContext(c) // Read request body (for recording details) var requestBody []byte @@ -84,23 +84,23 @@ func AuditMiddleware() gin.HandlerFunc { } } + ipAddress := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + durationMillis := int(duration.Milliseconds()) + // Async logging (don't block request) go func() { - ipAddress := c.ClientIP() - userAgent := c.GetHeader("User-Agent") - duration := int(duration.Milliseconds()) - //TODO resource instance extraction if errorMsg != "" { - if err := producer.LogFailedAction(ipAddress, userAgent, action, errorMsg, duration, userID, resource); err != nil { + if err := logger.LogFailedAction(ipAddress, userAgent, action, errorMsg, durationMillis, userID, resource); err != nil { logrus.Errorf("Failed to log audit action: %v", err) return } return } - if err := producer.LogUserAction(ipAddress, userAgent, action, string(detailsJSON), duration, userID, resource); err != nil { + if err := logger.LogUserAction(ipAddress, userAgent, action, string(detailsJSON), durationMillis, userID, resource); err != nil { logrus.Errorf("Failed to log audit action: %v", err) return } diff --git a/src/middleware/deps.go b/src/middleware/deps.go new file mode 100644 index 00000000..cd2638a4 --- /dev/null +++ b/src/middleware/deps.go @@ -0,0 +1,409 @@ +package middleware + +import ( + "errors" + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type permissionChecker interface { + CheckUserPermission(params *dto.CheckPermissionParams) (bool, error) + IsUserTeamAdmin(userID, teamID int) (bool, error) + IsUserInTeam(userID, teamID int) (bool, error) + IsTeamPublic(teamID int) (bool, error) + IsUserProjectAdmin(userID, projectID int) (bool, error) + IsUserInProject(userID, projectID int) (bool, error) +} + +type auditLogger interface { + LogFailedAction(ipAddress, userAgent, action, errorMsg string, duration, userID int, resourceName consts.ResourceName) error + LogUserAction(ipAddress, userAgent, action, details string, duration, userID int, resourceName consts.ResourceName) error +} + +type Service interface { + permissionChecker + auditLogger +} + +const middlewareServiceContextKey = "middleware.service" + +func NewService(db *gorm.DB) Service { return &dbBackedMiddlewareService{db: db} } + +func InjectService(service Service) gin.HandlerFunc { + if service == nil { + service = noopMiddlewareService{} + } + + return func(c *gin.Context) { + c.Set(middlewareServiceContextKey, service) + c.Next() + } +} + +func permissionCheckerFromContext(c *gin.Context) permissionChecker { return serviceFromContext(c) } + +func auditLoggerFromContext(c *gin.Context) auditLogger { return serviceFromContext(c) } + +func serviceFromContext(c *gin.Context) Service { + if c == nil { + return noopMiddlewareService{} + } + service, ok := c.Get(middlewareServiceContextKey) + if !ok { + return noopMiddlewareService{} + } + middlewareService, ok := service.(Service) + if !ok || middlewareService == nil { + return noopMiddlewareService{} + } + return middlewareService +} + +type dbBackedMiddlewareService struct { + db *gorm.DB +} + +func (s *dbBackedMiddlewareService) CheckUserPermission(params *dto.CheckPermissionParams) (bool, error) { + if err := params.Validate(); err != nil { + return false, fmt.Errorf("invalid request: %w", err) + } + + permission, err := s.getPermissionByActionAndResource(params.Action, params.Scope, params.ResourceName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, fmt.Errorf("failed to find target permission: %w", err) + } + + return s.checkUserHasPermission(params, permission.ID) +} + +func (s *dbBackedMiddlewareService) IsUserInTeam(userID, teamID int) (bool, error) { + ut, err := s.getUserTeamRole(userID, teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return ut != nil, nil +} + +func (s *dbBackedMiddlewareService) IsUserTeamAdmin(userID, teamID int) (bool, error) { + ut, err := s.getUserTeamRole(userID, teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return ut != nil && ut.Role != nil && ut.Role.Name == consts.RoleTeamAdmin.String(), nil +} + +func (s *dbBackedMiddlewareService) IsTeamPublic(teamID int) (bool, error) { + team, err := s.getTeamByID(teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return team.IsPublic, nil +} + +func (s *dbBackedMiddlewareService) IsUserInProject(userID, projectID int) (bool, error) { + up, err := s.getUserProjectRole(userID, projectID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return up != nil, nil +} + +func (s *dbBackedMiddlewareService) IsUserProjectAdmin(userID, projectID int) (bool, error) { + up, err := s.getUserProjectRole(userID, projectID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return up != nil && up.Role != nil && up.Role.Name == consts.RoleProjectAdmin.String(), nil +} + +func (s *dbBackedMiddlewareService) LogFailedAction(ipAddress, userAgent, action, errorMsg string, duration, userID int, resourceName consts.ResourceName) error { + if resourceName == "" { + return fmt.Errorf("resource name cannot be empty") + } + + log := &model.AuditLog{ + IPAddress: ipAddress, + UserAgent: userAgent, + Duration: duration, + Action: action, + ErrorMsg: errorMsg, + UserID: userID, + State: consts.AuditLogStateFailed, + Status: consts.CommonEnabled, + } + + return s.db.Transaction(func(tx *gorm.DB) error { + resource, err := s.getResourceByName(tx, resourceName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: resource %s not found", consts.ErrNotFound, resourceName) + } + return fmt.Errorf("failed to get resource: %w", err) + } + + log.ResourceID = resource.ID + return s.createAuditLog(tx, log) + }) +} + +func (s *dbBackedMiddlewareService) LogUserAction(ipAddress, userAgent, action, details string, duration, userID int, resourceName consts.ResourceName) error { + if resourceName == "" { + return fmt.Errorf("resource name cannot be empty") + } + + log := &model.AuditLog{ + IPAddress: ipAddress, + UserAgent: userAgent, + Duration: duration, + Action: action, + Details: details, + UserID: userID, + State: consts.AuditLogStateSuccess, + Status: consts.CommonEnabled, + } + + return s.db.Transaction(func(tx *gorm.DB) error { + resource, err := s.getResourceByName(tx, resourceName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: resource %s not found", consts.ErrNotFound, resourceName) + } + return fmt.Errorf("failed to get resource: %w", err) + } + + log.ResourceID = resource.ID + return s.createAuditLog(tx, log) + }) +} + +func (s *dbBackedMiddlewareService) getPermissionByActionAndResource(action consts.ActionName, scope consts.ResourceScope, resourceName consts.ResourceName) (*model.Permission, error) { + var permission model.Permission + if err := s.db. + Select("permissions.*"). + Joins("JOIN resources ON permissions.resource_id = resources.id"). + Where("permissions.action = ? AND permissions.scope = ? AND resources.name = ?", action, scope, resourceName). + Where("permissions.status != ?", consts.CommonDeleted). + First(&permission).Error; err != nil { + return nil, err + } + return &permission, nil +} + +func (s *dbBackedMiddlewareService) checkUserHasPermission(params *dto.CheckPermissionParams, permissionID int) (bool, error) { + directQuery := s.buildDirectPermissionQuery(params.UserID, permissionID, params.ProjectID, params.ContainerID, params.DatasetID) + globalRoleQuery := s.buildGlobalRolePermissionQuery(params.UserID, permissionID) + finalQuery := s.db.Table("(? UNION ALL ?) as base", directQuery, globalRoleQuery) + + if params.TeamID != nil { + finalQuery = s.db.Table("(? UNION ALL ?) as combined", finalQuery, s.buildTeamRolePermissionQuery(params.UserID, permissionID, *params.TeamID)) + } + if params.ProjectID != nil { + finalQuery = s.db.Table("(? UNION ALL ?) as combined", finalQuery, s.buildProjectRolePermissionQuery(params.UserID, permissionID, *params.ProjectID)) + } + if params.ContainerID != nil { + finalQuery = s.db.Table("(? UNION ALL ?) as combined", finalQuery, s.buildContainerRolePermissionQuery(params.UserID, permissionID, *params.ContainerID)) + } + if params.DatasetID != nil { + finalQuery = s.db.Table("(? UNION ALL ?) as combined", finalQuery, s.buildDatasetRolePermissionQuery(params.UserID, permissionID, *params.DatasetID)) + } + + var count int64 + if err := finalQuery.Limit(1).Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to check user permission: %w", err) + } + return count > 0, nil +} + +func (s *dbBackedMiddlewareService) buildDirectPermissionQuery(userID int, permissionID int, projectID, containerID, datasetID *int) *gorm.DB { + query := s.db. + Select("up.permission_id"). + Table("user_permissions up"). + Where("up.user_id = ? AND up.permission_id = ?", userID, permissionID). + Where("up.grant_type = ?", consts.GrantTypeGrant). + Where("up.expires_at IS NULL OR up.expires_at > ?", time.Now()) + + if projectID != nil { + query = query.Where("up.project_id IS NULL OR up.project_id = ?", *projectID) + } else { + query = query.Where("up.project_id IS NULL") + } + if containerID != nil { + query = query.Where("up.container_id IS NULL OR up.container_id = ?", *containerID) + } else { + query = query.Where("up.container_id IS NULL") + } + if datasetID != nil { + query = query.Where("up.dataset_id IS NULL OR up.dataset_id = ?", *datasetID) + } else { + query = query.Where("up.dataset_id IS NULL") + } + + return query +} + +func (s *dbBackedMiddlewareService) buildGlobalRolePermissionQuery(userID int, permissionID int) *gorm.DB { + return s.db. + Select("rp.permission_id"). + Table("role_permissions rp"). + Joins("JOIN user_roles ur ON rp.role_id = ur.role_id"). + Where("ur.user_id = ? AND rp.permission_id = ?", userID, permissionID) +} + +func (s *dbBackedMiddlewareService) buildTeamRolePermissionQuery(userID int, permissionID int, teamID int) *gorm.DB { + return s.db. + Select("rp.permission_id"). + Table("role_permissions rp"). + Joins("JOIN user_teams ut ON rp.role_id = ut.role_id"). + Where("ut.user_id = ? AND ut.team_id = ? AND rp.permission_id = ?", userID, teamID, permissionID). + Where("ut.status = ?", consts.CommonEnabled) +} + +func (s *dbBackedMiddlewareService) buildProjectRolePermissionQuery(userID int, permissionID int, projectID int) *gorm.DB { + return s.db. + Select("rp.permission_id"). + Table("role_permissions rp"). + Joins("JOIN user_projects upr ON rp.role_id = upr.role_id"). + Where("upr.user_id = ? AND upr.project_id = ? AND rp.permission_id = ?", userID, projectID, permissionID). + Where("upr.status = ?", consts.CommonEnabled) +} + +func (s *dbBackedMiddlewareService) buildContainerRolePermissionQuery(userID int, permissionID int, containerID int) *gorm.DB { + return s.db. + Select("rp.permission_id"). + Table("role_permissions rp"). + Joins("JOIN user_containers uc ON rp.role_id = uc.role_id"). + Where("uc.user_id = ? AND uc.container_id = ? AND rp.permission_id = ?", userID, containerID, permissionID). + Where("uc.status = ?", consts.CommonEnabled) +} + +func (s *dbBackedMiddlewareService) buildDatasetRolePermissionQuery(userID int, permissionID int, datasetID int) *gorm.DB { + return s.db. + Select("rp.permission_id"). + Table("role_permissions rp"). + Joins("JOIN user_datasets ud ON rp.role_id = ud.role_id"). + Where("ud.user_id = ? AND ud.dataset_id = ? AND rp.permission_id = ?", userID, datasetID, permissionID). + Where("ud.status = ?", consts.CommonEnabled) +} + +func (s *dbBackedMiddlewareService) getUserTeamRole(userID, teamID int) (*model.UserTeam, error) { + var userTeam model.UserTeam + if err := s.db.Preload("Role"). + Where("user_id = ? AND team_id = ? AND status = ?", userID, teamID, consts.CommonEnabled). + First(&userTeam).Error; err != nil { + return nil, err + } + return &userTeam, nil +} + +func (s *dbBackedMiddlewareService) getTeamByID(teamID int) (*model.Team, error) { + var team model.Team + if err := s.db.Where("id = ?", teamID).First(&team).Error; err != nil { + return nil, err + } + return &team, nil +} + +func (s *dbBackedMiddlewareService) getUserProjectRole(userID, projectID int) (*model.UserProject, error) { + var userProject model.UserProject + if err := s.db.Preload("Role"). + Where("user_id = ? AND project_id = ? AND status = ?", userID, projectID, consts.CommonEnabled). + First(&userProject).Error; err != nil { + return nil, err + } + return &userProject, nil +} + +func (s *dbBackedMiddlewareService) getResourceByName(db *gorm.DB, resourceName consts.ResourceName) (*model.Resource, error) { + var resource model.Resource + if err := db.Where("name = ? AND status != ?", resourceName, consts.CommonDeleted).First(&resource).Error; err != nil { + return nil, err + } + return &resource, nil +} + +func (s *dbBackedMiddlewareService) createAuditLog(db *gorm.DB, log *model.AuditLog) error { + return db.Create(log).Error +} + +type noopPermissionChecker struct{} + +func (noopPermissionChecker) CheckUserPermission(*dto.CheckPermissionParams) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopPermissionChecker) IsUserTeamAdmin(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopPermissionChecker) IsUserInTeam(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopPermissionChecker) IsTeamPublic(int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopPermissionChecker) IsUserProjectAdmin(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopPermissionChecker) IsUserInProject(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} + +type noopMiddlewareService struct{} + +func (noopMiddlewareService) CheckUserPermission(*dto.CheckPermissionParams) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopMiddlewareService) IsUserTeamAdmin(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopMiddlewareService) IsUserInTeam(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopMiddlewareService) IsTeamPublic(int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopMiddlewareService) IsUserProjectAdmin(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} +func (noopMiddlewareService) IsUserInProject(int, int) (bool, error) { + return false, fmt.Errorf("permission checker not initialized") +} + +type noopAuditLogger struct{} + +func (noopAuditLogger) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { + return fmt.Errorf("audit logger not initialized") +} +func (noopAuditLogger) LogUserAction(string, string, string, string, int, int, consts.ResourceName) error { + return fmt.Errorf("audit logger not initialized") +} + +func (noopMiddlewareService) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { + return fmt.Errorf("audit logger not initialized") +} +func (noopMiddlewareService) LogUserAction(string, string, string, string, int, int, consts.ResourceName) error { + return fmt.Errorf("audit logger not initialized") +} diff --git a/src/middleware/permission.go b/src/middleware/permission.go index 0ec009c0..5e282a65 100644 --- a/src/middleware/permission.go +++ b/src/middleware/permission.go @@ -7,7 +7,6 @@ import ( "aegis/consts" "aegis/dto" - "aegis/service/producer" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" @@ -17,6 +16,7 @@ type permissionContext struct { userID int isAdmin bool roles []string + checker permissionChecker teamID *int projectID *int containerID *int @@ -63,6 +63,7 @@ func extractPermissionContext(c *gin.Context) (*permissionContext, string) { userID: userID, isAdmin: isAdmin, roles: roles, + checker: permissionCheckerFromContext(c), } // Extract optional IDs from URL parameters @@ -143,7 +144,7 @@ func withPermissionCheck(checkFunc permissionCheckFunc) gin.HandlerFunc { // singlePermission creates a check for a single permission func singlePermission(permission consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { - return producer.CheckUserPermission(&dto.CheckPermissionParams{ + return ctx.checker.CheckUserPermission(&dto.CheckPermissionParams{ UserID: ctx.userID, Action: permission.Action, Scope: permission.Scope, @@ -161,7 +162,7 @@ func singlePermission(permission consts.PermissionRule) permissionCheckFunc { func anyPermission(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { - hasPermission, err := producer.CheckUserPermission( + hasPermission, err := ctx.checker.CheckUserPermission( &dto.CheckPermissionParams{ UserID: ctx.userID, Action: perm.Action, @@ -189,7 +190,7 @@ func anyPermission(permissions []consts.PermissionRule) permissionCheckFunc { func allPermissions(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { - hasPermission, err := producer.CheckUserPermission( + hasPermission, err := ctx.checker.CheckUserPermission( &dto.CheckPermissionParams{ UserID: ctx.userID, Action: perm.Action, @@ -267,7 +268,7 @@ func teamAccessCheck(requireAdmin bool) permissionCheckFunc { // If admin access required, check team admin status if requireAdmin { - isTeamAdmin, err := producer.IsUserTeamAdmin(ctx.userID, *ctx.teamID) + isTeamAdmin, err := ctx.checker.IsUserTeamAdmin(ctx.userID, *ctx.teamID) if err != nil { return false, err } @@ -275,13 +276,13 @@ func teamAccessCheck(requireAdmin bool) permissionCheckFunc { } // For member access: check if member OR team is public - isMember, err := producer.IsUserInTeam(ctx.userID, *ctx.teamID) + isMember, err := ctx.checker.IsUserInTeam(ctx.userID, *ctx.teamID) if err == nil && isMember { return true, nil } // Check if team is public - isPublic, err := producer.IsTeamPublic(*ctx.teamID) + isPublic, err := ctx.checker.IsTeamPublic(*ctx.teamID) if err == nil && isPublic { return true, nil } @@ -304,7 +305,7 @@ func projectAccessCheck(requireAdmin bool) permissionCheckFunc { // Check project admin status if required if requireAdmin { - isProjectAdmin, err := producer.IsUserProjectAdmin(ctx.userID, *ctx.projectID) + isProjectAdmin, err := ctx.checker.IsUserProjectAdmin(ctx.userID, *ctx.projectID) if err != nil { return false, err } @@ -312,7 +313,7 @@ func projectAccessCheck(requireAdmin bool) permissionCheckFunc { } // Check if user is project member - isMember, err := producer.IsUserInProject(ctx.userID, *ctx.projectID) + isMember, err := ctx.checker.IsUserInProject(ctx.userID, *ctx.projectID) if err != nil { return false, err } diff --git a/src/database/entity.go b/src/model/entity.go similarity index 98% rename from src/database/entity.go rename to src/model/entity.go index 7d78838a..322b2e5c 100644 --- a/src/database/entity.go +++ b/src/model/entity.go @@ -1,4 +1,4 @@ -package database +package model import ( "fmt" @@ -412,6 +412,25 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { return nil } +type UserAccessKey struct { + ID int `gorm:"primaryKey;autoIncrement"` + UserID int `gorm:"not null;index:idx_user_access_key_owner_status"` + Name string `gorm:"not null;size:128"` + Description string `gorm:"type:text"` + AccessKey string `gorm:"not null;size:64"` + SecretHash string `gorm:"not null;size:255"` + SecretCiphertext string `gorm:"not null;type:text"` + LastUsedAt *time.Time + ExpiresAt *time.Time + Status consts.StatusType `gorm:"not null;default:1;index:idx_user_access_key_owner_status"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + ActiveAccessKey string `gorm:"type:varchar(64) GENERATED ALWAYS AS (CASE WHEN status >= 0 THEN access_key ELSE NULL END) VIRTUAL;uniqueIndex:idx_active_user_access_key"` + + User *User `gorm:"foreignKey:UserID"` +} + // Role table type Role struct { ID int `gorm:"primaryKey;autoIncrement"` // Unique identifier diff --git a/src/database/entity_helper.go b/src/model/entity_helper.go similarity index 98% rename from src/database/entity_helper.go rename to src/model/entity_helper.go index 4e1cc2e7..3f5f6ca7 100644 --- a/src/database/entity_helper.go +++ b/src/model/entity_helper.go @@ -1,4 +1,4 @@ -package database +package model import ( "database/sql/driver" diff --git a/src/model/view.go b/src/model/view.go new file mode 100644 index 00000000..44dd4e9d --- /dev/null +++ b/src/model/view.go @@ -0,0 +1,46 @@ +package model + +import ( + "time" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" +) + +// FaultInjectionNoIssues view model +type FaultInjectionNoIssues struct { + ID int `gorm:"column:datapack_id"` + Name string `gorm:"column:datapack_name"` + FaultType chaos.ChaosType `gorm:"column:fault_type"` + Category chaos.SystemType `gorm:"column:category"` + EngineConfig string `gorm:"column:engine_config"` + LabelKey string `gorm:"column:label_key"` + LabelValue string `gorm:"column:value_key"` + CreatedAt time.Time `gorm:"column:created_at"` +} + +func (FaultInjectionNoIssues) TableName() string { + return "fault_injection_no_issues" +} + +// FaultInjectionWithIssues view model +type FaultInjectionWithIssues struct { + ID int `gorm:"column:datapack_id"` + Name string `gorm:"column:datapack_name"` + FaultType chaos.ChaosType `gorm:"column:fault_type"` + Category chaos.SystemType `gorm:"column:category"` + EngineConfig string `gorm:"column:engine_config"` + LabelKey string `gorm:"column:label_key"` + LabelValue string `gorm:"column:value_key"` + CreatedAt time.Time `gorm:"column:created_at"` + Issues string `gorm:"column:issues"` + AbnormalAvgDuration float64 `gorm:"column:abnormal_avg_duration"` + NormalAvgDuration float64 `gorm:"column:normal_avg_duration"` + AbnormalSuccRate float64 `gorm:"column:abnormal_succ_rate"` + NormalSuccRate float64 `gorm:"column:normal_succ_rate"` + AbnormalP99 float64 `gorm:"column:abnormal_p99"` + NormalP99 float64 `gorm:"column:normal_p99"` +} + +func (FaultInjectionWithIssues) TableName() string { + return "fault_injection_with_issues" +} diff --git a/src/module/auth/api_types.go b/src/module/auth/api_types.go new file mode 100644 index 00000000..11508040 --- /dev/null +++ b/src/module/auth/api_types.go @@ -0,0 +1,259 @@ +package authmodule + +import ( + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + usermodule "aegis/module/user" +) + +const usernamePattern = `^[a-zA-Z0-9_]{3,20}$` + +type RegisterReq struct { + Username string `json:"username" binding:"required" example:"newuser"` + Email string `json:"email" binding:"required,email" example:"user@example.com"` + Password string `json:"password" binding:"required,min=8" example:"password123"` +} + +func (req *RegisterReq) Validate() error { + usernameRegex := regexp.MustCompile(usernamePattern) + if !usernameRegex.MatchString(req.Username) { + return fmt.Errorf("username must be 3-20 characters and contain only letters, numbers, and underscores") + } + if len(req.Password) == 0 { + return fmt.Errorf("password is required") + } + if len(req.Password) < 8 { + return fmt.Errorf("password must be at least 8 characters long") + } + return nil +} + +type LoginReq struct { + Username string `json:"username" binding:"required" example:"admin"` + Password string `json:"password" binding:"required" example:"password123"` +} + +func (req *LoginReq) Validate() error { + usernameRegex := regexp.MustCompile(usernamePattern) + if !usernameRegex.MatchString(req.Username) { + return fmt.Errorf("invalid username or password") + } + if req.Password == "" { + return fmt.Errorf("invalid username or password") + } + return nil +} + +type TokenRefreshReq struct { + Token string `json:"token" binding:"required" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` +} + +func (req *TokenRefreshReq) Validate() error { + if req.Token == "" { + return fmt.Errorf("invalid token") + } + return nil +} + +type ChangePasswordReq struct { + OldPassword string `json:"old_password" binding:"required" example:"oldpassword123"` + NewPassword string `json:"new_password" binding:"required,min=8" example:"newpassword123"` +} + +func (req *ChangePasswordReq) Validate() error { + if req.OldPassword == "" { + return fmt.Errorf("old_password is required") + } + if len(req.OldPassword) < 8 { + return fmt.Errorf("old_password must be at least 8 characters long") + } + if req.NewPassword == "" { + return fmt.Errorf("new_password is required") + } + if len(req.NewPassword) < 8 { + return fmt.Errorf("new_password must be at least 8 characters long") + } + return nil +} + +type CreateAccessKeyReq struct { + Name string `json:"name" binding:"required" example:"ci-bot"` + Description string `json:"description,omitempty" example:"SDK credential for CI pipeline"` + ExpiresAt *time.Time `json:"expires_at,omitempty" example:"2026-12-31T23:59:59Z"` +} + +func (req *CreateAccessKeyReq) Validate() error { + if req == nil { + return fmt.Errorf("request is required") + } + if req.Name == "" { + return fmt.Errorf("name is required") + } + if len(req.Name) > 128 { + return fmt.Errorf("name must be no more than 128 characters long") + } + if req.ExpiresAt != nil && req.ExpiresAt.Before(time.Now()) { + return fmt.Errorf("expires_at must be in the future") + } + return nil +} + +type ListAccessKeyReq struct { + dto.PaginationReq +} + +func (req *ListAccessKeyReq) Validate() error { + if req == nil { + return fmt.Errorf("request is required") + } + return req.PaginationReq.Validate() +} + +type AccessKeyTokenReq struct { + AccessKey string `header:"X-Access-Key" example:"ak_1234567890abcdef"` + Timestamp string `header:"X-Timestamp" example:"1713333333"` + Nonce string `header:"X-Nonce" example:"abc123"` + Signature string `header:"X-Signature" example:"4cf2f2cbb93d..."` +} + +func (req *AccessKeyTokenReq) Validate() error { + if req == nil { + return fmt.Errorf("request is required") + } + req.AccessKey = strings.TrimSpace(req.AccessKey) + req.Timestamp = strings.TrimSpace(req.Timestamp) + req.Nonce = strings.TrimSpace(req.Nonce) + req.Signature = strings.ToLower(strings.TrimSpace(req.Signature)) + if req.AccessKey == "" || req.Timestamp == "" || req.Nonce == "" || req.Signature == "" { + return fmt.Errorf("X-Access-Key, X-Timestamp, X-Nonce and X-Signature are required") + } + if _, err := strconv.ParseInt(req.Timestamp, 10, 64); err != nil { + return fmt.Errorf("X-Timestamp must be a unix timestamp in seconds") + } + if len(req.Nonce) > 128 { + return fmt.Errorf("X-Nonce must be no more than 128 characters long") + } + return nil +} + +func (req *AccessKeyTokenReq) TimestampUnix() (int64, error) { + return strconv.ParseInt(req.Timestamp, 10, 64) +} + +func (req *AccessKeyTokenReq) CanonicalString(method, path string) string { + return strings.Join([]string{ + strings.ToUpper(method), + path, + req.AccessKey, + req.Timestamp, + req.Nonce, + }, "\n") +} + +type LoginResp struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` + ExpiresAt time.Time `json:"expires_at" example:"2024-12-31T23:59:59Z"` + User UserInfo `json:"user"` +} + +type TokenRefreshResp struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` + ExpiresAt time.Time `json:"expires_at" example:"2024-12-31T23:59:59Z"` +} + +type AccessKeyInfo struct { + ID int `json:"id" example:"12"` + Name string `json:"name" example:"ci-bot"` + Description string `json:"description,omitempty" example:"SDK credential for CI pipeline"` + AccessKey string `json:"access_key" example:"ak_1234567890abcdef"` + Status consts.StatusType `json:"status" example:"1"` + LastUsedAt *time.Time `json:"last_used_at,omitempty" example:"2026-04-17T12:00:00Z"` + ExpiresAt *time.Time `json:"expires_at,omitempty" example:"2026-12-31T23:59:59Z"` + CreatedAt time.Time `json:"created_at" example:"2026-04-17T11:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2026-04-17T11:00:00Z"` +} + +func NewAccessKeyInfo(key *model.UserAccessKey) *AccessKeyInfo { + if key == nil { + return nil + } + return &AccessKeyInfo{ + ID: key.ID, + Name: key.Name, + Description: key.Description, + AccessKey: key.AccessKey, + Status: key.Status, + LastUsedAt: key.LastUsedAt, + ExpiresAt: key.ExpiresAt, + CreatedAt: key.CreatedAt, + UpdatedAt: key.UpdatedAt, + } +} + +type AccessKeyWithSecretResp struct { + AccessKeyInfo + SecretKey string `json:"secret_key" example:"sk_abcdefghijklmnopqrstuvwxyz123456"` +} + +type ListAccessKeyResp struct { + Items []AccessKeyInfo `json:"items"` + Pagination dto.PaginationInfo `json:"pagination"` +} + +type AccessKeyTokenResp struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access_key.jwt"` + TokenType string `json:"token_type" example:"Bearer"` + ExpiresAt time.Time `json:"expires_at" example:"2026-04-17T12:00:00Z"` + AuthType string `json:"auth_type" example:"access_key"` + AccessKey string `json:"access_key" example:"ak_1234567890abcdef"` +} + +type UserProfileResp struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FullName string `json:"full_name"` + Avatar string `json:"avatar,omitempty"` + Phone string `json:"phone,omitempty"` + LastLoginAt *time.Time `json:"last_login_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + + ContainerRoles []usermodule.UserContainerInfo `json:"container_roles,omitempty"` + DatasetRoles []usermodule.UserDatasetInfo `json:"dataset_roles,omitempty"` + ProjectRoles []usermodule.UserProjectInfo `json:"project_roles,omitempty"` +} + +func NewUserProfileResp(user *model.User) *UserProfileResp { + return &UserProfileResp{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + FullName: user.FullName, + Avatar: user.Avatar, + Phone: user.Phone, + LastLoginAt: user.LastLoginAt, + CreatedAt: user.CreatedAt, + } +} + +type UserInfo struct { + ID int `json:"id" example:"1"` + Username string `json:"username" example:"admin"` + Avatar string `json:"avatar,omitempty"` + Role string `json:"role,omitempty"` +} + +func NewUserInfo(user *model.User) *UserInfo { + return &UserInfo{ + ID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + } +} diff --git a/src/module/auth/handler.go b/src/module/auth/handler.go new file mode 100644 index 00000000..a80ff41e --- /dev/null +++ b/src/module/auth/handler.go @@ -0,0 +1,506 @@ +package authmodule + +import ( + "aegis/httpx" + "net/http" + + "aegis/consts" + "aegis/dto" + "aegis/middleware" + "aegis/utils" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// Login handles user authentication +// +// @Summary User login +// @Description Authenticate user with username and password +// @Tags Authentication +// @ID login +// @Accept json +// @Produce json +// @Param request body LoginReq true "Login credentials" +// @Success 200 {object} dto.GenericResponse[LoginResp] "Login successful" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid user name or password" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/login [post] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) Login(c *gin.Context) { + var req LoginReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusUnauthorized, err.Error()) + return + } + + resp, err := h.service.Login(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusOK, "Login successful", resp) +} + +// Register handles user registration +// +// @Summary User registration +// @Description Register a new user account +// @Tags Authentication +// @ID register_user +// @Accept json +// @Produce json +// @Param request body RegisterReq true "Registration details" +// @Success 201 {object} dto.GenericResponse[UserInfo] "Registration successful" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 409 {object} dto.GenericResponse[any] "User already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/register [post] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) Register(c *gin.Context) { + var req RegisterReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.Register(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusCreated, "Registration successful", resp) +} + +// RefreshToken handles JWT token refresh +// +// @Summary Refresh JWT token +// @Description Refresh an existing JWT token +// @Tags Authentication +// @ID refresh_auth_token +// @Accept json +// @Produce json +// @Param request body TokenRefreshReq true "Token refresh request" +// @Success 200 {object} dto.GenericResponse[TokenRefreshResp] "Token refreshed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/refresh [post] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) RefreshToken(c *gin.Context) { + var req TokenRefreshReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusUnauthorized, err.Error()) + return + } + + resp, err := h.service.RefreshToken(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusOK, "Token refreshed successfully", resp) +} + +// Logout handles user logout +// +// @Summary User logout +// @Description Logout user and invalidate token +// @Tags Authentication +// @ID logout +// @Produce json +// @Success 200 {object} dto.GenericResponse[any] "Logout successful" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid authorization header" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/logout [post] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) Logout(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + token, err := utils.ExtractTokenFromHeader(authHeader) + if err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid authorization header") + return + } + + claims, err := utils.ValidateToken(token) + if err != nil { + dto.ErrorResponse(c, http.StatusUnauthorized, "Invalid token") + return + } + + err = h.service.Logout(c.Request.Context(), claims) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse[any](c, http.StatusOK, "Logged out successfully", nil) +} + +// ChangePassword handles password change +// +// @Summary Change user password +// @Description Change password for authenticated user +// @Tags Authentication +// @ID change_password +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body ChangePasswordReq true "Password change request" +// @Success 200 {object} dto.GenericResponse[any] "Password changed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/change-password [post] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) ChangePassword(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + var req ChangePasswordReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + err := h.service.ChangePassword(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse[any](c, http.StatusOK, "Password changed successfully", nil) +} + +// GetProfile handles getting current user profile +// +// @Summary Get current user profile +// @Description Get profile information for authenticated user +// @Tags Authentication +// @ID get_current_user_profile +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.GenericResponse[UserProfileResp] "Profile retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/profile [get] +// @x-api-type {"portal":"true","admin":"true"} +func (h *Handler) GetProfile(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + resp, err := h.service.GetProfile(c.Request.Context(), userID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusOK, "Profile retrieved successfully", resp) +} + +// CreateAccessKey handles access key creation for the current user. +// +// @Summary Create access key +// @Description Create an AK/SK credential for the current authenticated user. This Portal response is the only time the `secret_key` is returned in plaintext, so callers must save it immediately. +// @Tags Authentication +// @ID create_access_key +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateAccessKeyReq true "Access key create request" +// @Success 201 {object} dto.GenericResponse[AccessKeyWithSecretResp] "Access key created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys [post] +// @x-api-type {"portal":"true"} +func (h *Handler) CreateAccessKey(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + var req CreateAccessKeyReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.CreateAccessKey(c.Request.Context(), userID, &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusCreated, "Access key created successfully", resp) +} + +// ListAccessKeys lists access keys for the current user. +// +// @Summary List access keys +// @Description List AK/SK credentials owned by the current authenticated user +// @Tags Authentication +// @ID list_access_keys +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" +// @Param size query int false "Page size" +// @Success 200 {object} dto.GenericResponse[ListAccessKeyResp] "Access keys listed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListAccessKeys(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + var req ListAccessKeyReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ListAccessKeys(c.Request.Context(), userID, &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} + +// GetAccessKey gets a single access key for the current user. +// +// @Summary Get access key detail +// @Description Get metadata for an AK/SK credential owned by the current authenticated user +// @Tags Authentication +// @ID get_access_key +// @Produce json +// @Security BearerAuth +// @Param access_key_id path int true "Access key ID" +// @Success 200 {object} dto.GenericResponse[AccessKeyInfo] "Access key detail retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys/{access_key_id} [get] +// @x-api-type {"portal":"true"} +func (h *Handler) GetAccessKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) + if !ok { + return + } + + resp, err := h.service.GetAccessKey(c.Request.Context(), userID, accessKeyID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} + +// DeleteAccessKey deletes an access key for the current user. +// +// @Summary Delete access key +// @Description Delete an AK/SK credential owned by the current authenticated user +// @Tags Authentication +// @ID delete_access_key +// @Produce json +// @Security BearerAuth +// @Param access_key_id path int true "Access key ID" +// @Success 204 {object} dto.GenericResponse[any] "Access key deleted successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys/{access_key_id} [delete] +// @x-api-type {"portal":"true"} +func (h *Handler) DeleteAccessKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) + if !ok { + return + } + + if httpx.HandleServiceError(c, h.service.DeleteAccessKey(c.Request.Context(), userID, accessKeyID)) { + return + } + + dto.JSONResponse[any](c, http.StatusNoContent, "Access key deleted successfully", nil) +} + +// DisableAccessKey disables an access key for the current user. +// +// @Summary Disable access key +// @Description Disable an AK/SK credential owned by the current authenticated user +// @Tags Authentication +// @ID disable_access_key +// @Produce json +// @Security BearerAuth +// @Param access_key_id path int true "Access key ID" +// @Success 200 {object} dto.GenericResponse[any] "Access key disabled successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys/{access_key_id}/disable [post] +// @x-api-type {"portal":"true"} +func (h *Handler) DisableAccessKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) + if !ok { + return + } + + if httpx.HandleServiceError(c, h.service.DisableAccessKey(c.Request.Context(), userID, accessKeyID)) { + return + } + + dto.JSONResponse[any](c, http.StatusOK, "Access key disabled successfully", nil) +} + +// EnableAccessKey enables an access key for the current user. +// +// @Summary Enable access key +// @Description Enable an AK/SK credential owned by the current authenticated user +// @Tags Authentication +// @ID enable_access_key +// @Produce json +// @Security BearerAuth +// @Param access_key_id path int true "Access key ID" +// @Success 200 {object} dto.GenericResponse[any] "Access key enabled successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys/{access_key_id}/enable [post] +// @x-api-type {"portal":"true"} +func (h *Handler) EnableAccessKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) + if !ok { + return + } + + if httpx.HandleServiceError(c, h.service.EnableAccessKey(c.Request.Context(), userID, accessKeyID)) { + return + } + + dto.JSONResponse[any](c, http.StatusOK, "Access key enabled successfully", nil) +} + +// RotateAccessKey rotates the secret key for an existing access key. +// +// @Summary Rotate access key secret +// @Description Rotate the secret half of an AK/SK credential owned by the current authenticated user +// @Tags Authentication +// @ID rotate_access_key +// @Produce json +// @Security BearerAuth +// @Param access_key_id path int true "Access key ID" +// @Success 200 {object} dto.GenericResponse[AccessKeyWithSecretResp] "Access key rotated successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/access-keys/{access_key_id}/rotate [post] +// @x-api-type {"portal":"true"} +func (h *Handler) RotateAccessKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) + if !ok { + return + } + + resp, err := h.service.RotateAccessKey(c.Request.Context(), userID, accessKeyID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusOK, "Access key rotated successfully", resp) +} + +// ExchangeAccessKeyToken exchanges AK/SK for a bearer token. +// +// @Summary Exchange access key for token +// @Description Exchange an AK/SK signed request for a short-lived bearer token. Access keys are created in Portal, while SDK and CLI callers use this endpoint with `X-Access-Key`, `X-Timestamp`, `X-Nonce`, and `X-Signature`. +// @Tags Authentication +// @ID exchange_access_key_token +// @Produce json +// @Param X-Access-Key header string true "Access key ID" +// @Param X-Timestamp header string true "Unix timestamp in seconds" +// @Param X-Nonce header string true "Unique request nonce" +// @Param X-Signature header string true "Hex encoded HMAC-SHA256 signature of METHOD\\nPATH\\nACCESS_KEY\\nTIMESTAMP\\nNONCE" +// @Success 200 {object} dto.GenericResponse[AccessKeyTokenResp] "Access key token issued successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid signature or replayed request" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/auth/access-key/token [post] +// @x-api-type {"sdk":"true"} +func (h *Handler) ExchangeAccessKeyToken(c *gin.Context) { + var req AccessKeyTokenReq + req.AccessKey = c.GetHeader("X-Access-Key") + req.Timestamp = c.GetHeader("X-Timestamp") + req.Nonce = c.GetHeader("X-Nonce") + req.Signature = c.GetHeader("X-Signature") + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ExchangeAccessKeyToken(c.Request.Context(), &req, c.Request.Method, c.Request.URL.Path) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusOK, "Access key token issued successfully", resp) +} + +func parseCurrentUserAndAccessKeyID(c *gin.Context) (int, int, bool) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return 0, 0, false + } + + accessKeyID, ok := httpx.ParsePositiveID(c, c.Param("access_key_id"), consts.URLPathID) + if !ok { + return 0, 0, false + } + + return userID, accessKeyID, true +} diff --git a/src/module/auth/module.go b/src/module/auth/module.go new file mode 100644 index 00000000..6abcccb7 --- /dev/null +++ b/src/module/auth/module.go @@ -0,0 +1,14 @@ +package authmodule + +import ( + "go.uber.org/fx" +) + +var Module = fx.Module("auth", + fx.Provide(NewUserRepository), + fx.Provide(NewRoleRepository), + fx.Provide(NewAccessKeyRepository), + fx.Provide(NewTokenStore), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/auth/repository.go b/src/module/auth/repository.go new file mode 100644 index 00000000..b965330d --- /dev/null +++ b/src/module/auth/repository.go @@ -0,0 +1,198 @@ +package authmodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + "time" + + "gorm.io/gorm" +) + +const ( + userOmitFields = "active_username" + userContainerOmitFields = "active_user_container" + userDatasetOmitFields = "active_user_dataset" + userProjectOmitFields = "active_user_project" +) + +type UserRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) withDB(db *gorm.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *UserRepository) Create(user *model.User) error { + if err := r.db.Omit(userOmitFields).Create(user).Error; err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + return nil +} + +func (r *UserRepository) GetByID(id int) (*model.User, error) { + var user model.User + if err := r.db.Where("id = ?", id).First(&user).Error; err != nil { + return nil, fmt.Errorf("failed to find user with id %d: %w", id, err) + } + return &user, nil +} + +func (r *UserRepository) GetByUsername(username string) (*model.User, error) { + var user model.User + if err := r.db.Where("username = ?", username).First(&user).Error; err != nil { + return nil, fmt.Errorf("failed to find user with username %s: %w", username, err) + } + return &user, nil +} + +func (r *UserRepository) GetByEmail(email string) (*model.User, error) { + var user model.User + if err := r.db.Where("email = ?", email).First(&user).Error; err != nil { + return nil, fmt.Errorf("failed to find user with email %s: %w", email, err) + } + return &user, nil +} + +func (r *UserRepository) Update(user *model.User) error { + if err := r.db.Omit(userOmitFields).Save(user).Error; err != nil { + return fmt.Errorf("failed to update user: %w", err) + } + return nil +} + +func (r *UserRepository) UpdateLoginTime(userID int) error { + now := r.db.NowFunc() + if err := r.db.Model(&model.User{}). + Where("id = ? AND status != ?", userID, consts.CommonDeleted). + Update("last_login_at", now).Error; err != nil { + return fmt.Errorf("failed to update user login time: %w", err) + } + return nil +} + +func (r *UserRepository) ListContainerRoles(userID int) ([]model.UserContainer, error) { + var userContainers []model.UserContainer + if err := r.db.Preload("Container"). + Preload("Role"). + Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). + Find(&userContainers).Error; err != nil { + return nil, fmt.Errorf("failed to get user-container associations of the specific user: %w", err) + } + return userContainers, nil +} + +func (r *UserRepository) ListDatasetRoles(userID int) ([]model.UserDataset, error) { + var userDatasets []model.UserDataset + if err := r.db.Preload("Dataset"). + Preload("Role"). + Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). + Find(&userDatasets).Error; err != nil { + return nil, fmt.Errorf("failed to get user-dataset associations of the specific user: %w", err) + } + return userDatasets, nil +} + +func (r *UserRepository) ListProjectRoles(userID int) ([]model.UserProject, error) { + var userProjects []model.UserProject + if err := r.db.Preload("Project"). + Preload("Role"). + Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). + Find(&userProjects).Error; err != nil { + return nil, fmt.Errorf("failed to get user-project associations of the specific user: %w", err) + } + return userProjects, nil +} + +type RoleRepository struct { + db *gorm.DB +} + +func NewRoleRepository(db *gorm.DB) *RoleRepository { + return &RoleRepository{db: db} +} + +func (r *RoleRepository) withDB(db *gorm.DB) *RoleRepository { + return &RoleRepository{db: db} +} + +func (r *RoleRepository) ListByUserID(userID int) ([]model.Role, error) { + var roles []model.Role + if err := r.db.Table("roles"). + Joins("JOIN user_roles ur ON ur.role_id = roles.id"). + Where("ur.user_id = ? AND roles.status = ?", userID, consts.CommonEnabled). + Find(&roles).Error; err != nil { + return nil, fmt.Errorf("failed to get global roles of the specific user: %w", err) + } + return roles, nil +} + +type AccessKeyRepository struct { + db *gorm.DB +} + +func NewAccessKeyRepository(db *gorm.DB) *AccessKeyRepository { + return &AccessKeyRepository{db: db} +} + +func (r *AccessKeyRepository) Create(key *model.UserAccessKey) error { + if err := r.db.Create(key).Error; err != nil { + return fmt.Errorf("failed to create access key: %w", err) + } + return nil +} + +func (r *AccessKeyRepository) ListByUserID(userID, limit, offset int) ([]model.UserAccessKey, int64, error) { + query := r.db.Model(&model.UserAccessKey{}). + Where("user_id = ? AND status != ?", userID, consts.CommonDeleted) + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count access keys: %w", err) + } + + var keys []model.UserAccessKey + if err := query.Order("id DESC").Limit(limit).Offset(offset).Find(&keys).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list access keys: %w", err) + } + return keys, total, nil +} + +func (r *AccessKeyRepository) GetByIDForUser(id, userID int) (*model.UserAccessKey, error) { + var key model.UserAccessKey + if err := r.db.Where("id = ? AND user_id = ? AND status != ?", id, userID, consts.CommonDeleted).First(&key).Error; err != nil { + return nil, fmt.Errorf("failed to find access key: %w", err) + } + return &key, nil +} + +func (r *AccessKeyRepository) GetByAccessKey(accessKey string) (*model.UserAccessKey, error) { + var key model.UserAccessKey + if err := r.db.Where("access_key = ? AND status != ?", accessKey, consts.CommonDeleted).First(&key).Error; err != nil { + return nil, fmt.Errorf("failed to find access key: %w", err) + } + return &key, nil +} + +func (r *AccessKeyRepository) Update(key *model.UserAccessKey) error { + if err := r.db.Save(key).Error; err != nil { + return fmt.Errorf("failed to update access key: %w", err) + } + return nil +} + +func (r *AccessKeyRepository) UpdateLastUsedAt(id int, usedAt time.Time) error { + if err := r.db.Model(&model.UserAccessKey{}).Where("id = ?", id).Update("last_used_at", usedAt).Error; err != nil { + return fmt.Errorf("failed to update access key last used time: %w", err) + } + return nil +} diff --git a/src/module/auth/service.go b/src/module/auth/service.go new file mode 100644 index 00000000..248e8d9a --- /dev/null +++ b/src/module/auth/service.go @@ -0,0 +1,524 @@ +package authmodule + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "time" + + "aegis/consts" + "aegis/model" + usermodule "aegis/module/user" + "aegis/utils" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +const accessKeySignatureTTL = 5 * time.Minute + +type Service struct { + userRepo *UserRepository + roleRepo *RoleRepository + accessKeyRepo *AccessKeyRepository + tokenStore *TokenStore +} + +func NewService(userRepo *UserRepository, roleRepo *RoleRepository, accessKeyRepo *AccessKeyRepository, tokenStore *TokenStore) *Service { + return &Service{ + userRepo: userRepo, + roleRepo: roleRepo, + accessKeyRepo: accessKeyRepo, + tokenStore: tokenStore, + } +} + +func (s *Service) Register(ctx context.Context, req *RegisterReq) (*UserInfo, error) { + if req == nil { + return nil, fmt.Errorf("register request is nil") + } + + var createdUser *model.User + err := s.userRepo.Transaction(func(tx *gorm.DB) error { + userRepo := s.userRepo.withDB(tx) + + if _, err := userRepo.GetByUsername(req.Username); err == nil { + return fmt.Errorf("%w: username is already taken", consts.ErrAlreadyExists) + } + + if _, err := userRepo.GetByEmail(req.Email); err == nil { + return fmt.Errorf("%w: email is already registered", consts.ErrAlreadyExists) + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + IsActive: true, + Status: consts.CommonEnabled, + } + + if err := userRepo.Create(user); err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + createdUser = user + return nil + }) + if err != nil { + return nil, err + } + + return NewUserInfo(createdUser), nil +} + +func (s *Service) Login(ctx context.Context, req *LoginReq) (*LoginResp, error) { + if req == nil { + return nil, fmt.Errorf("login request is nil") + } + + var loginedUser *model.User + var token string + var expiresAt time.Time + + err := s.userRepo.Transaction(func(tx *gorm.DB) error { + userRepo := s.userRepo.withDB(tx) + roleRepo := s.roleRepo.withDB(tx) + + user, err := userRepo.GetByUsername(req.Username) + if err != nil { + return fmt.Errorf("%w: invalid username or password", consts.ErrAuthenticationFailed) + } + + if !utils.VerifyPassword(req.Password, user.Password) { + return fmt.Errorf("%w: invalid username or password", consts.ErrAuthenticationFailed) + } + + token, expiresAt, err = s.generateTokenWithRoles(roleRepo, user) + if err != nil { + return err + } + + if err := userRepo.UpdateLoginTime(user.ID); err != nil { + logrus.Errorf("failed to update last login time for user %d: %v", user.ID, err) + } + + loginedUser = user + return nil + }) + if err != nil { + return nil, err + } + + roles, err := s.roleRepo.ListByUserID(loginedUser.ID) + if err != nil { + return nil, fmt.Errorf("failed to get user role: %w", err) + } + + if len(roles) == 0 { + return nil, fmt.Errorf("%w: user has no assigned role", consts.ErrPermissionDenied) + } + + info := NewUserInfo(loginedUser) + info.Role = roles[0].Name + + return &LoginResp{ + Token: token, + ExpiresAt: expiresAt, + User: *info, + }, nil +} + +func (s *Service) RefreshToken(ctx context.Context, req *TokenRefreshReq) (*TokenRefreshResp, error) { + if req == nil { + return nil, fmt.Errorf("token refresh request is nil") + } + + refreshClaims, err := utils.ValidateToken(req.Token) + if err != nil { + return nil, fmt.Errorf("token refresh failed: %w", err) + } + + user, err := s.userRepo.GetByID(refreshClaims.UserID) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + newToken, expiresAt, err := s.generateTokenWithRoles(s.roleRepo, user) + if err != nil { + return nil, err + } + + return &TokenRefreshResp{ + Token: newToken, + ExpiresAt: expiresAt, + }, nil +} + +func (s *Service) Logout(ctx context.Context, claims *utils.Claims) error { + metaData := map[string]any{ + "user_id": claims.UserID, + "reason": "User logout", + } + if err := s.tokenStore.AddTokenToBlacklist(ctx, claims.ID, claims.ExpiresAt.Time, metaData); err != nil { + logrus.Errorf("failed to add token to blacklist: %v", err) + return fmt.Errorf("failed to blacklist token: %w", err) + } + return nil +} + +func (s *Service) ChangePassword(ctx context.Context, req *ChangePasswordReq, userID int) error { + if req == nil { + return fmt.Errorf("change password request is nil") + } + + return s.userRepo.Transaction(func(tx *gorm.DB) error { + userRepo := s.userRepo.withDB(tx) + + user, err := userRepo.GetByID(userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return fmt.Errorf("failed to get user: %w", err) + } + + if !utils.VerifyPassword(req.OldPassword, user.Password) { + return fmt.Errorf("invalid old password") + } + + hashedPassword, err := utils.HashPassword(req.NewPassword) + if err != nil { + return fmt.Errorf("password hashing failed: %w", err) + } + user.Password = hashedPassword + + if err := userRepo.Update(user); err != nil { + return fmt.Errorf("failed to update password: %w", err) + } + + return nil + }) +} + +func (s *Service) GetProfile(ctx context.Context, userID int) (*UserProfileResp, error) { + user, err := s.userRepo.GetByID(userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + resp := NewUserProfileResp(user) + userContainers, userDatasets, userProjects, err := s.getAllUserResourceRoles(userID) + if err != nil { + return nil, fmt.Errorf("failed to get user resource roles: %w", err) + } + + resp.ContainerRoles = userContainers + resp.DatasetRoles = userDatasets + resp.ProjectRoles = userProjects + + return resp, nil +} + +func (s *Service) CreateAccessKey(ctx context.Context, userID int, req *CreateAccessKeyReq) (*AccessKeyWithSecretResp, error) { + if req == nil { + return nil, fmt.Errorf("access key create request is nil") + } + + accessKeyValue, err := generateCredentialValue("ak_", 16) + if err != nil { + return nil, fmt.Errorf("failed to generate access key: %w", err) + } + secretKeyValue, err := generateCredentialValue("sk_", 24) + if err != nil { + return nil, fmt.Errorf("failed to generate secret key: %w", err) + } + secretHash, err := utils.HashPassword(secretKeyValue) + if err != nil { + return nil, fmt.Errorf("failed to hash secret key: %w", err) + } + secretCiphertext, err := utils.EncryptAccessKeySecret(secretKeyValue) + if err != nil { + return nil, fmt.Errorf("failed to encrypt secret key: %w", err) + } + + key := &model.UserAccessKey{ + UserID: userID, + Name: req.Name, + Description: req.Description, + AccessKey: accessKeyValue, + SecretHash: secretHash, + SecretCiphertext: secretCiphertext, + ExpiresAt: req.ExpiresAt, + Status: consts.CommonEnabled, + } + if err := s.accessKeyRepo.Create(key); err != nil { + return nil, err + } + + resp := &AccessKeyWithSecretResp{ + AccessKeyInfo: *NewAccessKeyInfo(key), + SecretKey: secretKeyValue, + } + return resp, nil +} + +func (s *Service) ListAccessKeys(ctx context.Context, userID int, req *ListAccessKeyReq) (*ListAccessKeyResp, error) { + if req == nil { + return nil, fmt.Errorf("access key list request is nil") + } + + limit, offset := req.ToGormParams() + keys, total, err := s.accessKeyRepo.ListByUserID(userID, limit, offset) + if err != nil { + return nil, err + } + + items := make([]AccessKeyInfo, 0, len(keys)) + for i := range keys { + items = append(items, *NewAccessKeyInfo(&keys[i])) + } + + return &ListAccessKeyResp{ + Items: items, + Pagination: *req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*AccessKeyInfo, error) { + key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: access key not found", consts.ErrNotFound) + } + return nil, err + } + return NewAccessKeyInfo(key), nil +} + +func (s *Service) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { + key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: access key not found", consts.ErrNotFound) + } + return err + } + + key.Status = consts.CommonDeleted + return s.accessKeyRepo.Update(key) +} + +func (s *Service) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { + return s.setAccessKeyStatus(userID, accessKeyID, consts.CommonDisabled) +} + +func (s *Service) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { + return s.setAccessKeyStatus(userID, accessKeyID, consts.CommonEnabled) +} + +func (s *Service) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*AccessKeyWithSecretResp, error) { + key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: access key not found", consts.ErrNotFound) + } + return nil, err + } + + secretKeyValue, err := generateCredentialValue("sk_", 24) + if err != nil { + return nil, fmt.Errorf("failed to generate secret key: %w", err) + } + secretHash, err := utils.HashPassword(secretKeyValue) + if err != nil { + return nil, fmt.Errorf("failed to hash secret key: %w", err) + } + secretCiphertext, err := utils.EncryptAccessKeySecret(secretKeyValue) + if err != nil { + return nil, fmt.Errorf("failed to encrypt secret key: %w", err) + } + + key.SecretHash = secretHash + key.SecretCiphertext = secretCiphertext + if err := s.accessKeyRepo.Update(key); err != nil { + return nil, err + } + + return &AccessKeyWithSecretResp{ + AccessKeyInfo: *NewAccessKeyInfo(key), + SecretKey: secretKeyValue, + }, nil +} + +func (s *Service) ExchangeAccessKeyToken(ctx context.Context, req *AccessKeyTokenReq, method, path string) (*AccessKeyTokenResp, error) { + if req == nil { + return nil, fmt.Errorf("access key token request is nil") + } + + key, err := s.accessKeyRepo.GetByAccessKey(req.AccessKey) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: invalid access key or secret key", consts.ErrAuthenticationFailed) + } + return nil, err + } + + if key.Status != consts.CommonEnabled { + return nil, fmt.Errorf("%w: access key is disabled", consts.ErrAuthenticationFailed) + } + if key.ExpiresAt != nil && key.ExpiresAt.Before(time.Now()) { + return nil, fmt.Errorf("%w: access key is expired", consts.ErrAuthenticationFailed) + } + timestampUnix, err := req.TimestampUnix() + if err != nil { + return nil, fmt.Errorf("%w: invalid request timestamp", consts.ErrAuthenticationFailed) + } + now := time.Now() + requestTime := time.Unix(timestampUnix, 0) + if requestTime.Before(now.Add(-accessKeySignatureTTL)) || requestTime.After(now.Add(accessKeySignatureTTL)) { + return nil, fmt.Errorf("%w: request timestamp is outside the allowed window", consts.ErrAuthenticationFailed) + } + + secretKey, err := utils.DecryptAccessKeySecret(key.SecretCiphertext) + if err != nil { + return nil, fmt.Errorf("failed to decrypt access key secret: %w", err) + } + if !utils.VerifyAccessKeyRequestSignature(secretKey, req.CanonicalString(method, path), req.Signature) { + return nil, fmt.Errorf("%w: invalid access key signature", consts.ErrAuthenticationFailed) + } + if err := s.tokenStore.ReserveAccessKeyNonce(ctx, key.AccessKey, req.Nonce, accessKeySignatureTTL); err != nil { + return nil, err + } + + user, err := s.userRepo.GetByID(key.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: access key owner not found", consts.ErrAuthenticationFailed) + } + return nil, err + } + if !user.IsActive || user.Status != consts.CommonEnabled { + return nil, fmt.Errorf("%w: access key owner is inactive", consts.ErrAuthenticationFailed) + } + + token, expiresAt, err := s.generateAccessKeyTokenWithRoles(s.roleRepo, user, key.ID) + if err != nil { + return nil, err + } + + if err := s.accessKeyRepo.UpdateLastUsedAt(key.ID, time.Now()); err != nil { + logrus.WithError(err).Warn("failed to update access key last used time") + } + + return &AccessKeyTokenResp{ + Token: token, + TokenType: "Bearer", + ExpiresAt: expiresAt, + AuthType: "access_key", + AccessKey: key.AccessKey, + }, nil +} + +func (s *Service) generateTokenWithRoles(roleRepo *RoleRepository, user *model.User) (string, time.Time, error) { + roles, err := roleRepo.ListByUserID(user.ID) + if err != nil { + return "", time.Time{}, fmt.Errorf("failed to get user roles: %w", err) + } + + isAdmin := false + roleNames := make([]string, 0, len(roles)) + for _, role := range roles { + roleNames = append(roleNames, role.Name) + if role.Name == string(consts.RoleSuperAdmin) || role.Name == string(consts.RoleAdmin) { + isAdmin = true + } + } + + token, expiresAt, err := utils.GenerateToken(user.ID, user.Username, user.Email, user.IsActive, isAdmin, roleNames) + if err != nil { + return "", time.Time{}, fmt.Errorf("failed to generate token: %w", err) + } + + return token, expiresAt, nil +} + +func (s *Service) generateAccessKeyTokenWithRoles(roleRepo *RoleRepository, user *model.User, accessKeyID int) (string, time.Time, error) { + roles, err := roleRepo.ListByUserID(user.ID) + if err != nil { + return "", time.Time{}, fmt.Errorf("failed to get user roles: %w", err) + } + + isAdmin := false + roleNames := make([]string, 0, len(roles)) + for _, role := range roles { + roleNames = append(roleNames, role.Name) + if role.Name == string(consts.RoleSuperAdmin) || role.Name == string(consts.RoleAdmin) { + isAdmin = true + } + } + + token, expiresAt, err := utils.GenerateAccessKeyToken(user.ID, user.Username, user.Email, user.IsActive, isAdmin, roleNames, accessKeyID) + if err != nil { + return "", time.Time{}, fmt.Errorf("failed to generate access key token: %w", err) + } + + return token, expiresAt, nil +} + +func (s *Service) getAllUserResourceRoles(userID int) ([]usermodule.UserContainerInfo, []usermodule.UserDatasetInfo, []usermodule.UserProjectInfo, error) { + userContainers, err := s.userRepo.ListContainerRoles(userID) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to list user-container roles: %w", err) + } + containerRoles := make([]usermodule.UserContainerInfo, 0, len(userContainers)) + for _, uc := range userContainers { + containerRoles = append(containerRoles, *usermodule.NewUserContainerInfo(&uc)) + } + + userDatasets, err := s.userRepo.ListDatasetRoles(userID) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to list user-dataset roles: %w", err) + } + datasetRoles := make([]usermodule.UserDatasetInfo, 0, len(userDatasets)) + for _, ud := range userDatasets { + datasetRoles = append(datasetRoles, *usermodule.NewUserDatasetInfo(&ud)) + } + + userProjects, err := s.userRepo.ListProjectRoles(userID) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to list user-project roles: %w", err) + } + projectRoles := make([]usermodule.UserProjectInfo, 0, len(userProjects)) + for _, up := range userProjects { + projectRoles = append(projectRoles, *usermodule.NewUserProjectInfo(&up)) + } + + return containerRoles, datasetRoles, projectRoles, nil +} + +func (s *Service) setAccessKeyStatus(userID, accessKeyID int, status consts.StatusType) error { + key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: access key not found", consts.ErrNotFound) + } + return err + } + + key.Status = status + return s.accessKeyRepo.Update(key) +} + +func generateCredentialValue(prefix string, randomBytes int) (string, error) { + buf := make([]byte, randomBytes) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return prefix + hex.EncodeToString(buf), nil +} diff --git a/src/module/auth/service_test.go b/src/module/auth/service_test.go new file mode 100644 index 00000000..9bde36ef --- /dev/null +++ b/src/module/auth/service_test.go @@ -0,0 +1,230 @@ +package authmodule + +import ( + "database/sql/driver" + "fmt" + "regexp" + "testing" + "time" + + "aegis/consts" + "aegis/utils" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type passwordHashMatcher struct { + plain string +} + +func (m passwordHashMatcher) Match(v driver.Value) bool { + hash, ok := v.(string) + if !ok { + return false + } + return utils.VerifyPassword(m.plain, hash) +} + +func newAuthService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + service := NewService(NewUserRepository(db), NewRoleRepository(db), NewAccessKeyRepository(db), &TokenStore{}) + return service, mock, func() { + _ = sqlDB.Close() + } +} + +func TestAuthServiceRegisterSuccess(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE username = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs("new_user", 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE email = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs("new@example.com", 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `users` (`username`,`email`,`password`,`full_name`,`avatar`,`phone`,`last_login_at`,`is_active`,`status`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?,?,?,?,?)")). + WithArgs("new_user", "new@example.com", passwordHashMatcher{plain: "password123"}, "", "", "", nil, true, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(9, 1)) + mock.ExpectCommit() + + resp, err := service.Register(t.Context(), &RegisterReq{ + Username: "new_user", + Email: "new@example.com", + Password: "password123", + }) + + require.NoError(t, err) + require.Equal(t, "new_user", resp.Username) + require.Equal(t, 9, resp.ID) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAuthServiceLoginSuccess(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + now := time.Now() + hashedPassword, err := utils.HashPassword("password123") + require.NoError(t, err) + + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE username = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs("demo_user", 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(7, "demo_user", "demo@example.com", hashedPassword, "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT `roles`.`id`,`roles`.`name`,`roles`.`display_name`,`roles`.`description`,`roles`.`is_system`,`roles`.`status`,`roles`.`created_at`,`roles`.`updated_at`,`roles`.`active_name` FROM `roles` JOIN user_roles ur ON ur.role_id = roles.id WHERE ur.user_id = ? AND roles.status = ?")). + WithArgs(7, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(1, consts.RoleAdmin.String(), "Admin", "", true, consts.CommonEnabled, now, now, consts.RoleAdmin.String())) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `users` SET `last_login_at`=?,`updated_at`=? WHERE id = ? AND status != ?")). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), 7, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + mock.ExpectQuery(regexp.QuoteMeta("SELECT `roles`.`id`,`roles`.`name`,`roles`.`display_name`,`roles`.`description`,`roles`.`is_system`,`roles`.`status`,`roles`.`created_at`,`roles`.`updated_at`,`roles`.`active_name` FROM `roles` JOIN user_roles ur ON ur.role_id = roles.id WHERE ur.user_id = ? AND roles.status = ?")). + WithArgs(7, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(1, consts.RoleAdmin.String(), "Admin", "", true, consts.CommonEnabled, now, now, consts.RoleAdmin.String())) + + resp, err := service.Login(t.Context(), &LoginReq{ + Username: "demo_user", + Password: "password123", + }) + + require.NoError(t, err) + require.Equal(t, "demo_user", resp.User.Username) + require.Equal(t, consts.RoleAdmin.String(), resp.User.Role) + + claims, err := utils.ValidateToken(resp.Token) + require.NoError(t, err) + require.Equal(t, 7, claims.UserID) + require.True(t, claims.IsAdmin) + require.Contains(t, claims.Roles, consts.RoleAdmin.String()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAuthServiceRefreshTokenSuccess(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + now := time.Now() + token, _, err := utils.GenerateToken(7, "demo_user", "demo@example.com", true, false, []string{consts.RoleUser.String()}) + require.NoError(t, err) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs(7, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(7, "demo_user", "demo@example.com", "ignored", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT `roles`.`id`,`roles`.`name`,`roles`.`display_name`,`roles`.`description`,`roles`.`is_system`,`roles`.`status`,`roles`.`created_at`,`roles`.`updated_at`,`roles`.`active_name` FROM `roles` JOIN user_roles ur ON ur.role_id = roles.id WHERE ur.user_id = ? AND roles.status = ?")). + WithArgs(7, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(2, consts.RoleUser.String(), "User", "", true, consts.CommonEnabled, now, now, consts.RoleUser.String())) + + resp, err := service.RefreshToken(t.Context(), &TokenRefreshReq{Token: token}) + + require.NoError(t, err) + require.NotEmpty(t, resp.Token) + + claims, err := utils.ValidateToken(resp.Token) + require.NoError(t, err) + require.Equal(t, 7, claims.UserID) + require.Contains(t, claims.Roles, consts.RoleUser.String()) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAuthServiceCreateAccessKeySuccess(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `user_access_keys` (`user_id`,`name`,`description`,`access_key`,`secret_hash`,`secret_ciphertext`,`last_used_at`,`expires_at`,`status`,`created_at`,`updated_at`,`active_access_key`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")). + WithArgs(7, "ci-bot", "SDK credential", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), nil, nil, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). + WillReturnResult(sqlmock.NewResult(11, 1)) + mock.ExpectCommit() + + resp, err := service.CreateAccessKey(t.Context(), 7, &CreateAccessKeyReq{ + Name: "ci-bot", + Description: "SDK credential", + }) + + require.NoError(t, err) + require.Equal(t, 11, resp.ID) + require.Equal(t, "ci-bot", resp.Name) + require.NotEmpty(t, resp.AccessKey) + require.NotEmpty(t, resp.SecretKey) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAuthServiceExchangeAccessKeyTokenSuccess(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + now := time.Now() + secret := "sk_test_secret_123456" + secretHash, err := utils.HashPassword(secret) + require.NoError(t, err) + secretCiphertext, err := utils.EncryptAccessKeySecret(secret) + require.NoError(t, err) + req := &AccessKeyTokenReq{ + AccessKey: "ak_test_credential", + Timestamp: fmt.Sprintf("%d", now.Unix()), + Nonce: "nonce_123", + } + req.Signature = utils.SignAccessKeyRequest(secret, req.CanonicalString("POST", "/api/v2/auth/access-key/token")) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_access_keys` WHERE access_key = ? AND status != ? ORDER BY `user_access_keys`.`id` LIMIT ?")). + WithArgs("ak_test_credential", consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "user_id", "name", "description", "access_key", "secret_hash", "secret_ciphertext", "last_used_at", "expires_at", "status", "created_at", "updated_at", + }).AddRow(5, 7, "ci-bot", "SDK credential", "ak_test_credential", secretHash, secretCiphertext, nil, nil, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs(7, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(7, "demo_user", "demo@example.com", "ignored", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT `roles`.`id`,`roles`.`name`,`roles`.`display_name`,`roles`.`description`,`roles`.`is_system`,`roles`.`status`,`roles`.`created_at`,`roles`.`updated_at`,`roles`.`active_name` FROM `roles` JOIN user_roles ur ON ur.role_id = roles.id WHERE ur.user_id = ? AND roles.status = ?")). + WithArgs(7, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(2, consts.RoleUser.String(), "User", "", true, consts.CommonEnabled, now, now, consts.RoleUser.String())) + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_access_keys` SET `last_used_at`=?,`updated_at`=? WHERE id = ?")). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), 5). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + resp, err := service.ExchangeAccessKeyToken(t.Context(), req, "POST", "/api/v2/auth/access-key/token") + + require.NoError(t, err) + require.Equal(t, "Bearer", resp.TokenType) + require.Equal(t, "access_key", resp.AuthType) + + claims, err := utils.ValidateToken(resp.Token) + require.NoError(t, err) + require.Equal(t, 7, claims.UserID) + require.Equal(t, "access_key", claims.AuthType) + require.Equal(t, 5, claims.AccessKeyID) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/module/auth/token_store.go b/src/module/auth/token_store.go new file mode 100644 index 00000000..6f7e9b1a --- /dev/null +++ b/src/module/auth/token_store.go @@ -0,0 +1,58 @@ +package authmodule + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/consts" + redisinfra "aegis/infra/redis" +) + +const tokenBlacklistPrefix = "blacklist:token:%s" +const accessKeyNoncePrefix = "access_key:nonce:%s:%s" + +type TokenStore struct { + redis *redisinfra.Gateway +} + +func NewTokenStore(redis *redisinfra.Gateway) *TokenStore { + return &TokenStore{redis: redis} +} + +func (s *TokenStore) AddTokenToBlacklist(ctx context.Context, tokenID string, expiresAt time.Time, metaData map[string]any) error { + key := fmt.Sprintf(tokenBlacklistPrefix, tokenID) + + ttl := time.Until(expiresAt) + if ttl <= 0 { + return nil + } + + metaDataJSON, err := json.Marshal(metaData) + if err != nil { + return fmt.Errorf("failed to marshal metadata to JSON: %w", err) + } + + if err = s.redis.Set(ctx, key, string(metaDataJSON), ttl); err != nil { + return fmt.Errorf("failed to blacklist token in Redis: %w", err) + } + + return nil +} + +func (s *TokenStore) ReserveAccessKeyNonce(ctx context.Context, accessKey, nonce string, ttl time.Duration) error { + if s == nil || s.redis == nil { + return nil + } + + key := fmt.Sprintf(accessKeyNoncePrefix, accessKey, nonce) + ok, err := s.redis.SetNX(ctx, key, "1", ttl) + if err != nil { + return fmt.Errorf("failed to reserve access key nonce: %w", err) + } + if !ok { + return fmt.Errorf("%w: request nonce has already been used", consts.ErrAuthenticationFailed) + } + return nil +} diff --git a/src/dto/chaos_system.go b/src/module/chaossystem/api_types.go similarity index 84% rename from src/dto/chaos_system.go rename to src/module/chaossystem/api_types.go index 74fc4d94..68a795ab 100644 --- a/src/dto/chaos_system.go +++ b/src/module/chaossystem/api_types.go @@ -1,12 +1,14 @@ -package dto +package chaossystemmodule import ( - "aegis/database" "encoding/json" "time" + + "aegis/dto" + "aegis/model" ) -// CreateChaosSystemReq represents the request to create a new chaos system +// CreateChaosSystemReq represents the request to create a new chaos system. type CreateChaosSystemReq struct { Name string `json:"name" binding:"required"` DisplayName string `json:"display_name" binding:"required"` @@ -16,7 +18,7 @@ type CreateChaosSystemReq struct { Description string `json:"description"` } -// UpdateChaosSystemReq represents the request to update a chaos system +// UpdateChaosSystemReq represents the request to update a chaos system. type UpdateChaosSystemReq struct { DisplayName *string `json:"display_name"` NsPattern *string `json:"ns_pattern"` @@ -25,7 +27,7 @@ type UpdateChaosSystemReq struct { Description *string `json:"description"` } -// ChaosSystemResp represents a chaos system in API responses +// ChaosSystemResp represents a chaos system in API responses. type ChaosSystemResp struct { ID int `json:"id"` Name string `json:"name"` @@ -39,13 +41,13 @@ type ChaosSystemResp struct { UpdatedAt time.Time `json:"updated_at"` } -// ListChaosSystemReq represents the request to list chaos systems +// ListChaosSystemReq represents the request to list chaos systems. type ListChaosSystemReq struct { - PaginationReq + dto.PaginationReq } -// NewChaosSystemResp creates a ChaosSystemResp from a database System -func NewChaosSystemResp(s *database.System) *ChaosSystemResp { +// NewChaosSystemResp creates a ChaosSystemResp from a system model. +func NewChaosSystemResp(s *model.System) *ChaosSystemResp { return &ChaosSystemResp{ ID: s.ID, Name: s.Name, @@ -60,19 +62,19 @@ func NewChaosSystemResp(s *database.System) *ChaosSystemResp { } } -// UpsertSystemMetadataReq represents a single metadata upsert request +// UpsertSystemMetadataReq represents a single metadata upsert request. type UpsertSystemMetadataReq struct { - MetadataType string `json:"metadata_type" binding:"required"` // "service_endpoint", "java_class_method", "database_operation", "grpc_operation", "network_dependency" + MetadataType string `json:"metadata_type" binding:"required"` ServiceName string `json:"service_name" binding:"required"` Data json.RawMessage `json:"data" binding:"required"` } -// BulkUpsertSystemMetadataReq represents a bulk metadata upsert request +// BulkUpsertSystemMetadataReq represents a bulk metadata upsert request. type BulkUpsertSystemMetadataReq struct { Items []UpsertSystemMetadataReq `json:"items" binding:"required,dive"` } -// SystemMetadataResp represents system metadata in API responses +// SystemMetadataResp represents system metadata in API responses. type SystemMetadataResp struct { ID int `json:"id"` SystemName string `json:"system_name"` @@ -83,8 +85,8 @@ type SystemMetadataResp struct { UpdatedAt time.Time `json:"updated_at"` } -// NewSystemMetadataResp creates a SystemMetadataResp from a database SystemMetadata -func NewSystemMetadataResp(m *database.SystemMetadata) *SystemMetadataResp { +// NewSystemMetadataResp creates a SystemMetadataResp from a metadata model. +func NewSystemMetadataResp(m *model.SystemMetadata) *SystemMetadataResp { return &SystemMetadataResp{ ID: m.ID, SystemName: m.SystemName, diff --git a/src/handlers/v2/systems.go b/src/module/chaossystem/handler.go similarity index 69% rename from src/handlers/v2/systems.go rename to src/module/chaossystem/handler.go index f1c6ae8a..a6934303 100644 --- a/src/handlers/v2/systems.go +++ b/src/module/chaossystem/handler.go @@ -1,16 +1,23 @@ -package v2 +package chaossystemmodule import ( + "aegis/httpx" "net/http" "aegis/consts" "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // ListChaosSystemsHandler handles listing chaos systems with pagination // // @Summary List chaos systems @@ -21,28 +28,26 @@ import ( // @Security BearerAuth // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ChaosSystemResp]] "Systems retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ChaosSystemResp]] "Systems retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems [get] -func ListChaosSystemsHandler(c *gin.Context) { - var req dto.ListChaosSystemReq +// @x-api-type {"admin":"true"} +func (h *Handler) ListSystems(c *gin.Context) { + var req ListChaosSystemReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListChaosSystemsService(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListSystems(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -55,22 +60,21 @@ func ListChaosSystemsHandler(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "System ID" -// @Success 200 {object} dto.GenericResponse[dto.ChaosSystemResp] "System retrieved successfully" +// @Success 200 {object} dto.GenericResponse[ChaosSystemResp] "System retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid system ID" // @Failure 404 {object} dto.GenericResponse[any] "System not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id} [get] -func GetChaosSystemHandler(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") +// @x-api-type {"admin":"true"} +func (h *Handler) GetSystem(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") if !ok { return } - - resp, err := producer.GetChaosSystemService(id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetSystem(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -83,25 +87,24 @@ func GetChaosSystemHandler(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.CreateChaosSystemReq true "System creation request" -// @Success 201 {object} dto.GenericResponse[dto.ChaosSystemResp] "System created successfully" +// @Param request body CreateChaosSystemReq true "System creation request" +// @Success 201 {object} dto.GenericResponse[ChaosSystemResp] "System created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 409 {object} dto.GenericResponse[any] "System already exists" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems [post] -func CreateChaosSystemHandler(c *gin.Context) { - var req dto.CreateChaosSystemReq +// @x-api-type {"admin":"true"} +func (h *Handler) CreateSystem(c *gin.Context) { + var req CreateChaosSystemReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - - resp, err := producer.CreateChaosSystemService(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateSystem(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusCreated, "System created successfully", resp) } @@ -115,29 +118,27 @@ func CreateChaosSystemHandler(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "System ID" -// @Param request body dto.UpdateChaosSystemReq true "System update request" -// @Success 200 {object} dto.GenericResponse[dto.ChaosSystemResp] "System updated successfully" +// @Param request body UpdateChaosSystemReq true "System update request" +// @Success 200 {object} dto.GenericResponse[ChaosSystemResp] "System updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 404 {object} dto.GenericResponse[any] "System not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id} [put] -func UpdateChaosSystemHandler(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") +// @x-api-type {"admin":"true"} +func (h *Handler) UpdateSystem(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") if !ok { return } - - var req dto.UpdateChaosSystemReq + var req UpdateChaosSystemReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - - resp, err := producer.UpdateChaosSystemService(id, &req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateSystem(c.Request.Context(), id, &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -155,17 +156,15 @@ func UpdateChaosSystemHandler(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "System not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id} [delete] -func DeleteChaosSystemHandler(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") +// @x-api-type {"admin":"true"} +func (h *Handler) DeleteSystem(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") if !ok { return } - - err := producer.DeleteChaosSystemService(id) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteSystem(c.Request.Context(), id)) { return } - dto.JSONResponse[any](c, http.StatusOK, "System deleted successfully", nil) } @@ -179,29 +178,26 @@ func DeleteChaosSystemHandler(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "System ID" -// @Param request body dto.BulkUpsertSystemMetadataReq true "Metadata upsert request" +// @Param request body BulkUpsertSystemMetadataReq true "Metadata upsert request" // @Success 200 {object} dto.GenericResponse[any] "Metadata upserted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 404 {object} dto.GenericResponse[any] "System not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id}/metadata [post] -func UpsertChaosSystemMetadataHandler(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") +// @x-api-type {"admin":"true"} +func (h *Handler) UpsertMetadata(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") if !ok { return } - - var req dto.BulkUpsertSystemMetadataReq + var req BulkUpsertSystemMetadataReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - - err := producer.UpsertChaosSystemMetadataService(id, &req) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.UpsertMetadata(c.Request.Context(), id, &req)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Metadata upserted successfully", nil) } @@ -215,23 +211,20 @@ func UpsertChaosSystemMetadataHandler(c *gin.Context) { // @Security BearerAuth // @Param id path int true "System ID" // @Param type query string false "Metadata type filter" -// @Success 200 {object} dto.GenericResponse[[]dto.SystemMetadataResp] "Metadata retrieved successfully" +// @Success 200 {object} dto.GenericResponse[[]SystemMetadataResp] "Metadata retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid system ID" // @Failure 404 {object} dto.GenericResponse[any] "System not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id}/metadata [get] -func ListChaosSystemMetadataHandler(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") +// @x-api-type {"admin":"true"} +func (h *Handler) ListMetadata(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "system ID") if !ok { return } - - metadataType := c.Query("type") - - resp, err := producer.ListChaosSystemMetadataService(id, metadataType) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListMetadata(c.Request.Context(), id, c.Query("type")) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } diff --git a/src/module/chaossystem/module.go b/src/module/chaossystem/module.go new file mode 100644 index 00000000..ef1bdeca --- /dev/null +++ b/src/module/chaossystem/module.go @@ -0,0 +1,9 @@ +package chaossystemmodule + +import "go.uber.org/fx" + +var Module = fx.Module("chaos_system", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/chaossystem/repository.go b/src/module/chaossystem/repository.go new file mode 100644 index 00000000..f8c18994 --- /dev/null +++ b/src/module/chaossystem/repository.go @@ -0,0 +1,105 @@ +package chaossystemmodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) ListSystems(limit, offset int) ([]model.System, int64, error) { + var ( + systems []model.System + total int64 + ) + + query := r.db.Model(&model.System{}).Where("status != ?", consts.CommonDeleted) + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count systems: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&systems).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list systems: %w", err) + } + return systems, total, nil +} + +func (r *Repository) GetSystemByID(id int) (*model.System, error) { + var system model.System + if err := r.db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&system).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) + } + return nil, fmt.Errorf("failed to find system with id %d: %w", id, err) + } + return &system, nil +} + +func (r *Repository) CreateSystem(system *model.System) error { + if err := r.db.Create(system).Error; err != nil { + return fmt.Errorf("failed to create system: %w", err) + } + return nil +} + +func (r *Repository) UpdateSystem(id int, updates map[string]interface{}) error { + result := r.db.Model(&model.System{}). + Where("id = ? AND status != ?", id, consts.CommonDeleted). + Updates(updates) + if err := result.Error; err != nil { + return fmt.Errorf("failed to update system with id %d: %w", id, err) + } + if result.RowsAffected == 0 { + return fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) + } + return nil +} + +func (r *Repository) DeleteSystem(id int) error { + result := r.db.Model(&model.System{}). + Where("id = ? AND status != ?", id, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if err := result.Error; err != nil { + return fmt.Errorf("failed to delete system with id %d: %w", id, err) + } + if result.RowsAffected == 0 { + return fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) + } + return nil +} + +func (r *Repository) UpsertSystemMetadata(meta *model.SystemMetadata) error { + if err := r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "system_name"}, {Name: "metadata_type"}, {Name: "service_name"}}, + DoUpdates: clause.AssignmentColumns([]string{"data", "updated_at"}), + }).Create(meta).Error; err != nil { + var existing model.SystemMetadata + if findErr := r.db.Where("system_name = ? AND metadata_type = ? AND service_name = ?", + meta.SystemName, meta.MetadataType, meta.ServiceName).First(&existing).Error; findErr == nil { + return r.db.Model(&existing).Updates(map[string]any{"data": meta.Data}).Error + } + return fmt.Errorf("failed to upsert system metadata: %w", err) + } + return nil +} + +func (r *Repository) ListSystemMetadata(systemName, metadataType string) ([]model.SystemMetadata, error) { + var metas []model.SystemMetadata + query := r.db.Where("system_name = ?", systemName) + if metadataType != "" { + query = query.Where("metadata_type = ?", metadataType) + } + if err := query.Find(&metas).Error; err != nil { + return nil, fmt.Errorf("failed to list system metadata: %w", err) + } + return metas, nil +} diff --git a/src/service/producer/chaos_system.go b/src/module/chaossystem/service.go similarity index 54% rename from src/service/producer/chaos_system.go rename to src/module/chaossystem/service.go index 1c45bcbe..3dc5d7bc 100644 --- a/src/service/producer/chaos_system.go +++ b/src/module/chaossystem/service.go @@ -1,50 +1,53 @@ -package producer +package chaossystemmodule import ( + "context" "fmt" "regexp" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" + "aegis/model" chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" ) -// ListChaosSystemsService lists chaos systems with pagination -func ListChaosSystemsService(req *dto.ListChaosSystemReq) (*dto.ListResp[dto.ChaosSystemResp], error) { - limit, offset := req.ToGormParams() +type Service struct { + repo *Repository +} - systems, total, err := repository.ListSystems(database.DB, limit, offset) +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) ListSystems(_ context.Context, req *ListChaosSystemReq) (*dto.ListResp[ChaosSystemResp], error) { + limit, offset := req.ToGormParams() + systems, total, err := s.repo.ListSystems(limit, offset) if err != nil { return nil, fmt.Errorf("failed to list systems: %w", err) } - items := make([]dto.ChaosSystemResp, 0, len(systems)) - for _, s := range systems { - items = append(items, *dto.NewChaosSystemResp(&s)) + items := make([]ChaosSystemResp, 0, len(systems)) + for _, item := range systems { + items = append(items, *NewChaosSystemResp(&item)) } - return &dto.ListResp[dto.ChaosSystemResp]{ + return &dto.ListResp[ChaosSystemResp]{ Items: items, Pagination: req.ConvertToPaginationInfo(total), }, nil } -// GetChaosSystemService retrieves a single chaos system by ID -func GetChaosSystemService(id int) (*dto.ChaosSystemResp, error) { - system, err := repository.GetSystemByID(database.DB, id) +func (s *Service) GetSystem(_ context.Context, id int) (*ChaosSystemResp, error) { + system, err := s.repo.GetSystemByID(id) if err != nil { return nil, err } - return dto.NewChaosSystemResp(system), nil + return NewChaosSystemResp(system), nil } -// CreateChaosSystemService creates a new chaos system and registers it with chaos-experiment -func CreateChaosSystemService(req *dto.CreateChaosSystemReq) (*dto.ChaosSystemResp, error) { - // Validate regex patterns +func (s *Service) CreateSystem(_ context.Context, req *CreateChaosSystemReq) (*ChaosSystemResp, error) { if _, err := regexp.Compile(req.NsPattern); err != nil { return nil, fmt.Errorf("invalid ns_pattern regex: %w: %w", err, consts.ErrBadRequest) } @@ -52,7 +55,7 @@ func CreateChaosSystemService(req *dto.CreateChaosSystemReq) (*dto.ChaosSystemRe return nil, fmt.Errorf("invalid extract_pattern regex: %w: %w", err, consts.ErrBadRequest) } - system := &database.System{ + system := &model.System{ Name: req.Name, DisplayName: req.DisplayName, NsPattern: req.NsPattern, @@ -63,11 +66,9 @@ func CreateChaosSystemService(req *dto.CreateChaosSystemReq) (*dto.ChaosSystemRe Status: consts.CommonEnabled, } - if err := repository.CreateSystem(database.DB, system); err != nil { + if err := s.repo.CreateSystem(system); err != nil { return nil, fmt.Errorf("failed to create system: %w", err) } - - // Register with chaos-experiment if err := chaos.RegisterSystem(chaos.SystemConfig{ Name: system.Name, NsPattern: system.NsPattern, @@ -76,18 +77,16 @@ func CreateChaosSystemService(req *dto.CreateChaosSystemReq) (*dto.ChaosSystemRe logrus.WithError(err).Warnf("Failed to register system %s with chaos-experiment", system.Name) } - return dto.NewChaosSystemResp(system), nil + return NewChaosSystemResp(system), nil } -// UpdateChaosSystemService updates a chaos system and re-registers it -func UpdateChaosSystemService(id int, req *dto.UpdateChaosSystemReq) (*dto.ChaosSystemResp, error) { - system, err := repository.GetSystemByID(database.DB, id) +func (s *Service) UpdateSystem(_ context.Context, id int, req *UpdateChaosSystemReq) (*ChaosSystemResp, error) { + system, err := s.repo.GetSystemByID(id) if err != nil { return nil, err } updates := make(map[string]interface{}) - if req.DisplayName != nil { updates["display_name"] = *req.DisplayName } @@ -109,22 +108,17 @@ func UpdateChaosSystemService(id int, req *dto.UpdateChaosSystemReq) (*dto.Chaos if req.Description != nil { updates["description"] = *req.Description } - if len(updates) == 0 { - return dto.NewChaosSystemResp(system), nil + return NewChaosSystemResp(system), nil } - if err := repository.UpdateSystem(database.DB, id, updates); err != nil { + if err := s.repo.UpdateSystem(id, updates); err != nil { return nil, err } - - // Reload the system to get updated fields - system, err = repository.GetSystemByID(database.DB, id) + system, err = s.repo.GetSystemByID(id) if err != nil { return nil, err } - - // Re-register with chaos-experiment if err := chaos.RegisterSystem(chaos.SystemConfig{ Name: system.Name, NsPattern: system.NsPattern, @@ -133,70 +127,58 @@ func UpdateChaosSystemService(id int, req *dto.UpdateChaosSystemReq) (*dto.Chaos logrus.WithError(err).Warnf("Failed to re-register system %s with chaos-experiment", system.Name) } - return dto.NewChaosSystemResp(system), nil + return NewChaosSystemResp(system), nil } -// DeleteChaosSystemService soft-deletes a chaos system -func DeleteChaosSystemService(id int) error { - system, err := repository.GetSystemByID(database.DB, id) +func (s *Service) DeleteSystem(_ context.Context, id int) error { + system, err := s.repo.GetSystemByID(id) if err != nil { return err } - if system.IsBuiltin { return fmt.Errorf("cannot delete builtin system %s: %w", system.Name, consts.ErrBadRequest) } - - if err := repository.DeleteSystem(database.DB, id); err != nil { + if err := s.repo.DeleteSystem(id); err != nil { return err } - - // Unregister from chaos-experiment if err := chaos.UnregisterSystem(system.Name); err != nil { logrus.WithError(err).Warnf("Failed to unregister system %s from chaos-experiment", system.Name) } - return nil } -// UpsertChaosSystemMetadataService bulk upserts metadata for a system -func UpsertChaosSystemMetadataService(id int, req *dto.BulkUpsertSystemMetadataReq) error { - system, err := repository.GetSystemByID(database.DB, id) +func (s *Service) UpsertMetadata(_ context.Context, id int, req *BulkUpsertSystemMetadataReq) error { + system, err := s.repo.GetSystemByID(id) if err != nil { return err } for _, item := range req.Items { - meta := &database.SystemMetadata{ + meta := &model.SystemMetadata{ SystemName: system.Name, MetadataType: item.MetadataType, ServiceName: item.ServiceName, Data: string(item.Data), } - if err := repository.UpsertSystemMetadata(database.DB, meta); err != nil { + if err := s.repo.UpsertSystemMetadata(meta); err != nil { return fmt.Errorf("failed to upsert metadata (type=%s, service=%s): %w", item.MetadataType, item.ServiceName, err) } } - return nil } -// ListChaosSystemMetadataService lists metadata for a system, optionally filtered by type -func ListChaosSystemMetadataService(id int, metadataType string) ([]dto.SystemMetadataResp, error) { - system, err := repository.GetSystemByID(database.DB, id) +func (s *Service) ListMetadata(_ context.Context, id int, metadataType string) ([]SystemMetadataResp, error) { + system, err := s.repo.GetSystemByID(id) if err != nil { return nil, err } - - metas, err := repository.ListSystemMetadata(database.DB, system.Name, metadataType) + metas, err := s.repo.ListSystemMetadata(system.Name, metadataType) if err != nil { return nil, fmt.Errorf("failed to list system metadata: %w", err) } - - items := make([]dto.SystemMetadataResp, 0, len(metas)) - for _, m := range metas { - items = append(items, *dto.NewSystemMetadataResp(&m)) + items := make([]SystemMetadataResp, 0, len(metas)) + for _, meta := range metas { + items = append(items, *NewSystemMetadataResp(&meta)) } - return items, nil } diff --git a/src/module/container/api_types.go b/src/module/container/api_types.go new file mode 100644 index 00000000..70185560 --- /dev/null +++ b/src/module/container/api_types.go @@ -0,0 +1,667 @@ +package containermodule + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/utils" +) + +// CreateContainerReq represents container creation request. +type CreateContainerReq struct { + Name string `json:"name" binding:"required"` + Type *consts.ContainerType `json:"type"` + README string `json:"readme" binding:"omitempty"` + IsPublic *bool `json:"is_public"` + + VersionReq *CreateContainerVersionReq `json:"version" binding:"omitempty"` +} + +func (req *CreateContainerReq) Validate() error { + req.Name = strings.TrimSpace(req.Name) + + if req.Name == "" { + return fmt.Errorf("container name cannot be empty") + } + if req.IsPublic == nil { + req.IsPublic = utils.BoolPtr(true) + } + if req.Type == nil { + return fmt.Errorf("container type is required") + } + if err := validateContainerType(req.Type); err != nil { + return err + } + if req.VersionReq != nil { + if err := req.VersionReq.Validate(); err != nil { + return fmt.Errorf("invalid container version request: %v", err) + } + } + + return nil +} + +func (req *CreateContainerReq) ConvertToContainer() *model.Container { + container := &model.Container{ + Name: req.Name, + Type: *req.Type, + README: req.README, + IsPublic: *req.IsPublic, + Status: consts.CommonEnabled, + } + + if req.VersionReq != nil { + container.Versions = []model.ContainerVersion{ + *req.VersionReq.ConvertToContainerVersion(), + } + } + + return container +} + +// ListContainerReq represents container list query parameters. +type ListContainerReq struct { + dto.PaginationReq + Type *consts.ContainerType `form:"type"` + IsPublic *bool `form:"is_public"` + Status *consts.StatusType `form:"status"` +} + +func (req *ListContainerReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if err := validateContainerType(req.Type); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +// UpdateContainerReq represents container update request. +type UpdateContainerReq struct { + README *string `json:"readme" binding:"omitempty"` + IsPublic *bool `json:"is_public" binding:"omitempty"` + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *UpdateContainerReq) Validate() error { + return validateStatus(req.Status, true) +} + +func (req *UpdateContainerReq) PatchContainerModel(target *model.Container) { + if req.README != nil { + target.README = *req.README + } + if req.IsPublic != nil { + target.IsPublic = *req.IsPublic + } + if req.Status != nil { + target.Status = *req.Status + } +} + +// ManageContainerLabelReq represents container label management request. +type ManageContainerLabelReq struct { + AddLabels []dto.LabelItem `json:"add_labels" binding:"omitempty"` + RemoveLabels []string `json:"remove_labels" binding:"omitempty"` +} + +func (req *ManageContainerLabelReq) Validate() error { + if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { + return fmt.Errorf("at least one of add_labels or remove_labels must be provided") + } + if err := validateLabelItems(req.AddLabels); err != nil { + return err + } + for i, key := range req.RemoveLabels { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("empty label key at index %d in remove_labels", i) + } + } + return nil +} + +// ContainerResp represents basic container summary information. +type ContainerResp struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsPublic bool `json:"is_public"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +func NewContainerResp(container *model.Container) *ContainerResp { + resp := &ContainerResp{ + ID: container.ID, + Name: container.Name, + Type: consts.GetContainerTypeName(container.Type), + IsPublic: container.IsPublic, + Status: consts.GetStatusTypeName(container.Status), + CreatedAt: container.CreatedAt, + UpdatedAt: container.UpdatedAt, + } + + if len(container.Labels) > 0 { + resp.Labels = make([]dto.LabelItem, 0, len(container.Labels)) + for _, label := range container.Labels { + resp.Labels = append(resp.Labels, dto.LabelItem{Key: label.Key, Value: label.Value}) + } + } + return resp +} + +// ContainerDetailResp represents detailed container information. +type ContainerDetailResp struct { + ContainerResp + + README string `json:"readme"` + + Versions []ContainerVersionResp `json:"versions"` +} + +func NewContainerDetailResp(container *model.Container) *ContainerDetailResp { + return &ContainerDetailResp{ + ContainerResp: *NewContainerResp(container), + README: container.README, + } +} + +type CreateContainerVersionReq struct { + Name string `json:"name" binding:"required"` + GithubLink string `json:"github_link" binding:"omitempty"` + ImageRef string `json:"image_ref" binding:"required"` + Command string `json:"command" binding:"omitempty"` + EnvVarRequests []CreateParameterConfigReq `json:"env_vars" binding:"omitempty"` + HelmConfigRequest *CreateHelmConfigReq `json:"helm_config" binding:"omitempty"` +} + +func (req *CreateContainerVersionReq) Validate() error { + req.Name = strings.TrimSpace(req.Name) + req.ImageRef = strings.TrimSpace(req.ImageRef) + + if req.Name == "" { + return fmt.Errorf("name cannot be empty") + } + if req.ImageRef == "" { + return fmt.Errorf("docker image reference cannot be empty") + } + + if req.GithubLink != "" { + req.GithubLink = strings.TrimSpace(req.GithubLink) + if err := utils.IsValidGitHubLink(req.GithubLink); err != nil { + return fmt.Errorf("invalid github link: %s, %v", req.GithubLink, err) + } + } + if _, _, _, err := utils.ParseSemanticVersion(req.Name); err != nil { + return fmt.Errorf("invalid semantic version: %s, %v", req.Name, err) + } + if _, _, _, _, err := utils.ParseFullImageRefernce(req.ImageRef); err != nil { + return fmt.Errorf("invalid docker image reference: %s, %v", req.ImageRef, err) + } + + for idx, envVarReq := range req.EnvVarRequests { + if err := envVarReq.Validate(); err != nil { + return fmt.Errorf("invalid env var at index %d: %v", idx, err) + } + } + if req.HelmConfigRequest != nil { + if err := req.HelmConfigRequest.Validate(); err != nil { + return fmt.Errorf("invalid helm config: %v", err) + } + } + + return nil +} + +func (req *CreateContainerVersionReq) ConvertToContainerVersion() *model.ContainerVersion { + version := &model.ContainerVersion{ + Name: req.Name, + ImageRef: req.ImageRef, + Command: req.Command, + Status: consts.CommonEnabled, + } + + if len(req.EnvVarRequests) > 0 { + params := make([]model.ParameterConfig, 0, len(req.EnvVarRequests)) + for _, envVarReq := range req.EnvVarRequests { + params = append(params, *envVarReq.ConvertToParameterConfig()) + } + version.EnvVars = params + } + + if req.HelmConfigRequest != nil { + version.HelmConfig = req.HelmConfigRequest.ConvertToHelmConfig() + } + + return version +} + +type CreateHelmConfigReq struct { + Version string `json:"version" binding:"required"` + ChartName string `json:"chart_name" binding:"required"` + RepoName string `json:"repo_name" binding:"required"` + RepoURL string `json:"repo_url" binding:"required"` + DynamicValues []CreateParameterConfigReq `json:"dynamic_values" binding:"omitempty" swaggertype:"object"` +} + +func (req *CreateHelmConfigReq) Validate() error { + req.Version = strings.TrimSpace(req.Version) + req.ChartName = strings.TrimSpace(req.ChartName) + req.RepoName = strings.TrimSpace(req.RepoName) + req.RepoURL = strings.TrimSpace(req.RepoURL) + + if req.Version == "" { + if _, _, _, err := utils.ParseSemanticVersion(req.Version); err != nil { + return fmt.Errorf("invalid semantic version: %s, %v", req.Version, err) + } + } + if req.ChartName == "" { + return fmt.Errorf("chart name cannot be empty") + } + if req.RepoName == "" { + return fmt.Errorf("repository name cannot be empty") + } + if req.RepoURL == "" { + return fmt.Errorf("repository URL cannot be empty") + } + if _, err := url.ParseRequestURI(req.RepoURL); err != nil { + return fmt.Errorf("invalid repository URL: %s, %w", req.RepoURL, err) + } + for i, val := range req.DynamicValues { + if err := val.Validate(); err != nil { + return fmt.Errorf("invalid parameter config at index %d: %w", i, err) + } + } + + return nil +} + +func (req *CreateHelmConfigReq) ConvertToHelmConfig() *model.HelmConfig { + cfg := &model.HelmConfig{ + Version: req.Version, + ChartName: req.ChartName, + RepoName: req.RepoName, + RepoURL: req.RepoURL, + } + + if len(req.DynamicValues) > 0 { + params := make([]model.ParameterConfig, 0, len(req.DynamicValues)) + for _, val := range req.DynamicValues { + params = append(params, *val.ConvertToParameterConfig()) + } + cfg.DynamicValues = params + } + + return cfg +} + +type CreateParameterConfigReq struct { + Key string `json:"key" binding:"required"` + Type consts.ParameterType `json:"type" binding:"required"` + Category consts.ParameterCategory `json:"category" binding:"required"` + ValueType consts.ValueDataType `json:"value_type" binding:"omitempty"` + Description string `json:"description" binding:"omitempty"` + DefaultValue *string `json:"default_value" binding:"omitempty"` + TemplateString *string `json:"template_string" binding:"omitempty"` + Required bool `json:"required"` + Overridable *bool `json:"overridable" binding:"omitempty"` +} + +func (req *CreateParameterConfigReq) Validate() error { + if req.Key == "" { + return fmt.Errorf("parameter key cannot be empty") + } + if _, exists := consts.ValidParameterTypes[req.Type]; !exists { + return fmt.Errorf("invalid parameter type: %v", req.Type) + } + if _, exists := consts.ValidParameterCategories[req.Category]; !exists { + return fmt.Errorf("invalid parameter category: %v", req.Category) + } + if req.Type == consts.ParameterTypeFixed && req.Required && req.DefaultValue == nil { + return fmt.Errorf("default value is required for fixed parameter type when marked as required") + } + if req.Type == consts.ParameterTypeDynamic && req.TemplateString == nil { + return fmt.Errorf("template string is required for dynamic parameter type") + } + return nil +} + +func (req *CreateParameterConfigReq) ConvertToParameterConfig() *model.ParameterConfig { + config := &model.ParameterConfig{ + Key: req.Key, + Type: req.Type, + Category: req.Category, + ValueType: req.ValueType, + Description: req.Description, + DefaultValue: req.DefaultValue, + TemplateString: req.TemplateString, + Required: req.Required, + Overridable: true, + } + if req.Overridable != nil { + config.Overridable = *req.Overridable + } + return config +} + +type ListContainerVersionReq struct { + dto.PaginationReq + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *ListContainerVersionReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +type SearchContainerReq struct { + dto.AdvancedSearchReq[string] + Name *string `json:"name,omitempty"` + Image *string `json:"image,omitempty"` + Tag *string `json:"tag,omitempty"` + Type *string `json:"type,omitempty"` + Command *string `json:"command,omitempty"` + Status *int `json:"status,omitempty"` +} + +func (csr *SearchContainerReq) ConvertToSearchRequest() *dto.SearchReq[string] { + sr := csr.ConvertAdvancedToSearch() + if csr.Name != nil { + sr.AddFilter("name", dto.OpLike, *csr.Name) + } + if csr.Image != nil { + sr.AddFilter("image", dto.OpLike, *csr.Image) + } + if csr.Tag != nil { + sr.AddFilter("tag", dto.OpEqual, *csr.Tag) + } + if csr.Type != nil { + sr.AddFilter("type", dto.OpEqual, *csr.Type) + } + if csr.Command != nil { + sr.AddFilter("command", dto.OpLike, *csr.Command) + } + return sr +} + +type SubmitBuildContainerReq struct { + ImageName string `json:"image_name" binding:"required"` + Tag string `json:"tag" binding:"omitempty"` + GithubRepository string `json:"github_repository" binding:"required"` + GithubBranch string `json:"github_branch" binding:"omitempty"` + GithubCommit string `json:"github_commit" binding:"omitempty"` + GithubToken string `json:"github_token" binding:"omitempty"` + SubPath string `json:"sub_path" binding:"omitempty"` + Options *dto.BuildOptions `json:"build_options" binding:"omitempty"` +} + +func (req *SubmitBuildContainerReq) Validate() error { + req.ImageName = strings.TrimSpace(req.ImageName) + req.GithubRepository = strings.TrimSpace(req.GithubRepository) + + if req.ImageName == "" { + return fmt.Errorf("container image name cannot be empty") + } + if req.Tag != "" { + req.Tag = strings.TrimSpace(req.Tag) + } + parts := strings.Split(req.GithubRepository, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("invalid repository format, expected 'owner/repo'") + } + if req.GithubBranch != "" { + req.GithubBranch = strings.TrimSpace(req.GithubBranch) + if err := utils.IsValidGitHubBranch(req.GithubBranch); err != nil { + return err + } + } + if req.GithubCommit != "" { + req.GithubCommit = strings.TrimSpace(req.GithubCommit) + if err := utils.IsValidGitHubCommit(req.GithubCommit); err != nil { + return err + } + } + if req.GithubToken != "" { + req.GithubToken = strings.TrimSpace(req.GithubToken) + if err := utils.IsValidGitHubToken(req.GithubToken); err != nil { + return err + } + } + if req.Tag == "" { + req.Tag = "latest" + } + if req.GithubBranch == "" { + req.GithubBranch = "main" + } + if req.SubPath == "" { + req.SubPath = "." + } + return req.Options.Validate() +} + +func (req *SubmitBuildContainerReq) ValidateInfoContent(sourcePath string) error { + if req.ImageName == "" { + tomlPath := filepath.Join(sourcePath, dto.InfoFileName) + content, err := utils.ReadTomlFile(tomlPath) + if err != nil { + return err + } + + if name, ok := content[dto.InfoNameField].(string); ok && name != "" { + req.ImageName = name + } else { + return fmt.Errorf("%s does not contain a valid name field", dto.InfoFileName) + } + } + return nil +} + +type UpdateContainerVersionReq struct { + GithubLink *string `json:"github_link" binding:"omitempty"` + Command *string `json:"command" binding:"omitempty"` + Status *consts.StatusType `json:"status" binding:"omitempty"` + HelmConfigRequest *UpdateHelmConfigReq `json:"helm_config" binding:"omitempty"` +} + +func (req *UpdateContainerVersionReq) Validate() error { + if req.GithubLink != nil { + trimmedLink := strings.TrimSpace(*req.GithubLink) + *req.GithubLink = trimmedLink + if trimmedLink != "" { + if err := utils.IsValidGitHubLink(trimmedLink); err != nil { + return fmt.Errorf("invalid GitHub link '%s': %v", trimmedLink, err) + } + } + } + if req.Command != nil { + *req.Command = strings.TrimSpace(*req.Command) + } + if req.Status != nil { + if err := validateStatus(req.Status, true); err != nil { + return err + } + } + if req.HelmConfigRequest != nil { + if err := req.HelmConfigRequest.Validate(); err != nil { + return fmt.Errorf("invalid helm config: %v", err) + } + } + return nil +} + +func (req *UpdateContainerVersionReq) PatchContainerVersionModel(target *model.ContainerVersion) { + if req.GithubLink != nil { + target.GithubLink = *req.GithubLink + } + if req.Command != nil { + target.Command = *req.Command + } + if req.Status != nil { + target.Status = *req.Status + } +} + +type UpdateHelmConfigReq struct { + RepoURL *string `json:"repo_url" binding:"omitempty"` + RepoName *string `json:"repo_name" binding:"omitempty"` + ChartName *string `json:"chart_name" binding:"omitempty"` + DynamicValues *map[string]any `json:"dynamic_values" binding:"omitempty" swaggertype:"object"` +} + +func (req *UpdateHelmConfigReq) Validate() error { + if req.RepoURL != nil { + trimmedURL := strings.TrimSpace(*req.RepoURL) + *req.RepoURL = trimmedURL + if trimmedURL == "" { + return fmt.Errorf("repository URL cannot be empty if provided") + } + if _, err := url.Parse(trimmedURL); err != nil { + return fmt.Errorf("invalid repository URL format: %s. Error: %v", trimmedURL, err) + } + } + if req.RepoName != nil { + *req.RepoName = strings.TrimSpace(*req.RepoName) + } + if req.ChartName != nil { + *req.ChartName = strings.TrimSpace(*req.ChartName) + } + return nil +} + +func (req *UpdateHelmConfigReq) PatchHelmConfigModel(target *model.HelmConfig) error { + if req.RepoURL != nil { + target.RepoURL = *req.RepoURL + } + if req.RepoName != nil { + target.RepoName = *req.RepoName + } + if req.ChartName != nil { + target.ChartName = *req.ChartName + } + return nil +} + +type ContainerVersionResp struct { + ID int `json:"id"` + Name string `json:"name"` + ImageRef string `json:"image_ref"` + Usage int `json:"usage"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewContainerVersionResp(version *model.ContainerVersion) *ContainerVersionResp { + return &ContainerVersionResp{ + ID: version.ID, + Name: version.Name, + ImageRef: version.ImageRef, + Usage: version.Usage, + UpdatedAt: version.UpdatedAt, + } +} + +type ContainerVersionDetailResp struct { + ContainerVersionResp + GithubLink string `json:"github_link"` + Command string `json:"command"` + EnvVars string `json:"env_vars"` + HelmConfig *HelmConfigDetailResp `json:"helm_config,omitempty"` +} + +func NewContainerVersionDetailResp(version *model.ContainerVersion) *ContainerVersionDetailResp { + return &ContainerVersionDetailResp{ + ContainerVersionResp: *NewContainerVersionResp(version), + GithubLink: version.GithubLink, + Command: version.Command, + } +} + +type ListContainerVersionResp struct { + Items []ContainerVersionResp `json:"items"` + Pagination dto.PaginationInfo `json:"pagination"` +} + +type HelmConfigDetailResp struct { + ID int `json:"id"` + Version string `json:"version"` + ChartName string `json:"chart_name"` + RepoName string `json:"repo_name"` + RepoURL string `json:"repo_url"` + LocalPath string `json:"local_path,omitempty"` + ValueFile string `json:"value_file,omitempty"` + Values map[string]any `json:"values"` +} + +func NewHelmConfigDetailResp(cfg *model.HelmConfig) (*HelmConfigDetailResp, error) { + return &HelmConfigDetailResp{ + ID: cfg.ID, + Version: cfg.Version, + ChartName: cfg.ChartName, + RepoName: cfg.RepoName, + RepoURL: cfg.RepoURL, + LocalPath: cfg.LocalPath, + ValueFile: cfg.ValueFile, + }, nil +} + +type UploadHelmValueFileResp struct { + FilePath string `json:"file_path"` + FileName string `json:"file_name"` +} + +type UploadHelmChartResp struct { + FilePath string `json:"file_path"` + FileName string `json:"file_name"` + Checksum string `json:"checksum"` +} + +type SubmitContainerBuildResp struct { + GroupID string `json:"group_id"` + TraceID string `json:"trace_id"` + TaskID string `json:"task_id"` +} + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} + +func validateLabelItems(items []dto.LabelItem) error { + for i, label := range items { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d in add_labels", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d in add_labels", i) + } + } + return nil +} + +func validateContainerType(containerType *consts.ContainerType) error { + if containerType != nil { + if _, exists := consts.ValidContainerTypes[*containerType]; !exists { + return fmt.Errorf("invalid container type: %d", *containerType) + } + } + return nil +} diff --git a/src/module/container/build_gateway.go b/src/module/container/build_gateway.go new file mode 100644 index 00000000..f879dcbb --- /dev/null +++ b/src/module/container/build_gateway.go @@ -0,0 +1,87 @@ +package containermodule + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "aegis/config" + "aegis/utils" + + "github.com/sirupsen/logrus" +) + +type BuildGateway struct { + containerBasePath string + registry string + namespace string + repoURLBuilder func(*SubmitBuildContainerReq) string + commandRunner func(string, ...string) *exec.Cmd +} + +func NewBuildGateway() *BuildGateway { + return &BuildGateway{ + containerBasePath: config.GetString("jfs.container_path"), + registry: config.GetString("harbor.registry"), + namespace: config.GetString("harbor.namespace"), + repoURLBuilder: func(req *SubmitBuildContainerReq) string { + repoURL := fmt.Sprintf("https://github.com/%s.git", req.GithubRepository) + if req.GithubToken != "" { + repoURL = fmt.Sprintf("https://%s@github.com/%s.git", req.GithubToken, req.GithubRepository) + } + return repoURL + }, + commandRunner: exec.Command, + } +} + +func (g *BuildGateway) BuildImageRef(imageName, tag string) string { + return fmt.Sprintf("%s/%s/%s:%s", g.registry, g.namespace, imageName, tag) +} + +func (g *BuildGateway) PrepareGitHubSource(req *SubmitBuildContainerReq) (string, error) { + targetDir := filepath.Join(g.containerBasePath, req.ImageName, fmt.Sprintf("build_%d", time.Now().Unix())) + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create target directory: %w", err) + } + + repoURL := g.repoURLBuilder(req) + + gitCmd := []string{"git", "clone"} + if req.GithubBranch != "" { + gitCmd = append(gitCmd, "--branch", req.GithubBranch, "--single-branch") + } + gitCmd = append(gitCmd, repoURL, targetDir) + + cmd := g.commandRunner(gitCmd[0], gitCmd[1:]...) + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to clone repository: %w", err) + } + + if req.GithubCommit != "" { + cmd = g.commandRunner("git", "-C", targetDir, "checkout", req.GithubCommit) + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to checkout commit %s: %w", req.GithubCommit, err) + } + } + + if req.SubPath != "" && req.SubPath != "." { + sourcePath := filepath.Join(targetDir, req.SubPath) + if _, err := os.Stat(sourcePath); os.IsNotExist(err) { + return "", fmt.Errorf("sub path '%s' does not exist in repository", req.SubPath) + } + + newTargetDir := filepath.Join(g.containerBasePath, req.ImageName, fmt.Sprintf("build_final_%d", time.Now().Unix())) + if err := utils.CopyDir(sourcePath, newTargetDir); err != nil { + return "", fmt.Errorf("failed to copy subdirectory: %w", err) + } + if err := os.RemoveAll(targetDir); err != nil { + logrus.WithField("target_dir", targetDir).Warnf("failed to remove temporary directory: %v", err) + } + targetDir = newTargetDir + } + + return targetDir, nil +} diff --git a/src/module/container/build_gateway_test.go b/src/module/container/build_gateway_test.go new file mode 100644 index 00000000..3211565f --- /dev/null +++ b/src/module/container/build_gateway_test.go @@ -0,0 +1,84 @@ +package containermodule + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/viper" +) + +func TestBuildGatewayBuildImageRef(t *testing.T) { + gateway := &BuildGateway{ + registry: "registry.example.com", + namespace: "team-a", + } + + if got := gateway.BuildImageRef("demo", "v1"); got != "registry.example.com/team-a/demo:v1" { + t.Fatalf("unexpected image ref: %s", got) + } +} + +func TestBuildGatewayPrepareGitHubSourceCopiesSubPath(t *testing.T) { + tmpDir := t.TempDir() + repoDir := filepath.Join(tmpDir, "repo") + if err := os.MkdirAll(filepath.Join(repoDir, "subdir"), 0o755); err != nil { + t.Fatalf("mkdir repo: %v", err) + } + if err := os.WriteFile(filepath.Join(repoDir, "subdir", "payload.txt"), []byte("payload"), 0o644); err != nil { + t.Fatalf("write payload: %v", err) + } + + runGit(t, repoDir, "init") + runGit(t, repoDir, "config", "user.email", "codex@example.com") + runGit(t, repoDir, "config", "user.name", "Codex") + runGit(t, repoDir, "add", ".") + runGit(t, repoDir, "commit", "-m", "init") + + viper.Set("jfs.container_path", tmpDir) + gateway := &BuildGateway{ + containerBasePath: tmpDir, + registry: "registry.example.com", + namespace: "team-a", + repoURLBuilder: func(*SubmitBuildContainerReq) string { + return repoDir + }, + commandRunner: exec.Command, + } + + req := &SubmitBuildContainerReq{ + ImageName: "demo", + GithubRepository: "owner/repo", + GithubBranch: "master", + SubPath: "subdir", + } + + targetDir, err := gateway.PrepareGitHubSource(req) + if err != nil { + t.Fatalf("PrepareGitHubSource failed: %v", err) + } + + if filepath.Base(targetDir) == "subdir" { + t.Fatalf("expected copied final directory, got raw subdir path: %s", targetDir) + } + + content, err := os.ReadFile(filepath.Join(targetDir, "payload.txt")) + if err != nil { + t.Fatalf("read copied file: %v", err) + } + if string(content) != "payload" { + t.Fatalf("unexpected copied content: %s", string(content)) + } +} + +func runGit(t *testing.T, dir string, args ...string) { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %s failed: %v\n%s", strings.Join(args, " "), err, string(output)) + } +} diff --git a/src/module/container/core.go b/src/module/container/core.go new file mode 100644 index 00000000..4b06500c --- /dev/null +++ b/src/module/container/core.go @@ -0,0 +1,23 @@ +package containermodule + +import ( + "aegis/model" + + "gorm.io/gorm" +) + +func CreateContainerCore(tx *gorm.DB, container *model.Container, userID int) (*model.Container, error) { + service := NewService(NewRepository(tx), NewBuildGateway(), NewHelmFileStore(), nil) + return service.createContainerCore(service.repo, container, userID) +} + +func UploadHelmValueFileFromPath(tx *gorm.DB, containerName string, helmConfig *model.HelmConfig, srcFilePath string) error { + store := NewHelmFileStore() + targetPath, err := store.SaveValueFile(containerName, nil, srcFilePath) + if err != nil { + return err + } + + helmConfig.ValueFile = targetPath + return NewRepository(tx).UpdateHelmConfig(helmConfig) +} diff --git a/src/module/container/file_store.go b/src/module/container/file_store.go new file mode 100644 index 00000000..5bc2dc13 --- /dev/null +++ b/src/module/container/file_store.go @@ -0,0 +1,86 @@ +package containermodule + +import ( + "fmt" + "mime/multipart" + "os" + "path/filepath" + "time" + + "aegis/config" + "aegis/utils" + + "github.com/sirupsen/logrus" +) + +type HelmFileStore struct { + basePath string +} + +func NewHelmFileStore() *HelmFileStore { + return &HelmFileStore{basePath: config.GetString("jfs.dataset_path")} +} + +func (s *HelmFileStore) SaveChart(containerName string, file *multipart.FileHeader) (string, string, error) { + if s.basePath == "" { + return "", "", fmt.Errorf("jfs.dataset_path is not configured") + } + + targetDir := filepath.Join(s.basePath, "helm-charts") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return "", "", fmt.Errorf("failed to create directory: %w", err) + } + + targetPath := filepath.Join( + targetDir, + fmt.Sprintf("%s_chart_%d%s", containerName, time.Now().Unix(), filepath.Ext(file.Filename)), + ) + if err := utils.CopyFileFromFileHeader(file, targetPath); err != nil { + return "", "", fmt.Errorf("failed to save chart file: %w", err) + } + + checksum, err := utils.CalculateFileSHA256(targetPath) + if err != nil { + logrus.WithField("file_path", targetPath).Warnf("failed to calculate checksum: %v", err) + checksum = "" + } + + logrus.WithFields(logrus.Fields{ + "file_path": targetPath, + "checksum": checksum, + }).Info("Helm chart package uploaded successfully") + + return targetPath, checksum, nil +} + +func (s *HelmFileStore) SaveValueFile(containerName string, srcFileHeader *multipart.FileHeader, srcFilePath string) (string, error) { + if s.basePath == "" { + return "", fmt.Errorf("jfs.dataset_path is not configured") + } + + targetDir := filepath.Join(s.basePath, "helm-values") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + timestamp := time.Now().Unix() + var targetPath string + + switch { + case srcFileHeader != nil: + targetPath = filepath.Join(targetDir, fmt.Sprintf("%s_values_%d%s", containerName, timestamp, filepath.Ext(srcFileHeader.Filename))) + if err := utils.CopyFileFromFileHeader(srcFileHeader, targetPath); err != nil { + return "", fmt.Errorf("failed to save file: %w", err) + } + case srcFilePath != "": + targetPath = filepath.Join(targetDir, fmt.Sprintf("%s_values_%d%s", containerName, timestamp, filepath.Ext(srcFilePath))) + if err := utils.CopyFile(srcFilePath, targetPath); err != nil { + return "", fmt.Errorf("failed to save file: %w", err) + } + default: + return "", fmt.Errorf("either source file header or source file path is required") + } + + logrus.WithField("file_path", targetPath).Info("Helm values file uploaded successfully") + return targetPath, nil +} diff --git a/src/module/container/file_store_test.go b/src/module/container/file_store_test.go new file mode 100644 index 00000000..1fada913 --- /dev/null +++ b/src/module/container/file_store_test.go @@ -0,0 +1,84 @@ +package containermodule + +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/spf13/viper" +) + +func TestHelmFileStoreSaveChartAndValueFile(t *testing.T) { + tmpDir := t.TempDir() + viper.Set("jfs.dataset_path", tmpDir) + + store := &HelmFileStore{basePath: tmpDir} + fileHeader := newMultipartFileHeader(t, "chart.tgz", []byte("chart-bytes")) + + chartPath, checksum, err := store.SaveChart("pedestal", fileHeader) + if err != nil { + t.Fatalf("SaveChart failed: %v", err) + } + if checksum == "" { + t.Fatalf("expected checksum to be populated") + } + if !filepath.IsAbs(chartPath) && filepath.Dir(chartPath) == "." { + t.Fatalf("expected chart path to include target directory, got %s", chartPath) + } + + chartContent, err := os.ReadFile(chartPath) + if err != nil { + t.Fatalf("read saved chart: %v", err) + } + if string(chartContent) != "chart-bytes" { + t.Fatalf("unexpected chart content: %s", string(chartContent)) + } + + valueHeader := newMultipartFileHeader(t, "values.yaml", []byte("key: value\n")) + valuePath, err := store.SaveValueFile("pedestal", valueHeader, "") + if err != nil { + t.Fatalf("SaveValueFile failed: %v", err) + } + + valueContent, err := os.ReadFile(valuePath) + if err != nil { + t.Fatalf("read saved values file: %v", err) + } + if string(valueContent) != "key: value\n" { + t.Fatalf("unexpected values content: %s", string(valueContent)) + } +} + +func newMultipartFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader { + t.Helper() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", filename) + if err != nil { + t.Fatalf("create form file: %v", err) + } + if _, err := io.Copy(part, bytes.NewReader(content)); err != nil { + t.Fatalf("write multipart content: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + if err := req.ParseMultipartForm(int64(body.Len()) + 1024); err != nil { + t.Fatalf("parse multipart form: %v", err) + } + + fileHeaders := req.MultipartForm.File["file"] + if len(fileHeaders) != 1 { + t.Fatalf("expected one file header, got %d", len(fileHeaders)) + } + return fileHeaders[0] +} diff --git a/src/handlers/v2/containers.go b/src/module/container/handler.go similarity index 69% rename from src/handlers/v2/containers.go rename to src/module/container/handler.go index e5068cc7..9c38dcf7 100644 --- a/src/handlers/v2/containers.go +++ b/src/module/container/handler.go @@ -1,6 +1,7 @@ -package v2 +package containermodule import ( + "aegis/httpx" "context" "net/http" "path/filepath" @@ -8,14 +9,18 @@ import ( "aegis/consts" "aegis/dto" - "aegis/handlers" "aegis/middleware" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) -// ===================== Container ===================== +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} // CreateContainer handles container creation for v2 API // @@ -26,23 +31,23 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.CreateContainerReq true "Container creation request" -// @Success 201 {object} dto.GenericResponse[dto.ContainerResp] "Container created successfully" +// @Param request body CreateContainerReq true "Container creation request" +// @Success 201 {object} dto.GenericResponse[ContainerResp] "Container created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Conflict error" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers [post] -// @x-api-type {"sdk":"true"} -func CreateContainer(c *gin.Context) { +// @x-api-type {} +func (h *Handler) CreateContainer(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req dto.CreateContainerReq + var req CreateContainerReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -53,8 +58,8 @@ func CreateContainer(c *gin.Context) { return } - resp, err := producer.CreateContainer(&req, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateContainer(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } @@ -77,16 +82,14 @@ func CreateContainer(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [delete] -func DeleteContainer(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) DeleteContainer(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - err = producer.DeleteContainer(containerID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteContainer(c.Request.Context(), containerID)) { return } @@ -102,24 +105,22 @@ func DeleteContainer(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param container_id path int true "Container ID" -// @Success 200 {object} dto.GenericResponse[dto.ContainerDetailResp] "Container retrieved successfully" +// @Success 200 {object} dto.GenericResponse[ContainerDetailResp] "Container retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [get] -// @x-api-type {"sdk":"true"} -func GetContainer(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) GetContainer(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - resp, err := producer.GetContainerDetail(containerID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetContainer(c.Request.Context(), containerID) + if httpx.HandleServiceError(c, err) { return } @@ -139,15 +140,15 @@ func GetContainer(c *gin.Context) { // @Param type query consts.ContainerType false "Container type filter" // @Param is_public query bool false "Container public visibility filter" // @Param status query consts.StatusType false "Container status filter" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ContainerResp]] "Containers retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ContainerResp]] "Containers retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers [get] -// @x-api-type {"sdk":"true"} -func ListContainers(c *gin.Context) { - var req dto.ListContainerReq +// @x-api-type {} +func (h *Handler) ListContainers(c *gin.Context) { + var req ListContainerReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -158,8 +159,8 @@ func ListContainers(c *gin.Context) { return } - resp, err := producer.ListContainers(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListContainers(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -176,37 +177,78 @@ func ListContainers(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param container_id path int true "Container ID" -// @Param request body dto.UpdateContainerReq true "Container update request" -// @Success 202 {object} dto.GenericResponse[dto.ContainerResp] "Container updated successfully" +// @Param request body UpdateContainerReq true "Container update request" +// @Success 202 {object} dto.GenericResponse[ContainerResp] "Container updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [patch] -func UpdateContainer(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) UpdateContainer(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - var req dto.UpdateContainerReq + var req UpdateContainerReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - resp, err := producer.UpdateContainer(&req, containerID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateContainer(c.Request.Context(), &req, containerID) + if httpx.HandleServiceError(c, err) { return } dto.JSONResponse[any](c, http.StatusAccepted, "Container updated successfully", resp) } -// ===================== Container Version ===================== +// ManageContainerCustomLabels manages container custom labels (key-value pairs) +// +// @Summary Manage container custom labels +// @Description Add or remove custom labels (key-value pairs) for a container +// @Tags Containers +// @ID manage_container_labels +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param container_id path int true "Container ID" +// @Param manage body ManageContainerLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[ContainerResp] "Labels managed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/containers/{container_id}/labels [patch] +// @x-api-type {} +func (h *Handler) ManageContainerCustomLabels(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { + return + } + + var req ManageContainerLabelReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ManageContainerLabels(c.Request.Context(), &req, containerID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} // CreateContainerVersion handles container version creation for v2 API // @@ -218,30 +260,28 @@ func UpdateContainer(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param container_id path int true "Container ID" -// @Param request body dto.CreateContainerVersionReq true "Container version creation request" -// @Success 201 {object} dto.GenericResponse[dto.ContainerVersionResp] "Container version created successfully" +// @Param request body CreateContainerVersionReq true "Container version creation request" +// @Success 201 {object} dto.GenericResponse[ContainerVersionResp] "Container version created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Conflict error" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions [post] -// @x-api-type {"sdk":"true"} -func CreateContainerVersion(c *gin.Context) { +// @x-api-type {} +func (h *Handler) CreateContainerVersion(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + containerID, ok := parseContainerID(c) + if !ok { return } - var req dto.CreateContainerVersionReq + var req CreateContainerVersionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -252,8 +292,8 @@ func CreateContainerVersion(c *gin.Context) { return } - resp, err := producer.CreateContainerVersion(&req, containerID, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateContainerVersion(c.Request.Context(), &req, containerID, userID) + if httpx.HandleServiceError(c, err) { return } @@ -277,16 +317,14 @@ func CreateContainerVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [delete] -func DeleteContainerVersion(c *gin.Context) { - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container version ID") +// @x-api-type {} +func (h *Handler) DeleteContainerVersion(c *gin.Context) { + versionID, ok := parseVersionID(c, "Invalid container version ID") + if !ok { return } - err = producer.DeleteContainerVersion(versionID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteContainerVersion(c.Request.Context(), versionID)) { return } @@ -303,31 +341,26 @@ func DeleteContainerVersion(c *gin.Context) { // @Security BearerAuth // @Param container_id path int true "Container ID" // @Param version_id path int true "Container Version ID" -// @Success 200 {object} dto.GenericResponse[dto.ContainerVersionDetailResp] "Container version retrieved successfully" +// @Success 200 {object} dto.GenericResponse[ContainerVersionDetailResp] "Container version retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [get] -// @x-api-type {"sdk":"true"} -func GetContainerVersion(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) GetContainerVersion(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container version ID") + versionID, ok := parseVersionID(c, "Invalid container version ID") + if !ok { return } - resp, err := producer.GetContainerVersionDetail(containerID, versionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetContainerVersion(c.Request.Context(), containerID, versionID) + if httpx.HandleServiceError(c, err) { return } @@ -346,22 +379,20 @@ func GetContainerVersion(c *gin.Context) { // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) // @Param status query consts.StatusType false "Container version status filter" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ContainerVersionResp]] "Container versions retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ContainerVersionResp]] "Container versions retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions [get] -// @x-api-type {"sdk":"true"} -func ListContainerVersions(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) ListContainerVersions(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - var req dto.ListContainerVersionReq + var req ListContainerVersionReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -372,8 +403,8 @@ func ListContainerVersions(c *gin.Context) { return } - resp, err := producer.ListContainerVersions(&req, containerID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListContainerVersions(c.Request.Context(), &req, containerID) + if httpx.HandleServiceError(c, err) { return } @@ -391,88 +422,39 @@ func ListContainerVersions(c *gin.Context) { // @Security BearerAuth // @Param container_id path int true "Container ID" // @Param version_id path int true "Container Version ID" -// @Param request body dto.UpdateContainerVersionReq true "Container version update request" -// @Success 202 {object} dto.GenericResponse[dto.ContainerVersionResp] "Container version updated successfully" +// @Param request body UpdateContainerVersionReq true "Container version update request" +// @Success 202 {object} dto.GenericResponse[ContainerVersionResp] "Container version updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID/request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [patch] -func UpdateContainerVersion(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") +// @x-api-type {} +func (h *Handler) UpdateContainerVersion(c *gin.Context) { + containerID, ok := parseContainerID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container version ID") + versionID, ok := parseVersionID(c, "Invalid container version ID") + if !ok { return } - var req dto.UpdateContainerVersionReq + var req UpdateContainerVersionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - resp, err := producer.UpdateContainerVersion(&req, containerID, versionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateContainerVersion(c.Request.Context(), &req, containerID, versionID) + if httpx.HandleServiceError(c, err) { return } dto.JSONResponse[any](c, http.StatusAccepted, "Container version updated successfully", resp) } -// ManageContainerCustomLabels manages container custom labels (key-value pairs) -// -// @Summary Manage container custom labels -// @Description Add or remove custom labels (key-value pairs) for a container -// @Tags Containers -// @ID manage_container_labels -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param manage body dto.ManageContainerLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[dto.ContainerResp] "Labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/containers/{container_id}/labels [patch] -func ManageContainerCustomLabels(c *gin.Context) { - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") - return - } - - var req dto.ManageContainerLabelReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - - resp, err := producer.ManageContainerLabels(&req, containerID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - // SubmitContainerBuilding handles submitting a container build task // // @Summary Submit container building @@ -482,16 +464,16 @@ func ManageContainerCustomLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.SubmitBuildContainerReq true "Container build request" -// @Success 200 {object} dto.GenericResponse[dto.SubmitContainerBuildResp] "Container build task submitted successfully" +// @Param request body SubmitBuildContainerReq true "Container build request" +// @Success 200 {object} dto.GenericResponse[SubmitContainerBuildResp] "Container build task submitted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Required files not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/build [post] -// @x-api-type {"sdk":"true"} -func SubmitContainerBuilding(c *gin.Context) { +// @x-api-type {} +func (h *Handler) SubmitContainerBuilding(c *gin.Context) { groupID := c.GetString("groupID") userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -499,14 +481,7 @@ func SubmitContainerBuilding(c *gin.Context) { return } - ctx, ok := c.Get(middleware.SpanContextKey) - if !ok { - dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get span context") - return - } - spanCtx := ctx.(context.Context) - - var req dto.SubmitBuildContainerReq + var req SubmitBuildContainerReq if err := c.ShouldBind(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -517,8 +492,8 @@ func SubmitContainerBuilding(c *gin.Context) { return } - resp, err := producer.ProduceContainerBuildingTask(spanCtx, &req, groupID, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.SubmitContainerBuilding(spanContextFromGin(c), &req, groupID, userID) + if httpx.HandleServiceError(c, err) { return } @@ -537,31 +512,27 @@ func SubmitContainerBuilding(c *gin.Context) { // @Param container_id path int true "Container ID" // @Param version_id path int true "Container Version ID" // @Param file formData file true "Helm chart package (.tgz)" -// @Success 200 {object} dto.GenericResponse[dto.UploadHelmChartResp] "Chart uploaded successfully" +// @Success 200 {object} dto.GenericResponse[UploadHelmChartResp] "Chart uploaded successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id}/helm-chart [post] -func UploadHelmChart(c *gin.Context) { +// @x-api-type {} +func (h *Handler) UploadHelmChart(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + containerID, ok := parseContainerID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container version ID") + versionID, ok := parseVersionID(c, "Invalid container version ID") + if !ok { return } @@ -571,16 +542,14 @@ func UploadHelmChart(c *gin.Context) { return } - filename := file.Filename - ext := filepath.Ext(filename) - if ext != ".tgz" && ext != ".tar.gz" { + ext := filepath.Ext(file.Filename) + if ext != ".tgz" && ext != ".gz" { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid file type: only .tgz or .tar.gz files are allowed") return } - // Call service layer to handle chart upload - resp, err := producer.UploadHelmChart(file, containerID, versionID, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UploadHelmChart(c.Request.Context(), file, containerID, versionID, userID) + if httpx.HandleServiceError(c, err) { return } @@ -599,53 +568,76 @@ func UploadHelmChart(c *gin.Context) { // @Param container_id path int true "Container ID" // @Param version_id path int true "Container Version ID" // @Param file formData file true "Helm values YAML file" -// @Success 200 {object} dto.GenericResponse[dto.UploadHelmValueFileResp] "File uploaded successfully" +// @Success 200 {object} dto.GenericResponse[UploadHelmValueFileResp] "File uploaded successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id}/helm-values [post] -func UploadHelmValueFile(c *gin.Context) { +// @x-api-type {} +func (h *Handler) UploadHelmValueFile(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + containerID, ok := parseContainerID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container version ID") + versionID, ok := parseVersionID(c, "Invalid container version ID") + if !ok { return } - // Get uploaded file file, err := c.FormFile("file") if err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "No file uploaded or invalid file: "+err.Error()) return } - filename := file.Filename - ext := filepath.Ext(filename) + ext := filepath.Ext(file.Filename) if ext != ".yaml" && ext != ".yml" { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid file type: only .yaml or .yml files are allowed") return } - // Call service layer to handle file upload - resp, err := producer.UploadHelmValueFile(file, containerID, versionID, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UploadHelmValueFile(c.Request.Context(), file, containerID, versionID, userID) + if httpx.HandleServiceError(c, err) { return } dto.SuccessResponse(c, resp) } + +func parseContainerID(c *gin.Context) (int, bool) { + containerIDStr := c.Param(consts.URLPathContainerID) + containerID, err := strconv.Atoi(containerIDStr) + if err != nil || containerID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + return 0, false + } + return containerID, true +} + +func parseVersionID(c *gin.Context, message string) (int, bool) { + versionIDStr := c.Param(consts.URLPathVersionID) + versionID, err := strconv.Atoi(versionIDStr) + if err != nil || versionID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, message) + return 0, false + } + return versionID, true +} + +func spanContextFromGin(c *gin.Context) context.Context { + ctx, ok := c.Get(middleware.SpanContextKey) + if ok { + if spanCtx, ok := ctx.(context.Context); ok { + return spanCtx + } + } + return c.Request.Context() +} diff --git a/src/module/container/module.go b/src/module/container/module.go new file mode 100644 index 00000000..e988a324 --- /dev/null +++ b/src/module/container/module.go @@ -0,0 +1,11 @@ +package containermodule + +import "go.uber.org/fx" + +var Module = fx.Module("container", + fx.Provide(NewRepository), + fx.Provide(NewBuildGateway), + fx.Provide(NewHelmFileStore), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/container/repository.go b/src/module/container/repository.go new file mode 100644 index 00000000..264799c0 --- /dev/null +++ b/src/module/container/repository.go @@ -0,0 +1,361 @@ +package containermodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ( + containerCommonOmitFields = "active_name" + containerModelOmitFields = "Versions" + containerVersionModelOmitFields = "active_version_key,HelmConfig,EnvVars" + helmConfigModelOmitFields = "Values" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) GetRoleByName(name string) (*model.Role, error) { + var role model.Role + if err := r.db.Where("name = ? and status != ?", name, consts.CommonDeleted).First(&role).Error; err != nil { + return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) + } + return &role, nil +} + +func (r *Repository) CreateContainer(container *model.Container) error { + if err := r.db.Omit(containerCommonOmitFields, containerModelOmitFields).Create(container).Error; err != nil { + return fmt.Errorf("failed to create container: %w", err) + } + return nil +} + +func (r *Repository) CreateUserContainer(userContainer *model.UserContainer) error { + if err := r.db.Omit("active_user_container").Create(userContainer).Error; err != nil { + return fmt.Errorf("failed to create user-container association: %w", err) + } + return nil +} + +func (r *Repository) BatchDeleteContainerVersions(containerID int) (int64, error) { + result := r.db.Model(&model.ContainerVersion{}). + Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to batch soft delete container versions for container %d: %w", containerID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) RemoveUsersFromContainer(containerID int) (int64, error) { + result := r.db.Model(&model.UserContainer{}). + Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to delete user-container associations for container %d: %w", containerID, err) + } + return result.RowsAffected, nil +} + +func (r *Repository) ClearContainerLabels(containerIDs []int, labelIDs []int) error { + if len(containerIDs) == 0 { + return nil + } + + query := r.db.Table("container_labels").Where("container_id IN (?)", containerIDs) + if len(labelIDs) > 0 { + query = query.Where("label_id IN (?)", labelIDs) + } + if err := query.Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear container-label associations: %w", err) + } + return nil +} + +func (r *Repository) DeleteContainer(containerID int) (int64, error) { + result := r.db.Model(&model.Container{}). + Where("id = ? AND status != ?", containerID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to delete container %d: %w", containerID, err) + } + return result.RowsAffected, nil +} + +func (r *Repository) GetContainerByID(containerID int) (*model.Container, error) { + var container model.Container + if err := r.db.Where("id = ? AND status != ?", containerID, consts.CommonDeleted).First(&container).Error; err != nil { + return nil, fmt.Errorf("failed to find container with id %d: %w", containerID, err) + } + return &container, nil +} + +func (r *Repository) ListContainerVersionsByContainerID(containerID int) ([]model.ContainerVersion, error) { + var versions []model.ContainerVersion + if err := r.db. + Preload("Container"). + Preload("HelmConfig"). + Where("container_id = ?", containerID). + Find(&versions).Error; err != nil { + return nil, fmt.Errorf("failed to list container versions for container %d: %w", containerID, err) + } + return versions, nil +} + +func (r *Repository) ListContainers(limit, offset int, containerType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]model.Container, int64, error) { + var ( + containers []model.Container + total int64 + ) + + query := r.db.Model(&model.Container{}) + if containerType != nil { + query = query.Where("type = ?", *containerType) + } + if isPublic != nil { + query = query.Where("is_public = ?", *isPublic) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count containers: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&containers).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list containers: %w", err) + } + return containers, total, nil +} + +func (r *Repository) ListContainerLabels(containerIDs []int) (map[int][]model.Label, error) { + if len(containerIDs) == 0 { + return nil, nil + } + + type containerLabelResult struct { + model.Label + ContainerID int `gorm:"column:container_id"` + } + + var flatResults []containerLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN container_labels cl ON cl.label_id = labels.id"). + Where("cl.container_id IN (?)", containerIDs). + Select("labels.*, cl.container_id"). + Find(&flatResults).Error; err != nil { + return nil, fmt.Errorf("failed to batch query container labels: %w", err) + } + + labelsMap := make(map[int][]model.Label, len(containerIDs)) + for _, id := range containerIDs { + labelsMap[id] = []model.Label{} + } + for _, res := range flatResults { + labelsMap[res.ContainerID] = append(labelsMap[res.ContainerID], res.Label) + } + return labelsMap, nil +} + +func (r *Repository) UpdateContainer(container *model.Container) error { + if err := r.db.Omit(containerCommonOmitFields).Save(container).Error; err != nil { + return fmt.Errorf("failed to update container: %w", err) + } + return nil +} + +func (r *Repository) AddContainerLabels(containerLabels []model.ContainerLabel) error { + if len(containerLabels) == 0 { + return nil + } + if err := r.db.Create(&containerLabels).Error; err != nil { + return fmt.Errorf("failed to add container-label associations: %w", err) + } + return nil +} + +func (r *Repository) ListLabelIDsByKeyAndContainerID(containerID int, keys []string) ([]int, error) { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN container_labels cl ON cl.label_id = l.id"). + Where("cl.container_id = ? AND l.label_key IN (?)", containerID, keys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by keys for container %d: %w", containerID, err) + } + return labelIDs, nil +} + +func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + Clauses(clause.Returning{}). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch decrease label usages: %w", err) + } + return nil +} + +func (r *Repository) ListLabelsByContainerID(containerID int) ([]model.Label, error) { + var labels []model.Label + if err := r.db.Model(&model.Label{}). + Joins("JOIN container_labels cl ON cl.label_id = labels.id"). + Where("cl.container_id = ?", containerID). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels for container %d: %w", containerID, err) + } + return labels, nil +} + +func (r *Repository) BatchCreateContainerVersions(versions []model.ContainerVersion) error { + if len(versions) == 0 { + return fmt.Errorf("no container versions to create") + } + if err := r.db.Omit(containerVersionModelOmitFields).Create(&versions).Error; err != nil { + return fmt.Errorf("failed to batch create container versions: %w", err) + } + return nil +} + +func (r *Repository) BatchCreateOrFindParameterConfigs(params []model.ParameterConfig) error { + if len(params) == 0 { + return nil + } + if err := r.db.Clauses(clause.OnConflict{OnConstraint: "idx_unique_config", DoNothing: true}).Create(¶ms).Error; err != nil { + return fmt.Errorf("failed to batch create parameter configs: %w", err) + } + return nil +} + +func (r *Repository) ListParameterConfigsByKeys(configs []model.ParameterConfig) ([]model.ParameterConfig, error) { + if len(configs) == 0 { + return []model.ParameterConfig{}, nil + } + + var results []model.ParameterConfig + query := r.db.Model(&model.ParameterConfig{}) + conditions := r.db.Where("1 = 0") + for _, cfg := range configs { + conditions = conditions.Or(r.db.Where("config_key = ? AND type = ? AND category = ?", cfg.Key, cfg.Type, cfg.Category)) + } + if err := query.Where(conditions).Find(&results).Error; err != nil { + return nil, fmt.Errorf("failed to list parameter configs by keys: %w", err) + } + return results, nil +} + +func (r *Repository) AddContainerVersionEnvVars(envVars []model.ContainerVersionEnvVar) error { + if len(envVars) == 0 { + return nil + } + if err := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&envVars).Error; err != nil { + return fmt.Errorf("failed to add container version env vars: %w", err) + } + return nil +} + +func (r *Repository) BatchCreateHelmConfigs(helmConfigs []*model.HelmConfig) error { + if len(helmConfigs) == 0 { + return fmt.Errorf("no helm configs to create") + } + if err := r.db.Omit(helmConfigModelOmitFields).Create(helmConfigs).Error; err != nil { + return fmt.Errorf("failed to batch create helm configs: %v", err) + } + return nil +} + +func (r *Repository) AddHelmConfigValues(helmValues []model.HelmConfigValue) error { + if len(helmValues) == 0 { + return nil + } + if err := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&helmValues).Error; err != nil { + return fmt.Errorf("failed to add helm config values: %w", err) + } + return nil +} + +func (r *Repository) DeleteContainerVersion(versionID int) (int64, error) { + result := r.db.Model(&model.ContainerVersion{}). + Where("id = ? AND status != ?", versionID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to soft delete container version %d: %w", versionID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) GetContainerVersionByID(versionID int) (*model.ContainerVersion, error) { + var version model.ContainerVersion + if err := r.db. + Preload("Container"). + Preload("HelmConfig"). + Where("id = ?", versionID). + First(&version).Error; err != nil { + return nil, fmt.Errorf("failed to find container version with id %d: %w", versionID, err) + } + return &version, nil +} + +func (r *Repository) ListContainerVersions(limit, offset int, containerID int, status *consts.StatusType) ([]model.ContainerVersion, int64, error) { + var ( + versions []model.ContainerVersion + total int64 + ) + + query := r.db.Model(&model.ContainerVersion{}).Where("container_id = ?", containerID) + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count container versions: %v", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&versions).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list container versions: %v", err) + } + return versions, total, nil +} + +func (r *Repository) UpdateContainerVersion(version *model.ContainerVersion) error { + if err := r.db.Omit(containerVersionModelOmitFields).Save(version).Error; err != nil { + return fmt.Errorf("failed to update container version: %w", err) + } + return nil +} + +func (r *Repository) GetHelmConfigByContainerVersionID(versionID int) (*model.HelmConfig, error) { + var helmConfig model.HelmConfig + if err := r.db.Preload("ContainerVersion").Where("container_version_id = ?", versionID).First(&helmConfig).Error; err != nil { + return nil, fmt.Errorf("failed to find helm config for version id %d: %w", versionID, err) + } + return &helmConfig, nil +} + +func (r *Repository) UpdateHelmConfig(helmConfig *model.HelmConfig) error { + if err := r.db.Save(helmConfig).Error; err != nil { + return fmt.Errorf("failed to update helm config: %w", err) + } + return nil +} diff --git a/src/module/container/service.go b/src/module/container/service.go new file mode 100644 index 00000000..46de4a6c --- /dev/null +++ b/src/module/container/service.go @@ -0,0 +1,657 @@ +package containermodule + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + "aegis/model" + "aegis/service/common" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository + build *BuildGateway + helmFiles *HelmFileStore + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, build *BuildGateway, helmFiles *HelmFileStore, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, build: build, helmFiles: helmFiles, redis: redis} +} + +func (s *Service) CreateContainer(_ context.Context, req *CreateContainerReq, userID int) (*ContainerResp, error) { + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + container := req.ConvertToContainer() + err := s.repo.Transaction(func(tx *gorm.DB) error { + createdContainer, err := s.createContainerCore(s.repo.withDB(tx), container, userID) + if err != nil { + return fmt.Errorf("failed to create container: %w", err) + } + container = createdContainer + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to create container: %w", err) + } + + return NewContainerResp(container), nil +} + +func (s *Service) DeleteContainer(_ context.Context, containerID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if _, err := repo.BatchDeleteContainerVersions(containerID); err != nil { + return fmt.Errorf("failed to delete container versions: %w", err) + } + if _, err := repo.RemoveUsersFromContainer(containerID); err != nil { + return fmt.Errorf("failed to remove all users from container: %w", err) + } + if err := repo.ClearContainerLabels([]int{containerID}, nil); err != nil { + return fmt.Errorf("failed to clear container labels: %w", err) + } + rows, err := repo.DeleteContainer(containerID) + if err != nil { + return fmt.Errorf("failed to delete container: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: container id %d not found", consts.ErrNotFound, containerID) + } + return nil + }) +} + +func (s *Service) GetContainer(_ context.Context, containerID int) (*ContainerDetailResp, error) { + container, err := s.repo.GetContainerByID(containerID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) + } + return nil, fmt.Errorf("failed to get container: %w", err) + } + + versions, err := s.repo.ListContainerVersionsByContainerID(container.ID) + if err != nil { + return nil, fmt.Errorf("failed to get container versions: %w", err) + } + + resp := NewContainerDetailResp(container) + for _, version := range versions { + resp.Versions = append(resp.Versions, *NewContainerVersionResp(&version)) + } + + return resp, nil +} + +func (s *Service) ListContainers(_ context.Context, req *ListContainerReq) (*dto.ListResp[ContainerResp], error) { + limit, offset := req.ToGormParams() + + containers, total, err := s.repo.ListContainers(limit, offset, req.Type, req.IsPublic, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list containers: %w", err) + } + + containerIDs := make([]int, 0, len(containers)) + for _, container := range containers { + containerIDs = append(containerIDs, container.ID) + } + + labelsMap, err := s.repo.ListContainerLabels(containerIDs) + if err != nil { + return nil, fmt.Errorf("failed to list container labels: %w", err) + } + + items := make([]ContainerResp, 0, len(containers)) + for i := range containers { + if labels, ok := labelsMap[containers[i].ID]; ok { + containers[i].Labels = labels + } + items = append(items, *NewContainerResp(&containers[i])) + } + + return &dto.ListResp[ContainerResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateContainer(_ context.Context, req *UpdateContainerReq, containerID int) (*ContainerResp, error) { + var updatedContainer *model.Container + + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + container, err := repo.GetContainerByID(containerID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: container with id %d not found", consts.ErrNotFound, containerID) + } + return fmt.Errorf("failed to get container: %w", err) + } + + req.PatchContainerModel(container) + if err := repo.UpdateContainer(container); err != nil { + return fmt.Errorf("failed to update container: %w", err) + } + + updatedContainer = container + return nil + }); err != nil { + return nil, err + } + + return NewContainerResp(updatedContainer), nil +} + +func (s *Service) ManageContainerLabels(_ context.Context, req *ManageContainerLabelReq, containerID int) (*ContainerResp, error) { + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + var managedContainer *model.Container + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + container, err := repo.GetContainerByID(containerID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: container not found", consts.ErrNotFound) + } + return err + } + + if len(req.AddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + containerLabels := make([]model.ContainerLabel, 0, len(labels)) + for _, label := range labels { + containerLabels = append(containerLabels, model.ContainerLabel{ + ContainerID: containerID, + LabelID: label.ID, + }) + } + + if err := repo.AddContainerLabels(containerLabels); err != nil { + return fmt.Errorf("failed to add container labels: %w", err) + } + } + + if len(req.RemoveLabels) > 0 { + labelIDs, err := repo.ListLabelIDsByKeyAndContainerID(containerID, req.RemoveLabels) + if err != nil { + return fmt.Errorf("failed to find label IDs: %w", err) + } + + if len(labelIDs) > 0 { + if err := repo.ClearContainerLabels([]int{containerID}, labelIDs); err != nil { + return fmt.Errorf("failed to delete container-label associations: %w", err) + } + + if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + return fmt.Errorf("failed to decrease label usage counts: %w", err) + } + } + } + + labels, err := repo.ListLabelsByContainerID(container.ID) + if err != nil { + return fmt.Errorf("failed to get container labels: %w", err) + } + + container.Labels = labels + managedContainer = container + return nil + }); err != nil { + return nil, err + } + + return NewContainerResp(managedContainer), nil +} + +func (s *Service) CreateContainerVersion(_ context.Context, req *CreateContainerVersionReq, containerID, userID int) (*ContainerVersionResp, error) { + if req == nil { + return nil, fmt.Errorf("create container version request is nil") + } + + version := req.ConvertToContainerVersion() + version.ContainerID = containerID + version.UserID = userID + + var createdVersion *model.ContainerVersion + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + versions, err := s.createContainerVersionsCore(repo, []model.ContainerVersion{*version}) + if err != nil { + return fmt.Errorf("failed to create container version: %w", err) + } + + createdVersion = &versions[0] + return nil + }); err != nil { + return nil, fmt.Errorf("failed to create container version: %w", err) + } + + return NewContainerVersionResp(createdVersion), nil +} + +func (s *Service) DeleteContainerVersion(_ context.Context, versionID int) error { + rows, err := s.repo.DeleteContainerVersion(versionID) + if err != nil { + return fmt.Errorf("failed to delete container version: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: container version id %d not found", consts.ErrNotFound, versionID) + } + return nil +} + +func (s *Service) GetContainerVersion(_ context.Context, containerID, versionID int) (*ContainerVersionDetailResp, error) { + if _, err := s.repo.GetContainerByID(containerID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) + } + return nil, fmt.Errorf("failed to get container: %w", err) + } + + version, err := s.repo.GetContainerVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) + } + return nil, fmt.Errorf("failed to get container version: %w", err) + } + + resp := NewContainerVersionDetailResp(version) + + helmConfig, err := s.repo.GetHelmConfigByContainerVersionID(version.ID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("failed to get helm config: %w", err) + } + if helmConfig != nil { + helmConfigResp, err := NewHelmConfigDetailResp(helmConfig) + if err != nil { + return nil, fmt.Errorf("failed to convert helm config: %w", err) + } + resp.HelmConfig = helmConfigResp + } + + return resp, nil +} + +func (s *Service) ListContainerVersions(_ context.Context, req *ListContainerVersionReq, containerID int) (*dto.ListResp[ContainerVersionResp], error) { + limit, offset := req.ToGormParams() + + versions, total, err := s.repo.ListContainerVersions(limit, offset, containerID, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list container versions: %w", err) + } + + items := make([]ContainerVersionResp, len(versions)) + for i := range versions { + items[i] = *NewContainerVersionResp(&versions[i]) + } + + return &dto.ListResp[ContainerVersionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateContainerVersion(_ context.Context, req *UpdateContainerVersionReq, containerID, versionID int) (*ContainerVersionResp, error) { + _ = containerID + + var updatedVersion *model.ContainerVersion + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + version, err := repo.GetContainerVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) + } + return fmt.Errorf("failed to get container version: %w", err) + } + + req.PatchContainerVersionModel(version) + if err := repo.UpdateContainerVersion(version); err != nil { + return fmt.Errorf("failed to update container version: %w", err) + } + + updatedVersion = version + + if req.HelmConfigRequest != nil { + helmConfig, err := repo.GetHelmConfigByContainerVersionID(version.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("helm config not found for version id %d", versionID) + } + return fmt.Errorf("failed to get helm config: %w", err) + } + + if err := req.HelmConfigRequest.PatchHelmConfigModel(helmConfig); err != nil { + return fmt.Errorf("failed to patch helm config model: %w", err) + } + if err := repo.UpdateHelmConfig(helmConfig); err != nil { + return fmt.Errorf("failed to update helm config: %w", err) + } + } + + return nil + }); err != nil { + return nil, err + } + + return NewContainerVersionResp(updatedVersion), nil +} + +func (s *Service) SubmitContainerBuilding(ctx context.Context, req *SubmitBuildContainerReq, groupID string, userID int) (*SubmitContainerBuildResp, error) { + if req == nil { + return nil, fmt.Errorf("build container request is nil") + } + db := s.repo.db + + sourcePath, err := s.build.PrepareGitHubSource(req) + if err != nil { + return nil, fmt.Errorf("failed to process GitHub source: %w", err) + } + + if err := req.ValidateInfoContent(sourcePath); err != nil { + return nil, fmt.Errorf("invalid container info content: %w", err) + } + if err := req.Options.ValidateRequiredFiles(sourcePath); err != nil { + return nil, fmt.Errorf("invalid container options: %w", err) + } + + imageRef := s.build.BuildImageRef(req.ImageName, req.Tag) + payload := map[string]any{ + consts.BuildImageRef: imageRef, + consts.BuildSourcePath: sourcePath, + consts.BuildBuildOptions: req.Options, + } + + task := &dto.UnifiedTask{ + Type: consts.TaskTypeBuildContainer, + Immediate: true, + Payload: payload, + GroupID: groupID, + UserID: userID, + State: consts.TaskPending, + } + task.SetGroupCtx(ctx) + + if err := common.SubmitTaskWithDB(ctx, db, s.redis, task); err != nil { + return nil, fmt.Errorf("failed to submit container building task: %w", err) + } + + return &SubmitContainerBuildResp{ + GroupID: task.GroupID, + TraceID: task.TraceID, + TaskID: task.TaskID, + }, nil +} + +func (s *Service) UploadHelmChart(_ context.Context, file *multipart.FileHeader, containerID, versionID, userID int) (*UploadHelmChartResp, error) { + _ = userID + + containerVersion, err := s.validateHelmConfigVersion(containerID, versionID) + if err != nil { + return nil, err + } + + targetPath, checksum, err := s.helmFiles.SaveChart(containerVersion.Container.Name, file) + if err != nil { + return nil, err + } + filename := file.Filename + containerVersion.HelmConfig.LocalPath = targetPath + containerVersion.HelmConfig.Checksum = checksum + if err := s.repo.UpdateHelmConfig(containerVersion.HelmConfig); err != nil { + return nil, fmt.Errorf("failed to update helm config: %w", err) + } + + return &UploadHelmChartResp{ + FilePath: targetPath, + FileName: filename, + Checksum: checksum, + }, nil +} + +func (s *Service) UploadHelmValueFile(_ context.Context, file *multipart.FileHeader, containerID, versionID, userID int) (*UploadHelmValueFileResp, error) { + _ = userID + + containerVersion, err := s.validateHelmConfigVersion(containerID, versionID) + if err != nil { + return nil, err + } + + if err := s.uploadHelmValueFileCore(containerVersion.Container.Name, containerVersion.HelmConfig, file, ""); err != nil { + return nil, fmt.Errorf("failed to upload helm value file: %w", err) + } + + return &UploadHelmValueFileResp{ + FilePath: containerVersion.HelmConfig.ValueFile, + FileName: file.Filename, + }, nil +} + +func (s *Service) createContainerCore(repo *Repository, container *model.Container, userID int) (*model.Container, error) { + role, err := repo.GetRoleByName(consts.RoleContainerAdmin.String()) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: role %v not found", consts.ErrNotFound, consts.RoleContainerAdmin) + } + return nil, fmt.Errorf("failed to get project owner role: %w", err) + } + + if err := repo.CreateContainer(container); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return nil, consts.ErrAlreadyExists + } + return nil, err + } + + if err := repo.CreateUserContainer(&model.UserContainer{ + UserID: userID, + ContainerID: container.ID, + RoleID: role.ID, + Status: consts.CommonEnabled, + }); err != nil { + return nil, fmt.Errorf("failed to associate container with user: %w", err) + } + + if len(container.Versions) > 0 { + for i := range container.Versions { + container.Versions[i].ContainerID = container.ID + container.Versions[i].UserID = userID + } + + if _, err := s.createContainerVersionsCore(repo, container.Versions); err != nil { + return nil, fmt.Errorf("failed to create container versions: %w", err) + } + } + + return container, nil +} + +func (s *Service) createContainerVersionsCore(repo *Repository, versions []model.ContainerVersion) ([]model.ContainerVersion, error) { + if len(versions) == 0 { + return nil, nil + } + + if err := repo.BatchCreateContainerVersions(versions); err != nil { + return nil, fmt.Errorf("failed to create container versions: %w", err) + } + + type envVarWithVersionIdx struct { + envVar model.ParameterConfig + versionIdx int + } + + envVarsWithIdx := make([]envVarWithVersionIdx, 0) + for versionIdx, version := range versions { + for _, envVar := range version.EnvVars { + envVarsWithIdx = append(envVarsWithIdx, envVarWithVersionIdx{ + envVar: envVar, + versionIdx: versionIdx, + }) + } + } + + if len(envVarsWithIdx) > 0 { + envVars := make([]model.ParameterConfig, len(envVarsWithIdx)) + for i, item := range envVarsWithIdx { + envVars[i] = item.envVar + } + + if err := repo.BatchCreateOrFindParameterConfigs(envVars); err != nil { + return nil, fmt.Errorf("failed to create parameter configs: %w", err) + } + + actualEnvVars, err := repo.ListParameterConfigsByKeys(envVars) + if err != nil { + return nil, fmt.Errorf("failed to list parameter configs: %w", err) + } + + configMap := make(map[string]int, len(actualEnvVars)) + for _, cfg := range actualEnvVars { + key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) + configMap[key] = cfg.ID + } + + relations := make([]model.ContainerVersionEnvVar, 0, len(envVarsWithIdx)) + for _, item := range envVarsWithIdx { + cfg := item.envVar + key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) + paramID, ok := configMap[key] + if !ok { + return nil, fmt.Errorf("parameter config not found after creation: %s", key) + } + relations = append(relations, model.ContainerVersionEnvVar{ + ContainerVersionID: versions[item.versionIdx].ID, + ParameterConfigID: paramID, + }) + } + + if err := repo.AddContainerVersionEnvVars(relations); err != nil { + return nil, fmt.Errorf("failed to create container version env var relations: %w", err) + } + } + + helmConfigs := make([]*model.HelmConfig, 0) + for versionIdx := range versions { + if versions[versionIdx].HelmConfig != nil { + versions[versionIdx].HelmConfig.ContainerVersionID = versions[versionIdx].ID + helmConfigs = append(helmConfigs, versions[versionIdx].HelmConfig) + } + } + + if len(helmConfigs) == 0 { + return versions, nil + } + + if err := repo.BatchCreateHelmConfigs(helmConfigs); err != nil { + return nil, fmt.Errorf("failed to create helm configs: %w", err) + } + + type helmValueWithConfigIdx struct { + value model.ParameterConfig + helmConfigIdx int + } + + helmValuesWithIdx := make([]helmValueWithConfigIdx, 0) + for helmConfigIdx, helmConfig := range helmConfigs { + for _, value := range helmConfig.DynamicValues { + helmValuesWithIdx = append(helmValuesWithIdx, helmValueWithConfigIdx{ + value: value, + helmConfigIdx: helmConfigIdx, + }) + } + } + + if len(helmValuesWithIdx) == 0 { + return versions, nil + } + + helmValues := make([]model.ParameterConfig, len(helmValuesWithIdx)) + for i, item := range helmValuesWithIdx { + helmValues[i] = item.value + } + + if err := repo.BatchCreateOrFindParameterConfigs(helmValues); err != nil { + return nil, fmt.Errorf("failed to create helm parameter configs: %w", err) + } + + actualHelmValues, err := repo.ListParameterConfigsByKeys(helmValues) + if err != nil { + return nil, fmt.Errorf("failed to list helm parameter configs: %w", err) + } + + configMap := make(map[string]int, len(actualHelmValues)) + for _, cfg := range actualHelmValues { + key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) + configMap[key] = cfg.ID + } + + relations := make([]model.HelmConfigValue, 0, len(helmValuesWithIdx)) + for _, item := range helmValuesWithIdx { + cfg := item.value + key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) + paramID, ok := configMap[key] + if !ok { + return nil, fmt.Errorf("helm parameter config not found after creation: %s", key) + } + relations = append(relations, model.HelmConfigValue{ + HelmConfigID: helmConfigs[item.helmConfigIdx].ID, + ParameterConfigID: paramID, + }) + } + + if err := repo.AddHelmConfigValues(relations); err != nil { + return nil, fmt.Errorf("failed to create helm config value relations: %w", err) + } + + return versions, nil +} + +func (s *Service) uploadHelmValueFileCore(containerName string, helmConfig *model.HelmConfig, srcFileHeader *multipart.FileHeader, srcFilePath string) error { + targetPath, err := s.helmFiles.SaveValueFile(containerName, srcFileHeader, srcFilePath) + if err != nil { + return err + } + helmConfig.ValueFile = targetPath + if err := s.repo.UpdateHelmConfig(helmConfig); err != nil { + return fmt.Errorf("failed to update helm config: %w", err) + } + + return nil +} +func (s *Service) validateHelmConfigVersion(containerID, versionID int) (*model.ContainerVersion, error) { + containerVersion, err := s.repo.GetContainerVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: container version %d not found", consts.ErrNotFound, versionID) + } + return nil, fmt.Errorf("failed to get container version: %w", err) + } + + if containerVersion.ContainerID != containerID { + return nil, fmt.Errorf("version %d does not belong to container %d", versionID, containerID) + } + if containerVersion.Container == nil || containerVersion.Container.Type != consts.ContainerTypePedestal { + return nil, fmt.Errorf("only pedestal container versions support Helm configurations") + } + if containerVersion.HelmConfig == nil { + return nil, fmt.Errorf("container version %d does not have an associated Helm configuration", versionID) + } + + return containerVersion, nil +} diff --git a/src/module/dataset/api_types.go b/src/module/dataset/api_types.go new file mode 100644 index 00000000..a3d6f2fd --- /dev/null +++ b/src/module/dataset/api_types.go @@ -0,0 +1,348 @@ +package datasetmodule + +import ( + "fmt" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + injectionmodule "aegis/module/injection" + "aegis/utils" +) + +// CreateDatasetReq represents dataset creation request. +type CreateDatasetReq struct { + Name string `json:"name" binding:"required"` + Type string `json:"type" binding:"required"` + Description string `json:"description" binding:"omitempty"` + IsPublic *bool `json:"is_public" binding:"omitempty"` + + VersionReq *CreateDatasetVersionReq `json:"version" binding:"omitempty"` +} + +func (req *CreateDatasetReq) Validate() error { + req.Name = strings.TrimSpace(req.Name) + req.Type = strings.TrimSpace(req.Type) + + if req.Name == "" { + return fmt.Errorf("dataset name cannot be empty") + } + if req.Type == "" { + return fmt.Errorf("dataset type cannot be empty") + } + if req.IsPublic == nil { + req.IsPublic = utils.BoolPtr(true) + } + if req.VersionReq != nil { + if err := req.VersionReq.Validate(); err != nil { + return fmt.Errorf("invalid dataset version request: %v", err) + } + } + return nil +} + +func (req *CreateDatasetReq) ConvertToDataset() *model.Dataset { + return &model.Dataset{ + Name: req.Name, + Type: req.Type, + Description: req.Description, + IsPublic: *req.IsPublic, + Status: consts.CommonEnabled, + } +} + +// ListDatasetReq represents dataset list query parameters. +type ListDatasetReq struct { + dto.PaginationReq + Type string `form:"type" binding:"omitempty"` + IsPublic *bool `form:"is_public" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListDatasetReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +// UpdateDatasetReq represents dataset update request. +type UpdateDatasetReq struct { + Description *string `json:"description" binding:"omitempty"` + IsPublic *bool `json:"is_public" binding:"omitempty"` + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *UpdateDatasetReq) Validate() error { + return validateStatus(req.Status, true) +} + +func (req *UpdateDatasetReq) PatchDatasetModel(target *model.Dataset) { + if req.Description != nil { + target.Description = *req.Description + } + if req.IsPublic != nil { + target.IsPublic = *req.IsPublic + } + if req.Status != nil { + target.Status = *req.Status + } +} + +// ManageDatasetLabelReq represents dataset label management request. +type ManageDatasetLabelReq struct { + AddLabels []dto.LabelItem `json:"add_labels" binding:"omitempty"` + RemoveLabels []string `json:"remove_labels" binding:"omitempty"` +} + +func (req *ManageDatasetLabelReq) Validate() error { + if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { + return fmt.Errorf("at least one of add_labels or remove_labels must be provided") + } + if err := validateLabelItems(req.AddLabels); err != nil { + return err + } + for i, key := range req.RemoveLabels { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("empty label key at index %d in remove_labels", i) + } + } + return nil +} + +// DatasetResp represents dataset summary information. +type DatasetResp struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsPublic bool `json:"is_public"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +func NewDatasetResp(dataset *model.Dataset) *DatasetResp { + resp := &DatasetResp{ + ID: dataset.ID, + Name: dataset.Name, + Type: dataset.Type, + IsPublic: dataset.IsPublic, + Status: consts.GetStatusTypeName(dataset.Status), + CreatedAt: dataset.CreatedAt, + UpdatedAt: dataset.UpdatedAt, + } + + if len(dataset.Labels) > 0 { + resp.Labels = make([]dto.LabelItem, 0, len(dataset.Labels)) + for _, l := range dataset.Labels { + resp.Labels = append(resp.Labels, dto.LabelItem{Key: l.Key, Value: l.Value}) + } + } + return resp +} + +// DatasetDetailResp represents detailed dataset information. +type DatasetDetailResp struct { + DatasetResp + Description string `json:"description"` + Versions []DatasetVersionResp `json:"versions"` +} + +func NewDatasetDetailResp(dataset *model.Dataset) *DatasetDetailResp { + return &DatasetDetailResp{ + DatasetResp: *NewDatasetResp(dataset), + Description: dataset.Description, + } +} + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} + +func validateLabelItems(items []dto.LabelItem) error { + for i, label := range items { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d in add_labels", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d in add_labels", i) + } + } + return nil +} + +// SearchDatasetReq represents advanced dataset search. +type SearchDatasetReq struct { + dto.AdvancedSearchReq[consts.DatasetField] + + NamePattern string `json:"name_pattern" binding:"omitempty"` + IncludeVersions bool `json:"include_versions" binding:"omitempty"` +} + +func (req *SearchDatasetReq) Validate() error { + if err := req.AdvancedSearchReq.Validate(); err != nil { + return err + } + for i, sortField := range req.Sort { + if _, valid := consts.DatasetAllowedFields[sortField.Field]; !valid { + return fmt.Errorf("invalid sort_by field at index %d: %s", i, sortField.Field) + } + } + for i, field := range req.GroupBy { + if _, valid := consts.DatasetAllowedFields[field]; !valid { + return fmt.Errorf("invalid group_by field at index %d: %s", i, field) + } + } + return nil +} + +func (req *SearchDatasetReq) ConvertToSearchReq() *dto.SearchReq[consts.DatasetField] { + sr := req.ConvertAdvancedToSearch() + + if req.NamePattern != "" { + sr.AddFilter("name", dto.OpLike, req.NamePattern) + } + if req.IncludeVersions { + sr.AddInclude("Versions") + } + + return sr +} + +// ManageDatasetVersionInjectionReq represents datapack membership changes for a dataset version. +type ManageDatasetVersionInjectionReq struct { + AddDatapacks []string `json:"add_datapacks" binding:"omitempty"` + RemoveDatapacks []string `json:"remove_datapacks" binding:"omitempty"` +} + +func (req *ManageDatasetVersionInjectionReq) Validate() error { + if len(req.AddDatapacks) == 0 && len(req.RemoveDatapacks) == 0 { + return fmt.Errorf("at least one of add_injections or remove_injections must be provided") + } + + for i, datapack := range req.AddDatapacks { + if strings.TrimSpace(datapack) == "" { + return fmt.Errorf("empty datapack name at index %d in add_datapacks", i) + } + } + for i, datapack := range req.RemoveDatapacks { + if strings.TrimSpace(datapack) == "" { + return fmt.Errorf("empty datapack name at index %d in add_datapacks", i) + } + } + + return nil +} + +// CreateDatasetVersionReq represents dataset version creation. +type CreateDatasetVersionReq struct { + Name string `json:"name" binding:"required"` + Datapacks []string `json:"datapacks" binding:"omitempty"` +} + +func (req *CreateDatasetVersionReq) Validate() error { + req.Name = strings.TrimSpace(req.Name) + + if req.Name == "" { + return fmt.Errorf("name cannot be empty") + } + if _, _, _, err := utils.ParseSemanticVersion(req.Name); err != nil { + return fmt.Errorf("invalid semantic version: %s, %v", req.Name, err) + } + for i, datapack := range req.Datapacks { + if strings.TrimSpace(datapack) == "" { + return fmt.Errorf("empty datapack name at index %d", i) + } + } + + return nil +} + +func (req *CreateDatasetVersionReq) ConvertToDatasetVersion() *model.DatasetVersion { + return &model.DatasetVersion{ + Name: req.Name, + Status: consts.CommonEnabled, + } +} + +// ListDatasetVersionReq represents dataset version list query parameters. +type ListDatasetVersionReq struct { + dto.PaginationReq + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *ListDatasetVersionReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +// UpdateDatasetVersionReq represents mutable dataset version fields. +type UpdateDatasetVersionReq struct { + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *UpdateDatasetVersionReq) Validate() error { + return validateStatus(req.Status, true) +} + +func (req *UpdateDatasetVersionReq) PatchDatasetVersionModel(target *model.DatasetVersion) { + if req.Status != nil { + target.Status = *req.Status + } +} + +// DatasetVersionResp represents dataset version summary information. +type DatasetVersionResp struct { + ID int `json:"id"` + Name string `json:"name"` + Checksum string `json:"checksum"` + FileCount int `json:"file_count"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewDatasetVersionResp(version *model.DatasetVersion) *DatasetVersionResp { + return &DatasetVersionResp{ + ID: version.ID, + Name: version.Name, + Checksum: version.Checksum, + FileCount: version.FileCount, + UpdatedAt: version.UpdatedAt, + } +} + +// DatasetVersionDetailResp represents dataset version details including datapacks. +type DatasetVersionDetailResp struct { + DatasetVersionResp + + Datapacks []injectionmodule.InjectionResp `json:"datapacks,omitempty"` +} + +func NewDatasetVersionDetailResp(version *model.DatasetVersion) *DatasetVersionDetailResp { + resp := &DatasetVersionDetailResp{ + DatasetVersionResp: *NewDatasetVersionResp(version), + } + + if len(version.Datapacks) > 0 { + resp.Datapacks = make([]injectionmodule.InjectionResp, 0, len(version.Datapacks)) + for _, datapack := range version.Datapacks { + resp.Datapacks = append(resp.Datapacks, *injectionmodule.NewInjectionResp(&datapack)) + } + } + + return resp +} diff --git a/src/module/dataset/core.go b/src/module/dataset/core.go new file mode 100644 index 00000000..1bdeb646 --- /dev/null +++ b/src/module/dataset/core.go @@ -0,0 +1,12 @@ +package datasetmodule + +import ( + "aegis/model" + + "gorm.io/gorm" +) + +func CreateDatasetCore(tx *gorm.DB, dataset *model.Dataset, versions []model.DatasetVersion, userID int) (*model.Dataset, error) { + service := NewService(NewRepository(tx), NewDatapackFileStore()) + return service.createDatasetCore(service.repo, dataset, versions, userID) +} diff --git a/src/module/dataset/file_store.go b/src/module/dataset/file_store.go new file mode 100644 index 00000000..b26c06dc --- /dev/null +++ b/src/module/dataset/file_store.go @@ -0,0 +1,69 @@ +package datasetmodule + +import ( + "archive/zip" + "fmt" + "io/fs" + "path/filepath" + + "aegis/config" + "aegis/consts" + "aegis/model" + "aegis/utils" +) + +type DatapackFileStore struct { + basePath string +} + +func NewDatapackFileStore() *DatapackFileStore { + return &DatapackFileStore{basePath: config.GetString("jfs.dataset_path")} +} + +func (s *DatapackFileStore) PackageToZip(zipWriter *zip.Writer, datapacks []model.FaultInjection, excludeRules []utils.ExculdeRule) error { + for i := range datapacks { + if err := s.packageDatapackToZip(zipWriter, &datapacks[i], excludeRules); err != nil { + return err + } + } + return nil +} + +func (s *DatapackFileStore) packageDatapackToZip(zipWriter *zip.Writer, datapack *model.FaultInjection, excludeRules []utils.ExculdeRule) error { + if datapack.State < consts.DatapackBuildSuccess { + return fmt.Errorf("datapack %s is not in a downloadable state", datapack.Name) + } + + workDir := filepath.Join(s.basePath, datapack.Name) + if !utils.IsAllowedPath(workDir) { + return fmt.Errorf("invalid path access to %s", workDir) + } + + err := filepath.WalkDir(workDir, func(path string, dir fs.DirEntry, err error) error { + if err != nil || dir.IsDir() { + return err + } + + relPath, _ := filepath.Rel(workDir, path) + fullRelPath := filepath.Join(consts.DownloadFilename, filepath.Base(workDir), relPath) + fileName := filepath.Base(path) + + for _, rule := range excludeRules { + if utils.MatchFile(fileName, rule) { + return nil + } + } + + fileInfo, err := dir.Info() + if err != nil { + return err + } + + return utils.AddToZip(zipWriter, fileInfo, path, filepath.ToSlash(fullRelPath)) + }) + if err != nil { + return fmt.Errorf("failed to package datapack %s: %w", datapack.Name, err) + } + + return nil +} diff --git a/src/module/dataset/file_store_test.go b/src/module/dataset/file_store_test.go new file mode 100644 index 00000000..4e10a3f3 --- /dev/null +++ b/src/module/dataset/file_store_test.go @@ -0,0 +1,75 @@ +package datasetmodule + +import ( + "archive/zip" + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "aegis/consts" + "aegis/model" + "aegis/utils" + + "github.com/spf13/viper" +) + +func TestDatapackFileStorePackageToZip(t *testing.T) { + tmpDir := t.TempDir() + viper.Set("jfs.dataset_path", tmpDir) + + datapackDir := filepath.Join(tmpDir, "datapack-a") + if err := os.MkdirAll(filepath.Join(datapackDir, "nested"), 0o755); err != nil { + t.Fatalf("mkdir datapack dir: %v", err) + } + if err := os.WriteFile(filepath.Join(datapackDir, "nested", "keep.txt"), []byte("hello"), 0o644); err != nil { + t.Fatalf("write datapack file: %v", err) + } + if err := os.WriteFile(filepath.Join(datapackDir, "skip.log"), []byte("skip"), 0o644); err != nil { + t.Fatalf("write excluded file: %v", err) + } + + store := &DatapackFileStore{basePath: tmpDir} + buf := &bytes.Buffer{} + zipWriter := zip.NewWriter(buf) + err := store.PackageToZip(zipWriter, []model.FaultInjection{{ + Name: "datapack-a", + State: consts.DatapackBuildSuccess, + }}, []utils.ExculdeRule{{Pattern: "*.log", IsGlob: true}}) + if err != nil { + t.Fatalf("PackageToZip failed: %v", err) + } + if err := zipWriter.Close(); err != nil { + t.Fatalf("close zip writer: %v", err) + } + + reader, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != nil { + t.Fatalf("open zip reader: %v", err) + } + + files := make(map[string]string, len(reader.File)) + for _, file := range reader.File { + rc, err := file.Open() + if err != nil { + t.Fatalf("open zip file %s: %v", file.Name, err) + } + content, err := io.ReadAll(rc) + _ = rc.Close() + if err != nil { + t.Fatalf("read zip file %s: %v", file.Name, err) + } + files[file.Name] = string(content) + } + + expected := filepath.ToSlash(filepath.Join(consts.DownloadFilename, "datapack-a", "nested", "keep.txt")) + if files[expected] != "hello" { + t.Fatalf("expected zip to contain %s with hello, got %q", expected, files[expected]) + } + + excluded := filepath.ToSlash(filepath.Join(consts.DownloadFilename, "datapack-a", "skip.log")) + if _, ok := files[excluded]; ok { + t.Fatalf("expected %s to be excluded", excluded) + } +} diff --git a/src/handlers/v2/datasets.go b/src/module/dataset/handler.go similarity index 69% rename from src/handlers/v2/datasets.go rename to src/module/dataset/handler.go index 4d12a6e1..1b118130 100644 --- a/src/handlers/v2/datasets.go +++ b/src/module/dataset/handler.go @@ -1,6 +1,7 @@ -package v2 +package datasetmodule import ( + "aegis/httpx" "archive/zip" "fmt" "net/http" @@ -8,14 +9,20 @@ import ( "aegis/consts" "aegis/dto" - "aegis/handlers" "aegis/middleware" - producer "aegis/service/producer" "aegis/utils" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // CreateDataset handles dataset creation // // @Summary Create dataset @@ -25,23 +32,23 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.CreateDatasetReq true "Dataset creation request" -// @Success 201 {object} dto.GenericResponse[dto.DatasetResp] "Dataset created successfully" +// @Param request body CreateDatasetReq true "Dataset creation request" +// @Success 201 {object} dto.GenericResponse[DatasetResp] "Dataset created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Conflict error" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets [post] -// @x-api-type {"sdk":"true"} -func CreateDataset(c *gin.Context) { +// @x-api-type {} +func (h *Handler) CreateDataset(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req dto.CreateDatasetReq + var req CreateDatasetReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -52,8 +59,8 @@ func CreateDataset(c *gin.Context) { return } - resp, err := producer.CreateDataset(&req, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateDataset(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } @@ -76,20 +83,18 @@ func CreateDataset(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [delete] -func DeleteDataset(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) DeleteDataset(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - err = producer.DeleteDataset(datasetID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteDataset(c.Request.Context(), datasetID)) { return } - dto.JSONResponse[any](c, http.StatusCreated, "Dataset deleted successfully", nil) + dto.JSONResponse[any](c, http.StatusNoContent, "Dataset deleted successfully", nil) } // GetDataset handles getting a single dataset by ID @@ -101,24 +106,22 @@ func DeleteDataset(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" -// @Success 200 {object} dto.GenericResponse[dto.DatasetDetailResp] "Dataset retrieved successfully" +// @Success 200 {object} dto.GenericResponse[DatasetDetailResp] "Dataset retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [get] -// @x-api-type {"sdk":"true"} -func GetDataset(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) GetDataset(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - resp, err := producer.GetDatasetDetail(datasetID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetDataset(c.Request.Context(), datasetID) + if httpx.HandleServiceError(c, err) { return } @@ -138,15 +141,15 @@ func GetDataset(c *gin.Context) { // @Param type query string false "Dataset type filter" // @Param is_public query bool false "Dataset public visibility filter" // @Param status query consts.StatusType false "Dataset status filter" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.DatasetResp]] "Datasets retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetResp]] "Datasets retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets [get] -// @x-api-type {"sdk":"true"} -func ListDatasets(c *gin.Context) { - var req dto.ListDatasetReq +// @x-api-type {} +func (h *Handler) ListDatasets(c *gin.Context) { + var req ListDatasetReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -157,8 +160,8 @@ func ListDatasets(c *gin.Context) { return } - resp, err := producer.ListDatasets(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListDatasets(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -174,16 +177,16 @@ func ListDatasets(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.SearchDatasetReq true "Dataset search request" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.DatasetDetailResp]] "Datasets retrieved successfully" +// @Param request body SearchDatasetReq true "Dataset search request" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetDetailResp]] "Datasets retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/search [post] -// @x-api-type {"sdk":"true"} -func SearchDataset(c *gin.Context) { - var req dto.SearchDatasetReq +// @x-api-type {} +func (h *Handler) SearchDataset(c *gin.Context) { + var req SearchDatasetReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -194,8 +197,8 @@ func SearchDataset(c *gin.Context) { return } - resp, err := producer.SearchDatasets(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.SearchDatasets(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -212,38 +215,40 @@ func SearchDataset(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" -// @Param request body dto.UpdateDatasetReq true "Dataset update request" -// @Success 202 {object} dto.GenericResponse[dto.DatasetResp] "Dataset updated successfully" +// @Param request body UpdateDatasetReq true "Dataset update request" +// @Success 202 {object} dto.GenericResponse[DatasetResp] "Dataset updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [patch] -func UpdateDataset(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) UpdateDataset(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - var req dto.UpdateDatasetReq + var req UpdateDatasetReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - resp, err := producer.UpdateDataset(&req, datasetID) - if handlers.HandleServiceError(c, err) { + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.UpdateDataset(c.Request.Context(), &req, datasetID) + if httpx.HandleServiceError(c, err) { return } dto.JSONResponse[any](c, http.StatusAccepted, "Dataset updated successfully", resp) } -// ===================== Dataset-Label API ===================== - // ManageDatasetCustomLabels manages dataset custom labels (key-value pairs) // // @Summary Manage dataset custom labels @@ -254,23 +259,22 @@ func UpdateDataset(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" -// @Param manage body dto.ManageDatasetLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[dto.DatasetResp] "Labels managed successfully" +// @Param manage body ManageDatasetLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[DatasetResp] "Labels managed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/labels [patch] -func ManageDatasetCustomLabels(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) ManageDatasetCustomLabels(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - var req dto.ManageDatasetLabelReq + var req ManageDatasetLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -281,8 +285,8 @@ func ManageDatasetCustomLabels(c *gin.Context) { return } - resp, err := producer.ManageDatasetLabels(&req, datasetID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ManageDatasetLabels(c.Request.Context(), &req, datasetID) + if httpx.HandleServiceError(c, err) { return } @@ -299,30 +303,28 @@ func ManageDatasetCustomLabels(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" -// @Param request body dto.CreateDatasetVersionReq true "Dataset version creation request" -// @Success 201 {object} dto.GenericResponse[dto.DatasetVersionResp] "Dataset version created successfully" +// @Param request body CreateDatasetVersionReq true "Dataset version creation request" +// @Success 201 {object} dto.GenericResponse[DatasetVersionResp] "Dataset version created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Conflict error" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions [post] -// @x-api-type {"sdk":"true"} -func CreateDatasetVersion(c *gin.Context) { +// @x-api-type {} +func (h *Handler) CreateDatasetVersion(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") + datasetID, ok := parseDatasetID(c) + if !ok { return } - var req dto.CreateDatasetVersionReq + var req CreateDatasetVersionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -333,8 +335,8 @@ func CreateDatasetVersion(c *gin.Context) { return } - resp, err := producer.CreateDatasetVersion(&req, datasetID, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateDatasetVersion(c.Request.Context(), &req, datasetID, userID) + if httpx.HandleServiceError(c, err) { return } @@ -358,16 +360,14 @@ func CreateDatasetVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [delete] -func DeleteDatasetVersion(c *gin.Context) { - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset version ID") +// @x-api-type {} +func (h *Handler) DeleteDatasetVersion(c *gin.Context) { + versionID, ok := parseDatasetVersionID(c) + if !ok { return } - err = producer.DeleteDatasetVersion(versionID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteDatasetVersion(c.Request.Context(), versionID)) { return } @@ -384,31 +384,26 @@ func DeleteDatasetVersion(c *gin.Context) { // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" // @Param version_id path int true "Dataset Version ID" -// @Success 200 {object} dto.GenericResponse[dto.DatasetVersionDetailResp] "Dataset version retrieved successfully" +// @Success 200 {object} dto.GenericResponse[DatasetVersionDetailResp] "Dataset version retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [get] -// @x-api-type {"sdk":"true"} -func GetDatasetVersion(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) GetDatasetVersion(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset version ID") + versionID, ok := parseDatasetVersionID(c) + if !ok { return } - resp, err := producer.GetDatasetVersionDetail(datasetID, versionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetDatasetVersion(c.Request.Context(), datasetID, versionID) + if httpx.HandleServiceError(c, err) { return } @@ -427,22 +422,20 @@ func GetDatasetVersion(c *gin.Context) { // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) // @Param status query consts.StatusType false "Dataset version status filter" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.DatasetVersionResp]] "Dataset versions retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetVersionResp]] "Dataset versions retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions [get] -// @x-api-type {"sdk":"true"} -func ListDatasetVersions(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) ListDatasetVersions(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - var req dto.ListDatasetVersionReq + var req ListDatasetVersionReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -453,8 +446,8 @@ func ListDatasetVersions(c *gin.Context) { return } - resp, err := producer.ListDatasetVersions(&req, datasetID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListDatasetVersions(c.Request.Context(), &req, datasetID) + if httpx.HandleServiceError(c, err) { return } @@ -472,30 +465,26 @@ func ListDatasetVersions(c *gin.Context) { // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" // @Param version_id path int true "Dataset Version ID" -// @Param request body dto.UpdateDatasetVersionReq true "Dataset version update request" -// @Success 202 {object} dto.GenericResponse[dto.DatasetVersionResp] "Dataset version updated successfully" +// @Param request body UpdateDatasetVersionReq true "Dataset version update request" +// @Success 202 {object} dto.GenericResponse[DatasetVersionResp] "Dataset version updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID/request format/request parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [patch] -func UpdateDatasetVersion(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) UpdateDatasetVersion(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset version ID") + versionID, ok := parseDatasetVersionID(c) + if !ok { return } - var req dto.UpdateDatasetVersionReq + var req UpdateDatasetVersionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -506,8 +495,8 @@ func UpdateDatasetVersion(c *gin.Context) { return } - resp, err := producer.UpdateDatasetVersion(&req, datasetID, versionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateDatasetVersion(c.Request.Context(), &req, datasetID, versionID) + if httpx.HandleServiceError(c, err) { return } @@ -530,24 +519,19 @@ func UpdateDatasetVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id}/download [get] -// @x-api-type {"sdk":"true"} -func DownloadDatasetVersion(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) DownloadDatasetVersion(c *gin.Context) { + datasetID, ok := parseDatasetID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid version ID") + versionID, ok := parseDatasetVersionID(c) + if !ok { return } - filename, err := producer.GetDatasetVersionFilename(datasetID, versionID) - if handlers.HandleServiceError(c, err) { + filename, err := h.service.GetDatasetVersionFilename(c.Request.Context(), datasetID, versionID) + if httpx.HandleServiceError(c, err) { return } @@ -557,15 +541,13 @@ func DownloadDatasetVersion(c *gin.Context) { zipWriter := zip.NewWriter(c.Writer) defer func() { _ = zipWriter.Close() }() - if err := producer.DownloadDatasetVersion(zipWriter, []utils.ExculdeRule{}, versionID); err != nil { + if err := h.service.DownloadDatasetVersion(c.Request.Context(), zipWriter, []utils.ExculdeRule{}, versionID); err != nil { delete(c.Writer.Header(), "Content-Disposition") c.Header("Content-Type", "application/json; charset=utf-8") - handlers.HandleServiceError(c, err) + httpx.HandleServiceError(c, err) } } -// ===================== DatasetVersion-Injection API ===================== - // ManageDatasetInjections manages dataset injections // // @Summary Manage dataset injections @@ -577,31 +559,26 @@ func DownloadDatasetVersion(c *gin.Context) { // @Security BearerAuth // @Param dataset_id path int true "Dataset ID" // @Param version_id path int true "Dataset Version ID" -// @Param manage body dto.ManageDatasetVersionInjectionReq true "Injection management request" -// @Success 200 {object} dto.GenericResponse[dto.DatasetVersionDetailResp] "Injections managed successfully" +// @Param manage body ManageDatasetVersionInjectionReq true "Injection management request" +// @Success 200 {object} dto.GenericResponse[DatasetVersionDetailResp] "Injections managed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/version/{version_id}/injections [patch] -// @x-api-type {"sdk":"true"} -func ManageDatasetVersionInjections(c *gin.Context) { - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") +// @x-api-type {} +func (h *Handler) ManageDatasetVersionInjections(c *gin.Context) { + _, ok := parseDatasetID(c) + if !ok { return } - - versionIDStr := c.Param(consts.URLPathVersionID) - versionID, err := strconv.Atoi(versionIDStr) - if err != nil || versionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset version ID") + versionID, ok := parseDatasetVersionID(c) + if !ok { return } - var req dto.ManageDatasetVersionInjectionReq + var req ManageDatasetVersionInjectionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -612,10 +589,30 @@ func ManageDatasetVersionInjections(c *gin.Context) { return } - resp, err := producer.ManageDatasetVersionInjections(&req, versionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ManageDatasetVersionInjections(c.Request.Context(), &req, versionID) + if httpx.HandleServiceError(c, err) { return } dto.SuccessResponse(c, resp) } + +func parseDatasetID(c *gin.Context) (int, bool) { + datasetIDStr := c.Param(consts.URLPathDatasetID) + datasetID, err := strconv.Atoi(datasetIDStr) + if err != nil || datasetID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") + return 0, false + } + return datasetID, true +} + +func parseDatasetVersionID(c *gin.Context) (int, bool) { + versionIDStr := c.Param(consts.URLPathVersionID) + versionID, err := strconv.Atoi(versionIDStr) + if err != nil || versionID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset version ID") + return 0, false + } + return versionID, true +} diff --git a/src/module/dataset/module.go b/src/module/dataset/module.go new file mode 100644 index 00000000..41e13127 --- /dev/null +++ b/src/module/dataset/module.go @@ -0,0 +1,10 @@ +package datasetmodule + +import "go.uber.org/fx" + +var Module = fx.Module("dataset", + fx.Provide(NewRepository), + fx.Provide(NewDatapackFileStore), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/dataset/repository.go b/src/module/dataset/repository.go new file mode 100644 index 00000000..37e71677 --- /dev/null +++ b/src/module/dataset/repository.go @@ -0,0 +1,377 @@ +package datasetmodule + +import ( + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/repository" + "fmt" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ( + datasetCommonOmitFields = "active_name" + datasetVersionModelOmitFields = "active_version_key" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) GetRoleByName(name string) (*model.Role, error) { + var role model.Role + if err := r.db.Where("name = ? and status != ?", name, consts.CommonDeleted).First(&role).Error; err != nil { + return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) + } + return &role, nil +} + +func (r *Repository) CreateDataset(dataset *model.Dataset) error { + if err := r.db.Omit(datasetCommonOmitFields).Create(dataset).Error; err != nil { + return fmt.Errorf("failed to create dataset: %v", err) + } + return nil +} + +func (r *Repository) CreateUserDataset(userDataset *model.UserDataset) error { + if err := r.db.Omit("active_user_dataset").Create(userDataset).Error; err != nil { + return fmt.Errorf("failed to create user-dataset association: %w", err) + } + return nil +} + +func (r *Repository) BatchDeleteDatasetVersions(datasetID int) (int64, error) { + result := r.db.Model(&model.DatasetVersion{}). + Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to batch soft delete dataset versions for dataset %d: %w", datasetID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) RemoveUsersFromDataset(datasetID int) (int64, error) { + result := r.db.Model(&model.UserDataset{}). + Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to delete user-dataset associations for dataset %d: %w", datasetID, err) + } + return result.RowsAffected, nil +} + +func (r *Repository) DeleteDataset(datasetID int) (int64, error) { + result := r.db.Model(&model.Dataset{}). + Where("id = ? AND status != ?", datasetID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to delete dataset: %v", err) + } + return result.RowsAffected, nil +} + +func (r *Repository) GetDatasetByID(datasetID int) (*model.Dataset, error) { + var dataset model.Dataset + if err := r.db.Where("id = ? AND status != ?", datasetID, consts.CommonDeleted).First(&dataset).Error; err != nil { + return nil, fmt.Errorf("failed to get dataset: %v", err) + } + return &dataset, nil +} + +func (r *Repository) ListDatasetVersionsByDatasetID(datasetID int) ([]model.DatasetVersion, error) { + var versions []model.DatasetVersion + if err := r.db.Where("dataset_id = ?", datasetID).Find(&versions).Error; err != nil { + return nil, fmt.Errorf("failed to list dataset versions for dataset %d: %w", datasetID, err) + } + return versions, nil +} + +func (r *Repository) ListDatasets(limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]model.Dataset, int64, error) { + var ( + datasets []model.Dataset + total int64 + ) + + query := r.db.Model(&model.Dataset{}) + if datasetType != "" { + query = query.Where("type = ?", datasetType) + } + if isPublic != nil { + query = query.Where("is_public = ?", *isPublic) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count datasets: %v", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&datasets).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list datasets: %v", err) + } + return datasets, total, nil +} + +func (r *Repository) SearchDatasets(searchReq *dto.SearchReq[consts.DatasetField]) ([]model.Dataset, int64, error) { + qb := repository.NewSearchQueryBuilder(r.db, consts.DatasetAllowedFields) + qb.ApplySearchReq(searchReq.Filters, searchReq.Keyword, searchReq.Sort, searchReq.GroupBy, model.Dataset{}) + qb.ApplyIncludes(searchReq.Includes) + qb.ApplyIncludeFields(searchReq.IncludeFields) + qb.ApplyExcludeFields(searchReq.ExcludeFields, model.Dataset{}) + + total, err := qb.GetCount() + if err != nil { + return nil, 0, fmt.Errorf("failed to count searched datasets: %w", err) + } + + query := qb.Query() + if searchReq.Size != 0 && searchReq.Page != 0 { + query = query.Offset(searchReq.GetOffset()).Limit(int(searchReq.Size)) + } + + var items []model.Dataset + if err := query.Find(&items).Error; err != nil { + return nil, 0, fmt.Errorf("failed to execute dataset search: %w", err) + } + return items, total, nil +} + +func (r *Repository) ListDatasetLabels(datasetIDs []int) (map[int][]model.Label, error) { + if len(datasetIDs) == 0 { + return nil, nil + } + + type datasetLabelResult struct { + model.Label + DatasetID int `gorm:"column:dataset_id"` + } + + var flatResults []datasetLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). + Where("dl.dataset_id IN (?)", datasetIDs). + Select("labels.*, dl.dataset_id"). + Find(&flatResults).Error; err != nil { + return nil, fmt.Errorf("failed to batch query dataset labels: %w", err) + } + + labelsMap := make(map[int][]model.Label, len(datasetIDs)) + for _, id := range datasetIDs { + labelsMap[id] = []model.Label{} + } + for _, res := range flatResults { + labelsMap[res.DatasetID] = append(labelsMap[res.DatasetID], res.Label) + } + return labelsMap, nil +} + +func (r *Repository) UpdateDataset(dataset *model.Dataset) error { + if err := r.db.Omit(datasetCommonOmitFields).Save(dataset).Error; err != nil { + return fmt.Errorf("failed to update dataset: %v", err) + } + return nil +} + +func (r *Repository) AddDatasetLabels(datasetLabels []model.DatasetLabel) error { + if len(datasetLabels) == 0 { + return nil + } + if err := r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "dataset_id"}, {Name: "label_id"}}, + DoNothing: true, + }).Create(&datasetLabels).Error; err != nil { + return fmt.Errorf("failed to add dataset-label associations: %w", err) + } + return nil +} + +func (r *Repository) ListLabelIDsByKeyAndDatasetID(datasetID int, keys []string) ([]int, error) { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN dataset_labels dl ON dl.label_id = l.id"). + Where("dl.dataset_id = ? AND l.label_key IN (?)", datasetID, keys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) + } + return labelIDs, nil +} + +func (r *Repository) ClearDatasetLabels(datasetIDs []int, labelIDs []int) error { + if len(datasetIDs) == 0 { + return nil + } + + query := r.db.Table("dataset_labels").Where("dataset_id IN (?)", datasetIDs) + if len(labelIDs) > 0 { + query = query.Where("label_id IN (?)", labelIDs) + } + if err := query.Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear dataset-label associations: %w", err) + } + return nil +} + +func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + Clauses(clause.Returning{}). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch decrease label usages: %w", err) + } + return nil +} + +func (r *Repository) ListLabelsByDatasetID(datasetID int) ([]model.Label, error) { + var labels []model.Label + if err := r.db.Model(&model.Label{}). + Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). + Where("dl.dataset_id = ?", datasetID). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels for dataset %d: %w", datasetID, err) + } + return labels, nil +} + +func (r *Repository) BatchCreateDatasetVersions(versions []model.DatasetVersion) error { + if len(versions) == 0 { + return fmt.Errorf("no dataset versions to create") + } + if err := r.db.Omit(datasetVersionModelOmitFields).Create(&versions).Error; err != nil { + return fmt.Errorf("failed to batch create dataset versions: %w", err) + } + return nil +} + +func (r *Repository) DeleteDatasetVersion(versionID int) (int64, error) { + result := r.db.Model(&model.DatasetVersion{}). + Where("id = ? AND status != ?", versionID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to soft delete dataset version %d: %w", versionID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) GetDatasetVersionByID(versionID int) (*model.DatasetVersion, error) { + var version model.DatasetVersion + if err := r.db.Preload("Datapacks").Where("id = ?", versionID).First(&version).Error; err != nil { + return nil, fmt.Errorf("failed to get dataset version: %v", err) + } + return &version, nil +} + +func (r *Repository) ListDatasetVersions(limit, offset int, datasetID int, status *consts.StatusType) ([]model.DatasetVersion, int64, error) { + var ( + versions []model.DatasetVersion + total int64 + ) + + query := r.db.Model(&model.DatasetVersion{}).Where("dataset_id = ?", datasetID) + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count dataset versions: %v", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&versions).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list dataset versions: %v", err) + } + return versions, total, nil +} + +func (r *Repository) UpdateDatasetVersion(version *model.DatasetVersion) error { + if err := r.db.Omit(datasetVersionModelOmitFields).Save(version).Error; err != nil { + return fmt.Errorf("failed to update dataset version: %w", err) + } + return nil +} + +func (r *Repository) ListInjectionIDsByNames(names []string) (map[string]int, error) { + if len(names) == 0 { + return map[string]int{}, nil + } + + var records []struct { + Name string `gorm:"column:name"` + ID int `gorm:"column:id"` + } + if err := r.db.Model(&model.FaultInjection{}). + Select("name, id"). + Where("state = ? AND status = ?", consts.DatapackBuildSuccess, consts.CommonEnabled). + Where("name IN (?)", names). + Find(&records).Error; err != nil { + return nil, fmt.Errorf("failed to query injection IDs: %w", err) + } + + result := make(map[string]int, len(records)) + for _, record := range records { + result[record.Name] = record.ID + } + return result, nil +} + +func (r *Repository) AddDatasetVersionInjections(items []model.DatasetVersionInjection) error { + if len(items) == 0 { + return nil + } + if err := r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "dataset_version_id"}, {Name: "injection_id"}}, + DoNothing: true, + }).Create(&items).Error; err != nil { + return fmt.Errorf("failed to add dataset-version-injection associations: %w", err) + } + return nil +} + +func (r *Repository) ClearDatasetVersionInjections(datasetVersionIDs []int, injectionIDs []int) error { + if len(datasetVersionIDs) == 0 { + return nil + } + + query := r.db.Table("dataset_version_injections").Where("dataset_version_id IN (?)", datasetVersionIDs) + if len(injectionIDs) > 0 { + query = query.Where("injection_id IN (?)", injectionIDs) + } + if err := query.Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear dataset-version-injection associations: %w", err) + } + return nil +} + +func (r *Repository) ListInjectionsByDatasetVersionID(versionID int, includeLabels bool) ([]model.FaultInjection, error) { + query := r.db.Model(&model.FaultInjection{}) + if includeLabels { + query = query.Preload("Labels") + } + + var injections []model.FaultInjection + if err := query. + Joins("JOIN dataset_version_injections dvi ON dvi.injection_id = id"). + Where("state = ? AND status != ?", consts.DatapackBuildSuccess, consts.CommonDeleted). + Where("dvi.dataset_version_id = ?", versionID). + Find(&injections).Error; err != nil { + return nil, fmt.Errorf("failed to list fault injections for dataset version %d: %w", versionID, err) + } + return injections, nil +} diff --git a/src/module/dataset/service.go b/src/module/dataset/service.go new file mode 100644 index 00000000..7530ed82 --- /dev/null +++ b/src/module/dataset/service.go @@ -0,0 +1,529 @@ +package datasetmodule + +import ( + "archive/zip" + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/service/common" + "aegis/utils" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository + datapacks *DatapackFileStore +} + +func NewService(repo *Repository, datapacks *DatapackFileStore) *Service { + return &Service{repo: repo, datapacks: datapacks} +} + +func (s *Service) CreateDataset(_ context.Context, req *CreateDatasetReq, userID int) (*DatasetResp, error) { + if req == nil { + return nil, fmt.Errorf("request cannot be nil") + } + + dataset := req.ConvertToDataset() + var versions []model.DatasetVersion + if req.VersionReq != nil { + versions = append(versions, *req.VersionReq.ConvertToDatasetVersion()) + } + + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + createdDataset, err := s.createDatasetCore(repo, dataset, versions, userID) + if err != nil { + return fmt.Errorf("failed to create dataset: %w", err) + } + dataset = createdDataset + return nil + }); err != nil { + return nil, fmt.Errorf("failed to create dataset: %w", err) + } + + return NewDatasetResp(dataset), nil +} + +func (s *Service) DeleteDataset(_ context.Context, datasetID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if _, err := repo.BatchDeleteDatasetVersions(datasetID); err != nil { + return fmt.Errorf("failed to delete dataset versions: %w", err) + } + if _, err := repo.RemoveUsersFromDataset(datasetID); err != nil { + return fmt.Errorf("failed to remove all users from dataset: %w", err) + } + rows, err := repo.DeleteDataset(datasetID) + if err != nil { + return fmt.Errorf("failed to delete dataset: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: dataset id %d not found", consts.ErrNotFound, datasetID) + } + return nil + }) +} + +func (s *Service) GetDataset(_ context.Context, datasetID int) (*DatasetDetailResp, error) { + dataset, err := s.repo.GetDatasetByID(datasetID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) + } + return nil, fmt.Errorf("failed to get dataset: %w", err) + } + + versions, err := s.repo.ListDatasetVersionsByDatasetID(dataset.ID) + if err != nil { + return nil, fmt.Errorf("failed to get dataset versions: %w", err) + } + + resp := NewDatasetDetailResp(dataset) + for _, version := range versions { + resp.Versions = append(resp.Versions, *NewDatasetVersionResp(&version)) + } + + return resp, nil +} + +func (s *Service) ListDatasets(_ context.Context, req *ListDatasetReq) (*dto.ListResp[DatasetResp], error) { + limit, offset := req.ToGormParams() + + datasets, total, err := s.repo.ListDatasets(limit, offset, req.Type, req.IsPublic, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list datasets: %w", err) + } + + datasetIDs := make([]int, 0, len(datasets)) + for _, dataset := range datasets { + datasetIDs = append(datasetIDs, dataset.ID) + } + + labelsMap, err := s.repo.ListDatasetLabels(datasetIDs) + if err != nil { + return nil, fmt.Errorf("failed to list dataset labels: %w", err) + } + + items := make([]DatasetResp, 0, len(datasets)) + for i := range datasets { + if labels, ok := labelsMap[datasets[i].ID]; ok { + datasets[i].Labels = labels + } + items = append(items, *NewDatasetResp(&datasets[i])) + } + + return &dto.ListResp[DatasetResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) SearchDatasets(_ context.Context, req *SearchDatasetReq) (*dto.ListResp[DatasetDetailResp], error) { + if req == nil { + return nil, fmt.Errorf("search dataset request is nil") + } + + results, total, err := s.repo.SearchDatasets(req.ConvertToSearchReq()) + if err != nil { + return nil, fmt.Errorf("failed to search datasets: %w", err) + } + + items := make([]DatasetDetailResp, 0, len(results)) + for i := range results { + items = append(items, *NewDatasetDetailResp(&results[i])) + } + + return &dto.ListResp[DatasetDetailResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateDataset(_ context.Context, req *UpdateDatasetReq, datasetID int) (*DatasetResp, error) { + var updatedDataset *model.Dataset + + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + dataset, err := repo.GetDatasetByID(datasetID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) + } + return fmt.Errorf("failed to get dataset: %w", err) + } + + req.PatchDatasetModel(dataset) + if err := repo.UpdateDataset(dataset); err != nil { + return fmt.Errorf("failed to update dataset: %w", err) + } + + updatedDataset = dataset + return nil + }); err != nil { + return nil, err + } + + return NewDatasetResp(updatedDataset), nil +} + +func (s *Service) ManageDatasetLabels(_ context.Context, req *ManageDatasetLabelReq, datasetID int) (*DatasetResp, error) { + if req == nil { + return nil, fmt.Errorf("manage dataset labels request is nil") + } + + var managedDataset *model.Dataset + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + dataset, err := repo.GetDatasetByID(datasetID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) + } + return fmt.Errorf("failed to get dataset: %w", err) + } + + if len(req.AddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + datasetLabels := make([]model.DatasetLabel, 0, len(labels)) + for _, label := range labels { + datasetLabels = append(datasetLabels, model.DatasetLabel{ + DatasetID: datasetID, + LabelID: label.ID, + }) + } + + if err := repo.AddDatasetLabels(datasetLabels); err != nil { + return fmt.Errorf("failed to add dataset labels: %w", err) + } + } + + if len(req.RemoveLabels) > 0 { + labelIDs, err := repo.ListLabelIDsByKeyAndDatasetID(datasetID, req.RemoveLabels) + if err != nil { + return fmt.Errorf("failed to find label ids by keys: %w", err) + } + + if len(labelIDs) > 0 { + if err := repo.ClearDatasetLabels([]int{datasetID}, labelIDs); err != nil { + return fmt.Errorf("failed to clear dataset labels: %w", err) + } + + if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + return fmt.Errorf("failed to decrease label usage counts: %w", err) + } + } + } + + labels, err := repo.ListLabelsByDatasetID(dataset.ID) + if err != nil { + return fmt.Errorf("failed to get dataset labels: %w", err) + } + + dataset.Labels = labels + managedDataset = dataset + return nil + }); err != nil { + return nil, err + } + + return NewDatasetResp(managedDataset), nil +} + +func (s *Service) CreateDatasetVersion(_ context.Context, req *CreateDatasetVersionReq, datasetID, userID int) (*DatasetVersionResp, error) { + if req == nil { + return nil, fmt.Errorf("create dataset version request is nil") + } + + version := req.ConvertToDatasetVersion() + version.DatasetID = datasetID + version.UserID = userID + + var createdVersion *model.DatasetVersion + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + versions, err := s.createDatasetVersionsCore(repo, []model.DatasetVersion{*version}) + if err != nil { + return fmt.Errorf("failed to create dataset version: %w", err) + } + + version := versions[0] + if len(req.Datapacks) > 0 { + if err := s.linkDatapacksToDatasetVersion(repo, version.ID, req.Datapacks); err != nil { + return fmt.Errorf("failed to link datapacks to dataset version: %w", err) + } + } + + createdVersion = &version + return nil + }); err != nil { + return nil, fmt.Errorf("failed to create dataset version: %w", err) + } + + return NewDatasetVersionResp(createdVersion), nil +} + +func (s *Service) DeleteDatasetVersion(_ context.Context, versionID int) error { + rows, err := s.repo.DeleteDatasetVersion(versionID) + if err != nil { + return fmt.Errorf("failed to delete dataset version: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: dataset version id %d not found", consts.ErrNotFound, versionID) + } + return nil +} + +func (s *Service) GetDatasetVersion(_ context.Context, datasetID, versionID int) (*DatasetVersionDetailResp, error) { + if _, err := s.repo.GetDatasetByID(datasetID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) + } + return nil, fmt.Errorf("failed to get dataset: %w", err) + } + + version, err := s.repo.GetDatasetVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) + } + return nil, fmt.Errorf("failed to get dataset version: %w", err) + } + + return NewDatasetVersionDetailResp(version), nil +} + +func (s *Service) ListDatasetVersions(_ context.Context, req *ListDatasetVersionReq, datasetID int) (*dto.ListResp[DatasetVersionResp], error) { + limit, offset := req.ToGormParams() + + versions, total, err := s.repo.ListDatasetVersions(limit, offset, datasetID, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list dataset versions: %w", err) + } + + items := make([]DatasetVersionResp, 0, len(versions)) + for i := range versions { + items = append(items, *NewDatasetVersionResp(&versions[i])) + } + + return &dto.ListResp[DatasetVersionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateDatasetVersion(_ context.Context, req *UpdateDatasetVersionReq, datasetID, versionID int) (*DatasetVersionResp, error) { + _ = datasetID + + var updatedVersion *model.DatasetVersion + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + version, err := repo.GetDatasetVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) + } + return fmt.Errorf("failed to get dataset version: %w", err) + } + + req.PatchDatasetVersionModel(version) + if err := repo.UpdateDatasetVersion(version); err != nil { + return fmt.Errorf("failed to update dataset version: %w", err) + } + + updatedVersion = version + return nil + }); err != nil { + return nil, fmt.Errorf("failed to update dataset version: %w", err) + } + + return NewDatasetVersionResp(updatedVersion), nil +} + +func (s *Service) GetDatasetVersionFilename(_ context.Context, datasetID, versionID int) (string, error) { + dataset, err := s.repo.GetDatasetByID(datasetID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) + } + return "", fmt.Errorf("failed to get dataset: %w", err) + } + + version, err := s.repo.GetDatasetVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) + } + return "", fmt.Errorf("failed to get dataset version: %w", err) + } + + return fmt.Sprintf("%s-%s", dataset.Name, version.Name), nil +} + +func (s *Service) DownloadDatasetVersion(_ context.Context, zipWriter *zip.Writer, excludeRules []utils.ExculdeRule, versionID int) error { + if zipWriter == nil { + return fmt.Errorf("zip writer cannot be nil") + } + + datapacks, err := s.repo.ListInjectionsByDatasetVersionID(versionID, false) + if err != nil { + return fmt.Errorf("failed to list datapacks for dataset version: %w", err) + } + + if err := s.datapacks.PackageToZip(zipWriter, datapacks, excludeRules); err != nil { + return fmt.Errorf("failed to package dataset to zip: %w", err) + } + + return nil +} + +func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageDatasetVersionInjectionReq, versionID int) (*DatasetVersionDetailResp, error) { + if req == nil { + return nil, fmt.Errorf("manage dataset version injections request is nil") + } + + var managedVersion *model.DatasetVersion + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + version, err := repo.GetDatasetVersionByID(versionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: dataset version id: %d", consts.ErrNotFound, versionID) + } + return fmt.Errorf("failed to get dataset version: %w", err) + } + + if len(req.AddDatapacks) > 0 { + if err := s.linkDatapacksToDatasetVersion(repo, versionID, req.AddDatapacks); err != nil { + return fmt.Errorf("failed to link datapacks to dataset version: %w", err) + } + } + + if len(req.RemoveDatapacks) > 0 { + injectionIDMap, err := repo.ListInjectionIDsByNames(req.RemoveDatapacks) + if err != nil { + return fmt.Errorf("failed to list injections by names: %w", err) + } + if len(injectionIDMap) != len(req.RemoveDatapacks) { + return fmt.Errorf("some datapacks to remove were not found") + } + + injectionIDs := make([]int, 0, len(req.RemoveDatapacks)) + for _, datapack := range req.RemoveDatapacks { + injectionID, ok := injectionIDMap[datapack] + if !ok { + return fmt.Errorf("injection not found: %s", datapack) + } + injectionIDs = append(injectionIDs, injectionID) + } + + if err := repo.ClearDatasetVersionInjections([]int{version.ID}, injectionIDs); err != nil { + return fmt.Errorf("failed to remove dataset version datapacks: %w", err) + } + } + + datapacks, err := repo.ListInjectionsByDatasetVersionID(version.ID, false) + if err != nil { + return fmt.Errorf("failed to list datapacks for dataset version: %w", err) + } + + version.Datapacks = datapacks + version.FileCount = version.FileCount + len(req.AddDatapacks) - len(req.RemoveDatapacks) + if err := repo.UpdateDatasetVersion(version); err != nil { + return fmt.Errorf("failed to update dataset version file count: %w", err) + } + + managedVersion = version + return nil + }) + if err != nil { + return nil, err + } + + return NewDatasetVersionDetailResp(managedVersion), nil +} + +func (s *Service) createDatasetCore(repo *Repository, dataset *model.Dataset, versions []model.DatasetVersion, userID int) (*model.Dataset, error) { + role, err := repo.GetRoleByName(consts.RoleDatasetAdmin.String()) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: role %v not found", consts.ErrNotFound, consts.RoleDatasetAdmin) + } + return nil, fmt.Errorf("failed to get dataset owner role: %w", err) + } + + if err := repo.CreateDataset(dataset); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return nil, consts.ErrAlreadyExists + } + return nil, err + } + + if err := repo.CreateUserDataset(&model.UserDataset{ + UserID: userID, + DatasetID: dataset.ID, + RoleID: role.ID, + Status: consts.CommonEnabled, + }); err != nil { + return nil, fmt.Errorf("failed to associate dataset with user: %w", err) + } + + if len(versions) > 0 { + for i := range versions { + versions[i].DatasetID = dataset.ID + versions[i].UserID = userID + } + + if _, err := s.createDatasetVersionsCore(repo, versions); err != nil { + return nil, fmt.Errorf("failed to create dataset versions: %w", err) + } + } + + return dataset, nil +} + +func (s *Service) createDatasetVersionsCore(repo *Repository, versions []model.DatasetVersion) ([]model.DatasetVersion, error) { + if len(versions) == 0 { + return nil, nil + } + + if err := repo.BatchCreateDatasetVersions(versions); err != nil { + return nil, fmt.Errorf("failed to create dataset versions: %w", err) + } + + return versions, nil +} + +func (s *Service) linkDatapacksToDatasetVersion(repo *Repository, versionID int, datapacks []string) error { + injectionIDMap, err := repo.ListInjectionIDsByNames(datapacks) + if err != nil { + return fmt.Errorf("failed to list injections by names: %w", err) + } + + items := make([]model.DatasetVersionInjection, 0, len(datapacks)) + for _, datapack := range datapacks { + injectionID, ok := injectionIDMap[datapack] + if !ok { + return fmt.Errorf("injection not found: %s", datapack) + } + items = append(items, model.DatasetVersionInjection{ + DatasetVersionID: versionID, + InjectionID: injectionID, + }) + } + + if err := repo.AddDatasetVersionInjections(items); err != nil { + return fmt.Errorf("failed to add dataset version injections: %w", err) + } + + return nil +} diff --git a/src/handlers/docs.go b/src/module/docs/swagger_models.go similarity index 87% rename from src/handlers/docs.go rename to src/module/docs/swagger_models.go index 9d7fa339..e2db65ca 100644 --- a/src/handlers/docs.go +++ b/src/module/docs/swagger_models.go @@ -1,9 +1,13 @@ -package handlers +package docsmodule import ( + groupmodule "aegis/module/group" + "github.com/gin-gonic/gin" ) +type GroupStreamEvent = groupmodule.GroupStreamEvent + // SwaggerModelsDoc is a documentation-only endpoint that ensures all DTO models are included in Swagger. // This endpoint should NEVER be registered in the actual router. // @@ -13,7 +17,7 @@ import ( // @Accept json // @Produce json // @Success 200 {object} dto.TraceStreamEvent "Trace-level stream event structure" -// @Success 200 {object} dto.GroupStreamEvent "Group-level stream event structure" +// @Success 200 {object} GroupStreamEvent "Group-level stream event structure" // @Success 200 {object} dto.DatapackInfo "Datapack information structure" // @Success 200 {object} dto.DatapackResult "Datapack result structure" // @Success 200 {object} dto.ExecutionInfo "Execution information structure" @@ -32,7 +36,7 @@ import ( // @Success 200 {object} consts.StatusType "Status type constants" // @Success 200 {object} consts.TaskState "Task state constants" // @Success 200 {object} consts.TaskType "Task type constants" -// @Success 200 {object} consts.SSEEventName "SSE event name constants" +// @Success 200 {object} consts.SSEEventName "SSE event name constants" // @Router /api/_docs/models [get] -// @x-api-type {"sdk":"true"} +// @x-api-type {} func SwaggerModelsDoc(c *gin.Context) {} diff --git a/src/dto/evaluation.go b/src/module/evaluation/api_types.go similarity index 67% rename from src/dto/evaluation.go rename to src/module/evaluation/api_types.go index 38a79e0a..c766e735 100644 --- a/src/dto/evaluation.go +++ b/src/module/evaluation/api_types.go @@ -1,24 +1,23 @@ -package dto +package evaluationmodule import ( - "aegis/config" - "aegis/database" "fmt" "time" + "aegis/config" + "aegis/dto" + "aegis/model" + executionmodule "aegis/module/execution" + chaos "github.com/OperationsPAI/chaos-experiment/handler" ) -// ===================================================================== -// Evaluation CRUD DTOs -// ===================================================================== - -// ListEvaluationReq represents the request for listing evaluations +// ListEvaluationReq represents the request for listing evaluations. type ListEvaluationReq struct { - PaginationReq + dto.PaginationReq } -// EvaluationResp represents an evaluation in API responses +// EvaluationResp represents an evaluation in API responses. type EvaluationResp struct { ID int `json:"id"` ProjectID *int `json:"project_id,omitempty"` @@ -37,8 +36,7 @@ type EvaluationResp struct { UpdatedAt time.Time `json:"updated_at"` } -// NewEvaluationResp creates an EvaluationResp from a database Evaluation -func NewEvaluationResp(eval *database.Evaluation) *EvaluationResp { +func NewEvaluationResp(eval *model.Evaluation) *EvaluationResp { return &EvaluationResp{ ID: eval.ID, ProjectID: eval.ProjectID, @@ -58,29 +56,25 @@ func NewEvaluationResp(eval *database.Evaluation) *EvaluationResp { } } -// Execution represents execution data for evaluation +// Execution represents execution data for evaluation. type Execution struct { - Items []GranularityResultItem `json:"items"` + Items []executionmodule.GranularityResultItem `json:"items"` } -// Conclusion represents evaluation conclusion +// Conclusion represents evaluation conclusion. type Conclusion struct { - Level string `json:"level"` // For example service level - Metric string `json:"metric"` // For example topk + Level string `json:"level"` + Metric string `json:"metric"` Rate float64 `json:"rate"` } -// EvaluateMetric represents evaluation metric function type +// EvaluateMetric represents evaluation metric function type. type EvaluateMetric func([]Execution) ([]Conclusion, error) -// ===================================================================== -// Batch Evaluate Datapack DTOs -// ===================================================================== - type EvaluateDatapackSpec struct { - Algorithm ContainerRef `json:"algorithm" binding:"required"` - Datapack string `json:"datapack" binding:"required"` - FilterLabels []LabelItem `json:"filter_labels" binding:"omitempty"` + Algorithm dto.ContainerRef `json:"algorithm" binding:"required"` + Datapack string `json:"datapack" binding:"required"` + FilterLabels []dto.LabelItem `json:"filter_labels" binding:"omitempty"` } func (spec *EvaluateDatapackSpec) Validate() error { @@ -90,12 +84,10 @@ func (spec *EvaluateDatapackSpec) Validate() error { if spec.Algorithm.Name == config.GetDetectorName() { return fmt.Errorf("detector algorithm cannot be used for evaluation") } - if spec.Datapack == "" { return fmt.Errorf("datapack cannot be empty") } - - return validateLabelItemsFiled(spec.FilterLabels) + return validateLabelItems(spec.FilterLabels) } type BatchEvaluateDatapackReq struct { @@ -115,9 +107,9 @@ func (req *BatchEvaluateDatapackReq) Validate() error { } type EvaluateDatapackRef struct { - Datapack string `json:"datapack"` - Groundtruths []chaos.Groundtruth `json:"groundtruths"` - ExecutionRefs []ExecutionRef `json:"execution_refs"` + Datapack string `json:"datapack"` + Groundtruths []chaos.Groundtruth `json:"groundtruths"` + ExecutionRefs []executionmodule.ExecutionRef `json:"execution_refs"` } type EvaluateDatapackItem struct { @@ -133,14 +125,10 @@ type BatchEvaluateDatapackResp struct { SuccessItems []EvaluateDatapackItem `json:"success_items"` } -// ===================================================================== -// Batch Evaluate Dataset DTOs -// ===================================================================== - type EvaluateDatasetSpec struct { - Algorithm ContainerRef `json:"algorithm" binding:"required"` - Dataset DatasetRef `json:"dataset" binding:"required"` - FilterLabels []LabelItem `json:"filter_labels" binding:"omitempty"` + Algorithm dto.ContainerRef `json:"algorithm" binding:"required"` + Dataset dto.DatasetRef `json:"dataset" binding:"required"` + FilterLabels []dto.LabelItem `json:"filter_labels" binding:"omitempty"` } func (spec *EvaluateDatasetSpec) Validate() error { @@ -150,12 +138,10 @@ func (spec *EvaluateDatasetSpec) Validate() error { if spec.Algorithm.Name == config.GetDetectorName() { return fmt.Errorf("detector algorithm cannot be used for evaluation") } - if err := spec.Dataset.Validate(); err != nil { return fmt.Errorf("invalid dataset: %w", err) } - - return validateLabelItemsFiled(spec.FilterLabels) + return validateLabelItems(spec.FilterLabels) } type BatchEvaluateDatasetReq struct { @@ -175,13 +161,13 @@ func (req *BatchEvaluateDatasetReq) Validate() error { } type EvaluateDatasetItem struct { - Algorithm string `json:"algorithm"` // Algorithm name - AlgorithmVersion string `json:"algorithm_version"` // Algorithm version - Dataset string `json:"dataset"` // Dataset name - DatasetVersion string `json:"dataset_version"` // Dataset version - TotalCount int `json:"total_count"` // Total number of datapacks in dataset - EvaluateRefs []EvaluateDatapackRef `json:"evalaute_refs"` // Evaluation refs for each dataset - NotExecutedDatapacks []string `json:"not_executed_datapacks"` // Datapacks that were not executed + Algorithm string `json:"algorithm"` + AlgorithmVersion string `json:"algorithm_version"` + Dataset string `json:"dataset"` + DatasetVersion string `json:"dataset_version"` + TotalCount int `json:"total_count"` + EvaluateRefs []EvaluateDatapackRef `json:"evalaute_refs"` + NotExecutedDatapacks []string `json:"not_executed_datapacks"` } type BatchEvaluateDatasetResp struct { @@ -190,3 +176,15 @@ type BatchEvaluateDatasetResp struct { SuccessCount int `json:"success_count"` SuccessItems []EvaluateDatasetItem `json:"success_items"` } + +func validateLabelItems(items []dto.LabelItem) error { + for i, label := range items { + if label.Key == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if label.Value == "" { + return fmt.Errorf("empty label value at index %d", i) + } + } + return nil +} diff --git a/src/handlers/v2/evaluations.go b/src/module/evaluation/handler.go similarity index 71% rename from src/handlers/v2/evaluations.go rename to src/module/evaluation/handler.go index b2b4da57..32b299c3 100644 --- a/src/handlers/v2/evaluations.go +++ b/src/module/evaluation/handler.go @@ -1,18 +1,24 @@ -package v2 +package evaluationmodule import ( + "aegis/httpx" "net/http" "aegis/consts" "aegis/dto" - "aegis/handlers" "aegis/middleware" - "aegis/service/analyzer" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // ListDatapackEvaluationResults retrieves evaluation data for multiple algorithm-datapack pairs // // @Summary List Datapack Evaluation Results @@ -22,22 +28,22 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.BatchEvaluateDatapackReq true "Batch evaluation request containing multiple algorithm-datapack pairs" -// @Success 200 {object} dto.GenericResponse[dto.BatchEvaluateDatapackResp] "Batch algorithm datapack evaluation data retrieved successfully" +// @Param request body BatchEvaluateDatapackReq true "Batch evaluation request containing multiple algorithm-datapack pairs" +// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatapackResp] "Batch algorithm datapack evaluation data retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/datapacks [post] -// @x-api-type {"sdk":"true"} -func ListDatapackEvaluationResults(c *gin.Context) { +// @x-api-type {} +func (h *Handler) ListDatapackEvaluationResults(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req dto.BatchEvaluateDatapackReq + var req BatchEvaluateDatapackReq if err := c.ShouldBindBodyWithJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -48,8 +54,8 @@ func ListDatapackEvaluationResults(c *gin.Context) { return } - resp, err := analyzer.ListDatapackEvaluationResults(&req, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListDatapackEvaluationResults(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } @@ -65,22 +71,22 @@ func ListDatapackEvaluationResults(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.BatchEvaluateDatasetReq true "Batch evaluation request containing multiple algorithm-dataset pairs" -// @Success 200 {object} dto.GenericResponse[dto.BatchEvaluateDatasetResp] "Batch algorithm dataset evaluation data retrieved successfully" +// @Param request body BatchEvaluateDatasetReq true "Batch evaluation request containing multiple algorithm-dataset pairs" +// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatasetResp] "Batch algorithm dataset evaluation data retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/datasets [post] -// @x-api-type {"sdk":"true"} -func ListDatasetEvaluationResults(c *gin.Context) { +// @x-api-type {} +func (h *Handler) ListDatasetEvaluationResults(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req dto.BatchEvaluateDatasetReq + var req BatchEvaluateDatasetReq if err := c.ShouldBindBodyWithJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -91,8 +97,8 @@ func ListDatasetEvaluationResults(c *gin.Context) { return } - resp, err := analyzer.ListDatasetEvaluationResults(&req, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListDatasetEvaluationResults(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } @@ -109,14 +115,14 @@ func ListDatasetEvaluationResults(c *gin.Context) { // @Security BearerAuth // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.EvaluationResp]] "Evaluations retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[EvaluationResp]] "Evaluations retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations [get] -// @x-api-type {"sdk":"true"} -func ListEvaluations(c *gin.Context) { - var req dto.ListEvaluationReq +// @x-api-type {} +func (h *Handler) ListEvaluations(c *gin.Context) { + var req ListEvaluationReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -127,8 +133,8 @@ func ListEvaluations(c *gin.Context) { return } - resp, err := producer.ListEvaluations(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListEvaluations(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -144,20 +150,20 @@ func ListEvaluations(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Evaluation ID" -// @Success 200 {object} dto.GenericResponse[dto.EvaluationResp] "Evaluation retrieved successfully" +// @Success 200 {object} dto.GenericResponse[EvaluationResp] "Evaluation retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" // @Failure 404 {object} dto.GenericResponse[any] "Evaluation not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/{id} [get] -// @x-api-type {"sdk":"true"} -func GetEvaluation(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") +// @x-api-type {} +func (h *Handler) GetEvaluation(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") if !ok { return } - resp, err := producer.GetEvaluation(id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetEvaluation(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } @@ -178,15 +184,14 @@ func GetEvaluation(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Evaluation not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/{id} [delete] -// @x-api-type {"sdk":"true"} -func DeleteEvaluation(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") +// @x-api-type {} +func (h *Handler) DeleteEvaluation(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") if !ok { return } - err := producer.DeleteEvaluation(id) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteEvaluation(c.Request.Context(), id)) { return } diff --git a/src/module/evaluation/module.go b/src/module/evaluation/module.go new file mode 100644 index 00000000..03ef4844 --- /dev/null +++ b/src/module/evaluation/module.go @@ -0,0 +1,9 @@ +package evaluationmodule + +import "go.uber.org/fx" + +var Module = fx.Module("evaluation", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/repository/evaluation.go b/src/module/evaluation/repository.go similarity index 58% rename from src/repository/evaluation.go rename to src/module/evaluation/repository.go index cf7cce99..f1524042 100644 --- a/src/repository/evaluation.go +++ b/src/module/evaluation/repository.go @@ -1,37 +1,42 @@ -package repository +package evaluationmodule import ( - "fmt" - "aegis/consts" - "aegis/database" + "aegis/model" + "fmt" "gorm.io/gorm" ) -func ListEvaluations(db *gorm.DB, limit, offset int) ([]database.Evaluation, int64, error) { - var evaluations []database.Evaluation - var total int64 +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) ListEvaluations(limit, offset int) ([]model.Evaluation, int64, error) { + var ( + evaluations []model.Evaluation + total int64 + ) - query := db.Model(&database.Evaluation{}). + query := r.db.Model(&model.Evaluation{}). Where("status != ?", consts.CommonDeleted) - if err := query.Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("failed to count evaluations: %w", err) } - - if err := query.Select("id, project_id, algorithm_name, algorithm_version, datapack_name, dataset_name, dataset_version, eval_type, precision, recall, f1_score, accuracy, status, created_at, updated_at").Limit(limit).Offset(offset).Order("updated_at DESC").Find(&evaluations).Error; err != nil { + if err := query.Select("id, project_id, algorithm_name, algorithm_version, datapack_name, dataset_name, dataset_version, eval_type, precision, recall, f1_score, accuracy, status, created_at, updated_at"). + Limit(limit).Offset(offset).Order("updated_at DESC").Find(&evaluations).Error; err != nil { return nil, 0, fmt.Errorf("failed to list evaluations: %w", err) } - return evaluations, total, nil } -func GetEvaluationByID(db *gorm.DB, id int) (*database.Evaluation, error) { - var evaluation database.Evaluation - if err := db. - Where("id = ? AND status != ?", id, consts.CommonDeleted). - First(&evaluation).Error; err != nil { +func (r *Repository) GetEvaluationByID(id int) (*model.Evaluation, error) { + var evaluation model.Evaluation + if err := r.db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&evaluation).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("evaluation with id %d: %w", id, consts.ErrNotFound) } @@ -40,15 +45,8 @@ func GetEvaluationByID(db *gorm.DB, id int) (*database.Evaluation, error) { return &evaluation, nil } -func CreateEvaluation(db *gorm.DB, eval *database.Evaluation) error { - if err := db.Create(eval).Error; err != nil { - return fmt.Errorf("failed to create evaluation: %w", err) - } - return nil -} - -func DeleteEvaluation(db *gorm.DB, id int) error { - result := db.Model(&database.Evaluation{}). +func (r *Repository) DeleteEvaluation(id int) error { + result := r.db.Model(&model.Evaluation{}). Where("id = ? AND status != ?", id, consts.CommonDeleted). Update("status", consts.CommonDeleted) if err := result.Error; err != nil { diff --git a/src/service/analyzer/evaluation.go b/src/module/evaluation/service.go similarity index 61% rename from src/service/analyzer/evaluation.go rename to src/module/evaluation/service.go index dff991cb..a1c8084d 100644 --- a/src/service/analyzer/evaluation.go +++ b/src/module/evaluation/service.go @@ -1,20 +1,31 @@ -package analyzer +package evaluationmodule import ( + "context" + "encoding/json" + "fmt" + "aegis/consts" - "aegis/database" "aegis/dto" + "aegis/model" + executionmodule "aegis/module/execution" "aegis/repository" "aegis/service/common" - "encoding/json" - "fmt" chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) -// ListDatapackEvaluationResults retrieves evaluation data for multiple algorithm-datapack pairs -func ListDatapackEvaluationResults(req *dto.BatchEvaluateDatapackReq, userID int) (*dto.BatchEvaluateDatapackResp, error) { +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEvaluateDatapackReq, userID int) (*BatchEvaluateDatapackResp, error) { if req == nil { return nil, fmt.Errorf("batch evaluate datapack request is nil") } @@ -24,12 +35,12 @@ func ListDatapackEvaluationResults(req *dto.BatchEvaluateDatapackReq, userID int algorithms = append(algorithms, &req.Specs[i].Algorithm) } - algorithmVersionResults, err := common.MapRefsToContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(s.repo.db, algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } - successItems := make([]dto.EvaluateDatapackItem, 0, len(req.Specs)) + successItems := make([]EvaluateDatapackItem, 0, len(req.Specs)) failedItems := make([]string, 0) for i := range req.Specs { @@ -42,31 +53,23 @@ func ListDatapackEvaluationResults(req *dto.BatchEvaluateDatapackReq, userID int continue } - labelConditions := make([]map[string]string, 0, len(spec.FilterLabels)) - for _, label := range spec.FilterLabels { - labelConditions = append(labelConditions, map[string]string{ - "key": label.Key, - "value": label.Value, - }) - } - - executions, err := repository.ListExecutionsByDatapackFilter(database.DB, algorithmVersion.ID, spec.Datapack, labelConditions) + labelConditions := dto.ConvertLabelItemsToConditions(spec.FilterLabels) + executions, err := repository.ListExecutionsByDatapackFilter(s.repo.db, algorithmVersion.ID, spec.Datapack, labelConditions) if err != nil { failedItems = append(failedItems, fmt.Sprintf("%s - failed to query executions: %v", specIdentifier, err)) continue } - if len(executions) == 0 { failedItems = append(failedItems, fmt.Sprintf("%s - no executions found", specIdentifier)) continue } - refs := make([]dto.ExecutionRef, 0, len(executions)) + refs := make([]executionmodule.ExecutionRef, 0, len(executions)) for _, execution := range executions { - refs = append(refs, dto.NewExecutionGranularityRef(&execution)) + refs = append(refs, executionmodule.NewExecutionGranularityRef(&execution)) } - evaluateRef := dto.EvaluateDatapackRef{ + evaluateRef := EvaluateDatapackRef{ Datapack: spec.Datapack, ExecutionRefs: refs, } @@ -81,17 +84,15 @@ func ListDatapackEvaluationResults(req *dto.BatchEvaluateDatapackReq, userID int } } - item := dto.EvaluateDatapackItem{ + successItems = append(successItems, EvaluateDatapackItem{ Algorithm: algorithmVersion.Container.Name, AlgorithmVersion: algorithmVersion.Name, EvaluateDatapackRef: evaluateRef, - } - successItems = append(successItems, item) + }) } - // Persist successful evaluations to the database - persistEvaluations("datapack", successItems, func(item *dto.EvaluateDatapackItem) *database.Evaluation { - return &database.Evaluation{ + persistEvaluations(s.repo.db, "datapack", successItems, func(item *EvaluateDatapackItem) *model.Evaluation { + return &model.Evaluation{ AlgorithmName: item.Algorithm, AlgorithmVersion: item.AlgorithmVersion, DatapackName: item.Datapack, @@ -100,17 +101,15 @@ func ListDatapackEvaluationResults(req *dto.BatchEvaluateDatapackReq, userID int } }) - resp := dto.BatchEvaluateDatapackResp{ + return &BatchEvaluateDatapackResp{ SuccessCount: len(successItems), SuccessItems: successItems, FailedCount: len(failedItems), FailedItems: failedItems, - } - return &resp, nil + }, nil } -// ListDatasetEvaluationResults retrieves evaluation results for multiple dataset-algorithm pairs -func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) (*dto.BatchEvaluateDatasetResp, error) { +func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEvaluateDatasetReq, userID int) (*BatchEvaluateDatasetResp, error) { if req == nil { return nil, fmt.Errorf("batch evaluate datapack request is nil") } @@ -122,17 +121,17 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) datasets = append(datasets, &req.Specs[i].Dataset) } - algorithmVersionResults, err := common.MapRefsToContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(s.repo.db, algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } - datasetVersionResults, err := common.MapRefsToDatasetVersions(datasets, userID) + datasetVersionResults, err := common.MapRefsToDatasetVersionsWithDB(s.repo.db, datasets, userID) if err != nil { return nil, fmt.Errorf("failed to map dataset refs to versions: %w", err) } - successItems := make([]dto.EvaluateDatasetItem, 0, len(req.Specs)) + successItems := make([]EvaluateDatasetItem, 0, len(req.Specs)) failedItems := make([]string, 0) for i := range req.Specs { @@ -152,44 +151,41 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) } labelConditions := dto.ConvertLabelItemsToConditions(spec.FilterLabels) - - executions, err := repository.ListExecutionsByDatasetFilter(database.DB, algorithmVersion.ID, datasetVersion.ID, labelConditions) + executions, err := repository.ListExecutionsByDatasetFilter(s.repo.db, algorithmVersion.ID, datasetVersion.ID, labelConditions) if err != nil { failedItems = append(failedItems, fmt.Sprintf("%s - failed to query executions: %v", specIdentifier, err)) continue } - if len(executions) == 0 { failedItems = append(failedItems, fmt.Sprintf("%s - no executions found", specIdentifier)) continue } - executionMap := make(map[string][]database.Execution) + executionMap := make(map[string][]model.Execution) for _, execution := range executions { name := execution.Datapack.Name if _, exists := executionMap[name]; !exists { - executionMap[name] = make([]database.Execution, 0) - } else { - executionMap[name] = append(executionMap[name], execution) + executionMap[name] = make([]model.Execution, 0) } + executionMap[name] = append(executionMap[name], execution) } - notExecutedDatapacks := []string{} + notExecutedDatapacks := make([]string, 0) for _, datapack := range datasetVersion.Datapacks { if _, exists := executionMap[datapack.Name]; !exists { notExecutedDatapacks = append(notExecutedDatapacks, datapack.Name) } } - evaluateRefs := make([]dto.EvaluateDatapackRef, 0, len(executionMap)) - for datapack_name, groupedExecutions := range executionMap { - refs := make([]dto.ExecutionRef, 0, len(groupedExecutions)) + evaluateRefs := make([]EvaluateDatapackRef, 0, len(executionMap)) + for datapackName, groupedExecutions := range executionMap { + refs := make([]executionmodule.ExecutionRef, 0, len(groupedExecutions)) for _, execution := range groupedExecutions { - refs = append(refs, dto.NewExecutionGranularityRef(&execution)) + refs = append(refs, executionmodule.NewExecutionGranularityRef(&execution)) } - evaluateRef := dto.EvaluateDatapackRef{ - Datapack: datapack_name, + evaluateRef := EvaluateDatapackRef{ + Datapack: datapackName, ExecutionRefs: refs, } @@ -197,7 +193,7 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) if datapack != nil { groundtruths, err := getGroundtruths(datapack) if err != nil { - logrus.Warnf("failed to get groundtruth for datapack %s: %v", datapack_name, err) + logrus.Warnf("failed to get groundtruth for datapack %s: %v", datapackName, err) } else { evaluateRef.Groundtruths = groundtruths } @@ -206,7 +202,7 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) evaluateRefs = append(evaluateRefs, evaluateRef) } - item := dto.EvaluateDatasetItem{ + successItems = append(successItems, EvaluateDatasetItem{ Algorithm: algorithmVersion.Container.Name, AlgorithmVersion: algorithmVersion.Name, Dataset: datasetVersion.Dataset.Name, @@ -214,14 +210,11 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) TotalCount: len(datasetVersion.Datapacks), EvaluateRefs: evaluateRefs, NotExecutedDatapacks: notExecutedDatapacks, - } - - successItems = append(successItems, item) + }) } - // Persist successful evaluations to the database - persistEvaluations("dataset", successItems, func(item *dto.EvaluateDatasetItem) *database.Evaluation { - return &database.Evaluation{ + persistEvaluations(s.repo.db, "dataset", successItems, func(item *EvaluateDatasetItem) *model.Evaluation { + return &model.Evaluation{ AlgorithmName: item.Algorithm, AlgorithmVersion: item.AlgorithmVersion, DatasetName: item.Dataset, @@ -231,23 +224,50 @@ func ListDatasetEvaluationResults(req *dto.BatchEvaluateDatasetReq, userID int) } }) - resp := dto.BatchEvaluateDatasetResp{ + return &BatchEvaluateDatasetResp{ SuccessCount: len(successItems), SuccessItems: successItems, FailedCount: len(failedItems), FailedItems: failedItems, + }, nil +} + +func (s *Service) ListEvaluations(_ context.Context, req *ListEvaluationReq) (*dto.ListResp[EvaluationResp], error) { + limit, offset := req.ToGormParams() + evaluations, total, err := s.repo.ListEvaluations(limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list evaluations: %w", err) + } + + items := make([]EvaluationResp, 0, len(evaluations)) + for _, evaluation := range evaluations { + items = append(items, *NewEvaluationResp(&evaluation)) + } + + return &dto.ListResp[EvaluationResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetEvaluation(_ context.Context, id int) (*EvaluationResp, error) { + evaluation, err := s.repo.GetEvaluationByID(id) + if err != nil { + return nil, err } - return &resp, nil + return NewEvaluationResp(evaluation), nil +} + +func (s *Service) DeleteEvaluation(_ context.Context, id int) error { + return s.repo.DeleteEvaluation(id) } -// persistEvaluations batch-persists evaluation results to the database. -// The toEval function maps each item to a database.Evaluation (without ResultJSON). -func persistEvaluations[T any](evalType string, items []T, toEval func(*T) *database.Evaluation) { +func persistEvaluations[T any](db *gorm.DB, evalType string, items []T, toEval func(*T) *model.Evaluation) { if len(items) == 0 { return } - evals := make([]database.Evaluation, 0, len(items)) + evals := make([]model.Evaluation, 0, len(items)) for i := range items { eval := toEval(&items[i]) resultJSON, err := json.Marshal(&items[i]) @@ -260,13 +280,12 @@ func persistEvaluations[T any](evalType string, items []T, toEval func(*T) *data evals = append(evals, *eval) } - if err := database.DB.Create(&evals).Error; err != nil { + if err := db.Create(&evals).Error; err != nil { logrus.Warnf("failed to batch persist %d %s evaluations: %v", len(evals), evalType, err) } } -// getGroundtruths extracts the ground truth from a datapack's engine configuration -func getGroundtruths(datapack *database.FaultInjection) ([]chaos.Groundtruth, error) { +func getGroundtruths(datapack *model.FaultInjection) ([]chaos.Groundtruth, error) { chaosGroundtruths := make([]chaos.Groundtruth, 0, len(datapack.Groundtruths)) for _, gt := range datapack.Groundtruths { chaosGroundtruths = append(chaosGroundtruths, *gt.ConvertToChaosGroundtruth()) diff --git a/src/dto/execution.go b/src/module/execution/api_types.go similarity index 69% rename from src/dto/execution.go rename to src/module/execution/api_types.go index 337143a0..6af3cd98 100644 --- a/src/dto/execution.go +++ b/src/module/execution/api_types.go @@ -1,24 +1,26 @@ -package dto +package executionmodule import ( - "aegis/config" - "aegis/consts" - "aegis/database" "fmt" "strings" "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/model" ) -// ExecutionRef represents execution granularity results for evaluation +// ExecutionRef represents execution granularity results for evaluation. type ExecutionRef struct { - ExecutionID int `json:"execution_id"` // Execution ID - ExecutionDuration float64 `json:"execution_duration"` // Execution duration in seconds - DetectorResults []DetectorResultItem `json:"detector_results"` // Detector results - Predictions []GranularityResultItem `json:"predictions"` // Algorithm predictions - ExecutedAt time.Time `json:"executed_at"` // Execution time + ExecutionID int `json:"execution_id"` + ExecutionDuration float64 `json:"execution_duration"` + DetectorResults []DetectorResultItem `json:"detector_results"` + Predictions []GranularityResultItem `json:"predictions"` + ExecutedAt time.Time `json:"executed_at"` } -func NewExecutionGranularityRef(execution *database.Execution) ExecutionRef { +func NewExecutionGranularityRef(execution *model.Execution) ExecutionRef { ref := &ExecutionRef{ ExecutionID: execution.ID, ExecutionDuration: execution.Duration, @@ -44,10 +46,10 @@ func NewExecutionGranularityRef(execution *database.Execution) ExecutionRef { return *ref } -// BatchDeleteExecutionReq represents the request to batch delete executions +// BatchDeleteExecutionReq represents the request to batch delete executions. type BatchDeleteExecutionReq struct { - IDs []int `json:"ids" binding:"omitempty"` // List of injection IDs for deletion - Labels []LabelItem `json:"labels" binding:"omitempty"` // List of label keys to match for deletion + IDs []int `json:"ids" binding:"omitempty"` + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` } func (req *BatchDeleteExecutionReq) Validate() error { @@ -91,12 +93,13 @@ func (req *BatchDeleteExecutionReq) Validate() error { return nil } +// ListExecutionReq represents execution list query parameters. type ListExecutionReq struct { - PaginationReq + dto.PaginationReq State *consts.ExecutionState `form:"state" binding:"omitempty"` Status *consts.StatusType `form:"status" binding:"omitempty"` Labels []string `form:"labels" binding:"omitempty"` - DatapackID *int `form:"datapack_id" binding:"omitempty"` // Filter by datapack ID + DatapackID *int `form:"datapack_id" binding:"omitempty"` } func (req *ListExecutionReq) Validate() error { @@ -106,19 +109,19 @@ func (req *ListExecutionReq) Validate() error { if err := validateExecutionStates(req.State); err != nil { return err } - if err := validateStatusField(req.Status, false); err != nil { + if err := validateExecutionStatus(req.Status); err != nil { return err } - if err := validateLabelsField(req.Labels); err != nil { + if err := validateExecutionLabels(req.Labels); err != nil { return err } return nil } -// ManageExecutionLabelReq Represents the request to manage labels for an execution +// ManageExecutionLabelReq represents the request to manage labels for an execution. type ManageExecutionLabelReq struct { - AddLabels []LabelItem `json:"add_labels"` // List of labels to add - RemoveLabels []string `json:"remove_labels"` // List of label keys to remove + AddLabels []dto.LabelItem `json:"add_labels"` + RemoveLabels []string `json:"remove_labels"` } func (req *ManageExecutionLabelReq) Validate() error { @@ -144,10 +147,11 @@ func (req *ManageExecutionLabelReq) Validate() error { return nil } +// ExecutionSpec represents a single execution request item. type ExecutionSpec struct { - Algorithm ContainerSpec `json:"algorithm" binding:"required"` - Datapack *string `json:"datapack" binding:"omitempty"` - Dataset *DatasetRef `json:"dataset" binding:"omitempty"` + Algorithm dto.ContainerSpec `json:"algorithm" binding:"required"` + Datapack *string `json:"datapack" binding:"omitempty"` + Dataset *dto.DatasetRef `json:"dataset" binding:"omitempty"` } func (spec *ExecutionSpec) Validate() error { @@ -160,11 +164,8 @@ func (spec *ExecutionSpec) Validate() error { if hasDatapack && hasDataset { return fmt.Errorf("cannot specify both datapack and dataset") } - - if hasDatapack { - if *spec.Datapack == "" { - return fmt.Errorf("datapack name cannot be empty") - } + if hasDatapack && *spec.Datapack == "" { + return fmt.Errorf("datapack name cannot be empty") } if hasDataset { @@ -183,31 +184,29 @@ func (spec *ExecutionSpec) Validate() error { return nil } -// SubmitExecutionReq represents the request to submit execution tasks +// SubmitExecutionReq represents the request to submit execution tasks. type SubmitExecutionReq struct { ProjectName string `json:"project_name" binding:"required"` Specs []ExecutionSpec `json:"specs" binding:"required"` - Labels []LabelItem `json:"labels" binding:"omitempty"` + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` } func (req *SubmitExecutionReq) Validate() error { if req.ProjectName == "" { return fmt.Errorf("project_name is required") } - if len(req.Specs) == 0 { return fmt.Errorf("at least one execution spec is required") } - for i, spec := range req.Specs { if err := spec.Validate(); err != nil { return fmt.Errorf("invalid execution spec at index %d: %w", i, err) } } - - return validateLabelItemsFiled(req.Labels) + return validateExecutionLabelItems(req.Labels) } +// ExecutionResp represents execution summary information. type ExecutionResp struct { ID int `json:"id"` Duration float64 `json:"duration"` @@ -222,11 +221,10 @@ type ExecutionResp struct { DatapackName string `json:"datapack_name,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - - Labels []LabelItem `json:"labels,omitempty"` + Labels []dto.LabelItem `json:"labels,omitempty"` } -func NewExecutionResp(execution *database.Execution, labels []database.Label) *ExecutionResp { +func NewExecutionResp(execution *model.Execution, labels []model.Label) *ExecutionResp { resp := &ExecutionResp{ ID: execution.ID, Duration: execution.Duration, @@ -247,17 +245,15 @@ func NewExecutionResp(execution *database.Execution, labels []database.Label) *E } if len(labels) > 0 { - resp.Labels = make([]LabelItem, 0, len(execution.Labels)) - for _, l := range execution.Labels { - resp.Labels = append(resp.Labels, LabelItem{ - Key: l.Key, - Value: l.Value, - }) + resp.Labels = make([]dto.LabelItem, 0, len(labels)) + for _, label := range labels { + resp.Labels = append(resp.Labels, dto.LabelItem{Key: label.Key, Value: label.Value}) } } return resp } +// ExecutionDetailResp represents execution detail information. type ExecutionDetailResp struct { ExecutionResp @@ -265,12 +261,13 @@ type ExecutionDetailResp struct { GranularityResults []GranularityResultItem `json:"granularity_results,omitempty"` } -func NewExecutionDetailResp(execution *database.Execution, labels []database.Label) *ExecutionDetailResp { +func NewExecutionDetailResp(execution *model.Execution, labels []model.Label) *ExecutionDetailResp { return &ExecutionDetailResp{ ExecutionResp: *NewExecutionResp(execution, labels), } } +// SubmitExecutionItem describes a single submitted execution task. type SubmitExecutionItem struct { Index int `json:"index"` TraceID string `json:"trace_id"` @@ -281,19 +278,59 @@ type SubmitExecutionItem struct { DatasetID *int `json:"dataset_id,omitempty"` } -// SubmitExecutionResp represents the response for submitting execution tasks +// SubmitExecutionResp represents the response for submitting execution tasks. type SubmitExecutionResp struct { GroupID string `json:"group_id"` Items []SubmitExecutionItem `json:"items"` } func validateExecutionStates(state *consts.ExecutionState) error { - if state != nil { - if *state < 0 { - return fmt.Errorf("state must be a non-negative integer") + if state == nil { + return nil + } + if *state < 0 { + return fmt.Errorf("state must be a non-negative integer") + } + if _, exists := consts.ValidExecutionStates[*state]; !exists { + return fmt.Errorf("invalid state: %d", *state) + } + return nil +} + +func validateExecutionStatus(statusPtr *consts.StatusType) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + return nil +} + +func validateExecutionLabels(labels []string) error { + for i, label := range labels { + parts := strings.SplitN(label, ":", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid label format at index %d: %q, expected key:value", i, label) } - if _, exists := consts.ValidExecutionStates[*state]; !exists { - return fmt.Errorf("invalid state: %d", *state) + if strings.TrimSpace(parts[0]) == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if strings.TrimSpace(parts[1]) == "" { + return fmt.Errorf("empty label value at index %d", i) + } + } + return nil +} + +func validateExecutionLabelItems(items []dto.LabelItem) error { + for i, label := range items { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d", i) } } return nil diff --git a/src/handlers/v2/executions.go b/src/module/execution/handler.go similarity index 63% rename from src/handlers/v2/executions.go rename to src/module/execution/handler.go index dec6abeb..04e91046 100644 --- a/src/handlers/v2/executions.go +++ b/src/module/execution/handler.go @@ -1,40 +1,56 @@ -package v2 +package executionmodule import ( - "aegis/consts" - "aegis/dto" - "aegis/handlers" - "aegis/middleware" - producer "aegis/service/producer" + "aegis/httpx" "context" "net/http" "strconv" + "aegis/consts" + "aegis/dto" + "aegis/middleware" + "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) -// BatchDeleteExecutions handles batch deletion of executions +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// ListProjectExecutions lists all algorithm executions for a project // -// @Summary Batch delete executions -// @Description Batch delete executions by IDs or labels with cascading deletion of related records -// @Tags Executions -// @ID batch_delete_executions -// @Accept json +// @Summary List project executions +// @Description Get paginated list of algorithm executions for a specific project +// @Tags Projects +// @ID list_project_executions // @Produce json // @Security BearerAuth -// @Param request body dto.BatchDeleteExecutionReq true "Batch delete request" -// @Success 200 {object} dto.GenericResponse[any] "Executions deleted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/executions/batch-delete [post] -func BatchDeleteExecutions(c *gin.Context) { - var req dto.BatchDeleteExecutionReq - if err := c.ShouldBindJSON(&req); err != nil { +// @Param project_id path int true "Project ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ExecutionResp]] "Executions retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/executions [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListProjectExecutions(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } + + var req ListExecutionReq + if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } @@ -44,50 +60,68 @@ func BatchDeleteExecutions(c *gin.Context) { return } - var err error - if len(req.IDs) > 0 { - err = producer.BatchDeleteExecutionsByIDs(req.IDs) - } else { - err = producer.BatchDeleteExecutionsByLabels(req.Labels) - } - - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListProjectExecutions(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Executions deleted successfully", nil) + dto.SuccessResponse(c, resp) } -// GetExecution handles getting a single execution by ID +// SubmitAlgorithmExecution submits batch algorithm execution for multiple datapacks or datasets // -// @Summary Get execution by ID -// @Description Get detailed information about a specific execution +// @Summary Submit batch algorithm execution +// @Description Submit multiple algorithm execution tasks in batch. Supports mixing datapack (v1 compatible) and dataset (v2 feature) executions. // @Tags Executions -// @ID get_execution_by_id +// @ID run_algorithm +// @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Execution ID" -// @Success 200 {object} dto.GenericResponse[dto.ExecutionDetailResp] "Execution retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/executions/{id} [get] -// @x-api-type {"sdk":"true"} -func GetExecution(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid execution ID") +// @Param project_id path int true "Project ID" +// @Param request body SubmitExecutionReq true "Algorithm execution request" +// @Success 200 {object} dto.GenericResponse[SubmitExecutionResp] "Algorithm execution submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project, algorithm, datapack or dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/executions/execute [post] +// @x-api-type {"portal":"true"} +func (h *Handler) SubmitAlgorithmExecution(c *gin.Context) { + groupID := c.GetString("groupID") + userID, exists := middleware.GetCurrentUserID(c) + if !exists { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + spanCtx, span, ok := spanFromGin(c) + if !ok { + return + } + + var req SubmitExecutionReq + if err := c.ShouldBindJSON(&req); err != nil { + span.SetStatus(codes.Error, "validation error in SubmitAlgorithmExecution: "+err.Error()) + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - resp, err := producer.GetExecutionDetail(id) - if handlers.HandleServiceError(c, err) { + if err := req.Validate(); err != nil { + span.SetStatus(codes.Error, "validation error in SubmitAlgorithmExecution: "+err.Error()) + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.SubmitAlgorithmExecution(spanCtx, &req, groupID, userID) + if err != nil { + span.SetStatus(codes.Error, "service error in SubmitAlgorithmExecution: "+err.Error()) + logrus.Errorf("Failed to submit algorithm execution: %v", err) + httpx.HandleServiceError(c, err) return } + span.SetStatus(codes.Ok, "Successfully submitted algorithm execution") dto.SuccessResponse(c, resp) } @@ -104,30 +138,56 @@ func GetExecution(c *gin.Context) { // @Param state query consts.ExecutionState false "Filter by execution state" // @Param status query consts.StatusType false "Filter by status" // @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:test')" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ExecutionResp]] "Executions retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ExecutionResp]] "Executions retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions [get] -// @x-api-type {"sdk":"true"} -func ListExecutions(c *gin.Context) { - var req dto.ListExecutionReq +// @x-api-type {} +func (h *Handler) ListExecutions(c *gin.Context) { + var req ListExecutionReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListExecutions(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListExecutions(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } + dto.SuccessResponse(c, resp) +} +// GetExecution handles getting a single execution by ID +// +// @Summary Get execution by ID +// @Description Get detailed information about a specific execution +// @Tags Executions +// @ID get_execution_by_id +// @Produce json +// @Security BearerAuth +// @Param id path int true "Execution ID" +// @Success 200 {object} dto.GenericResponse[ExecutionDetailResp] "Execution retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/executions/{id} [get] +// @x-api-type {} +func (h *Handler) GetExecution(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") + if !ok { + return + } + resp, err := h.service.GetExecution(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { + return + } dto.SuccessResponse(c, resp) } @@ -144,13 +204,12 @@ func ListExecutions(c *gin.Context) { // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/labels [get] -// @x-api-type {"sdk":"true"} -func ListAvaliableExecutionLabels(c *gin.Context) { - labels, err := producer.ListAvaliableExecutionLabels() - if handlers.HandleServiceError(c, err) { +// @x-api-type {} +func (h *Handler) ListAvailableExecutionLabels(c *gin.Context) { + labels, err := h.service.ListAvailableLabels(c.Request.Context()) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, labels) } @@ -164,100 +223,67 @@ func ListAvaliableExecutionLabels(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Execution ID" -// @Param manage body dto.ManageExecutionLabelReq true "Custom label management request" -// @Success 200 {object} dto.GenericResponse[dto.ExecutionResp] "Custom labels managed successfully" +// @Param manage body ManageExecutionLabelReq true "Custom label management request" +// @Success 200 {object} dto.GenericResponse[ExecutionResp] "Custom labels managed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID or request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Execution not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{id}/labels [patch] -func ManageExecutionCustomLabels(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid execution ID") +// @x-api-type {} +func (h *Handler) ManageExecutionCustomLabels(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") + if !ok { return } - - var req dto.ManageExecutionLabelReq + var req ManageExecutionLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ManageExecutionLabels(&req, id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ManageLabels(c.Request.Context(), &req, id) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } -// SubmitAlgorithmExecution submits batch algorithm execution for multiple datapacks or datasets +// BatchDeleteExecutions handles batch deletion of executions // -// @Summary Submit batch algorithm execution -// @Description Submit multiple algorithm execution tasks in batch. Supports mixing datapack (v1 compatible) and dataset (v2 feature) executions. +// @Summary Batch delete executions +// @Description Batch delete executions by IDs or labels with cascading deletion of related records // @Tags Executions -// @ID run_algorithm +// @ID batch_delete_executions // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.SubmitExecutionReq true "Algorithm execution request" -// @Success 200 {object} dto.GenericResponse[dto.SubmitExecutionResp] "Algorithm execution submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project, algorithm, datapack or dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/executions/execute [post] -// @x-api-type {"sdk":"true"} -func SubmitAlgorithmExecution(c *gin.Context) { - groupID := c.GetString("groupID") - userID, exists := middleware.GetCurrentUserID(c) - if !exists { - dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") - return - } - - ctx, ok := c.Get(middleware.SpanContextKey) - if !ok { - logrus.Error("failed to get span context from gin.Context") - dto.ErrorResponse(c, http.StatusInternalServerError, "failed to get span context") - return - } - - spanCtx := ctx.(context.Context) - span := trace.SpanFromContext(spanCtx) - - var req dto.SubmitExecutionReq +// @Param request body BatchDeleteExecutionReq true "Batch delete request" +// @Success 200 {object} dto.GenericResponse[any] "Executions deleted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/executions/batch-delete [post] +// @x-api-type {} +func (h *Handler) BatchDeleteExecutions(c *gin.Context) { + var req BatchDeleteExecutionReq if err := c.ShouldBindJSON(&req); err != nil { - span.SetStatus(codes.Error, "validation error in SubmitAlgorithmExecution: "+err.Error()) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { - span.SetStatus(codes.Error, "validation error in SubmitAlgorithmExecution: "+err.Error()) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ProduceAlgorithmExeuctionTasks(spanCtx, &req, groupID, userID) - if err != nil { - span.SetStatus(codes.Error, "service error in SubmitAlgorithmExecution: "+err.Error()) - logrus.Errorf("Failed to submit algorithm execution: %v", err) - handlers.HandleServiceError(c, err) + if httpx.HandleServiceError(c, h.service.BatchDelete(c.Request.Context(), &req)) { return } - - span.SetStatus(codes.Ok, "Successfully submitted algorithm execution") - dto.SuccessResponse(c, resp) + dto.JSONResponse[any](c, http.StatusNoContent, "Executions deleted successfully", nil) } // UploadDetectorResults uploads detector results @@ -270,39 +296,33 @@ func SubmitAlgorithmExecution(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param execution_id path int true "Execution ID" -// @Param request body dto.UploadDetectorResultReq true "Detector results" -// @Success 200 {object} dto.GenericResponse[dto.UploadExecutionResultResp] "Results uploaded successfully" +// @Param request body UploadDetectorResultReq true "Detector results" +// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid executionID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Execution not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/detector_results [post] -// @x-api-type {"sdk":"true"} -func UploadDetectorResults(c *gin.Context) { - executionIDStr := c.Param(consts.URLPathExecutionID) - executionID, err := strconv.Atoi(executionIDStr) - if err != nil || executionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid execution ID") +// @x-api-type {} +func (h *Handler) UploadDetectorResults(c *gin.Context) { + executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") + if !ok { return } - - var req dto.UploadDetectorResultReq + var req UploadDetectorResultReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.BatchCreateDetectorResults(&req, executionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UploadDetectorResults(c.Request.Context(), &req, executionID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -316,38 +336,54 @@ func UploadDetectorResults(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param execution_id path int true "Execution ID" -// @Param request body dto.UploadGranularityResultReq true "Granularity results" -// @Success 200 {object} dto.GenericResponse[dto.UploadExecutionResultResp] "Results uploaded successfully" +// @Param request body UploadGranularityResultReq true "Granularity results" +// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid exeuction ID or invalid request form or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Execution not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/granularity_results [post] -// @x-api-type {"sdk":"true"} -func UploadGranularityResults(c *gin.Context) { - executionIDStr := c.Param(consts.URLPathExecutionID) - executionID, err := strconv.Atoi(executionIDStr) - if err != nil || executionID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid execution ID") +// @x-api-type {} +func (h *Handler) UploadGranularityResults(c *gin.Context) { + executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") + if !ok { return } - - var req dto.UploadGranularityResultReq + var req UploadGranularityResultReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.BatchCreateGranularityResults(&req, executionID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UploadGranularityResults(c.Request.Context(), &req, executionID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } + +func spanFromGin(c *gin.Context) (context.Context, trace.Span, bool) { + ctx, ok := c.Get(middleware.SpanContextKey) + if !ok { + logrus.Error("failed to get span context from gin.Context") + dto.ErrorResponse(c, http.StatusInternalServerError, "failed to get span context") + return nil, nil, false + } + + spanCtx := ctx.(context.Context) + return spanCtx, trace.SpanFromContext(spanCtx), true +} + +func parseProjectID(c *gin.Context) (int, bool) { + projectIDStr := c.Param(consts.URLPathProjectID) + projectID, err := strconv.Atoi(projectIDStr) + if err != nil || projectID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") + return 0, false + } + return projectID, true +} diff --git a/src/module/execution/module.go b/src/module/execution/module.go new file mode 100644 index 00000000..9044df26 --- /dev/null +++ b/src/module/execution/module.go @@ -0,0 +1,9 @@ +package executionmodule + +import "go.uber.org/fx" + +var Module = fx.Module("execution", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/execution/repository.go b/src/module/execution/repository.go new file mode 100644 index 00000000..37f9ba30 --- /dev/null +++ b/src/module/execution/repository.go @@ -0,0 +1,412 @@ +package executionmodule + +import ( + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/model" + "errors" + "fmt" + "strings" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) getProjectByName(name string) (*model.Project, error) { + var project model.Project + if err := r.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { + return nil, fmt.Errorf("failed to find project with name %s: %w", name, err) + } + return &project, nil +} + +func (r *Repository) listProjectExecutionsView(projectID, limit, offset int) ([]model.Execution, int64, error) { + var ( + executions []model.Execution + total int64 + ) + + baseQuery := r.db.Model(&model.Execution{}). + Joins("JOIN tasks ON tasks.id = executions.task_id"). + Joins("JOIN traces on traces.id = tasks.trace_id"). + Where("traces.project_id = ? AND executions.status != ?", projectID, consts.CommonDeleted) + + if err := baseQuery.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count executions for project %d: %w", projectID, err) + } + if err := baseQuery. + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Benchmark.Container"). + Preload("Datapack.Pedestal.Container"). + Preload("DatasetVersion"). + Limit(limit). + Offset(offset). + Order("executions.updated_at DESC"). + Find(&executions).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list executions for project %d: %w", projectID, err) + } + return r.attachExecutionLabels(executions, total) +} + +func (r *Repository) listExecutionsView(limit, offset int, req *ListExecutionReq) ([]model.Execution, int64, error) { + labelConditions := make([]map[string]string, 0, len(req.Labels)) + for _, item := range req.Labels { + parts := strings.SplitN(item, ":", 2) + condition := map[string]string{"key": parts[0], "value": ""} + if len(parts) > 1 { + condition["value"] = parts[1] + } + labelConditions = append(labelConditions, condition) + } + + var ( + executions []model.Execution + total int64 + ) + + query := r.db.Model(&model.Execution{}). + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Benchmark.Container"). + Preload("Datapack.Pedestal.Container"). + Preload("DatasetVersion"). + Preload("Task.Trace.Project") + if req.State != nil { + query = query.Where("event = ?", *req.State) + } + if req.Status != nil { + query = query.Where("status = ?", *req.Status) + } + for _, condition := range labelConditions { + subQuery := r.db.Table("execution_injection_labels eil"). + Select("eil.execution_id"). + Joins("JOIN labels ON labels.id = eil.label_id"). + Where("labels.label_key = ? AND labels.label_value = ?", condition["key"], condition["value"]) + query = query.Where("executions.id IN (?)", subQuery) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count executions: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&executions).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list executions: %w", err) + } + return r.attachExecutionLabels(executions, total) +} + +func (r *Repository) getExecutionView(executionID int) (*model.Execution, []model.Label, error) { + var execution model.Execution + if err := r.db. + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Benchmark.Container"). + Preload("Datapack.Pedestal.Container"). + Preload("DatasetVersion"). + Preload("Task.Trace.Project"). + Where("id = ? AND status != ?", executionID, consts.CommonDeleted). + First(&execution).Error; err != nil { + return nil, nil, fmt.Errorf("failed to find execution result with id %d: %w", executionID, err) + } + + var labels []model.Label + if err := r.db.Table("labels"). + Joins("JOIN execution_injection_labels eil ON labels.id = eil.label_id"). + Where("eil.execution_id = ?", execution.ID). + Find(&labels).Error; err != nil { + return nil, nil, fmt.Errorf("failed to get execution labels: %w", err) + } + return &execution, labels, nil +} + +func (r *Repository) getExecutionResultView(executionID int) (*model.Execution, []model.Label, []model.DetectorResult, []model.GranularityResult, error) { + execution, labels, err := r.getExecutionView(executionID) + if err != nil { + return nil, nil, nil, nil, err + } + + if execution.AlgorithmVersion.Container.Name == config.GetDetectorName() { + var detectorResults []model.DetectorResult + if err := r.db.Where("execution_id = ?", execution.ID).Find(&detectorResults).Error; err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to get detector results: %w", err) + } + return execution, labels, detectorResults, nil, nil + } + + var granularityResults []model.GranularityResult + if err := r.db.Where("execution_id = ?", execution.ID).Find(&granularityResults).Error; err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to get granularity results: %w", err) + } + return execution, labels, nil, granularityResults, nil +} + +func (r *Repository) listAvailableExecutionLabels() ([]model.Label, error) { + var labels []model.Label + if err := r.db. + Where("status != ?", consts.CommonDeleted). + Order("usage_count DESC, created_at DESC"). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels: %w", err) + } + + executionLabels := make([]model.Label, 0) + for _, label := range labels { + if label.Category == consts.ExecutionCategory { + executionLabels = append(executionLabels, label) + } + } + return executionLabels, nil +} + +func (r *Repository) listExecutionLabelIDsByKeys(executionID int, keys []string) ([]int, error) { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN execution_injection_labels eil ON eil.label_id = l.id"). + Where("eil.execution_id = ? AND l.label_key IN (?)", executionID, keys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) + } + return labelIDs, nil +} + +func (r *Repository) loadExecutionLabelIDsByItems(conditions []map[string]string, category consts.LabelCategory) (map[string]int, error) { + if len(conditions) == 0 { + return map[string]int{}, nil + } + + query := r.db.Model(&model.Label{}). + Where("status != ? AND category = ?", consts.CommonDeleted, category) + orBuilder := r.db.Where("1 = 0") + for _, condition := range conditions { + andBuilder := r.db.Where("1 = 1") + if key, ok := condition["key"]; ok { + andBuilder = andBuilder.Where("label_key = ?", key) + } + if value, ok := condition["value"]; ok { + andBuilder = andBuilder.Where("label_value = ?", value) + } + orBuilder = orBuilder.Or(andBuilder) + } + + var labels []model.Label + if err := query.Where(orBuilder).Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list label IDs by conditions: %w", err) + } + + result := make(map[string]int, len(labels)) + for _, label := range labels { + result[label.Key+":"+label.Value] = label.ID + } + return result, nil +} + +func (r *Repository) AddExecutionLabels(executionID int, labelIDs []int) error { + if len(labelIDs) == 0 { + return nil + } + + executionLabels := make([]model.ExecutionInjectionLabel, 0, len(labelIDs)) + for _, labelID := range labelIDs { + executionLabels = append(executionLabels, model.ExecutionInjectionLabel{ + ExecutionID: executionID, + LabelID: labelID, + }) + } + if err := r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "execution_id"}, {Name: "label_id"}}, + DoNothing: true, + }).Create(&executionLabels).Error; err != nil { + return fmt.Errorf("failed to add execution-label associatons: %w", err) + } + return nil +} + +func (r *Repository) ClearExecutionLabels(executionIDs []int, labelIDs []int) error { + if len(executionIDs) == 0 { + return nil + } + + query := r.db.Table("execution_injection_labels").Where("execution_id IN (?)", executionIDs) + if len(labelIDs) > 0 { + query = query.Where("label_id IN (?)", labelIDs) + } + if err := query.Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear execution labels: %w", err) + } + return nil +} + +func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + Clauses(clause.Returning{}). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch decrease label usages: %w", err) + } + return nil +} + +func (r *Repository) ListExecutionIDsByLabelItems(labelItems []dto.LabelItem) ([]int, error) { + labelConditions := make([]map[string]string, 0, len(labelItems)) + for _, item := range labelItems { + labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) + } + + var executionIDs []int + query := r.db.Model(&model.Execution{}). + Select("DISTINCT executions.id"). + Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). + Joins("JOIN labels ON labels.id = eil.label_id"). + Where("executions.status != ?", consts.CommonDeleted) + + var whereClauses []string + var whereArgs []any + for _, condition := range labelConditions { + whereClauses = append(whereClauses, "(labels.label_key = ? AND labels.label_value = ?)") + whereArgs = append(whereArgs, condition["key"], condition["value"]) + } + if len(whereClauses) > 0 { + query = query.Where(strings.Join(whereClauses, " OR "), whereArgs...) + } + + if err := query.Pluck("executions.id", &executionIDs).Error; err != nil { + return nil, fmt.Errorf("failed to list execution IDs by labels: %w", err) + } + return executionIDs, nil +} + +func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { + if len(executionIDs) == 0 { + return nil + } + if err := r.db.Where("execution_id IN (?)", executionIDs). + Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { + return fmt.Errorf("failed to delete execution labels: %w", err) + } + if err := r.db.Model(&model.Execution{}). + Where("id IN (?) AND status != ?", executionIDs, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return fmt.Errorf("failed to batch delete executions: %w", err) + } + return nil +} + +func (r *Repository) UpdateExecutionDuration(executionID int, duration float64) error { + var execution model.Execution + if err := r.db. + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Benchmark.Container"). + Preload("Datapack.Pedestal.Container"). + Preload("DatasetVersion"). + Preload("Task.Trace.Project"). + Where("id = ? AND status != ?", executionID, consts.CommonDeleted). + First(&execution).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) + } + return fmt.Errorf("execution %d not found: %w", executionID, err) + } + + if execution.Status != consts.CommonEnabled { + return fmt.Errorf("must upload results for an active execution %d", executionID) + } + if execution.State == consts.ExecutionSuccess { + return fmt.Errorf("cannot upload results for a successful execution %d", executionID) + } + + result := r.db.Model(&model.Execution{}). + Where("id = ? AND status != ?", executionID, consts.CommonDeleted). + Updates(map[string]any{"duration": duration}) + if err := result.Error; err != nil { + return fmt.Errorf("failed to update execution %d duration: %w", executionID, err) + } + if result.RowsAffected == 0 { + return fmt.Errorf("execution not found or no changes made") + } + return nil +} + +func (r *Repository) SaveDetectorResults(results []model.DetectorResult) error { + if len(results) == 0 { + return fmt.Errorf("no detector results to save") + } + if err := r.db.Create(&results).Error; err != nil { + return fmt.Errorf("failed to save detector results: %w", err) + } + return nil +} + +func (r *Repository) SaveGranularityResults(results []model.GranularityResult) error { + if len(results) == 0 { + return fmt.Errorf("no granularity results to create") + } + for i := range results { + resultPtr := &results[i] + err := r.db.Omit("active_name").Create(resultPtr).Error + if err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: index %d", consts.ErrAlreadyExists, i) + } + return fmt.Errorf("failed to create record index %d: %w", i, err) + } + } + return nil +} + +func (r *Repository) attachExecutionLabels(executions []model.Execution, total int64) ([]model.Execution, int64, error) { + executionIDs := make([]int, 0, len(executions)) + for _, execution := range executions { + executionIDs = append(executionIDs, execution.ID) + } + + if len(executionIDs) == 0 { + return executions, total, nil + } + + type executionLabelResult struct { + model.Label + executionID int `gorm:"column:execution_id"` + } + + var flatResults []executionLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN execution_injection_labels eil ON eil.label_id = labels.id"). + Where("eil.execution_id IN (?)", executionIDs). + Select("labels.*, eil.execution_id"). + Find(&flatResults).Error; err != nil { + return nil, 0, fmt.Errorf("failed to batch query execution labels: %w", err) + } + + labelsMap := make(map[int][]model.Label, len(executionIDs)) + for _, id := range executionIDs { + labelsMap[id] = []model.Label{} + } + for _, res := range flatResults { + labelsMap[res.executionID] = append(labelsMap[res.executionID], res.Label) + } + + for i := range executions { + executions[i].Labels = labelsMap[executions[i].ID] + } + return executions, total, nil +} diff --git a/src/dto/algorithm_result.go b/src/module/execution/result_types.go similarity index 84% rename from src/dto/algorithm_result.go rename to src/module/execution/result_types.go index 63a9a095..a51c07ee 100644 --- a/src/dto/algorithm_result.go +++ b/src/module/execution/result_types.go @@ -1,12 +1,12 @@ -package dto +package executionmodule import ( - "aegis/database" + "aegis/model" "fmt" "time" ) -// DetectorResultItem Single detector result item +// DetectorResultItem is a single detector result payload item. type DetectorResultItem struct { SpanName string `json:"span_name" binding:"required"` Issues string `json:"issues" binding:"required"` @@ -29,26 +29,23 @@ func (item *DetectorResultItem) Validate() error { if item.Issues == "" { return fmt.Errorf("issues cannot be empty") } - if item.AbnormalSuccRate != nil && (*item.AbnormalSuccRate < 0 || *item.AbnormalSuccRate > 1) { return fmt.Errorf("abnormal_succ_rate must be between 0-1") } if item.NormalSuccRate != nil && (*item.NormalSuccRate < 0 || *item.NormalSuccRate > 1) { return fmt.Errorf("normal_succ_rate must be between 0-1") } - if item.AbnormalAvgDuration != nil && *item.AbnormalAvgDuration < 0 { return fmt.Errorf("abnormal_avg_duration cannot be negative") } if item.NormalAvgDuration != nil && *item.NormalAvgDuration < 0 { return fmt.Errorf("normal_avg_duration cannot be negative") } - return nil } -func (item DetectorResultItem) ConvertToDetectorResult(executionID int) *database.DetectorResult { - return &database.DetectorResult{ +func (item DetectorResultItem) ConvertToDetectorResult(executionID int) *model.DetectorResult { + return &model.DetectorResult{ SpanName: item.SpanName, Issues: item.Issues, AbnormalAvgDuration: item.AbnormalAvgDuration, @@ -65,7 +62,7 @@ func (item DetectorResultItem) ConvertToDetectorResult(executionID int) *databas } } -func NewDetectorResultItem(result *database.DetectorResult) DetectorResultItem { +func NewDetectorResultItem(result *model.DetectorResult) DetectorResultItem { return DetectorResultItem{ SpanName: result.SpanName, Issues: result.Issues, @@ -82,7 +79,7 @@ func NewDetectorResultItem(result *database.DetectorResult) DetectorResultItem { } } -// GranularityResultItem Single granularity result item +// GranularityResultItem is a single localization result payload item. type GranularityResultItem struct { Level string `json:"level" binding:"required"` Result string `json:"result" binding:"required"` @@ -90,7 +87,7 @@ type GranularityResultItem struct { Confidence float64 `json:"confidence" binding:"omitempty"` } -func (item *GranularityResultItem) Valiate() error { +func (item *GranularityResultItem) Validate() error { if item.Level == "" { return fmt.Errorf("level cannot be empty") } @@ -106,8 +103,8 @@ func (item *GranularityResultItem) Valiate() error { return nil } -func (item *GranularityResultItem) ConvertToGranularityResult(executionID int) *database.GranularityResult { - return &database.GranularityResult{ +func (item *GranularityResultItem) ConvertToGranularityResult(executionID int) *model.GranularityResult { + return &model.GranularityResult{ Level: item.Level, Result: item.Result, Rank: item.Rank, @@ -116,7 +113,7 @@ func (item *GranularityResultItem) ConvertToGranularityResult(executionID int) * } } -func NewGranularityResultItem(result *database.GranularityResult) GranularityResultItem { +func NewGranularityResultItem(result *model.GranularityResult) GranularityResultItem { return GranularityResultItem{ Level: result.Level, Result: result.Result, @@ -125,9 +122,9 @@ func NewGranularityResultItem(result *database.GranularityResult) GranularityRes } } -// DetectorResultRequest Detector result upload request +// UploadDetectorResultReq is the detector result upload request body. type UploadDetectorResultReq struct { - Duration float64 `json:"duration" binding:"required"` // Execution duration in seconds + Duration float64 `json:"duration" binding:"required"` Results []DetectorResultItem `json:"results" binding:"required"` } @@ -135,17 +132,14 @@ func (req *UploadDetectorResultReq) Validate() error { if len(req.Results) == 0 { return fmt.Errorf("at least one detection result is required") } - for i, result := range req.Results { if err := result.Validate(); err != nil { return fmt.Errorf("validation failed for result %d: %w", i+1, err) } } - return nil } -// HasAnomalies checks if detector results contain anomalies func (req *UploadDetectorResultReq) HasAnomalies() bool { for _, result := range req.Results { if result.Issues != "{}" && result.Issues != "" { @@ -155,9 +149,9 @@ func (req *UploadDetectorResultReq) HasAnomalies() bool { return false } -// GranularityResultRequest Granularity result upload request +// UploadGranularityResultReq is the granularity result upload request body. type UploadGranularityResultReq struct { - Duration float64 `json:"duration" binding:"required"` // Execution duration in seconds + Duration float64 `json:"duration" binding:"required"` Results []GranularityResultItem `json:"results" binding:"required,dive,required"` } @@ -165,25 +159,22 @@ func (req *UploadGranularityResultReq) Validate() error { if len(req.Results) == 0 { return fmt.Errorf("at least one granularity result is required") } - rankMap := make(map[int]bool) for i, result := range req.Results { - if err := result.Valiate(); err != nil { + if err := result.Validate(); err != nil { return fmt.Errorf("validation failed for result %d: %w", i+1, err) } - if rankMap[result.Rank] { return fmt.Errorf("rank %d appeared repeatedly", result.Rank) } rankMap[result.Rank] = true } - return nil } -// UploadExecutionResultResp Execution result upload response +// UploadExecutionResultResp is the upload response body. type UploadExecutionResultResp struct { ResultCount int `json:"result_count"` UploadedAt time.Time `json:"uploaded_at"` - HasAnomalies bool `json:"has_anomalies,omitempty"` // Only included for detector results + HasAnomalies bool `json:"has_anomalies,omitempty"` } diff --git a/src/module/execution/service.go b/src/module/execution/service.go new file mode 100644 index 00000000..d092a5aa --- /dev/null +++ b/src/module/execution/service.go @@ -0,0 +1,340 @@ +package executionmodule + +import ( + "context" + "errors" + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + "aegis/model" + "aegis/service/common" + "aegis/utils" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, redis: redis} +} + +func (s *Service) ListProjectExecutions(_ context.Context, req *ListExecutionReq, projectID int) (*dto.ListResp[ExecutionResp], error) { + var project model.Project + if err := s.repo.db.Where("id = ?", projectID).First(&project).Error; err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + + limit, offset := req.ToGormParams() + executions, total, err := s.repo.listProjectExecutionsView(projectID, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list executions for project %d: %w", projectID, err) + } + + items := make([]ExecutionResp, 0, len(executions)) + for i := range executions { + items = append(items, *NewExecutionResp(&executions[i], executions[i].Labels)) + } + + return &dto.ListResp[ExecutionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecutionReq, groupID string, userID int) (*SubmitExecutionResp, error) { + db := s.repo.db + + project, err := s.repo.getProjectByName(req.ProjectName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + + refs := make([]*dto.ContainerRef, 0, len(req.Specs)) + for i := range req.Specs { + refs = append(refs, &req.Specs[i].Algorithm.ContainerRef) + } + + algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeAlgorithm, userID) + if err != nil { + return nil, fmt.Errorf("failed to map container refs to versions: %w", err) + } + if len(algorithmVersionResults) == 0 { + return nil, fmt.Errorf("no valid algorithm versions found for the provided specs") + } + + var allExecutionItems []SubmitExecutionItem + for idx, spec := range req.Specs { + datapacks, datasetID, err := common.ExtractDatapacks(s.repo.db, spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) + if err != nil { + return nil, fmt.Errorf("failed to extract datapacks: %w", err) + } + + algorithmVersion, exists := algorithmVersionResults[refs[idx]] + if !exists { + return nil, fmt.Errorf("algorithm version not found for %v", spec.Algorithm) + } + + for _, datapack := range datapacks { + if datapack.StartTime == nil || datapack.EndTime == nil { + return nil, fmt.Errorf("datapack %s does not have valid start_time and end_time", datapack.Name) + } + + algorithmItem := dto.NewContainerVersionItem(&algorithmVersion) + envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.Algorithm.EnvVars, &algorithmVersion) + if err != nil { + return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) + } + algorithmItem.EnvVars = envVars + + payload := map[string]any{ + consts.ExecuteAlgorithm: algorithmItem, + consts.ExecuteDatapack: dto.NewInjectionItem(&datapack), + consts.ExecuteDatasetVersionID: utils.GetIntValue(datasetID, consts.DefaultInvalidID), + consts.ExecuteLabels: req.Labels, + } + + task := &dto.UnifiedTask{ + Type: consts.TaskTypeRunAlgorithm, + Immediate: true, + Payload: payload, + GroupID: groupID, + ProjectID: project.ID, + UserID: userID, + State: consts.TaskPending, + } + task.SetGroupCtx(ctx) + + if err := common.SubmitTaskWithDB(ctx, db, s.redis, task); err != nil { + return nil, fmt.Errorf("failed to submit task: %w", err) + } + + allExecutionItems = append(allExecutionItems, SubmitExecutionItem{ + Index: idx, + TraceID: task.TraceID, + TaskID: task.TaskID, + AlgorithmID: algorithmVersion.ContainerID, + AlgorithmVersionID: algorithmVersion.ID, + DatapackID: &datapack.ID, + }) + } + } + + return &SubmitExecutionResp{ + GroupID: groupID, + Items: allExecutionItems, + }, nil +} + +func (s *Service) ListExecutions(_ context.Context, req *ListExecutionReq) (*dto.ListResp[ExecutionResp], error) { + limit, offset := req.ToGormParams() + executions, total, err := s.repo.listExecutionsView(limit, offset, req) + if err != nil { + return nil, fmt.Errorf("failed to list executions: %w", err) + } + + items := make([]ExecutionResp, 0, len(executions)) + for i := range executions { + items = append(items, *NewExecutionResp(&executions[i], executions[i].Labels)) + } + + return &dto.ListResp[ExecutionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetExecution(_ context.Context, id int) (*ExecutionDetailResp, error) { + execution, labels, detectorResults, granularityResults, err := s.repo.getExecutionResultView(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: execution id: %d", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get execution: %w", err) + } + + resp := NewExecutionDetailResp(execution, labels) + if len(detectorResults) > 0 { + items := make([]DetectorResultItem, 0, len(detectorResults)) + for _, result := range detectorResults { + items = append(items, NewDetectorResultItem(&result)) + } + resp.DetectorResults = items + } + if len(granularityResults) > 0 { + items := make([]GranularityResultItem, 0, len(granularityResults)) + for _, result := range granularityResults { + items = append(items, NewGranularityResultItem(&result)) + } + resp.GranularityResults = items + } + return resp, nil +} + +func (s *Service) ListAvailableLabels(_ context.Context) ([]dto.LabelItem, error) { + labels, err := s.repo.listAvailableExecutionLabels() + if err != nil { + return nil, err + } + + items := make([]dto.LabelItem, 0, len(labels)) + for _, label := range labels { + items = append(items, dto.LabelItem{Key: label.Key, Value: label.Value}) + } + return items, nil +} + +func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, executionID int) (*ExecutionResp, error) { + if req == nil { + return nil, fmt.Errorf("manage execution labels request is nil") + } + + var managedExecution *model.Execution + var managedLabels []model.Label + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + execution, _, err := repo.getExecutionView(executionID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: execution id: %d", consts.ErrNotFound, executionID) + } + return fmt.Errorf("failed to get execution: %w", err) + } + + if len(req.AddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + labelIDs := make([]int, 0, len(labels)) + for _, label := range labels { + labelIDs = append(labelIDs, label.ID) + } + if err := repo.AddExecutionLabels(execution.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add execution labels: %w", err) + } + } + + if len(req.RemoveLabels) > 0 { + labelIDs, err := repo.listExecutionLabelIDsByKeys(execution.ID, req.RemoveLabels) + if err != nil { + return fmt.Errorf("failed to find label ids by keys: %w", err) + } + + if len(labelIDs) > 0 { + if err := repo.ClearExecutionLabels([]int{executionID}, labelIDs); err != nil { + return fmt.Errorf("failed to clear execution labels: %w", err) + } + if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + return fmt.Errorf("failed to decrease label usage counts: %w", err) + } + } + } + + reloadedExecution, labels, err := repo.getExecutionView(executionID) + if err != nil { + return fmt.Errorf("failed to reload execution labels: %w", err) + } + managedExecution = reloadedExecution + managedLabels = labels + return nil + }) + if err != nil { + return nil, err + } + + return NewExecutionResp(managedExecution, managedLabels), nil +} + +func (s *Service) BatchDelete(_ context.Context, req *BatchDeleteExecutionReq) error { + if len(req.IDs) > 0 { + return s.batchDeleteByIDs(req.IDs) + } + return s.batchDeleteByLabels(req.Labels) +} + +func (s *Service) UploadDetectorResults(_ context.Context, req *UploadDetectorResultReq, executionID int) (*UploadExecutionResultResp, error) { + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.UpdateExecutionDuration(executionID, req.Duration); err != nil { + return err + } + + results := make([]model.DetectorResult, 0, len(req.Results)) + for _, item := range req.Results { + results = append(results, *item.ConvertToDetectorResult(executionID)) + } + if err := repo.SaveDetectorResults(results); err != nil { + return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) + } + return nil + }) + if err != nil { + return nil, err + } + + return &UploadExecutionResultResp{ + ResultCount: len(req.Results), + UploadedAt: time.Now(), + HasAnomalies: req.HasAnomalies(), + }, nil +} + +func (s *Service) UploadGranularityResults(_ context.Context, req *UploadGranularityResultReq, executionID int) (*UploadExecutionResultResp, error) { + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.UpdateExecutionDuration(executionID, req.Duration); err != nil { + return err + } + + results := make([]model.GranularityResult, 0, len(req.Results)) + for _, item := range req.Results { + results = append(results, *item.ConvertToGranularityResult(executionID)) + } + if err := repo.SaveGranularityResults(results); err != nil { + return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) + } + return nil + }) + if err != nil { + return nil, err + } + + return &UploadExecutionResultResp{ + ResultCount: len(req.Results), + UploadedAt: time.Now(), + }, nil +} + +func (s *Service) batchDeleteByIDs(executionIDs []int) error { + if len(executionIDs) == 0 { + return nil + } + return s.repo.Transaction(func(tx *gorm.DB) error { + return s.repo.withDB(tx).BatchDeleteExecutions(executionIDs) + }) +} + +func (s *Service) batchDeleteByLabels(labelItems []dto.LabelItem) error { + if len(labelItems) == 0 { + return nil + } + executionIDs, err := s.repo.ListExecutionIDsByLabelItems(labelItems) + if err != nil { + return fmt.Errorf("failed to list execution ids by labels: %w", err) + } + return s.batchDeleteByIDs(executionIDs) +} diff --git a/src/module/execution/service_test.go b/src/module/execution/service_test.go new file mode 100644 index 00000000..c0b5441e --- /dev/null +++ b/src/module/execution/service_test.go @@ -0,0 +1,255 @@ +package executionmodule + +import ( + "regexp" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + "aegis/testutil" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newExecutionService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + addr, cleanupRedis := testutil.StartRedisStub(t) + viper.Set("redis.host", addr) + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db), redisinfra.NewGateway(nil)), mock, func() { + cleanupRedis() + _ = sqlDB.Close() + } +} + +func TestServiceListAvailableLabelsSuccess(t *testing.T) { + service, mock, cleanup := newExecutionService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `labels` WHERE status != ? ORDER BY usage_count DESC, created_at DESC")). + WithArgs(consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "label_key", "label_value", "category", "description", "color", "usage_count", "is_system", "status", "created_at", "updated_at", + }).AddRow(1, "source", "manual", consts.ExecutionCategory, "manual source", "#1890ff", 2, false, consts.CommonEnabled, now, now)) + + labels, err := service.ListAvailableLabels(t.Context()) + + require.NoError(t, err) + require.Len(t, labels, 1) + require.Equal(t, "source", labels[0].Key) + require.Equal(t, "manual", labels[0].Value) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceBatchDeleteEmptyRequestSucceeds(t *testing.T) { + service := NewService(nil, nil) + + err := service.BatchDelete(t.Context(), &BatchDeleteExecutionReq{}) + + require.NoError(t, err) +} + +func TestServiceListExecutionsSuccessWithLabelFilter(t *testing.T) { + service, mock, cleanup := newExecutionService(t) + defer cleanup() + + status := consts.CommonEnabled + req := &ListExecutionReq{ + Status: &status, + Labels: []string{"source:manual"}, + } + + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `executions` WHERE status = \\? AND executions\\.id IN \\(SELECT eil\\.execution_id FROM execution_injection_labels eil JOIN labels ON labels\\.id = eil\\.label_id WHERE labels\\.label_key = \\? AND labels\\.label_value = \\?\\)"). + WithArgs(status, "source", "manual"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery("SELECT \\* FROM `executions` WHERE status = \\? AND executions\\.id IN \\(SELECT eil\\.execution_id FROM execution_injection_labels eil JOIN labels ON labels\\.id = eil\\.label_id WHERE labels\\.label_key = \\? AND labels\\.label_value = \\?\\) ORDER BY updated_at DESC LIMIT \\?"). + WithArgs(status, "source", "manual", 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "duration", "task_id", "algorithm_version_id", "datapack_id", "dataset_version_id", "state", "status", "created_at", "updated_at", + })) + + resp, err := service.ListExecutions(t.Context(), req) + + require.NoError(t, err) + require.Empty(t, resp.Items) + require.Equal(t, int64(0), resp.Pagination.Total) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceUploadDetectorResultsSuccess(t *testing.T) { + service, mock, cleanup := newExecutionService(t) + defer cleanup() + + now := time.Now() + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `executions` WHERE id = ? AND status != ? ORDER BY `executions`.`id` LIMIT ?")). + WithArgs(12, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "duration", "task_id", "algorithm_version_id", "datapack_id", "dataset_version_id", "state", "status", "created_at", "updated_at", + }).AddRow(12, 0, nil, 5, 7, nil, consts.ExecutionInitial, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `container_versions` WHERE `container_versions`.`id` = ?")). + WithArgs(5). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "container_id", "registry", "namespace", "repository", "tag", "status", "created_at", "updated_at", + }).AddRow(5, "1.0.0", 8, "docker.io", "", "algo", "latest", consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `containers` WHERE `containers`.`id` = ?")). + WithArgs(8). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "type", "readme", "is_public", "status", "created_at", "updated_at", + }).AddRow(8, "algo", consts.ContainerTypeAlgorithm, "", true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fault_injections` WHERE `fault_injections`.`id` = ?")). + WithArgs(7). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "source", "fault_type", "category", "description", "engine_config", "groundtruth_source", "pre_duration", "benchmark_id", "pedestal_id", "task_id", "state", "status", "created_at", "updated_at", + }).AddRow(7, "dp-1", consts.DatapackSourceInjection, 0, "train-ticket", "", "{}", "auto", 0, nil, nil, nil, consts.DatapackInitial, consts.CommonEnabled, now, now)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `executions` SET `duration`=?,`updated_at`=? WHERE id = ? AND status != ?")). + WithArgs(12.5, sqlmock.AnyArg(), 12, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `detector_results`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + duration := 12.5 + resp, err := service.UploadDetectorResults(t.Context(), &UploadDetectorResultReq{ + Duration: duration, + Results: []DetectorResultItem{ + {SpanName: "checkout", Issues: `{"latency":true}`}, + }, + }, 12) + + require.NoError(t, err) + require.Equal(t, 1, resp.ResultCount) + require.True(t, resp.HasAnomalies) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceUploadGranularityResultsSuccess(t *testing.T) { + service, mock, cleanup := newExecutionService(t) + defer cleanup() + + now := time.Now() + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `executions` WHERE id = ? AND status != ? ORDER BY `executions`.`id` LIMIT ?")). + WithArgs(15, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "duration", "task_id", "algorithm_version_id", "datapack_id", "dataset_version_id", "state", "status", "created_at", "updated_at", + }).AddRow(15, 0, nil, 6, 9, nil, consts.ExecutionInitial, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `container_versions` WHERE `container_versions`.`id` = ?")). + WithArgs(6). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "container_id", "registry", "namespace", "repository", "tag", "status", "created_at", "updated_at", + }).AddRow(6, "1.0.0", 10, "docker.io", "", "algo", "latest", consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `containers` WHERE `containers`.`id` = ?")). + WithArgs(10). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "type", "readme", "is_public", "status", "created_at", "updated_at", + }).AddRow(10, "locator", consts.ContainerTypeAlgorithm, "", true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fault_injections` WHERE `fault_injections`.`id` = ?")). + WithArgs(9). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "source", "fault_type", "category", "description", "engine_config", "groundtruth_source", "pre_duration", "benchmark_id", "pedestal_id", "task_id", "state", "status", "created_at", "updated_at", + }).AddRow(9, "dp-2", consts.DatapackSourceInjection, 0, "train-ticket", "", "{}", "auto", 0, nil, nil, nil, consts.DatapackInitial, consts.CommonEnabled, now, now)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `executions` SET `duration`=?,`updated_at`=? WHERE id = ? AND status != ?")). + WithArgs(8.8, sqlmock.AnyArg(), 15, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `granularity_results`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + resp, err := service.UploadGranularityResults(t.Context(), &UploadGranularityResultReq{ + Duration: 8.8, + Results: []GranularityResultItem{ + {Level: "service", Result: "checkout", Rank: 1, Confidence: 0.91}, + }, + }, 15) + + require.NoError(t, err) + require.Equal(t, 1, resp.ResultCount) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceSubmitAlgorithmExecutionSuccess(t *testing.T) { + addr, cleanupRedis := testutil.StartRedisStub(t) + defer cleanupRedis() + viper.Set("redis.host", addr) + + service, mock, cleanup := newExecutionService(t) + defer cleanup() + + mock.MatchExpectationsInOrder(false) + + now := time.Now() + start := now.Add(-5 * time.Minute) + end := now.Add(-1 * time.Minute) + datapackName := "dp-1" + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `projects` WHERE name = ? AND status != ? ORDER BY `projects`.`id` LIMIT ?")). + WithArgs("demo-project", consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", + }).AddRow(3, "demo-project", "demo", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery("SELECT .* FROM container_versions cv .*"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "name_major", "name_minor", "name_patch", "github_link", "registry", "namespace", "repository", "tag", "command", "usage_count", "container_id", "user_id", "status", "created_at", "updated_at", + }).AddRow(5, "1.0.0", 1, 0, 0, "", "docker.io", "", "algo", "latest", "", 0, 8, 1, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `containers` WHERE `containers`.`id` = ?")). + WithArgs(8). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "type", "readme", "is_public", "status", "created_at", "updated_at", + }).AddRow(8, "algo", consts.ContainerTypeAlgorithm, "", true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fault_injections` WHERE name = ? AND status != ? ORDER BY `fault_injections`.`id` LIMIT ?")). + WithArgs(datapackName, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "source", "fault_type", "category", "description", "display_config", "engine_config", "groundtruths", "groundtruth_source", "pre_duration", "start_time", "end_time", "benchmark_id", "pedestal_id", "task_id", "state", "status", "created_at", "updated_at", + }).AddRow(7, datapackName, consts.DatapackSourceInjection, 0, "ts", "", nil, "{}", "[]", "auto", 5, start, end, nil, nil, nil, consts.DatapackDetectorSuccess, consts.CommonEnabled, now, now)) + mock.ExpectQuery("SELECT .* FROM `fault_injection_labels` .*"). + WillReturnRows(sqlmock.NewRows([]string{"fault_injection_id", "label_id"})) + mock.ExpectQuery("SELECT .* FROM `parameter_configs` JOIN container_version_env_vars .*"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "config_key", "type", "category", "value_type", "description", "default_value", "template_string", "required", "overridable", + })) + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `traces`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `tasks`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + resp, err := service.SubmitAlgorithmExecution(t.Context(), &SubmitExecutionReq{ + ProjectName: "demo-project", + Specs: []ExecutionSpec{ + { + Algorithm: dto.ContainerSpec{ + ContainerRef: dto.ContainerRef{Name: "algo", Version: "1.0.0"}, + }, + Datapack: &datapackName, + }, + }, + }, "group-1", 1) + + require.NoError(t, err) + require.Equal(t, "group-1", resp.GroupID) + require.Len(t, resp.Items, 1) + require.Equal(t, 5, resp.Items[0].AlgorithmVersionID) + require.Equal(t, 7, *resp.Items[0].DatapackID) + require.NotEmpty(t, resp.Items[0].TaskID) + require.NotEmpty(t, resp.Items[0].TraceID) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/module/group/api_types.go b/src/module/group/api_types.go new file mode 100644 index 00000000..6e66e205 --- /dev/null +++ b/src/module/group/api_types.go @@ -0,0 +1,119 @@ +package groupmodule + +import ( + "fmt" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/utils" +) + +// GroupStreamEvent represents a lightweight event pushed to group-level Redis stream. +type GroupStreamEvent struct { + TraceID string `json:"trace_id"` + State consts.TraceState `json:"state"` + LastEvent consts.EventType `json:"last_event"` +} + +func (e *GroupStreamEvent) ToRedisStream() map[string]any { + return map[string]any{ + consts.RdbEventTraceID: e.TraceID, + consts.RdbEventTraceState: e.State, + consts.RdbEventTraceLastEvent: e.LastEvent, + } +} + +type GetGroupStreamReq struct { + LastID string `form:"last_id" binding:"omitempty"` +} + +func (req *GetGroupStreamReq) Validate() error { + if req.LastID == "" { + req.LastID = "0" + } + if req.LastID == "0" { + return nil + } + if strings.Count(req.LastID, "-") != 1 { + return fmt.Errorf("invalid last_id format: must be '0' or a valid stream ID (e.g., 1678886400000-0)") + } + return nil +} + +type GetGroupStatsReq struct { + GroupID string `form:"group_id" binding:"required"` +} + +func (req *GetGroupStatsReq) Validate() error { + if !utils.IsValidUUID(req.GroupID) { + return fmt.Errorf("invalid group_id: must be a valid UUID") + } + return nil +} + +type TraceStatsItem struct { + TraceID string `json:"trace_id"` + Type string `json:"type"` + State string `json:"state"` + StartTime time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time,omitempty"` + CurrentEvent string `json:"current_event"` + CurrentTask string `json:"current_task"` + TaskTypeDurations map[string]float64 `json:"task_type_durations,omitempty" swaggertype:"object"` +} + +func NewTraceStats(trace *model.Trace) *TraceStatsItem { + detail := &TraceStatsItem{ + TraceID: trace.ID, + Type: consts.GetTraceTypeName(trace.Type), + State: consts.GetTraceStateName(trace.State), + StartTime: trace.StartTime, + EndTime: trace.EndTime, + CurrentEvent: trace.LastEvent.String(), + } + + if len(trace.Tasks) > 0 { + detail.CurrentTask = trace.Tasks[0].ID + + taskTypeMap := make(map[string][]model.Task) + for _, task := range trace.Tasks { + if task.State == consts.TaskCompleted || task.State == consts.TaskError { + taskTypeName := consts.GetTaskTypeName(task.Type) + taskTypeMap[taskTypeName] = append(taskTypeMap[taskTypeName], task) + } + } + + detail.TaskTypeDurations = make(map[string]float64) + for taskTypeName, tasks := range taskTypeMap { + totalDuration := 0.0 + for _, task := range tasks { + totalDuration += task.UpdatedAt.Sub(task.CreatedAt).Seconds() + } + detail.TaskTypeDurations[taskTypeName] = totalDuration / float64(len(tasks)) + } + } + + return detail +} + +type GroupStats struct { + TotalTraces int `json:"total_traces"` + AvgDuration float64 `json:"avg_duration"` + MinDuration float64 `json:"min_duration"` + MaxDuration float64 `json:"max_duration"` + TraceStateMap map[string][]TraceStatsItem `json:"trace_state_map"` +} + +func NewDefaultGroupStats() *GroupStats { + return &GroupStats{ + TotalTraces: 0, + AvgDuration: 0.0, + MinDuration: 0.0, + MaxDuration: 0.0, + } +} + +type GroupTraceListResp = dto.ListResp[TraceStatsItem] diff --git a/src/handlers/v2/groups.go b/src/module/group/handler.go similarity index 76% rename from src/handlers/v2/groups.go rename to src/module/group/handler.go index 4d761d85..15f43b1c 100644 --- a/src/handlers/v2/groups.go +++ b/src/module/group/handler.go @@ -1,23 +1,31 @@ -package v2 +package groupmodule import ( - "aegis/consts" - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - "aegis/utils" + "aegis/httpx" "context" "errors" "fmt" "net/http" "time" + "aegis/consts" + "aegis/dto" + "aegis/utils" + "github.com/gin-contrib/sse" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // GetGroupStats handles retrieval of group trace statistics // // @Summary Get statistics for a group of traces @@ -27,27 +35,28 @@ import ( // @Produce json // @Security BearerAuth // @Param group_id path string true "Group ID (UUID)" -// @Success 200 {object} dto.GenericResponse[dto.GroupStats] "Group trace statistics" +// @Success 200 {object} dto.GenericResponse[GroupStats] "Group trace statistics" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/groups/{group_id}/stats [get] -// @x-api-type {"sdk":"true"} -func GetGroupStats(c *gin.Context) { - var req dto.GetGroupStatsReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format") +// @x-api-type {} +func (h *Handler) GetGroupStats(c *gin.Context) { + groupID := c.Param(consts.URLPathGroupID) + if !utils.IsValidUUID(groupID) { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid group ID") return } + req := GetGroupStatsReq{GroupID: groupID} if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - stats, err := producer.GetGroupStats(&req) - if handlers.HandleServiceError(c, err) { + stats, err := h.service.GetGroupStats(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -72,15 +81,15 @@ func GetGroupStats(c *gin.Context) { // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/groups/{group_id}/stream [get] // @x-request-type {"stream":"true"} -// @x-api-type {"sdk":"true"} -func GetGroupStream(c *gin.Context) { +// @x-api-type {} +func (h *Handler) GetGroupStream(c *gin.Context) { groupID := c.Param(consts.URLPathGroupID) if !utils.IsValidUUID(groupID) { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid group ID") return } - var req dto.GetGroupStreamReq + var req GetGroupStreamReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format") return @@ -104,16 +113,14 @@ func GetGroupStream(c *gin.Context) { "stream_key": streamKey, }) - processor, err := producer.NewGroupStreamProcessor(groupID) + processor, err := h.service.NewGroupStreamProcessor(groupID) if err != nil { logEntry.Errorf("Failed to initialize group stream processor: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize group stream: %v", err)) return } - // Read historical events (traces that already completed before SSE connection) - logEntry.Info("Reading historical group stream events") - historical, err := producer.ReadGroupStreamMessages(ctx, streamKey, req.LastID, 100, 0) + historical, err := h.service.ReadGroupStreamMessages(ctx, streamKey, req.LastID, 100, 0) if err != nil { logEntry.Errorf("Failed to read historical group stream events: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to read group event history") @@ -129,26 +136,20 @@ func GetGroupStream(c *gin.Context) { } if completed { - logEntry.Info("Group completed during historical events, closing stream") return } req.LastID = lastID } - // Switch to real-time monitoring - logEntry.Infof("Switching to real-time group event monitoring from ID: %s", req.LastID) for { select { case <-ctx.Done(): - logEntry.Info("Request context done") return - default: - newMessages, err := producer.ReadGroupStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) + newMessages, err := h.service.ReadGroupStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - logEntry.Infof("Context done while reading group stream: %v", err) return } @@ -169,16 +170,14 @@ func GetGroupStream(c *gin.Context) { req.LastID = lastID if completed { - logEntry.Info("All traces in group completed, closing stream") - time.Sleep(1 * time.Second) + time.Sleep(time.Second) return } } } } -// sendGroupSSEEvents processes group stream messages and sends them as SSE events -func sendGroupSSEEvents(c *gin.Context, processor *producer.GroupStreamProcessor, streams []redis.XStream) (string, bool, error) { +func sendGroupSSEEvents(c *gin.Context, processor *GroupStreamProcessor, streams []redis.XStream) (string, bool, error) { if len(streams) == 0 || len(streams[0].Messages) == 0 { return "", false, fmt.Errorf("no messages to process") } diff --git a/src/module/group/module.go b/src/module/group/module.go new file mode 100644 index 00000000..a9704595 --- /dev/null +++ b/src/module/group/module.go @@ -0,0 +1,9 @@ +package groupmodule + +import "go.uber.org/fx" + +var Module = fx.Module("group", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/group/repository.go b/src/module/group/repository.go new file mode 100644 index 00000000..101736fb --- /dev/null +++ b/src/module/group/repository.go @@ -0,0 +1,38 @@ +package groupmodule + +import ( + "aegis/consts" + "aegis/model" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) GetTracesByGroupID(groupID string) ([]model.Trace, error) { + var traces []model.Trace + if err := r.db.Model(&model.Trace{}). + Preload("Tasks"). + Where("group_id = ? AND status != ?", groupID, consts.CommonDeleted). + Order("start_time DESC"). + Find(&traces).Error; err != nil { + return nil, err + } + return traces, nil +} + +func (r *Repository) CountTracesByGroupID(groupID string) (int64, error) { + var count int64 + if err := r.db.Model(&model.Trace{}). + Where("group_id = ? AND status != ?", groupID, consts.CommonDeleted). + Count(&count).Error; err != nil { + return 0, err + } + return count, nil +} diff --git a/src/service/producer/group.go b/src/module/group/service.go similarity index 55% rename from src/service/producer/group.go rename to src/module/group/service.go index 28147f35..dad3cea5 100644 --- a/src/service/producer/group.go +++ b/src/module/group/service.go @@ -1,34 +1,38 @@ -package producer +package groupmodule import ( - "aegis/client" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" "context" "fmt" "slices" "strconv" "time" + "aegis/consts" + redisinfra "aegis/infra/redis" + "github.com/redis/go-redis/v9" ) -// GetGroupStats retrieves statistics for a group of traces -func GetGroupStats(req *dto.GetGroupStatsReq) (*dto.GroupStats, error) { +type Service struct { + repo *Repository + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, redis: redis} +} + +func (s *Service) GetGroupStats(_ context.Context, req *GetGroupStatsReq) (*GroupStats, error) { if req == nil { return nil, fmt.Errorf("request cannot be nil") } - // Query all traces belonging to this group - traces, err := repository.GetTracesByGroupID(database.DB, req.GroupID) + traces, err := s.repo.GetTracesByGroupID(req.GroupID) if err != nil { return nil, fmt.Errorf("failed to query traces for group %s: %w", req.GroupID, err) } - if len(traces) == 0 { - return dto.NewDefaultGroupStats(), nil + return NewDefaultGroupStats(), nil } durations := make([]float64, 0, len(traces)) @@ -41,17 +45,13 @@ func GetGroupStats(req *dto.GetGroupStatsReq) (*dto.GroupStats, error) { } } - traceStateMap := make(map[string][]dto.TraceStatsItem, 4) + traceStateMap := make(map[string][]TraceStatsItem, 4) for _, trace := range traces { stateName := consts.GetTraceStateName(trace.State) - if _, exists := traceStateMap[stateName]; !exists { - traceStateMap[stateName] = make([]dto.TraceStatsItem, 0) - } - - traceStateMap[stateName] = append(traceStateMap[stateName], *dto.NewTraceStats(&trace)) + traceStateMap[stateName] = append(traceStateMap[stateName], *NewTraceStats(&trace)) } - return &dto.GroupStats{ + return &GroupStats{ TotalTraces: len(traces), AvgDuration: totalDuration / float64(len(durations)), MinDuration: slices.Min(durations), @@ -60,23 +60,11 @@ func GetGroupStats(req *dto.GetGroupStatsReq) (*dto.GroupStats, error) { }, nil } -// ===================== Group Stream Service ===================== - -// GroupStreamProcessor tracks group-level trace completion for SSE streaming. -// It counts how many traces have reached terminal states (Completed/Failed) -// and determines when the group stream should be considered complete. -type GroupStreamProcessor struct { - totalTraces int - finishedCount int -} - -// NewGroupStreamProcessor creates a processor that tracks progress for a group -func NewGroupStreamProcessor(groupID string) (*GroupStreamProcessor, error) { - total, err := repository.CountTracesByGroupID(database.DB, groupID) +func (s *Service) NewGroupStreamProcessor(groupID string) (*GroupStreamProcessor, error) { + total, err := s.repo.CountTracesByGroupID(groupID) if err != nil { return nil, fmt.Errorf("failed to count traces for group %s: %w", groupID, err) } - if total == 0 { return nil, fmt.Errorf("the group %s does not exist", groupID) } @@ -87,8 +75,24 @@ func NewGroupStreamProcessor(groupID string) (*GroupStreamProcessor, error) { }, nil } -// ProcessGroupMessage processes a single group stream Redis message and returns a GroupStreamEvent -func (p *GroupStreamProcessor) ProcessGroupMessage(msg redis.XMessage) (*dto.GroupStreamEvent, error) { +func (s *Service) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if lastID == "" { + lastID = "0" + } + + messages, err := s.redis.XRead(ctx, []string{streamKey, lastID}, count, block) + if err != nil { + return nil, fmt.Errorf("failed to read group stream messages: %w", err) + } + return messages, nil +} + +type GroupStreamProcessor struct { + totalTraces int + finishedCount int +} + +func (p *GroupStreamProcessor) ProcessGroupMessage(msg redis.XMessage) (*GroupStreamEvent, error) { traceID, ok := msg.Values[consts.RdbEventTraceID].(string) if !ok || traceID == "" { return nil, fmt.Errorf("missing or invalid %s in group stream message", consts.RdbEventTraceID) @@ -102,37 +106,20 @@ func (p *GroupStreamProcessor) ProcessGroupMessage(msg redis.XMessage) (*dto.Gro if err != nil { return nil, fmt.Errorf("invalid trace state value %s in group stream message: %w", stateStr, err) } - state := consts.TraceState(stateInt) lastEventStr, ok := msg.Values[consts.RdbEventTraceLastEvent].(string) if !ok { return nil, fmt.Errorf("missing or invalid %s in group stream message", consts.RdbEventTraceLastEvent) } - lastEvent := consts.EventType(lastEventStr) p.finishedCount++ - - return &dto.GroupStreamEvent{ + return &GroupStreamEvent{ TraceID: traceID, - State: state, - LastEvent: lastEvent, + State: consts.TraceState(stateInt), + LastEvent: consts.EventType(lastEventStr), }, nil } -// IsCompleted returns true when all traces in the group have reached terminal states func (p *GroupStreamProcessor) IsCompleted() bool { return p.totalTraces > 0 && p.finishedCount >= p.totalTraces } - -// ReadGroupStreamMessages reads messages from the group-level Redis stream -func ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { - if lastID == "" { - lastID = "0" - } - - messages, err := client.RedisXRead(ctx, []string{streamKey, lastID}, count, block) - if err != nil { - return nil, fmt.Errorf("failed to read group stream messages: %w", err) - } - return messages, nil -} diff --git a/src/module/injection/api_types.go b/src/module/injection/api_types.go new file mode 100644 index 00000000..dafe9a64 --- /dev/null +++ b/src/module/injection/api_types.go @@ -0,0 +1,903 @@ +package injectionmodule + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/utils" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" +) + +// BatchDeleteInjectionReq represents the request to batch delete injections +type BatchDeleteInjectionReq struct { + IDs []int `json:"ids,omitempty"` // List of injection IDs for deletion + Labels []dto.LabelItem `json:"labels,omitempty"` // List of label keys to match for deletion +} + +func (req *BatchDeleteInjectionReq) Validate() error { + hasIDs := len(req.IDs) > 0 + hasLabels := len(req.Labels) > 0 + + criteriaCount := 0 + if hasIDs { + criteriaCount++ + } + if hasLabels { + criteriaCount++ + } + + if criteriaCount == 0 { + return fmt.Errorf("must provide one of: ids, labels, or tags") + } + if criteriaCount > 1 { + return fmt.Errorf("can only specify one deletion criteria (ids, labels, or tags)") + } + + if hasIDs { + for i, id := range req.IDs { + if id <= 0 { + return fmt.Errorf("invalid id at index %d: %d", i, id) + } + } + } + + if hasLabels { + for i, label := range req.Labels { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d", i) + } + } + } + + return nil +} + +// CloneInjectionReq represents the request to clone an injection +type CloneInjectionReq struct { + Name string `json:"name" binding:"required"` // New name for cloned injection + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` // Optional labels for cloned injection +} + +// InjectionLogsResp represents the response for injection logs +type InjectionLogsResp struct { + InjectionID int `json:"injection_id"` + TaskID string `json:"task_id,omitempty"` + Logs []string `json:"logs"` +} + +// TriggerDatasetBuildItemResponse represents the response for a single injection in batch trigger +type TriggerDatasetBuildItemResponse struct { + TaskID string `json:"task_id"` + TraceID string `json:"trace_id"` + InjectionName string `json:"injection_name"` + Benchmark string `json:"benchmark"` + Namespace string `json:"namespace"` + Message string `json:"message"` +} + +// TriggerDatasetBuildError represents an error during dataset build trigger +type TriggerDatasetBuildError struct { + InjectionName string `json:"injection_name"` + Error string `json:"error"` +} + +// TriggerFailedDatapackRebuildRequest represents the request for triggering rebuild of failed datapacks +type TriggerFailedDatapackRebuildRequest struct { + Namespace string `json:"namespace,omitempty"` // Optional namespace, defaults to "ts" + Days *int `json:"days,omitempty"` // Number of days to look back, defaults to 3 +} + +// TriggerFailedDatapackRebuildResponse represents the response for triggering rebuild of failed datapacks +type TriggerFailedDatapackRebuildResponse struct { + SuccessCount int `json:"success_count"` + SuccessItems []TriggerDatasetBuildItemResponse `json:"success_items"` + FailedCount int `json:"failed_count"` + FailedItems []TriggerDatasetBuildError `json:"failed_items,omitempty"` + TotalFound int `json:"total_found"` // Total number of failed datapacks found + DaysSearched int `json:"days_searched"` // Number of days searched + SearchCutoff string `json:"search_cutoff"` // ISO timestamp of search cutoff + Message string `json:"message"` +} + +// TriggerFailedDatapackRebuildProgressEvent represents a single progress event for SSE +type TriggerFailedDatapackRebuildProgressEvent struct { + Type string `json:"type"` // "start", "progress", "item_success", "item_error", "complete", "error" + Message string `json:"message"` // Human readable message + TotalFound int `json:"total_found"` // Total number of failed datapacks found + CurrentIndex int `json:"current_index"` // Current processing index (0-based) + Progress float64 `json:"progress"` // Progress percentage (0-100) + SuccessCount int `json:"success_count"` // Number of successful triggers so far + FailedCount int `json:"failed_count"` // Number of failed triggers so far + CurrentItem *TriggerDatasetBuildItemResponse `json:"current_item,omitempty"` // Current successful item + CurrentError *TriggerDatasetBuildError `json:"current_error,omitempty"` // Current error item + EstimatedTime *time.Duration `json:"estimated_time,omitempty"` // Estimated remaining time + FinalResponse *TriggerFailedDatapackRebuildResponse `json:"final_response,omitempty"` // Final response (only for "complete" type) +} + +type InjectionFieldMappingResp struct { + StatusMap map[int]string `json:"status" swaggertype:"object"` + FaultTypeMap map[chaos.ChaosType]string `json:"fault_type" swaggertype:"object"` + FaultResourceMap map[string]chaos.ChaosResourceMapping `json:"fault_resource" swaggertype:"object"` +} + +type ListInjectionFilters struct { + FaultType *chaos.ChaosType + Category *chaos.SystemType + Benchmark string + State *consts.DatapackState + Status *consts.StatusType + LabelConditions []map[string]string +} + +// ListInjectionReq represents the request to list injections with various filters +type ListInjectionReq struct { + dto.PaginationReq + Type *chaos.ChaosType `form:"fault_type" binding:"omitempty"` + Category *chaos.SystemType `form:"category" binding:"omitempty"` + Benchmark string `form:"benchmark" binding:"omitempty"` + State *consts.DatapackState `form:"state" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` + Labels []string `form:"labels" binding:"omitempty"` +} + +func (req *ListInjectionReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if err := validateChaosType(req.Type); err != nil { + return err + } + // Only validate category if it's provided (not nil) + if req.Category != nil && !req.Category.IsValid() { + return fmt.Errorf("invalid category: %s", *req.Category) + } + if err := validateDatapackState(req.State); err != nil { + return err + } + if err := validateInjectionStatus(req.Status, false); err != nil { + return err + } + if err := validateInjectionLabels(req.Labels); err != nil { + return err + } + + return nil +} + +func (req *ListInjectionReq) ToFilterOptions() *ListInjectionFilters { + labelConditions := make([]map[string]string, 0, len(req.Labels)) + for _, item := range req.Labels { + parts := strings.SplitN(item, ":", 2) + labelConditions = append(labelConditions, map[string]string{ + "key": parts[0], + "value": parts[1], + }) + } + + return &ListInjectionFilters{ + FaultType: req.Type, + Benchmark: req.Benchmark, + State: req.State, + Status: req.Status, + LabelConditions: labelConditions, + } +} + +// SearchInjectionReq represents the request to search fault injections with advanced filters +type SearchInjectionReq struct { + dto.AdvancedSearchReq[consts.InjectionField] + TaskIDs []string `json:"task_ids" binding:"omitempty"` + Names []string `json:"names" binding:"omitempty"` + NamePattern string `json:"name_pattern" binding:"omitempty"` + FaultTypes []chaos.ChaosType `json:"fault_types" binding:"omitempty"` + Categories []chaos.SystemType `json:"categories" binding:"omitempty"` + States []consts.DatapackState `json:"states" binding:"omitempty"` + Benchmarks []string `json:"benchmarks" binding:"omitempty"` + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` // Custom labels to filter by + StartTime *dto.DateRange `json:"start_time" binding:"omitempty"` + EndTime *dto.DateRange `json:"end_time" binding:"omitempty"` + IncludeLabels bool `json:"include_labels" binding:"omitempty"` // Whether to include labels in the response + IncludeTask bool `json:"include_task" binding:"omitempty"` // Whether to include task details in the response +} + +func (req *SearchInjectionReq) Validate() error { + if err := req.AdvancedSearchReq.Validate(); err != nil { + return err + } + + for i, id := range req.TaskIDs { + if strings.TrimSpace(id) == "" { + return fmt.Errorf("empty task ID at index %d", i) + } + if !utils.IsValidUUID(id) { + return fmt.Errorf("invalid task ID format at index %d: %s", i, id) + } + } + + if len(req.Names) > 0 && req.NamePattern != "" { + return fmt.Errorf("can only specify one of names or name_pattern for filtering") + } + + for i, name := range req.Names { + if strings.TrimSpace(name) == "" { + return fmt.Errorf("empty injection name at index %d", i) + } + } + + if err := validateInjectionLabelItems(req.Labels); err != nil { + return err + } + + if req.StartTime != nil { + if err := req.StartTime.Validate(); err != nil { + return fmt.Errorf("invalid start_time: %w", err) + } + } + if req.EndTime != nil { + if err := req.EndTime.Validate(); err != nil { + return fmt.Errorf("invalid end_time: %w", err) + } + } + + for i, sortField := range req.Sort { + if _, valid := consts.InjectionAllowedFields[sortField.Field]; !valid { + return fmt.Errorf("invalid sort_by field at index %d: %s", i, sortField.Field) + } + } + + for i, field := range req.GroupBy { + if _, valid := consts.InjectionAllowedFields[field]; !valid { + return fmt.Errorf("invalid group_by field at index %d: %s", i, field) + } + } + + return nil +} + +func (req *SearchInjectionReq) ConvertToSearchReq() *dto.SearchReq[consts.InjectionField] { + sr := req.ConvertAdvancedToSearch() + + if len(req.TaskIDs) > 0 { + sr.AddFilter("task_id", dto.OpIn, req.TaskIDs) + } + if len(req.Names) > 0 { + sr.AddFilter("name", dto.OpIn, req.Names) + } + if req.NamePattern != "" { + sr.AddFilter("name", dto.OpLike, req.NamePattern) + } + if len(req.Benchmarks) > 0 { + sr.AddFilter("benchmark", dto.OpIn, req.Benchmarks) + } + + if len(req.FaultTypes) > 0 { + faultTypeValues := make([]string, len(req.FaultTypes)) + for i, ft := range req.FaultTypes { + faultTypeValues[i] = fmt.Sprintf("%d", ft) + } + sr.AddFilter("fault_type", dto.OpIn, faultTypeValues) + } + if len(req.Categories) > 0 { + categoryValues := make([]string, len(req.Categories)) + for i, ct := range req.Categories { + categoryValues[i] = ct.String() + } + sr.AddFilter("category", dto.OpIn, categoryValues) + } + + if len(req.States) > 0 { + stateValues := make([]string, len(req.States)) + for i, st := range req.States { + stateValues[i] = fmt.Sprintf("%d", st) + } + sr.AddFilter("state", dto.OpIn, stateValues) + } + + if req.StartTime != nil { + if req.StartTime.From != nil && req.StartTime.To != nil { + sr.AddFilter("created_at", dto.OpDateBetween, []any{req.StartTime.From, req.StartTime.To}) + } else if req.StartTime.From != nil { + sr.AddFilter("created_at", dto.OpDateAfter, req.StartTime.From) + } else if req.StartTime.To != nil { + sr.AddFilter("created_at", dto.OpDateBefore, req.StartTime.To) + } + } + if req.EndTime != nil { + if req.EndTime.From != nil && req.EndTime.To != nil { + sr.AddFilter("created_at", dto.OpDateBetween, []any{req.EndTime.From, req.EndTime.To}) + } else if req.EndTime.From != nil { + sr.AddFilter("created_at", dto.OpDateAfter, req.EndTime.From) + } else if req.EndTime.To != nil { + sr.AddFilter("created_at", dto.OpDateBefore, req.EndTime.To) + } + } + + if req.IncludeLabels { + sr.AddInclude("Labels") + } + if req.IncludeTask { + sr.AddInclude("Task") + } + + return sr +} + +// SubmitInjectionReq represents a request to submit fault injection tasks with parallel fault support +// Each element in Specs represents a batch of faults to be injected in parallel within a single experiment +type SubmitInjectionReq struct { + ProjectName string `json:"project_name" binding:"omitempty"` // Project name + Pedestal *dto.ContainerSpec `json:"pedestal" binding:"required"` // Pedestal (workload) configuration + Benchmark *dto.ContainerSpec `json:"benchmark" binding:"required"` // Benchmark (detector) configuration + Interval int `json:"interval" binding:"required,min=1"` // Total experiment interval in minutes + PreDuration int `json:"pre_duration" binding:"required,min=1"` // Normal data collection duration before fault injection + Specs [][]chaos.Node `json:"specs" binding:"required"` // Fault injection specs - 2D array where each sub-array is a batch of parallel faults + Algorithms []dto.ContainerSpec `json:"algorithms" binding:"omitempty"` // RCA algorithms to execute (optional) + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` // Labels to attach to the injection +} + +func (req *SubmitInjectionReq) Validate() error { + if req.Pedestal == nil { + return fmt.Errorf("pedestal must not be nil") + } else { + if err := req.Pedestal.Validate(); err != nil { + return fmt.Errorf("invalid pedestal: %w", err) + } + } + + if req.Benchmark == nil { + return fmt.Errorf("benchmark must not be nil") + } + if req.Interval <= req.PreDuration { + return fmt.Errorf("interval must be greater than pre_duration") + } + if len(req.Specs) == 0 { + return fmt.Errorf("specs must not be empty") + } + + if req.Algorithms != nil { + for idx, algorithm := range req.Algorithms { + if err := algorithm.Validate(); err != nil { + return fmt.Errorf("invalid algorithm at index %d: %w", idx, err) + } + if algorithm.Name == config.GetDetectorName() { + return fmt.Errorf("algorithm name %s is reserved and cannot be used", config.GetDetectorName()) + } + } + } + + if req.Labels == nil { + req.Labels = make([]dto.LabelItem, 0) + } + + return nil +} + +type UpdateGroundtruthReq struct { + Groundtruths []model.Groundtruth `json:"ground_truths" binding:"required"` +} + +func (req *UpdateGroundtruthReq) Validate() error { + if len(req.Groundtruths) == 0 { + return fmt.Errorf("at least one ground truth entry is required") + } + return nil +} + +type InjectionResp struct { + ID int `json:"id"` + Name string `json:"name"` + Source string `json:"source"` + FaultType string `json:"fault_type"` + Category string `json:"category"` + DisplayConfig map[string]any `json:"display_config,omitempty" swaggertype:"object"` + PreDuration int `json:"pre_duration"` + StartTime *time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + State consts.DatapackState `json:"state" swaggertype:"string"` + Status string `json:"status"` + GroundtruthSource string `json:"groundtruth_source"` + BenchmarkID *int `json:"benchmark_id"` + BenchmarkName string `json:"benchmark_name"` + PedestalID *int `json:"pedestal_id"` + PedestalName string `json:"pedestal_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +func NewInjectionResp(injection *model.FaultInjection) *InjectionResp { + resp := &InjectionResp{ + ID: injection.ID, + Name: injection.Name, + Source: string(injection.Source), + Category: injection.Category.String(), + PreDuration: injection.PreDuration, + StartTime: injection.StartTime, + EndTime: injection.EndTime, + State: injection.State, + Status: consts.GetStatusTypeName(injection.Status), + GroundtruthSource: injection.GroundtruthSource, + BenchmarkID: injection.BenchmarkID, + PedestalID: injection.PedestalID, + CreatedAt: injection.CreatedAt, + UpdatedAt: injection.UpdatedAt, + } + + if injection.FaultType == consts.Hybrid { + resp.FaultType = "hybrid" + } else { + resp.FaultType = chaos.ChaosTypeMap[injection.FaultType] + } + + if injection.DisplayConfig != nil { + var displayConfigData map[string]any + _ = json.Unmarshal([]byte(*injection.DisplayConfig), &displayConfigData) + resp.DisplayConfig = displayConfigData + } + + if injection.Benchmark != nil { + if injection.Benchmark.Container != nil { + resp.BenchmarkName = injection.Benchmark.Container.Name + } + } + if injection.Pedestal != nil { + if injection.Pedestal.Container != nil { + resp.PedestalName = injection.Pedestal.Container.Name + } + } + + // Get labels from associated Task instead of directly from injection + if len(injection.Labels) > 0 { + resp.Labels = make([]dto.LabelItem, 0, len(injection.Labels)) + for _, l := range injection.Labels { + resp.Labels = append(resp.Labels, dto.LabelItem{ + Key: l.Key, + Value: l.Value, + IsSystem: l.IsSystem, + }) + } + } + return resp +} + +type InjectionDetailResp struct { + InjectionResp + + TaskID string `json:"task_id"` + TraceID string `json:"trace_id"` + Source string `json:"source"` + + Description string `json:"description,omitempty"` + EngineConfig []map[string]any `json:"engine_config" swaggertype:"array,object"` + Groundtruths []chaos.Groundtruth `json:"ground_truth,omitempty"` + GroundtruthSource string `json:"groundtruth_source"` +} + +func NewInjectionDetailResp(injection *model.FaultInjection) *InjectionDetailResp { + injectionResp := NewInjectionResp(injection) + resp := &InjectionDetailResp{ + InjectionResp: *injectionResp, + Source: string(injection.Source), + Description: injection.Description, + GroundtruthSource: injection.GroundtruthSource, + } + + if injection.Task != nil { + resp.TaskID = injection.Task.ID + if injection.Task.Trace != nil { + resp.TraceID = injection.Task.Trace.ID + } + } + + if injection.EngineConfig != "" { + var engineConfigData []map[string]any + _ = json.Unmarshal([]byte(injection.EngineConfig), &engineConfigData) + resp.EngineConfig = engineConfigData + } + + resp.Groundtruths = make([]chaos.Groundtruth, 0, len(injection.Groundtruths)) + if len(injection.Groundtruths) > 0 { + for _, gt := range injection.Groundtruths { + resp.Groundtruths = append(resp.Groundtruths, *gt.ConvertToChaosGroundtruth()) + } + } + + return resp +} + +// InjectionMetadataResp represents the metadata response for injections +type InjectionMetadataResp struct { + Config *chaos.Node `json:"config"` + FaultTypeMap map[chaos.ChaosType]string `json:"fault_type_map"` + FaultResourceMap map[string]chaos.ChaosResourceMapping `json:"fault_resource_map"` + SystemResource chaos.SystemResource `json:"ns_resources"` +} + +type SubmitInjectionItem struct { + Index int `json:"index"` // Index of the batch this injection belongs to + TraceID string `json:"trace_id"` + TaskID string `json:"task_id"` +} + +// Structured warnings about duplications and conflicts +type InjectionWarnings struct { + DuplicateServicesInBatch []string `json:"duplicate_services_in_batch,omitempty"` // Warnings about duplicate service injections within the same batch + DuplicateBatchesInRequest []int `json:"duplicate_batches_in_request,omitempty"` // Batch indices that have duplicate configurations within this request + BatchesExistInDatabase []int `json:"batches_exist_in_database,omitempty"` // Batch indices that already exist in database +} + +type SubmitInjectionResp struct { + GroupID string `json:"group_id"` + Items []SubmitInjectionItem `json:"items"` + OriginalCount int `json:"original_count"` + Warnings *InjectionWarnings `json:"warnings,omitempty"` +} + +type SubmitDatapackBuildingReq struct { + ProjectName string `json:"project_name" binding:"omitempty"` + Specs []BuildingSpec `json:"specs" binding:"required"` + Labels []dto.LabelItem `json:"labels" binding:"omitempty"` +} + +func (req *SubmitDatapackBuildingReq) Validate() error { + if len(req.Specs) == 0 { + return fmt.Errorf("at least one datapack spec is required") + } + + for _, spec := range req.Specs { + if err := spec.Validate(); err != nil { + return fmt.Errorf("invalid datapack spec: %w", err) + } + } + + return validateInjectionLabelItems(req.Labels) +} + +// ManageInjectionLabelReq Represents the request to manage labels for an injection +type ManageInjectionLabelReq struct { + AddLabels []dto.LabelItem `json:"add_labels"` // List of labels to add + RemoveLabels []string `json:"remove_labels"` // List of label keys to remove +} + +func (req *ManageInjectionLabelReq) Validate() error { + if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { + return fmt.Errorf("at least one of add_labels or remove_labels must be provided") + } + + if err := validateInjectionLabelItems(req.AddLabels); err != nil { + return err + } + + for i, key := range req.RemoveLabels { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("empty label key at index %d in remove_labels", i) + } + } + + return nil +} + +// InjectionLabelOperation represents label operations for a single injection +type InjectionLabelOperation struct { + InjectionID int `json:"injection_id" binding:"required"` // Injection ID to manage + AddLabels []dto.LabelItem `json:"add_labels,omitempty"` // Labels to add to this injection + RemoveLabels []dto.LabelItem `json:"remove_labels,omitempty"` // Labels to remove from this injection +} + +// BatchManageInjectionLabelReq represents the request to batch manage injection labels +// Each injection can have its own set of label operations +type BatchManageInjectionLabelReq struct { + Items []InjectionLabelOperation `json:"items" binding:"required,min=1,dive"` // List of label operations per injection +} + +func (req *BatchManageInjectionLabelReq) Validate() error { + if len(req.Items) == 0 { + return fmt.Errorf("items list cannot be empty") + } + + seenIDs := make(map[int]struct{}, len(req.Items)) + for i, item := range req.Items { + if _, exists := seenIDs[item.InjectionID]; exists { + return fmt.Errorf("duplicate injection_id at index %d: %d", i, item.InjectionID) + } + seenIDs[item.InjectionID] = struct{}{} + + if item.InjectionID <= 0 { + return fmt.Errorf("invalid injection_id at index %d: %d", i, item.InjectionID) + } + + if len(item.AddLabels) == 0 && len(item.RemoveLabels) == 0 { + return fmt.Errorf("at least one of add_labels or remove_labels must be provided for injection_id %d at index %d", item.InjectionID, i) + } + + if err := validateInjectionLabelItems(item.AddLabels); err != nil { + return fmt.Errorf("invalid add_labels for injection_id %d at index %d: %w", item.InjectionID, i, err) + } + if err := validateInjectionLabelItems(item.RemoveLabels); err != nil { + return fmt.Errorf("invalid remove_labels for injection_id %d at index %d: %w", item.InjectionID, i, err) + } + } + + return nil +} + +// BatchManageInjectionLabelResp represents the response for batch injection label management +type BatchManageInjectionLabelResp struct { + FailedCount int `json:"failed_count"` + FailedItems []string `json:"failed_items"` + SuccessCount int `json:"success_count"` + SuccessItems []InjectionResp `json:"success_items"` +} + +// analysis +type ListInjectionNoIssuesReq struct { + Labels []string `form:"labels" binding:"omitempty"` + TimeRangeQuery +} + +func (req *ListInjectionNoIssuesReq) Validate() error { + if err := validateInjectionLabels(req.Labels); err != nil { + return err + } + return req.TimeRangeQuery.Validate() +} + +type ListInjectionWithIssuesReq struct { + Labels []string `form:"labels" binding:"omitempty"` + TimeRangeQuery +} + +func (req *ListInjectionWithIssuesReq) Validate() error { + if err := validateInjectionLabels(req.Labels); err != nil { + return err + } + return req.TimeRangeQuery.Validate() +} + +type InjectionNoIssuesResp struct { + ID int `json:"datapack_id"` + Name string `json:"datapack_name"` + FaultType string `json:"fault_type"` + Category string `json:"category"` + EngineConfig *chaos.Node `json:"engine_config"` +} + +func NewInjectionNoIssuesResp(entity model.FaultInjectionNoIssues) (*InjectionNoIssuesResp, error) { + var engineConfig *chaos.Node + err := json.Unmarshal([]byte(entity.EngineConfig), engineConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal engine config: %w", err) + } + + return &InjectionNoIssuesResp{ + ID: entity.ID, + Name: entity.Name, + FaultType: chaos.ChaosTypeMap[entity.FaultType], + Category: entity.Category.String(), + EngineConfig: engineConfig, + }, nil +} + +// InjectionWithIssuesResp represents the response for fault injections with issues +type InjectionWithIssuesResp struct { + ID int `json:"datapack_id"` + Name string `json:"datapack_name"` + FaultType string `json:"fault_type"` + Category string `json:"category"` + EngineConfig chaos.Node `json:"engine_config"` + Issues string `json:"issues"` + AbnormalAvgDuration float64 `json:"abnormal_avg_duration"` + NormalAvgDuration float64 `json:"normal_avg_duration"` + AbnormalSuccRate float64 `json:"abnormal_succ_rate"` + NormalSuccRate float64 `json:"normal_succ_rate"` + AbnormalP99 float64 `json:"abnormal_p99"` + NormalP99 float64 `json:"normal_p99"` +} + +func NewInjectionWithIssuesResp(entity model.FaultInjectionWithIssues) (*InjectionWithIssuesResp, error) { + var engineConfig chaos.Node + err := json.Unmarshal([]byte(entity.EngineConfig), &engineConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal engine config: %w", err) + } + return &InjectionWithIssuesResp{ + ID: entity.ID, + Name: entity.Name, + FaultType: chaos.ChaosTypeMap[entity.FaultType], + Category: entity.Category.String(), + EngineConfig: engineConfig, + Issues: entity.Issues, + AbnormalAvgDuration: entity.AbnormalAvgDuration, + NormalAvgDuration: entity.NormalAvgDuration, + AbnormalSuccRate: entity.AbnormalSuccRate, + NormalSuccRate: entity.NormalSuccRate, + AbnormalP99: entity.AbnormalP99, + NormalP99: entity.NormalP99, + }, nil +} + +// datapack +type BuildingSpec struct { + Benchmark dto.ContainerSpec `json:"benchmark" binding:"required"` + Datapack *string `json:"datapack" binding:"omitempty"` + Dataset *dto.DatasetRef `json:"dataset" binding:"omitempty"` + PreDuration *int `json:"pre_duration" binding:"omitempty"` +} + +func (spec *BuildingSpec) Validate() error { + hasDatapack := spec.Datapack != nil + hasDataset := spec.Dataset != nil + + if !hasDatapack && !hasDataset { + return fmt.Errorf("either datapack or dataset must be specified") + } + if hasDatapack && hasDataset { + return fmt.Errorf("cannot specify both datapack and dataset") + } + + if hasDatapack { + if *spec.Datapack == "" { + return fmt.Errorf("datapack name cannot be empty") + } + } + + if hasDataset { + if err := spec.Dataset.Validate(); err != nil { + return fmt.Errorf("invalid dataset: %w", err) + } + } + + if spec.PreDuration != nil && *spec.PreDuration <= 0 { + return fmt.Errorf("pre_duration must be greater than 0") + } + + return nil +} + +type SubmitBuildingItem struct { + Index int `json:"index"` + TraceID string `json:"trace_id"` + TaskID string `json:"task_id"` +} + +// SubmitDatapackResp represents the response for submitting datapack building tasks +type SubmitDatapackBuildingResp struct { + GroupID string `json:"group_id"` + Items []SubmitBuildingItem `json:"items"` +} + +// DatapackFileItem represents a file or directory in the datapack +type DatapackFileItem struct { + Name string `json:"name"` // File or directory name + Path string `json:"path"` // Relative path from datapack root + Size string `json:"size"` // File size in KB/MB format or directory info + ModTime *time.Time `json:"modified_at,omitempty"` // Last modification time (only for files) + Children []DatapackFileItem `json:"children,omitempty"` // Child items (only for directories) +} + +// DatapackFilesResp represents the response for listing datapack files +type DatapackFilesResp struct { + Files []DatapackFileItem `json:"files"` + FileCount int `json:"file_count"` // Number of files (excluding directories) + DirCount int `json:"dir_count"` // Number of directories +} + +// validateChaosType checks if the provided chaos type is valid +func validateChaosType(faultType *chaos.ChaosType) error { + if faultType != nil { + if _, exists := chaos.ChaosTypeMap[*faultType]; !exists { + return fmt.Errorf("invalid fault type: %d", faultType) + } + } + return nil +} + +// validateDatapackState checks if the provided datapack state is valid +func validateDatapackState(state *consts.DatapackState) error { + if state != nil { + if *state < 0 { + return fmt.Errorf("state must be a non-negative integer") + } + if _, exists := consts.ValidDatapackStates[consts.DatapackState(*state)]; !exists { + return fmt.Errorf("invalid state: %d", *state) + } + } + return nil +} + +func validateInjectionStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} + +func validateInjectionLabels(labels []string) error { + for i, label := range labels { + parts := strings.SplitN(label, ":", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid label format at index %d: %q, expected key:value", i, label) + } + if strings.TrimSpace(parts[0]) == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if strings.TrimSpace(parts[1]) == "" { + return fmt.Errorf("empty label value at index %d", i) + } + } + return nil +} + +func validateInjectionLabelItems(items []dto.LabelItem) error { + for i, label := range items { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d", i) + } + } + return nil +} + +// UploadDatapackReq represents the request to upload a manual datapack +type UploadDatapackReq struct { + Name string `form:"name" binding:"required"` + Description string `form:"description"` + Category string `form:"category"` + Labels string `form:"labels"` // JSON-encoded []dto.LabelItem + Groundtruths string `form:"ground_truths"` // JSON-encoded []Groundtruth +} + +func (req *UploadDatapackReq) Validate() error { + if strings.TrimSpace(req.Name) == "" { + return fmt.Errorf("name is required") + } + return nil +} + +func (req *UploadDatapackReq) ParseLabels() ([]dto.LabelItem, error) { + if req.Labels == "" { + return nil, nil + } + var labels []dto.LabelItem + if err := json.Unmarshal([]byte(req.Labels), &labels); err != nil { + return nil, fmt.Errorf("invalid labels JSON: %w", err) + } + return labels, nil +} + +func (req *UploadDatapackReq) ParseGroundtruths() ([]model.Groundtruth, error) { + if req.Groundtruths == "" { + return nil, nil + } + var gts []model.Groundtruth + if err := json.Unmarshal([]byte(req.Groundtruths), >s); err != nil { + return nil, fmt.Errorf("invalid ground_truths JSON: %w", err) + } + return gts, nil +} + +// UploadDatapackResp represents the response for uploading a manual datapack +type UploadDatapackResp struct { + ID int `json:"id"` + Name string `json:"name"` +} diff --git a/src/module/injection/archive.go b/src/module/injection/archive.go new file mode 100644 index 00000000..6dc5dfc2 --- /dev/null +++ b/src/module/injection/archive.go @@ -0,0 +1,41 @@ +package injectionmodule + +import ( + "archive/zip" + "fmt" + "io/fs" + "path/filepath" + + "aegis/consts" + "aegis/utils" +) + +func packageDatapackDirectoryToZip(zipWriter *zip.Writer, workDir string, excludeRules []utils.ExculdeRule) error { + err := filepath.WalkDir(workDir, func(path string, dir fs.DirEntry, err error) error { + if err != nil || dir.IsDir() { + return err + } + + relPath, _ := filepath.Rel(workDir, path) + fullRelPath := filepath.Join(consts.DownloadFilename, filepath.Base(workDir), relPath) + fileName := filepath.Base(path) + + for _, rule := range excludeRules { + if utils.MatchFile(fileName, rule) { + return nil + } + } + + fileInfo, err := dir.Info() + if err != nil { + return err + } + + return utils.AddToZip(zipWriter, fileInfo, path, filepath.ToSlash(fullRelPath)) + }) + if err != nil { + return fmt.Errorf("failed to package datapack directory %s: %w", filepath.Base(workDir), err) + } + + return nil +} diff --git a/src/module/injection/datapack_store.go b/src/module/injection/datapack_store.go new file mode 100644 index 00000000..b2c8503e --- /dev/null +++ b/src/module/injection/datapack_store.go @@ -0,0 +1,353 @@ +package injectionmodule + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "aegis/config" + "aegis/consts" + "aegis/model" + "aegis/utils" + + "github.com/sirupsen/logrus" +) + +type DatapackStore struct { + basePath string +} + +func NewDatapackStore() *DatapackStore { + return &DatapackStore{basePath: config.GetString("jfs.dataset_path")} +} + +func (s *DatapackStore) RootDir(datapackName string) string { + return filepath.Join(s.basePath, datapackName) +} + +func (s *DatapackStore) Package(zipWriter *zip.Writer, datapackName string, excludeRules []utils.ExculdeRule) error { + workDir := s.RootDir(datapackName) + if !utils.IsAllowedPath(workDir) { + return fmt.Errorf("invalid path access to %s", workDir) + } + return packageDatapackDirectoryToZip(zipWriter, workDir, excludeRules) +} + +func (s *DatapackStore) BuildFileTree(datapackName, baseURL string, datapackID int) (*DatapackFilesResp, error) { + workDir := s.RootDir(datapackName) + if !utils.IsAllowedPath(workDir) { + return nil, fmt.Errorf("invalid path access to %s", workDir) + } + if _, err := os.Stat(workDir); os.IsNotExist(err) { + return nil, fmt.Errorf("datapack directory not found for datapack id %d", datapackID) + } + + resp := &DatapackFilesResp{ + Files: []DatapackFileItem{}, + FileCount: 0, + DirCount: 0, + } + + rootItems, err := buildFileTree(workDir, "", baseURL, datapackID, resp) + if err != nil { + return nil, err + } + resp.Files = rootItems + return resp, nil +} + +func (s *DatapackStore) OpenFile(datapackName, filePath string) (string, string, int64, io.ReadSeekCloser, error) { + fullPath, err := s.resolveFilePath(datapackName, filePath) + if err != nil { + return "", "", 0, nil, err + } + + file, err := os.Open(fullPath) + if err != nil { + return "", "", 0, nil, fmt.Errorf("failed to open file: %w", err) + } + + stat, err := file.Stat() + if err != nil { + _ = file.Close() + return "", "", 0, nil, fmt.Errorf("failed to stat file: %w", err) + } + + fileName := filepath.Base(fullPath) + contentType := "application/octet-stream" + switch filepath.Ext(fileName) { + case ".json": + contentType = "application/json" + case ".yaml", ".yml": + contentType = "application/x-yaml" + case ".txt", ".log": + contentType = "text/plain" + case ".csv": + contentType = "text/csv" + case ".xml": + contentType = "application/xml" + case ".html", ".htm": + contentType = "text/html" + case ".pdf": + contentType = "application/pdf" + case ".zip": + contentType = "application/zip" + case ".tar", ".gz", ".tgz": + contentType = "application/x-tar" + } + + return fileName, contentType, stat.Size(), file, nil +} + +func (s *DatapackStore) ResolveFilePath(datapackName, filePath string) (string, error) { + return s.resolveFilePath(datapackName, filePath) +} + +func (s *DatapackStore) resolveFilePath(datapackName, filePath string) (string, error) { + workDir := s.RootDir(datapackName) + if !utils.IsAllowedPath(workDir) { + return "", fmt.Errorf("invalid path access to %s", workDir) + } + + cleanPath := filepath.Clean(filePath) + fullPath := filepath.Join(workDir, cleanPath) + if !strings.HasPrefix(fullPath, workDir) { + return "", fmt.Errorf("invalid file path: path traversal detected") + } + if !utils.IsAllowedPath(fullPath) { + return "", fmt.Errorf("invalid file path access") + } + + fileInfo, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("%w: file not found: %s", consts.ErrNotFound, cleanPath) + } + return "", fmt.Errorf("failed to stat file: %w", err) + } + if fileInfo.IsDir() { + return "", fmt.Errorf("path is a directory, not a file: %s", cleanPath) + } + + return fullPath, nil +} + +func buildFileTree(workDir, relPath string, baseURL string, datapackID int, resp *DatapackFilesResp) ([]DatapackFileItem, error) { + _ = baseURL + _ = datapackID + currentPath := filepath.Join(workDir, relPath) + entries, err := os.ReadDir(currentPath) + if err != nil { + return nil, err + } + + var items []DatapackFileItem + for _, entry := range entries { + itemRelPath := filepath.Join(relPath, entry.Name()) + fileInfo, err := entry.Info() + if err != nil { + return nil, err + } + + item := DatapackFileItem{ + Name: entry.Name(), + Path: filepath.ToSlash(itemRelPath), + } + + if entry.IsDir() { + children, err := buildFileTree(workDir, itemRelPath, baseURL, datapackID, resp) + if err != nil { + return nil, err + } + item.Children = children + + subFolderCount := 0 + fileCount := 0 + for _, child := range children { + if len(child.Children) > 0 { + subFolderCount++ + } else { + fileCount++ + } + } + item.Size = fmt.Sprintf("%d subfolders, %d files", subFolderCount, fileCount) + resp.DirCount++ + } else { + fileSize := fileInfo.Size() + item.Size = formatFileSize(fileSize) + modTime := fileInfo.ModTime() + item.ModTime = &modTime + resp.FileCount++ + } + + items = append(items, item) + } + + return items, nil +} + +func formatFileSize(bytes int64) string { + const ( + kb = 1024 + mb = 1024 * 1024 + ) + + if bytes < mb { + return fmt.Sprintf("%.1fKB", float64(bytes)/float64(kb)) + } + return fmt.Sprintf("%.1fMB", float64(bytes)/float64(mb)) +} + +func (s *DatapackStore) CreateUploadTempFile() (*os.File, error) { + return os.CreateTemp("", "datapack-upload-*.zip") +} + +func (s *DatapackStore) ValidateArchive(zipPath string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("failed to open zip archive: %w", err) + } + defer func() { _ = r.Close() }() + + for _, f := range r.File { + name := filepath.Base(f.Name) + if validParquetFiles[name] { + return nil + } + } + + return fmt.Errorf("archive must contain at least one parquet file from: abnormal_traces.parquet, abnormal_metrics.parquet, abnormal_logs.parquet, normal_traces.parquet, normal_metrics.parquet, normal_logs.parquet") +} + +func (s *DatapackStore) EnsureDatapackDirAvailable(datapackName string) (string, error) { + if s.basePath == "" { + return "", fmt.Errorf("dataset path not configured") + } + targetDir := s.RootDir(datapackName) + if _, err := os.Stat(targetDir); err == nil { + return "", fmt.Errorf("%w: directory %s already exists", consts.ErrAlreadyExists, datapackName) + } + return targetDir, nil +} + +func (s *DatapackStore) ExtractArchive(zipPath, targetDir string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return fmt.Errorf("failed to open zip archive: %w", err) + } + defer func() { _ = r.Close() }() + + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return fmt.Errorf("failed to create target directory: %w", err) + } + + for _, f := range r.File { + destPath := filepath.Join(targetDir, f.Name) + if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(targetDir)+string(os.PathSeparator)) && + filepath.Clean(destPath) != filepath.Clean(targetDir) { + return fmt.Errorf("illegal file path in archive: %s", f.Name) + } + + if f.FileInfo().IsDir() { + if err := os.MkdirAll(destPath, 0o755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", f.Name, err) + } + continue + } + + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + return fmt.Errorf("failed to create parent directory for %s: %w", f.Name, err) + } + + outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", f.Name, err) + } + + rc, err := f.Open() + if err != nil { + _ = outFile.Close() + return fmt.Errorf("failed to open file in archive %s: %w", f.Name, err) + } + + _, err = io.Copy(outFile, rc) + _ = rc.Close() + _ = outFile.Close() + if err != nil { + return fmt.Errorf("failed to extract file %s: %w", f.Name, err) + } + } + + return nil +} + +func (s *DatapackStore) RemoveAll(path string) error { + return os.RemoveAll(path) +} + +func (s *DatapackStore) Remove(path string) error { + return os.Remove(path) +} + +func (s *DatapackStore) ExtractGroundtruths(dir string) []model.Groundtruth { + jsonPath := filepath.Join(dir, "injection.json") + data, err := os.ReadFile(jsonPath) + if err != nil { + logrus.Debugf("No injection.json found in %s: %v", dir, err) + return nil + } + + var parsed injectionJSONFile + if err := json.Unmarshal(data, &parsed); err != nil { + logrus.Warnf("Failed to parse injection.json in %s: %v", dir, err) + return nil + } + + rawGTs := parsed.Groundtruths + if len(rawGTs) == 0 { + rawGTs = parsed.GroundTruth + } + if len(rawGTs) == 0 { + return nil + } + + result := make([]model.Groundtruth, 0, len(rawGTs)) + for _, gt := range rawGTs { + result = append(result, model.Groundtruth{ + Service: gt.Service, + Pod: gt.Pod, + Container: gt.Container, + Metric: gt.Metric, + Function: gt.Function, + Span: gt.Span, + }) + } + return result +} + +type injectionJSONGroundtruth struct { + Service []string `json:"service,omitempty"` + Pod []string `json:"pod,omitempty"` + Container []string `json:"container,omitempty"` + Metric []string `json:"metric,omitempty"` + Function []string `json:"function,omitempty"` + Span []string `json:"span,omitempty"` +} + +type injectionJSONFile struct { + Groundtruths []injectionJSONGroundtruth `json:"ground_truths"` + GroundTruth []injectionJSONGroundtruth `json:"ground_truth"` +} + +var validParquetFiles = map[string]bool{ + "abnormal_traces.parquet": true, + "abnormal_metrics.parquet": true, + "abnormal_logs.parquet": true, + "normal_traces.parquet": true, + "normal_metrics.parquet": true, + "normal_logs.parquet": true, +} diff --git a/src/module/injection/datapack_store_test.go b/src/module/injection/datapack_store_test.go new file mode 100644 index 00000000..f652f7f8 --- /dev/null +++ b/src/module/injection/datapack_store_test.go @@ -0,0 +1,83 @@ +package injectionmodule + +import ( + "archive/zip" + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "aegis/utils" + + "github.com/spf13/viper" +) + +func TestDatapackStoreBuildTreeAndOpenFile(t *testing.T) { + tmpDir := t.TempDir() + viper.Set("jfs.dataset_path", tmpDir) + store := &DatapackStore{basePath: tmpDir} + + root := filepath.Join(tmpDir, "dp-one") + if err := os.MkdirAll(filepath.Join(root, "nested"), 0o755); err != nil { + t.Fatalf("mkdir root: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "nested", "data.txt"), []byte("hello"), 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + + resp, err := store.BuildFileTree("dp-one", "", 12) + if err != nil { + t.Fatalf("BuildFileTree failed: %v", err) + } + if resp.FileCount != 1 || resp.DirCount != 1 { + t.Fatalf("unexpected counts: files=%d dirs=%d", resp.FileCount, resp.DirCount) + } + + name, contentType, size, reader, err := store.OpenFile("dp-one", "nested/data.txt") + if err != nil { + t.Fatalf("OpenFile failed: %v", err) + } + defer func() { _ = reader.Close() }() + content, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("read file: %v", err) + } + if name != "data.txt" || contentType != "text/plain" || size != int64(len("hello")) || string(content) != "hello" { + t.Fatalf("unexpected file result: %s %s %d %q", name, contentType, size, string(content)) + } +} + +func TestDatapackStorePackageUsesExcludeRules(t *testing.T) { + tmpDir := t.TempDir() + viper.Set("jfs.dataset_path", tmpDir) + store := &DatapackStore{basePath: tmpDir} + + root := filepath.Join(tmpDir, "dp-two") + if err := os.MkdirAll(root, 0o755); err != nil { + t.Fatalf("mkdir root: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "keep.txt"), []byte("keep"), 0o644); err != nil { + t.Fatalf("write keep: %v", err) + } + if err := os.WriteFile(filepath.Join(root, "drop.log"), []byte("drop"), 0o644); err != nil { + t.Fatalf("write drop: %v", err) + } + + buf := &bytes.Buffer{} + zw := zip.NewWriter(buf) + if err := store.Package(zw, "dp-two", []utils.ExculdeRule{{Pattern: "*.log", IsGlob: true}}); err != nil { + t.Fatalf("Package failed: %v", err) + } + if err := zw.Close(); err != nil { + t.Fatalf("close zip: %v", err) + } + + zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != nil { + t.Fatalf("open zip: %v", err) + } + if len(zr.File) != 1 || filepath.Base(zr.File[0].Name) != "keep.txt" { + t.Fatalf("unexpected zip entries: %+v", zr.File) + } +} diff --git a/src/handlers/v2/injections.go b/src/module/injection/handler.go similarity index 60% rename from src/handlers/v2/injections.go rename to src/module/injection/handler.go index c429712a..4b24ee84 100644 --- a/src/handlers/v2/injections.go +++ b/src/module/injection/handler.go @@ -1,8 +1,7 @@ -package v2 +package injectionmodule import ( - "aegis/consts" - "aegis/utils" + "aegis/httpx" "archive/zip" "context" "fmt" @@ -11,10 +10,10 @@ import ( "strconv" "strings" + "aegis/consts" "aegis/dto" - "aegis/handlers" "aegis/middleware" - producer "aegis/service/producer" + "aegis/utils" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" @@ -24,139 +23,200 @@ import ( chaos "github.com/OperationsPAI/chaos-experiment/handler" ) -// BatchDeleteInjections +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// ListProjectInjections lists all fault injections for a project // -// @Summary Batch delete injections -// @Description Batch delete injections by IDs or labels or tags with cascading deletion of related records -// @Tags Injections -// @ID batch_delete_injections -// @Accept json +// @Summary List project fault injections +// @Description Get paginated list of fault injections for a specific project +// @Tags Projects +// @ID list_project_injections // @Produce json // @Security BearerAuth -// @Param batch_delete body dto.BatchDeleteInjectionReq true "Batch delete request" -// @Success 200 {object} dto.GenericResponse[any] "Injections deleted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/batch-delete [post] -func BatchDeleteInjections(c *gin.Context) { - var req dto.BatchDeleteInjectionReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) +// @Param project_id path int true "Project ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Success 200 {object} dto.GenericResponse[dto.ListResp[InjectionResp]] "Fault injections retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListProjectInjections(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { return } - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) + var req ListInjectionReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - var err error - if len(req.IDs) > 0 { - err = producer.BatchDeleteInjectionsByIDs(req.IDs) - } else { - err = producer.BatchDeleteInjectionsByLabels(req.Labels) + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return } - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListProjectInjections(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Injections deleted successfully", nil) + dto.SuccessResponse(c, resp) } -// GetInjection handles getting a single injection by ID +// SearchProjectInjections searches fault injections within a specific project // -// @Summary Get injection by ID -// @Description Get detailed information about a specific injection -// @Tags Injections -// @ID get_injection_by_id +// @Summary Search project fault injections +// @Description Advanced search for injections within a project with complex filtering +// @Tags Projects +// @ID search_project_injections +// @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" -// @Success 200 {object} dto.GenericResponse[dto.InjectionDetailResp] "Injection retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/{id} [get] -// @x-api-type {"sdk":"true"} -func GetInjection(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") +// @Param project_id path int true "Project ID" +// @Param search body SearchInjectionReq true "Search criteria" +// @Success 200 {object} dto.GenericResponse[dto.SearchResp[InjectionDetailResp]] "Search results" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections/search [post] +// @x-api-type {"portal":"true"} +func (h *Handler) SearchProjectInjections(c *gin.Context) { + projectID, ok := parseProjectID(c) if !ok { - logrus.WithField("idStr", idStr).Warn("GetInjection: invalid ID format or ID <= 0") return } - resp, err := producer.GetInjectionDetail(id) - if err != nil { - logrus.WithFields(logrus.Fields{ - "id": id, - "error": err.Error(), - }).Error("GetInjection: failed to get injection detail") - } + h.searchInjections(c, &projectID) +} - if handlers.HandleServiceError(c, err) { +// ListProjectFaultInjectionNoIssues lists fault injections without issues for a project +// +// @Summary List project fault injections without issues +// @Description Query fault injection records without issues within a project based on time range +// @Tags Projects +// @ID list_project_injections_no_issues +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Param labels query []string false "Filter by labels" +// @Param lookback query string false "Time range query" +// @Param custom_start_time query string false "Custom start time" +// @Param custom_end_time query string false "Custom end time" +// @Success 200 {object} dto.GenericResponse[[]InjectionNoIssuesResp] "Injections retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections/analysis/no-issues [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListProjectFaultInjectionNoIssues(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { return } - logrus.WithField("id", id).Info("GetInjection: successfully retrieved injection") - dto.SuccessResponse(c, resp) + h.listFaultInjectionNoIssues(c, &projectID) } -// GetInjectionMetadata +// ListProjectFaultInjectionWithIssues lists fault injections with issues for a project // -// @Summary Get Injection Metadata -// @Description Get injection-related metadata including configuration, field mappings, and system resources -// @Tags Injections -// @ID get_injection_metadata +// @Summary List project fault injections with issues +// @Description Query fault injection records with issues within a project based on time range +// @Tags Projects +// @ID list_project_injections_with_issues // @Produce json // @Security BearerAuth -// @Param system query chaos.SystemType true "System for config and resources metadata" -// @Success 200 {object} dto.GenericResponse[dto.InjectionMetadataResp] "Successfully returned metadata" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid system" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/metadata [get] -// @x-api-type {"sdk":"true"} -func GetInjectionMetadata(c *gin.Context) { - systemStr := c.Query("system") - - ctx := context.Background() - system := chaos.SystemType(systemStr) - - confNode, err := chaos.StructToNode[chaos.InjectionConf](string(system)) - if err != nil { - // K8s namespace/pods may not exist in dev environment — return partial metadata - logrus.Warnf("Failed to build injection config node: %v, continuing with nil config", err) +// @Param project_id path int true "Project ID" +// @Param labels query []string false "Filter by labels" +// @Param lookback query string false "Time range query" +// @Param custom_start_time query string false "Custom start time" +// @Param custom_end_time query string false "Custom end time" +// @Success 200 {object} dto.GenericResponse[[]InjectionWithIssuesResp] "Injections retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections/analysis/with-issues [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListProjectFaultInjectionWithIssues(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return } - faultResourceMap, err := chaos.GetChaosTypeResourceMappings() - if err != nil { - handlers.HandleServiceError(c, err) + h.listFaultInjectionWithIssues(c, &projectID) +} + +// SubmitProjectFaultInjection submits fault injections for a specific project +// +// @Summary Submit project fault injections +// @Description Submit multiple fault injection tasks for a specific project +// @Tags Projects +// @ID submit_project_fault_injection +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Param body body SubmitInjectionReq true "Fault injection request" +// @Success 200 {object} dto.GenericResponse[SubmitInjectionResp] "Injections submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections/inject [post] +// @x-api-type {"portal":"true"} +func (h *Handler) SubmitProjectFaultInjection(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { return } - resourceMap, err := chaos.GetSystemResourceMap(ctx) - if err != nil { - // Some systems may not be deployed in the current environment - logrus.Warnf("Failed to get system resource map: %v, using empty map", err) - resourceMap = make(map[chaos.SystemType]chaos.SystemResource) - } + h.submitFaultInjection(c, &projectID) +} - resource := resourceMap[system] +// SubmitProjectDatapackBuilding submits datapack building tasks for a specific project +// +// @Summary Submit project datapack buildings +// @Description Submit multiple datapack building tasks for a specific project +// @Tags Projects +// @ID submit_project_datapack_building +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Param body body SubmitDatapackBuildingReq true "Datapack building request" +// @Success 202 {object} dto.GenericResponse[SubmitDatapackBuildingResp] "Datapack buildings submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/injections/build [post] +// @x-api-type {"portal":"true"} +func (h *Handler) SubmitProjectDatapackBuilding(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } - dto.SuccessResponse(c, &dto.InjectionMetadataResp{ - Config: confNode, - FaultTypeMap: chaos.ChaosTypeMap, - FaultResourceMap: faultResourceMap, - SystemResource: resource, - }) + h.submitDatapackBuilding(c, &projectID) } // ListInjections handles listing injections with pagination and filtering @@ -174,30 +234,27 @@ func GetInjectionMetadata(c *gin.Context) { // @Param state query consts.DatapackState false "Filter by injection state" // @Param status query int false "Filter by status" // @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:chaos')" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.InjectionResp]] "Injections retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[InjectionResp]] "Injections retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections [get] -// @x-api-type {"sdk":"true"} -func ListInjections(c *gin.Context) { - var req dto.ListInjectionReq +// @x-api-type {} +func (h *Handler) ListInjections(c *gin.Context) { + var req ListInjectionReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListInjections(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListInjections(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -210,17 +267,54 @@ func ListInjections(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param search body dto.SearchInjectionReq true "Search criteria" -// @Success 200 {object} dto.GenericResponse[dto.SearchResp[dto.InjectionDetailResp]] "Search results" +// @Param search body SearchInjectionReq true "Search criteria" +// @Success 200 {object} dto.GenericResponse[dto.SearchResp[InjectionDetailResp]] "Search results" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/search [post] -// @x-api-type {"sdk":"true"} -func SearchInjections(c *gin.Context) { - searchInjectionsCommon(c, nil) -} +// @x-api-type {} +func (h *Handler) SearchInjections(c *gin.Context) { h.searchInjections(c, nil) } + +// SubmitFaultInjection submits batch fault injections +// +// @Summary Submit batch fault injections +// @Description Submit multiple fault injection tasks in batch +// @Tags Injections +// @ID inject_fault +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body SubmitInjectionReq true "Fault injection request body" +// @Success 200 {object} dto.GenericResponse[SubmitInjectionResp] "Fault injection submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/inject [post] +// @x-api-type {} +func (h *Handler) SubmitFaultInjection(c *gin.Context) { h.submitFaultInjection(c, nil) } + +// SubmitDatapackBuilding submits batch datapack buildings +// +// @Summary Submit batch datapack buildings +// @Description. Submit multiple datapack building tasks in batch +// @Tags Injections +// @ID build_datapack +// @Accept json +// @Produce json +// @Param body body SubmitDatapackBuildingReq true "Datapack building request body" +// @Success 202 {object} dto.GenericResponse[SubmitDatapackBuildingResp] "Datapack building submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/build [post] +// @x-api-type {} +func (h *Handler) SubmitDatapackBuilding(c *gin.Context) { h.submitDatapackBuilding(c, nil) } // ListFaultInjectionNoIssues // @@ -233,14 +327,12 @@ func SearchInjections(c *gin.Context) { // @Param lookback query string false "Time range query, supports custom relative time (1h/24h/7d) or custom, default not set" // @Param custom_start_time query string false "Custom start time, RFC3339 format, required when lookback=custom" Format(date-time) // @Param custom_end_time query string false "Custom end time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Success 200 {object} dto.GenericResponse[[]dto.InjectionNoIssuesResp] "Successfully returned fault injection records without issues" +// @Success 200 {object} dto.GenericResponse[[]InjectionNoIssuesResp] "Successfully returned fault injection records without issues" // @Failure 400 {object} dto.GenericResponse[any] "Request parameter error, such as incorrect time format or parameter validation failure, etc." // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/analysis/no-issues [get] -// @x-api-type {"sdk":"true"} -func ListFaultInjectionNoIssues(c *gin.Context) { - listFaultInjectionNoIssuesCommon(c, nil) -} +// @x-api-type {} +func (h *Handler) ListFaultInjectionNoIssues(c *gin.Context) { h.listFaultInjectionNoIssues(c, nil) } // ListFaultInjectionWithIssues // @@ -253,13 +345,89 @@ func ListFaultInjectionNoIssues(c *gin.Context) { // @Param lookback query string false "Time range query, supports custom relative time (1h/24h/7d) or custom, default not set" // @Param custom_start_time query string false "Custom start time, RFC3339 format, required when lookback=custom" Format(date-time) // @Param custom_end_time query string false "Custom end time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Success 200 {object} dto.GenericResponse[[]dto.InjectionWithIssuesResp] +// @Success 200 {object} dto.GenericResponse[[]InjectionWithIssuesResp] // @Failure 400 {object} dto.GenericResponse[any] "Request parameter error, such as incorrect time format or parameter validation failure, etc." // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/analysis/with-issues [get] -// @x-api-type {"sdk":"true"} -func ListFaultInjectionWithIssues(c *gin.Context) { - listFaultInjectionWithIssuesCommon(c, nil) +// @x-api-type {} +func (h *Handler) ListFaultInjectionWithIssues(c *gin.Context) { + h.listFaultInjectionWithIssues(c, nil) +} + +// GetInjection handles getting a single injection by ID +// +// @Summary Get injection by ID +// @Description Get detailed information about a specific injection +// @Tags Injections +// @ID get_injection_by_id +// @Produce json +// @Security BearerAuth +// @Param id path int true "Injection ID" +// @Success 200 {object} dto.GenericResponse[InjectionDetailResp] "Injection retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/{id} [get] +// @x-api-type {} +func (h *Handler) GetInjection(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") + if !ok { + return + } + resp, err := h.service.GetInjection(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// GetInjectionMetadata +// +// @Summary Get Injection Metadata +// @Description Get injection-related metadata including configuration, field mappings, and system resources +// @Tags Injections +// @ID get_injection_metadata +// @Produce json +// @Security BearerAuth +// @Param system query chaos.SystemType true "System for config and resources metadata" +// @Success 200 {object} dto.GenericResponse[InjectionMetadataResp] "Successfully returned metadata" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid system" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/metadata [get] +// @x-api-type {} +func (h *Handler) GetInjectionMetadata(c *gin.Context) { + systemStr := c.Query("system") + ctx := c.Request.Context() + system := chaos.SystemType(systemStr) + + confNode, err := chaos.StructToNode[chaos.InjectionConf](string(system)) + if err != nil { + logrus.Warnf("Failed to build injection config node: %v, continuing with nil config", err) + } + + faultResourceMap, err := chaos.GetChaosTypeResourceMappings() + if err != nil { + httpx.HandleServiceError(c, err) + return + } + + resourceMap, err := chaos.GetSystemResourceMap(ctx) + if err != nil { + logrus.Warnf("Failed to get system resource map: %v, using empty map", err) + resourceMap = make(map[chaos.SystemType]chaos.SystemResource) + } + + dto.SuccessResponse(c, &InjectionMetadataResp{ + Config: confNode, + FaultTypeMap: chaos.ChaosTypeMap, + FaultResourceMap: faultResourceMap, + SystemResource: resourceMap[system], + }) } // ManageInjectionCustomLabels manages injection custom labels (key-value pairs) @@ -272,38 +440,33 @@ func ListFaultInjectionWithIssues(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Injection ID" -// @Param manage body dto.ManageInjectionLabelReq true "Custom label management request" -// @Success 200 {object} dto.GenericResponse[dto.InjectionResp] "Custom labels managed successfully" +// @Param manage body ManageInjectionLabelReq true "Custom label management request" +// @Success 200 {object} dto.GenericResponse[InjectionResp] "Custom labels managed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID or request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/labels [patch] -// @x-api-type {"sdk":"true"} -func ManageInjectionCustomLabels(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") +// @x-api-type {} +func (h *Handler) ManageInjectionCustomLabels(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { return } - - var req dto.ManageInjectionLabelReq + var req ManageInjectionLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ManageInjectionLabels(&req, id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ManageLabels(c.Request.Context(), &req, id) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -316,75 +479,62 @@ func ManageInjectionCustomLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param batch_manage body dto.BatchManageInjectionLabelReq true "Batch manage label request" -// @Success 200 {object} dto.GenericResponse[dto.BatchManageInjectionLabelResp] "Injection labels managed successfully" +// @Param batch_manage body BatchManageInjectionLabelReq true "Batch manage label request" +// @Success 200 {object} dto.GenericResponse[BatchManageInjectionLabelResp] "Injection labels managed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/labels/batch [patch] -// @x-api-type {"sdk":"true"} -func BatchManageInjectionLabels(c *gin.Context) { - var req dto.BatchManageInjectionLabelReq +// @x-api-type {} +func (h *Handler) BatchManageInjectionLabels(c *gin.Context) { + var req BatchManageInjectionLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) return } - - resp, err := producer.BatchManageInjectionLabels(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.BatchManageLabels(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } -// SubmitFaultInjection submits batch fault injections +// BatchDeleteInjections // -// @Summary Submit batch fault injections -// @Description Submit multiple fault injection tasks in batch +// @Summary Batch delete injections +// @Description Batch delete injections by IDs or labels or tags with cascading deletion of related records // @Tags Injections -// @ID inject_fault +// @ID batch_delete_injections // @Accept json // @Produce json // @Security BearerAuth -// @Param body body dto.SubmitInjectionReq true "Fault injection request body" -// @Success 200 {object} dto.GenericResponse[dto.SubmitInjectionResp] "Fault injection submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/inject [post] -// @x-api-type {"sdk":"true"} -func SubmitFaultInjection(c *gin.Context) { - submitFaultInjectionCommon(c, nil) -} - -// SubmitDatapackBuilding submits batch datapack buildings -// -// @Summary Submit batch datapack buildings -// @Description. Submit multiple datapack building tasks in batch -// @Tags Injections -// @ID build_datapack -// @Accept json -// @Produce json -// @Param body body dto.SubmitDatapackBuildingReq true "Datapack building request body" -// @Success 202 {object} dto.GenericResponse[dto.SubmitDatapackBuildingResp] "Datapack building submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/build [post] -// @x-api-type {"sdk":"true"} -func SubmitDatapackBuilding(c *gin.Context) { - submitDatapackBuildingCommon(c, nil) +// @Param batch_delete body BatchDeleteInjectionReq true "Batch delete request" +// @Success 200 {object} dto.GenericResponse[any] "Injections deleted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/batch-delete [post] +// @x-api-type {} +func (h *Handler) BatchDeleteInjections(c *gin.Context) { + var req BatchDeleteInjectionReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) + return + } + if httpx.HandleServiceError(c, h.service.BatchDelete(c.Request.Context(), &req)) { + return + } + dto.JSONResponse[any](c, http.StatusNoContent, "Injections deleted successfully", nil) } // CloneInjection handles cloning an injection configuration @@ -397,32 +547,28 @@ func SubmitDatapackBuilding(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Injection ID" -// @Param body body dto.CloneInjectionReq true "Clone request" -// @Success 201 {object} dto.GenericResponse[dto.InjectionDetailResp] "Injection cloned successfully" +// @Param body body CloneInjectionReq true "Clone request" +// @Success 201 {object} dto.GenericResponse[InjectionDetailResp] "Injection cloned successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/clone [post] -// @x-api-type {"sdk":"true"} -func CloneInjection(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") +// @x-api-type {} +func (h *Handler) CloneInjection(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { return } - - var req dto.CloneInjectionReq + var req CloneInjectionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return } - - resp, err := producer.CloneInjection(id, &req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.Clone(c.Request.Context(), id, &req) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusCreated, "Injection cloned successfully", resp) } @@ -435,25 +581,22 @@ func CloneInjection(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Injection ID" -// @Success 200 {object} dto.GenericResponse[dto.InjectionLogsResp] "Logs retrieved successfully" +// @Success 200 {object} dto.GenericResponse[InjectionLogsResp] "Logs retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/logs [get] -// @x-api-type {"sdk":"true"} -func GetInjectionLogs(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") +// @x-api-type {} +func (h *Handler) GetInjectionLogs(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { return } - - resp, err := producer.GetInjectionLogs(id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetLogs(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusOK, "Logs retrieved successfully", resp) } @@ -472,29 +615,24 @@ func GetInjectionLogs(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/download [get] -// @x-api-type {"sdk":"true"} -func DownloadDatapack(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") +// @x-api-type {} +func (h *Handler) DownloadDatapack(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { return } - - filename, err := producer.GetDatapackFilename(id) - if handlers.HandleServiceError(c, err) { + filename, err := h.service.GetDatapackFilename(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } - c.Header("Content-Type", "application/zip") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip", filename)) - zipWriter := zip.NewWriter(c.Writer) defer func() { _ = zipWriter.Close() }() - - if err := producer.DownloadDatapack(zipWriter, []utils.ExculdeRule{}, id); err != nil { + if err := h.service.DownloadDatapack(c.Request.Context(), zipWriter, []utils.ExculdeRule{}, id); err != nil { delete(c.Writer.Header(), "Content-Disposition") c.Header("Content-Type", "application/json; charset=utf-8") - handlers.HandleServiceError(c, err) + httpx.HandleServiceError(c, err) } } @@ -507,31 +645,27 @@ func DownloadDatapack(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "Injection ID" -// @Success 200 {object} dto.GenericResponse[dto.DatapackFilesResp] "Files retrieved successfully" +// @Success 200 {object} dto.GenericResponse[DatapackFilesResp] "Files retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 404 {object} dto.GenericResponse[any] "Datapack not found or not ready" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files [get] -func ListDatapackFiles(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "datapack ID") +// @x-api-type {} +func (h *Handler) ListDatapackFiles(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { return } - - // Get base URL from request scheme := "http" if c.Request.TLS != nil { scheme = "https" } baseURL := fmt.Sprintf("%s://%s", scheme, c.Request.Host) - - resp, err := producer.GetDatapackFiles(id, baseURL) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetDatapackFiles(c.Request.Context(), id, baseURL) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -555,44 +689,35 @@ func ListDatapackFiles(c *gin.Context) { // @Failure 416 {object} dto.GenericResponse[any] "Range not satisfiable" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files/download [get] -func DownloadDatapackFile(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "datapack ID") +// @x-api-type {} +func (h *Handler) DownloadDatapackFile(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { return } - filePath := c.Query("path") if filePath == "" { dto.ErrorResponse(c, http.StatusBadRequest, "file path is required") return } - - fileName, contentType, fileSize, fileReader, err := producer.DownloadDatapackFile(id, filePath) - if handlers.HandleServiceError(c, err) { + fileName, contentType, fileSize, fileReader, err := h.service.DownloadDatapackFile(c.Request.Context(), id, filePath) + if httpx.HandleServiceError(c, err) { return } defer func() { _ = fileReader.Close() }() - c.Header("Content-Type", contentType) c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fileName)) c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Header("Accept-Ranges", "bytes") - - // Handle Range request for resumable download rangeHeader := c.GetHeader("Range") if rangeHeader != "" { serveRangeRequest(c, fileReader, fileSize, rangeHeader) return } - - // Full file response c.Header("Content-Length", strconv.FormatInt(fileSize, 10)) c.Status(http.StatusOK) - if _, err := io.Copy(c.Writer, fileReader); err != nil { logrus.WithError(err).Error("failed to stream file content") - return } } @@ -618,124 +743,123 @@ func DownloadDatapackFile(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Datapack or file not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files/query [get] -func QueryDatapackFile(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "datapack ID") +// @x-api-type {} +func (h *Handler) QueryDatapackFile(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { return } - filePath := c.Query("path") if filePath == "" { dto.ErrorResponse(c, http.StatusBadRequest, "file path is required") return } - - ctx := c.Request.Context() - - fileName, totalRows, reader, err := producer.QueryDatapackFileContent(ctx, id, filePath) - if err != nil { - if handlers.HandleServiceError(c, err) { - return - } + fileName, totalRows, reader, err := h.service.QueryDatapackFile(c.Request.Context(), id, filePath) + if err != nil && httpx.HandleServiceError(c, err) { + return } defer func() { _ = reader.Close() }() - - // Content-Length enables axios onDownloadProgress to calculate percentage c.Header("Content-Type", "application/vnd.apache.arrow.stream") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.arrow", fileName)) c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Header("X-Total-Rows", strconv.FormatInt(totalRows, 10)) c.Header("X-Accel-Buffering", "no") c.Status(http.StatusOK) - if _, err := io.Copy(c.Writer, reader); err != nil { logrus.Errorf("failed to stream file content: %v", err) - return } } -// ===================== Private Helper Functions ===================== - -// serveRangeRequest handles HTTP Range requests for partial content delivery. -// Supports single range requests in the format "bytes=start-end". -func serveRangeRequest(c *gin.Context, reader io.ReadSeeker, fileSize int64, rangeHeader string) { - // Parse "bytes=start-end" format - const prefix = "bytes=" - if !strings.HasPrefix(rangeHeader, prefix) { - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range format") +// UpdateGroundtruth handles updating ground truth for a datapack +// +// @Summary Update datapack ground truth +// @Description Update or set ground truth labels for a datapack (fault injection) +// @Tags Injections +// @ID update_groundtruth +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "Injection ID" +// @Param request body UpdateGroundtruthReq true "Ground truth data" +// @Success 200 {object} dto.GenericResponse[any] "Ground truth updated" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Router /api/v2/injections/{id}/groundtruth [put] +// @x-api-type {} +func (h *Handler) UpdateGroundtruth(c *gin.Context) { + id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") + if !ok { return } - - rangeSpec := strings.TrimPrefix(rangeHeader, prefix) - // Only support single range (no multi-range) - if strings.Contains(rangeSpec, ",") { - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "multi-range not supported") + var req UpdateGroundtruthReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - - parts := strings.SplitN(rangeSpec, "-", 2) - if len(parts) != 2 { - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range format") + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - var start, end int64 - var err error - - if parts[0] == "" { - // Suffix range: "bytes=-500" means last 500 bytes - suffix, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil || suffix <= 0 || suffix > fileSize { - c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range") - return - } - start = fileSize - suffix - end = fileSize - 1 - } else { - start, err = strconv.ParseInt(parts[0], 10, 64) - if err != nil || start < 0 || start >= fileSize { - c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range start") - return - } - - if parts[1] == "" { - // Open-ended range: "bytes=100-" means from 100 to end - end = fileSize - 1 - } else { - end, err = strconv.ParseInt(parts[1], 10, 64) - if err != nil || end < start || end >= fileSize { - c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) - dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range end") - return - } - } + if httpx.HandleServiceError(c, h.service.UpdateGroundtruth(c.Request.Context(), id, &req)) { + return } + dto.JSONResponse[any](c, http.StatusOK, "Groundtruth updated successfully", nil) +} - contentLength := end - start + 1 - - // Seek to start position - if _, err := reader.Seek(start, io.SeekStart); err != nil { - logrus.Errorf("failed to seek to range start: %v", err) - dto.ErrorResponse(c, http.StatusInternalServerError, "failed to seek to range start") +// UploadDatapack handles manual datapack upload +// +// @Summary Upload a manual datapack +// @Description Upload a zip archive as a manual datapack data source +// @Tags Injections +// @ID upload_datapack +// @Accept multipart/form-data +// @Produce json +// @Security BearerAuth +// @Param name formData string true "Datapack name" +// @Param description formData string false "Description" +// @Param category formData string false "Category" +// @Param labels formData string false "JSON-encoded labels" +// @Param ground_truths formData string false "JSON-encoded ground truths" +// @Param file formData file true "Zip archive file" +// @Success 201 {object} dto.GenericResponse[UploadDatapackResp] "Datapack uploaded successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/injections/upload [post] +// @x-api-type {} +func (h *Handler) UploadDatapack(c *gin.Context) { + fileHeader, err := c.FormFile("file") + if err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "file is required: "+err.Error()) return } + file, err := fileHeader.Open() + if err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "failed to open uploaded file: "+err.Error()) + return + } + defer func() { _ = file.Close() }() - c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) - c.Header("Content-Length", strconv.FormatInt(contentLength, 10)) - c.Status(http.StatusPartialContent) - - if _, err := io.CopyN(c.Writer, reader, contentLength); err != nil { - logrus.Errorf("failed to stream partial content: %v", err) + req := &UploadDatapackReq{ + Name: c.PostForm("name"), + Description: c.PostForm("description"), + Category: c.PostForm("category"), + Labels: c.PostForm("labels"), + Groundtruths: c.PostForm("groundtruths"), + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + resp, err := h.service.UploadDatapack(c.Request.Context(), req, file, fileHeader.Size) + if httpx.HandleServiceError(c, err) { return } + dto.JSONResponse(c, http.StatusCreated, "Datapack uploaded successfully", resp) } -// searchInjectionsCommon is the common logic for searching injections -func searchInjectionsCommon(c *gin.Context, projectID *int) { - var req dto.SearchInjectionReq +func (h *Handler) searchInjections(c *gin.Context, projectID *int) { + var req SearchInjectionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return @@ -746,19 +870,16 @@ func searchInjectionsCommon(c *gin.Context, projectID *int) { return } - // Note: Project filtering should be handled at the service layer - // For project-scoped calls, the service layer will filter by project - resp, err := producer.SearchInjections(&req, projectID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.Search(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { return } dto.SuccessResponse(c, resp) } -// listFaultInjectionNoIssuesCommon is the common logic for listing injections without issues -func listFaultInjectionNoIssuesCommon(c *gin.Context, projectID *int) { - var req dto.ListInjectionNoIssuesReq +func (h *Handler) listFaultInjectionNoIssues(c *gin.Context, projectID *int) { + var req ListInjectionNoIssuesReq if err := c.BindQuery(&req); err != nil { logrus.Errorf("failed to bind query parameters: %v", err) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query parameters") @@ -771,20 +892,16 @@ func listFaultInjectionNoIssuesCommon(c *gin.Context, projectID *int) { return } - // Note: Project filtering should be handled at the service layer - // For project-scoped calls, the service layer will filter by project - - items, err := producer.ListInjectionsNoIssues(&req, projectID) - if handlers.HandleServiceError(c, err) { + items, err := h.service.ListNoIssues(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { return } dto.SuccessResponse(c, items) } -// listFaultInjectionWithIssuesCommon is the common logic for listing injections with issues -func listFaultInjectionWithIssuesCommon(c *gin.Context, projectID *int) { - var req dto.ListInjectionWithIssuesReq +func (h *Handler) listFaultInjectionWithIssues(c *gin.Context, projectID *int) { + var req ListInjectionWithIssuesReq if err := c.BindQuery(&req); err != nil { logrus.Errorf("failed to bind query parameters: %v", err) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query parameters") @@ -797,19 +914,15 @@ func listFaultInjectionWithIssuesCommon(c *gin.Context, projectID *int) { return } - // Note: Project filtering should be handled at the service layer - // For project-scoped calls, the service layer will filter by project - - items, err := producer.ListInjectionsWithIssues(&req, projectID) - if handlers.HandleServiceError(c, err) { + items, err := h.service.ListWithIssues(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { return } dto.SuccessResponse(c, items) } -// submitFaultInjectionCommon is the common logic for submitting fault injections -func submitFaultInjectionCommon(c *gin.Context, projectID *int) { +func (h *Handler) submitFaultInjection(c *gin.Context, projectID *int) { groupID := c.GetString("groupID") userID, exists := middleware.GetCurrentUserID(c) if !exists { @@ -817,17 +930,12 @@ func submitFaultInjectionCommon(c *gin.Context, projectID *int) { return } - ctx, ok := c.Get(middleware.SpanContextKey) + spanCtx, span, ok := spanFromGin(c, "SubmitFaultInjection") if !ok { - logrus.Error("Failed to get span context from gin.Context in SubmitFaultInjection") - dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get span context") return } - spanCtx := ctx.(context.Context) - span := trace.SpanFromContext(spanCtx) - - var req dto.SubmitInjectionReq + var req SubmitInjectionReq if err := c.BindJSON(&req); err != nil { span.SetStatus(codes.Error, "validation error in SubmitFaultInjection: "+err.Error()) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) @@ -846,11 +954,11 @@ func submitFaultInjectionCommon(c *gin.Context, projectID *int) { return } - resp, err := producer.ProduceRestartPedestalTasks(spanCtx, &req, groupID, userID, projectID) + resp, err := h.service.SubmitFaultInjection(spanCtx, &req, groupID, userID, projectID) if err != nil { span.SetStatus(codes.Error, "service error in SubmitFaultInjection: "+err.Error()) logrus.Errorf("Failed to submit fault injection: %v", err) - handlers.HandleServiceError(c, err) + httpx.HandleServiceError(c, err) return } @@ -858,8 +966,7 @@ func submitFaultInjectionCommon(c *gin.Context, projectID *int) { dto.SuccessResponse(c, resp) } -// submitDatapackBuildingCommon is the common logic for submitting datapack buildings -func submitDatapackBuildingCommon(c *gin.Context, projectID *int) { +func (h *Handler) submitDatapackBuilding(c *gin.Context, projectID *int) { groupID := c.GetString("groupID") userID, exists := middleware.GetCurrentUserID(c) if !exists { @@ -867,17 +974,12 @@ func submitDatapackBuildingCommon(c *gin.Context, projectID *int) { return } - ctx, ok := c.Get(middleware.SpanContextKey) + spanCtx, span, ok := spanFromGin(c, "SubmitDatapackBuilding") if !ok { - logrus.Error("Failed to get span context from gin.Context in SubmitDatapackBuilding") - dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get span context") return } - spanCtx := ctx.(context.Context) - span := trace.SpanFromContext(spanCtx) - - var req dto.SubmitDatapackBuildingReq + var req SubmitDatapackBuildingReq if err := c.BindJSON(&req); err != nil { span.SetStatus(codes.Error, "validation error in SubmitDatapackBuilding: "+err.Error()) dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) @@ -896,11 +998,11 @@ func submitDatapackBuildingCommon(c *gin.Context, projectID *int) { return } - resp, err := producer.ProduceDatapackBuildingTasks(spanCtx, &req, groupID, userID, projectID) + resp, err := h.service.SubmitDatapackBuilding(spanCtx, &req, groupID, userID, projectID) if err != nil { span.SetStatus(codes.Error, "service error in SubmitDatapackBuilding: "+err.Error()) logrus.Errorf("Failed to submit datapack building: %v", err) - handlers.HandleServiceError(c, err) + httpx.HandleServiceError(c, err) return } @@ -908,104 +1010,88 @@ func submitDatapackBuildingCommon(c *gin.Context, projectID *int) { dto.SuccessResponse(c, resp) } -// UploadDatapack handles manual datapack upload -// -// @Summary Upload a manual datapack -// @Description Upload a zip archive as a manual datapack data source -// @Tags Injections -// @ID upload_datapack -// @Accept multipart/form-data -// @Produce json -// @Security BearerAuth -// @Param name formData string true "Datapack name" -// @Param description formData string false "Description" -// @Param category formData string false "Category" -// @Param labels formData string false "JSON-encoded labels" -// @Param ground_truths formData string false "JSON-encoded ground truths" -// @Param file formData file true "Zip archive file" -// @Success 201 {object} dto.GenericResponse[dto.UploadDatapackResp] "Datapack uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/upload [post] -func UploadDatapack(c *gin.Context) { - var req dto.UploadDatapackReq - if err := c.ShouldBind(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) - return +func spanFromGin(c *gin.Context, operation string) (context.Context, trace.Span, bool) { + ctx, ok := c.Get(middleware.SpanContextKey) + if !ok { + logrus.Errorf("Failed to get span context from gin.Context in %s", operation) + dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get span context") + return nil, nil, false } - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) - return - } + spanCtx := ctx.(context.Context) + return spanCtx, trace.SpanFromContext(spanCtx), true +} - file, header, err := c.Request.FormFile("file") - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "File is required: "+err.Error()) - return - } - defer func() { _ = file.Close() }() +func parsePositiveID(c *gin.Context, key, label string) (int, bool) { + id, ok := httpx.ParsePositiveID(c, c.Param(key), label) + return id, ok +} - // Validate .zip extension - if !strings.HasSuffix(strings.ToLower(header.Filename), ".zip") { - dto.ErrorResponse(c, http.StatusBadRequest, "Only .zip files are accepted") +func serveRangeRequest(c *gin.Context, reader io.ReadSeeker, fileSize int64, rangeHeader string) { + const prefix = "bytes=" + if !strings.HasPrefix(rangeHeader, prefix) { + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range format") return } - - // Max 2GB - const maxSize = 2 << 30 // 2GB - if header.Size > maxSize { - dto.ErrorResponse(c, http.StatusBadRequest, "File size exceeds maximum allowed size of 2GB") + rangeSpec := strings.TrimPrefix(rangeHeader, prefix) + if strings.Contains(rangeSpec, ",") { + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "multi-range not supported") return } - - resp, err := producer.UploadDatapack(&req, file, header.Size) - if handlers.HandleServiceError(c, err) { + parts := strings.SplitN(rangeSpec, "-", 2) + if len(parts) != 2 { + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range format") return } - - dto.JSONResponse(c, http.StatusCreated, "Datapack uploaded successfully", resp) -} - -// UpdateGroundtruth handles updating ground truth for a datapack -// -// @Summary Update datapack ground truth -// @Description Update or set ground truth labels for a datapack (fault injection) -// @Tags Injections -// @ID update_groundtruth -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path int true "Injection ID" -// @Param request body dto.UpdateGroundtruthReq true "Ground truth data" -// @Success 200 {object} dto.GenericResponse[any] "Ground truth updated" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Router /api/v2/injections/{id}/groundtruth [put] -func UpdateGroundtruth(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, ok := handlers.ParsePositiveID(c, idStr, "injection ID") - if !ok { - return + var start, end int64 + var err error + if parts[0] == "" { + suffix, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil || suffix <= 0 || suffix > fileSize { + c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range") + return + } + start = fileSize - suffix + end = fileSize - 1 + } else { + start, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil || start < 0 || start >= fileSize { + c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range start") + return + } + if parts[1] == "" { + end = fileSize - 1 + } else { + end, err = strconv.ParseInt(parts[1], 10, 64) + if err != nil || end < start || end >= fileSize { + c.Header("Content-Range", fmt.Sprintf("bytes */%d", fileSize)) + dto.ErrorResponse(c, http.StatusRequestedRangeNotSatisfiable, "invalid range end") + return + } + } } - - var req dto.UpdateGroundtruthReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + contentLength := end - start + 1 + if _, err := reader.Seek(start, io.SeekStart); err != nil { + logrus.Errorf("failed to seek to range start: %v", err) + dto.ErrorResponse(c, http.StatusInternalServerError, "failed to seek to range start") return } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) - return + c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) + c.Header("Content-Length", strconv.FormatInt(contentLength, 10)) + c.Status(http.StatusPartialContent) + if _, err := io.CopyN(c.Writer, reader, contentLength); err != nil { + logrus.Errorf("failed to stream partial content: %v", err) } +} - err := producer.UpdateGroundtruth(id, &req) - if handlers.HandleServiceError(c, err) { - return +func parseProjectID(c *gin.Context) (int, bool) { + projectIDStr := c.Param(consts.URLPathProjectID) + projectID, err := strconv.Atoi(projectIDStr) + if err != nil || projectID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") + return 0, false } - - logrus.WithField("id", id).Info("UpdateGroundtruth: successfully updated ground truth") - dto.JSONResponse[any](c, http.StatusOK, "Ground truth updated", nil) + return projectID, true } diff --git a/src/module/injection/module.go b/src/module/injection/module.go new file mode 100644 index 00000000..cabed310 --- /dev/null +++ b/src/module/injection/module.go @@ -0,0 +1,10 @@ +package injectionmodule + +import "go.uber.org/fx" + +var Module = fx.Module("injection", + fx.Provide(NewRepository), + fx.Provide(NewDatapackStore), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/service/producer/query_datapack_arrow.go b/src/module/injection/query_datapack_arrow.go similarity index 80% rename from src/service/producer/query_datapack_arrow.go rename to src/module/injection/query_datapack_arrow.go index f77b074e..cc16c717 100644 --- a/src/service/producer/query_datapack_arrow.go +++ b/src/module/injection/query_datapack_arrow.go @@ -1,6 +1,6 @@ //go:build duckdb_arrow -package producer +package injectionmodule import ( "context" @@ -15,9 +15,13 @@ import ( "github.com/sirupsen/logrus" ) -// QueryDatapackFileContent reads a parquet file and streams it as an Arrow IPC stream. -func QueryDatapackFileContent(ctx context.Context, datapackID int, filePath string) (string, int64, io.ReadCloser, error) { - fullPath, err := getFileFullPath(datapackID, filePath) +func (s *Service) queryDatapackFileContent(ctx context.Context, id int, filePath string) (string, int64, io.ReadCloser, error) { + injection, err := s.getReadyDatapack(id) + if err != nil { + return "", 0, nil, err + } + + fullPath, err := s.store.ResolveFilePath(injection.Name, filePath) if err != nil { return "", 0, nil, fmt.Errorf("invalid file path: %w", err) } @@ -45,7 +49,6 @@ func QueryDatapackFileContent(ctx context.Context, datapackID int, filePath stri return "", 0, nil, err } - // Inspect schema and build a SELECT that casts unsupported unsigned integer types. safeSQL, err := buildSafeParquetSQL(ctx, db, fullPath) if err != nil { return "", 0, nil, fmt.Errorf("failed to build safe parquet SQL: %w", err) @@ -96,9 +99,6 @@ func QueryDatapackFileContent(ctx context.Context, datapackID int, filePath stri return filepath.Base(fullPath), totalRows, pr, nil } -// buildSafeParquetSQL inspects the parquet file schema via DuckDB DESCRIBE and builds -// a SELECT that casts UINT64/UHUGEINT columns to signed BIGINT so Arrow IPC consumers -// that do not support unsigned 64-bit integers can still parse the stream. func buildSafeParquetSQL(ctx context.Context, db *sql.DB, filePath string) (string, error) { fallbackSQL := fmt.Sprintf("SELECT * FROM read_parquet('%s')", filePath) describeQuery := fmt.Sprintf("DESCRIBE SELECT * FROM read_parquet('%s')", filePath) @@ -134,7 +134,5 @@ func buildSafeParquetSQL(ctx context.Context, db *sql.DB, filePath string) (stri return fallbackSQL, nil } - safeSQL := fmt.Sprintf("SELECT %s FROM read_parquet('%s')", strings.Join(columns, ", "), filePath) - logrus.Infof("parquet query uses type casting: %s", safeSQL) - return safeSQL, nil + return fmt.Sprintf("SELECT %s FROM read_parquet('%s')", strings.Join(columns, ", "), filePath), nil } diff --git a/src/module/injection/query_datapack_noarrow.go b/src/module/injection/query_datapack_noarrow.go new file mode 100644 index 00000000..7d87b881 --- /dev/null +++ b/src/module/injection/query_datapack_noarrow.go @@ -0,0 +1,16 @@ +//go:build !duckdb_arrow + +package injectionmodule + +import ( + "context" + "fmt" + "io" +) + +func (s *Service) queryDatapackFileContent(ctx context.Context, id int, filePath string) (string, int64, io.ReadCloser, error) { + _ = ctx + _ = id + _ = filePath + return "", 0, nil, fmt.Errorf("QueryDatapackFileContent requires building with -tags duckdb_arrow") +} diff --git a/src/module/injection/repository.go b/src/module/injection/repository.go new file mode 100644 index 00000000..48a695a6 --- /dev/null +++ b/src/module/injection/repository.go @@ -0,0 +1,652 @@ +package injectionmodule + +import ( + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/repository" + "encoding/json" + "fmt" + "strings" + "time" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) LoadInjection(id int) (*model.FaultInjection, error) { + var injection model.FaultInjection + if err := r.db. + Preload("Task"). + Preload("Task.Trace"). + Preload("Benchmark.Container"). + Preload("Pedestal.Container"). + Where("id = ?", id). + First(&injection).Error; err != nil { + return nil, fmt.Errorf("failed to find injection with id %d: %w", id, err) + } + return &injection, nil +} + +func (r *Repository) FindInjectionByName(name string, preload bool) (*model.FaultInjection, error) { + query := r.db + if preload { + query = query.Preload("Labels") + } + + var injection model.FaultInjection + if err := query.Where("name = ? AND status != ?", name, consts.CommonDeleted). + First(&injection).Error; err != nil { + return nil, fmt.Errorf("failed to find injection with name %s: %w", name, err) + } + return &injection, nil +} + +func (r *Repository) CreateInjectionRecord(injection *model.FaultInjection) error { + if err := r.db.Create(injection).Error; err != nil { + return fmt.Errorf("failed to create injection: %w", err) + } + return nil +} + +func (r *Repository) UpdateGroundtruth(id int, groundtruths []model.Groundtruth, source string) error { + groundtruthJSON, err := json.Marshal(groundtruths) + if err != nil { + return fmt.Errorf("failed to marshal groundtruths: %w", err) + } + + result := r.db.Model(&model.FaultInjection{}). + Where("id = ? AND status != ?", id, consts.CommonDeleted). + Updates(map[string]any{ + "groundtruths": string(groundtruthJSON), + "groundtruth_source": source, + }) + if result.Error != nil { + return fmt.Errorf("failed to update groundtruth for injection %d: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("injection with id %d: %w", id, consts.ErrNotFound) + } + return nil +} + +func (r *Repository) AddInjectionLabels(injectionID int, labelIDs []int) error { + if len(labelIDs) == 0 { + return nil + } + + links := make([]model.FaultInjectionLabel, 0, len(labelIDs)) + for _, labelID := range labelIDs { + links = append(links, model.FaultInjectionLabel{ + FaultInjectionID: injectionID, + LabelID: labelID, + }) + } + if err := r.db.Create(&links).Error; err != nil { + return fmt.Errorf("failed to add injection labels: %w", err) + } + return nil +} + +func (r *Repository) ResolveProject(name string) (*model.Project, error) { + var project model.Project + if err := r.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { + return nil, fmt.Errorf("failed to find project with name %s: %w", name, err) + } + return &project, nil +} + +func (r *Repository) LoadTask(taskID string) (*model.Task, error) { + var task model.Task + if err := r.db. + Preload("FaultInjection.Benchmark.Container"). + Preload("FaultInjection.Pedestal.Container"). + Preload("Execution.AlgorithmVersion.Container"). + Preload("Execution.Datapack"). + Preload("Execution.DatasetVersion"). + Where("id = ? AND status != ?", taskID, consts.CommonDeleted). + First(&task).Error; err != nil { + return nil, fmt.Errorf("failed to find task with id %s: %w", taskID, err) + } + return &task, nil +} + +func (r *Repository) LoadPedestalHelmConfig(versionID int) (*model.HelmConfig, error) { + var helmConfig model.HelmConfig + if err := r.db.Preload("ContainerVersion"). + Where("container_version_id = ?", versionID). + First(&helmConfig).Error; err != nil { + return nil, fmt.Errorf("failed to find helm config for version id %d: %w", versionID, err) + } + return &helmConfig, nil +} + +func (r *Repository) ListExistingEngineConfigs(configs []string) ([]string, error) { + if len(configs) == 0 { + return []string{}, nil + } + + invalidLabelSubQuery := r.db.Table("fault_injection_labels fil"). + Select("fil.fault_injection_id"). + Joins("JOIN labels ON labels.id = fil.label_id"). + Where("labels.label_key = ? AND labels.label_value = ?", consts.LabelKeyTag, "invalid") + + var existing []string + if err := r.db.Model(&model.FaultInjection{}). + Select("engine_config"). + Where("engine_config IN (?) AND state >= ? AND status = ?", configs, consts.DatapackInjectSuccess, consts.CommonEnabled). + Where("fault_injections.id NOT IN (?)", invalidLabelSubQuery). + Pluck("engine_config", &existing).Error; err != nil { + return nil, err + } + return existing, nil +} + +func (r *Repository) ClearInjectionLabels(injectionIDs []int, labelIDs []int) error { + if len(injectionIDs) == 0 { + return nil + } + + query := r.db.Table("fault_injection_labels").Where("fault_injection_id IN (?)", injectionIDs) + if len(labelIDs) > 0 { + query = query.Where("label_id IN (?)", labelIDs) + } + if err := query.Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear injection labels: %w", err) + } + return nil +} + +func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch decrease label usages: %w", err) + } + return nil +} + +func (r *Repository) ListExecutionsByDatapackIDs(datapackIDs []int) ([]model.Execution, error) { + if len(datapackIDs) == 0 { + return []model.Execution{}, nil + } + + var executions []model.Execution + if err := r.db. + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Benchmark.Container"). + Preload("Datapack.Pedestal.Container"). + Preload("DatasetVersion"). + Preload("Task.Trace.Project"). + Where("datapack_id IN (?) AND status != ?", datapackIDs, consts.CommonDeleted). + Find(&executions).Error; err != nil { + return nil, fmt.Errorf("failed to list executions by datapack IDs: %w", err) + } + return executions, nil +} + +func (r *Repository) RemoveLabelsFromExecutions(executionIDs []int) error { + if len(executionIDs) == 0 { + return nil + } + if err := r.db.Where("execution_id IN (?)", executionIDs). + Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { + return fmt.Errorf("failed to remove all labels from executions %v: %w", executionIDs, err) + } + return nil +} + +func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { + if len(executionIDs) == 0 { + return nil + } + if err := r.db.Model(&model.Execution{}). + Where("id IN (?) AND status != ?", executionIDs, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return fmt.Errorf("failed to batch delete executions: %w", err) + } + return nil +} + +func (r *Repository) BatchDeleteInjections(injectionIDs []int) error { + if len(injectionIDs) == 0 { + return nil + } + if err := r.db.Model(&model.FaultInjection{}). + Where("id IN (?) AND status != ?", injectionIDs, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return fmt.Errorf("failed to batch delete injections: %w", err) + } + return nil +} + +func (r *Repository) DeleteInjectionsCascade(injectionIDs []int) error { + executions, err := r.ListExecutionsByDatapackIDs(injectionIDs) + if err != nil { + return fmt.Errorf("failed to list executions by datapack ids: %w", err) + } + + executionIDs := make([]int, 0, len(executions)) + for _, execution := range executions { + executionIDs = append(executionIDs, execution.ID) + } + + if len(executionIDs) > 0 { + if err := r.RemoveLabelsFromExecutions(executionIDs); err != nil { + return fmt.Errorf("failed to remove execution labels: %w", err) + } + if err := r.BatchDeleteExecutions(executionIDs); err != nil { + return fmt.Errorf("failed to delete executions: %w", err) + } + } + + if err := r.ClearInjectionLabels(injectionIDs, nil); err != nil { + return fmt.Errorf("failed to clear injection labels: %w", err) + } + if err := r.BatchDeleteInjections(injectionIDs); err != nil { + return fmt.Errorf("failed to delete injections: %w", err) + } + return nil +} + +func (r *Repository) GetInjectionWithLabels(injectionID int) (*model.FaultInjection, error) { + injection, err := r.LoadInjection(injectionID) + if err != nil { + return nil, err + } + + var labels []model.Label + if err := r.db.Table("labels"). + Joins("JOIN fault_injection_labels fil ON labels.id = fil.label_id"). + Where("fil.fault_injection_id = ?", injection.ID). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to get injection labels: %w", err) + } + injection.Labels = labels + return injection, nil +} + +func (r *Repository) LoadInjectionLabelIDsByItems(conditions []map[string]string, category consts.LabelCategory) (map[string]int, error) { + if len(conditions) == 0 { + return map[string]int{}, nil + } + + query := r.db.Model(&model.Label{}). + Where("status != ? AND category = ?", consts.CommonDeleted, category) + + orBuilder := r.db.Where("1 = 0") + for _, condition := range conditions { + andBuilder := r.db.Where("1 = 1") + if key, ok := condition["key"]; ok { + andBuilder = andBuilder.Where("label_key = ?", key) + } + if value, ok := condition["value"]; ok { + andBuilder = andBuilder.Where("label_value = ?", value) + } + orBuilder = orBuilder.Or(andBuilder) + } + + var labels []model.Label + if err := query.Where(orBuilder).Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list label IDs by conditions: %w", err) + } + + result := make(map[string]int, len(labels)) + for _, label := range labels { + result[label.Key+":"+label.Value] = label.ID + } + return result, nil +} + +func (r *Repository) LoadExistingInjectionsByID(injectionIDs []int) (map[int]*model.FaultInjection, error) { + injections, err := r.ListFaultInjectionsByIDWithLabels(injectionIDs) + if err != nil { + return nil, err + } + + result := make(map[int]*model.FaultInjection, len(injections)) + for i := range injections { + injection := injections[i] + result[injection.ID] = &injection + } + return result, nil +} + +func (r *Repository) ListInjectionsView(limit, offset int, filterOptions *ListInjectionFilters) ([]model.FaultInjection, int64, error) { + query := r.db.Model(&model.FaultInjection{}). + Preload("Benchmark.Container"). + Preload("Pedestal.Container"). + Preload("Task.Trace.Project"). + Preload("Labels") + if filterOptions.FaultType != nil { + query = query.Where("fault_type = ?", *filterOptions.FaultType) + } + if filterOptions.Category != nil { + query = query.Where("category = ?", *filterOptions.Category) + } + if filterOptions.Benchmark != "" { + query = query.Where("benchmark = ?", filterOptions.Benchmark) + } + if filterOptions.State != nil { + query = query.Where("state = ?", *filterOptions.State) + } + if filterOptions.Status != nil { + query = query.Where("status = ?", *filterOptions.Status) + } + if len(filterOptions.LabelConditions) > 0 { + for _, condition := range filterOptions.LabelConditions { + subQuery := r.db.Table("fault_injection_labels fil"). + Select("fil.fault_injection_id"). + Joins("JOIN labels ON labels.id = fil.label_id"). + Where("labels.label_key = ? AND labels.label_value = ?", condition["key"], condition["value"]) + query = query.Where("fault_injections.id IN (?)", subQuery) + } + } + + var ( + injections []model.FaultInjection + total int64 + ) + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count injections: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&injections).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list injections: %w", err) + } + + injectionIDs := make([]int, 0, len(injections)) + for _, injection := range injections { + injectionIDs = append(injectionIDs, injection.ID) + } + + labelsMap, err := r.listInjectionLabels(injectionIDs) + if err != nil { + return nil, 0, fmt.Errorf("failed to list injection labels: %w", err) + } + + for i := range injections { + if labels, exists := labelsMap[injections[i].ID]; exists { + injections[i].Labels = labels + } + } + + return injections, total, nil +} + +func (r *Repository) ListProjectInjectionsView(projectID, limit, offset int) ([]model.FaultInjection, int64, error) { + baseQuery := r.db.Model(&model.FaultInjection{}). + Joins("JOIN tasks ON tasks.id = fault_injections.task_id"). + Joins("JOIN traces on traces.id = tasks.trace_id"). + Where("traces.project_id = ? AND fault_injections.status != ?", projectID, consts.CommonDeleted) + + var ( + injections []model.FaultInjection + total int64 + ) + if err := baseQuery.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count injections for project %d: %w", projectID, err) + } + + if err := baseQuery. + Preload("Benchmark.Container"). + Preload("Pedestal.Container"). + Limit(limit). + Offset(offset). + Order("fault_injections.updated_at DESC"). + Find(&injections).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list injections for project %d: %w", projectID, err) + } + + injectionIDs := make([]int, 0, len(injections)) + for _, injection := range injections { + injectionIDs = append(injectionIDs, injection.ID) + } + + labelsMap, err := r.listInjectionLabels(injectionIDs) + if err != nil { + return nil, 0, fmt.Errorf("failed to list injection labels: %w", err) + } + + for i := range injections { + injections[i].Labels = labelsMap[injections[i].ID] + } + return injections, total, nil +} + +func (r *Repository) SearchInjections(req *SearchInjectionReq, projectID *int) ([]model.FaultInjection, int64, error) { + searchReq := req.ConvertToSearchReq() + if projectID != nil { + searchReq.AddFilter("project_id", dto.OpEqual, *projectID) + } + + qb := repository.NewSearchQueryBuilder(r.db, consts.InjectionAllowedFields) + qb.ApplySearchReq(searchReq.Filters, searchReq.Keyword, searchReq.Sort, searchReq.GroupBy, model.FaultInjection{}) + qb.ApplyIncludes(searchReq.Includes) + qb.ApplyIncludeFields(searchReq.IncludeFields) + qb.ApplyExcludeFields(searchReq.ExcludeFields, model.FaultInjection{}) + + total, err := qb.GetCount() + if err != nil { + return nil, 0, fmt.Errorf("failed to count searched injections: %w", err) + } + + query := qb.Query() + if searchReq.Size != 0 && searchReq.Page != 0 { + query = query.Offset(searchReq.GetOffset()).Limit(int(searchReq.Size)) + } + + var injections []model.FaultInjection + if err := query.Find(&injections).Error; err != nil { + return nil, 0, fmt.Errorf("failed to execute injection search: %w", err) + } + + if len(req.Labels) == 0 { + return injections, total, nil + } + + labelConditions := make([]map[string]string, 0, len(req.Labels)) + for _, item := range req.Labels { + labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) + } + + injectionIDs, err := r.listInjectionIDsByLabels(labelConditions) + if err != nil { + return nil, 0, fmt.Errorf("failed to list injection ids by labels: %w", err) + } + + injectionIDMap := make(map[int]struct{}, len(injectionIDs)) + for _, id := range injectionIDs { + injectionIDMap[id] = struct{}{} + } + + filtered := make([]model.FaultInjection, 0, len(injections)) + for _, injection := range injections { + if _, exists := injectionIDMap[injection.ID]; exists { + filtered = append(filtered, injection) + } + } + + return filtered, total, nil +} + +func (r *Repository) ListIssuesFreeInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionNoIssues, error) { + var injections []model.FaultInjectionNoIssues + query := r.db.Model(&model.FaultInjectionNoIssues{}). + Joins("JOIN fault_injections fi ON fi.id = fault_injection_no_issues.datapack_id"). + Joins("JOIN tasks t ON t.id = fi.task_id"). + Joins("JOIN traces tr ON tr.id = t.trace_id"). + Where("fi.status != ?", consts.CommonDeleted) + if projectID != nil { + query = query.Where("tr.project_id = ?", *projectID) + } + if startTime != nil { + query = query.Where("fi.created_at >= ?", *startTime) + } + if endTime != nil { + query = query.Where("fi.created_at <= ?", *endTime) + } + for _, condition := range labelConditions { + subQuery := r.db.Table("fault_injection_labels fil"). + Select("fil.fault_injection_id"). + Joins("JOIN labels l ON l.id = fil.label_id"). + Where("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) + query = query.Where("fi.id IN (?)", subQuery) + } + anomalySubQuery := r.db.Table("executions e"). + Select("DISTINCT fi2.id"). + Joins("JOIN fault_injections fi2 ON fi2.id = e.datapack_id"). + Where("e.status != ? AND e.has_anomaly = ?", consts.CommonDeleted, true) + query = query.Where("fi.id NOT IN (?)", anomalySubQuery) + if err := query.Scan(&injections).Error; err != nil { + return nil, fmt.Errorf("failed to list injections without issues: %w", err) + } + return injections, nil +} + +func (r *Repository) ListIssueInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionWithIssues, error) { + var injections []model.FaultInjectionWithIssues + query := r.db.Model(&model.FaultInjectionWithIssues{}). + Joins("JOIN fault_injections fi ON fi.id = fault_injection_with_issues.datapack_id"). + Joins("JOIN tasks t ON t.id = fi.task_id"). + Joins("JOIN traces tr ON tr.id = t.trace_id"). + Where("fi.status != ?", consts.CommonDeleted) + if projectID != nil { + query = query.Where("tr.project_id = ?", *projectID) + } + if startTime != nil { + query = query.Where("fi.created_at >= ?", *startTime) + } + if endTime != nil { + query = query.Where("fi.created_at <= ?", *endTime) + } + for _, condition := range labelConditions { + subQuery := r.db.Table("fault_injection_labels fil"). + Select("fil.fault_injection_id"). + Joins("JOIN labels l ON l.id = fil.label_id"). + Where("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) + query = query.Where("fi.id IN (?)", subQuery) + } + if err := query.Scan(&injections).Error; err != nil { + return nil, fmt.Errorf("failed to list injections with issues: %w", err) + } + return injections, nil +} + +func (r *Repository) ListInjectionLabelIDsByKeys(injectionID int, keys []string) ([]int, error) { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN fault_injection_labels fil ON fil.label_id = l.id"). + Where("fil.fault_injection_id = ? AND l.label_key IN (?)", injectionID, keys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by key '%v': %w", keys, err) + } + return labelIDs, nil +} + +func (r *Repository) ListFaultInjectionsByIDWithLabels(injectionIDs []int) ([]model.FaultInjection, error) { + if len(injectionIDs) == 0 { + return []model.FaultInjection{}, nil + } + + var injections []model.FaultInjection + if err := r.db. + Preload("Benchmark.Container"). + Preload("Pedestal.Container"). + Preload("Task.Trace.Project"). + Preload("Labels"). + Where("id IN (?) AND status != ?", injectionIDs, consts.CommonDeleted). + Find(&injections).Error; err != nil { + return nil, fmt.Errorf("failed to query fault injections: %w", err) + } + + labelsMap, err := r.listInjectionLabels(injectionIDs) + if err != nil { + return nil, fmt.Errorf("failed to list injection labels: %w", err) + } + + for i := range injections { + if labels, exists := labelsMap[injections[i].ID]; exists { + injections[i].Labels = labels + } + } + + return injections, nil +} + +func (r *Repository) ListInjectionIDsByLabelConditions(labelConditions []map[string]string) ([]int, error) { + return r.listInjectionIDsByLabels(labelConditions) +} + +func (r *Repository) listInjectionIDsByLabels(labelConditions []map[string]string) ([]int, error) { + var injectionIDs []int + query := r.db.Model(&model.FaultInjection{}). + Select("DISTINCT fault_injections.id"). + Joins("JOIN fault_injection_labels fil ON fil.fault_injection_id = fault_injections.id"). + Joins("JOIN labels ON labels.id = fil.label_id"). + Where("fault_injections.status != ?", consts.CommonDeleted) + + var whereClauses []string + var whereArgs []any + for _, condition := range labelConditions { + whereClauses = append(whereClauses, "(labels.label_key = ? AND labels.label_value = ?)") + whereArgs = append(whereArgs, condition["key"], condition["value"]) + } + if len(whereClauses) > 0 { + query = query.Where(strings.Join(whereClauses, " OR "), whereArgs...) + } + + if err := query.Pluck("fault_injections.id", &injectionIDs).Error; err != nil { + return nil, fmt.Errorf("failed to list injection IDs by labels: %v", err) + } + return injectionIDs, nil +} + +func (r *Repository) listInjectionLabels(injectionIDs []int) (map[int][]model.Label, error) { + labelsMap := make(map[int][]model.Label, len(injectionIDs)) + for _, id := range injectionIDs { + labelsMap[id] = []model.Label{} + } + if len(injectionIDs) == 0 { + return labelsMap, nil + } + + type injectionLabelResult struct { + model.Label + InjectionID int `gorm:"column:injection_id"` + } + + var flatResults []injectionLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN fault_injection_labels fil ON fil.label_id = labels.id"). + Where("fil.fault_injection_id IN (?)", injectionIDs). + Select("labels.*, fil.fault_injection_id as injection_id"). + Find(&flatResults).Error; err != nil { + return nil, fmt.Errorf("failed to batch query fault injection labels: %w", err) + } + + for _, result := range flatResults { + labelsMap[result.InjectionID] = append(labelsMap[result.InjectionID], result.Label) + } + return labelsMap, nil +} diff --git a/src/module/injection/service.go b/src/module/injection/service.go new file mode 100644 index 00000000..20659443 --- /dev/null +++ b/src/module/injection/service.go @@ -0,0 +1,956 @@ +package injectionmodule + +import ( + "archive/zip" + "context" + "errors" + "fmt" + "io" + "sort" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + lokiinfra "aegis/infra/loki" + redisinfra "aegis/infra/redis" + "aegis/model" + "aegis/service/common" + "aegis/utils" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" + "gorm.io/gorm" +) + +type Service struct { + repo *Repository + store *DatapackStore + lokiClient *lokiinfra.Client + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, store *DatapackStore, lokiClient *lokiinfra.Client, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, store: store, lokiClient: lokiClient, redis: redis} +} + +func (s *Service) ListProjectInjections(ctx context.Context, req *ListInjectionReq, projectID int) (*dto.ListResp[InjectionResp], error) { + var project model.Project + if err := s.repo.db.Where("id = ?", projectID).First(&project).Error; err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + + limit, offset := req.ToGormParams() + injections, total, err := s.repo.ListProjectInjectionsView(projectID, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list injections for project %d: %w", projectID, err) + } + + items := make([]InjectionResp, 0, len(injections)) + for _, injection := range injections { + items = append(items, *NewInjectionResp(&injection)) + } + + return &dto.ListResp[InjectionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) Search(ctx context.Context, req *SearchInjectionReq, projectID *int) (*dto.SearchResp[InjectionDetailResp], error) { + if req == nil { + return nil, fmt.Errorf("search injection request is nil") + } + injections, total, err := s.repo.SearchInjections(req, projectID) + if err != nil { + return nil, fmt.Errorf("failed to search injections: %w", err) + } + items := make([]InjectionDetailResp, 0, len(injections)) + for _, injection := range injections { + items = append(items, *NewInjectionDetailResp(&injection)) + } + + resp := &dto.SearchResp[InjectionDetailResp]{ + Pagination: req.ConvertToPaginationInfo(total), + } + if len(req.GroupBy) > 0 { + resp.Groups = dto.BuildGroupTree(items, req.GroupBy) + } else { + resp.Items = items + } + return resp, nil +} + +func (s *Service) ListNoIssues(ctx context.Context, req *ListInjectionNoIssuesReq, projectID *int) ([]InjectionNoIssuesResp, error) { + if len(req.Labels) == 0 { + return nil, nil + } + + labelConditions := make([]map[string]string, 0, len(req.Labels)) + for _, item := range req.Labels { + parts := splitLabelCondition(item) + labelConditions = append(labelConditions, map[string]string{"key": parts[0], "value": parts[1]}) + } + + opts, err := req.Convert() + if err != nil { + return nil, fmt.Errorf("invalid time range: %w", err) + } + + records, err := s.repo.ListIssuesFreeInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) + if err != nil { + return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) + } + + items := make([]InjectionNoIssuesResp, 0, len(records)) + for i, record := range records { + resp, err := NewInjectionNoIssuesResp(record) + if err != nil { + return nil, fmt.Errorf("failed to create InjectionNoIssuesResp at index %d: %w", i, err) + } + items = append(items, *resp) + } + return items, nil +} + +func (s *Service) ListWithIssues(ctx context.Context, req *ListInjectionWithIssuesReq, projectID *int) ([]InjectionWithIssuesResp, error) { + if len(req.Labels) == 0 { + return nil, nil + } + + labelConditions := make([]map[string]string, 0, len(req.Labels)) + for _, item := range req.Labels { + parts := splitLabelCondition(item) + labelConditions = append(labelConditions, map[string]string{"key": parts[0], "value": parts[1]}) + } + + opts, err := req.Convert() + if err != nil { + return nil, fmt.Errorf("invalid time range: %w", err) + } + + records, err := s.repo.ListIssueInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) + if err != nil { + return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) + } + + items := make([]InjectionWithIssuesResp, 0, len(records)) + for _, record := range records { + resp, err := NewInjectionWithIssuesResp(record) + if err != nil { + return nil, fmt.Errorf("failed to create InjectionNoIssuesResp: %w", err) + } + items = append(items, *resp) + } + return items, nil +} + +func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjectionReq, groupID string, userID int, projectID *int) (*SubmitInjectionResp, error) { + if req == nil { + return nil, fmt.Errorf("submit injection request is nil") + } + db := s.repo.db + + if projectID == nil { + project, err := s.repo.ResolveProject(req.ProjectName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + projectID = &project.ID + } + + pedestalVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, []*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) + if err != nil { + return nil, fmt.Errorf("failed to map pedestal container ref to version: %w", err) + } + pedestalVersion, exists := pedestalVersionResults[&req.Pedestal.ContainerRef] + if !exists { + return nil, fmt.Errorf("pedestal version not found for container: %s (version: %s)", req.Pedestal.Name, req.Pedestal.Version) + } + + helmConfig, err := s.repo.LoadPedestalHelmConfig(pedestalVersion.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: helm config not found for pedestal version id %d", consts.ErrNotFound, pedestalVersion.ID) + } + return nil, fmt.Errorf("failed to get helm config: %w", err) + } + + params := flattenYAMLToParameters(req.Pedestal.Payload, "") + helmValues, err := common.ListHelmConfigValuesWithDB(db, params, helmConfig) + if err != nil { + return nil, fmt.Errorf("failed to render pedestal helm values: %w", err) + } + + helmConfigItem := dto.NewHelmConfigItem(helmConfig) + helmConfigItem.DynamicValues = helmValues + + pedestalItem := dto.NewContainerVersionItem(&pedestalVersion) + pedestalItem.Extra = helmConfigItem + + benchmarkVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, []*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) + if err != nil { + return nil, fmt.Errorf("failed to map benchmark container ref to version: %w", err) + } + benchmarkVersion, exists := benchmarkVersionResults[&req.Benchmark.ContainerRef] + if !exists { + return nil, fmt.Errorf("benchmark version not found for container: %s (version: %s)", req.Benchmark.Name, req.Benchmark.Version) + } + + benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) + envVars, err := common.ListContainerVersionEnvVarsWithDB(db, req.Benchmark.EnvVars, &benchmarkVersion) + if err != nil { + return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) + } + benchmarkVersionItem.EnvVars = envVars + + processedItems := make([]injectionProcessItem, 0, len(req.Specs)) + var parseWarnings []string + for i := range req.Specs { + item, warning, err := parseBatchInjectionSpecs(pedestalItem.ContainerName, i, req.Specs[i]) + if err != nil { + return nil, fmt.Errorf("failed to parse injection spec batch %d: %w", i, err) + } + if warning != "" { + parseWarnings = append(parseWarnings, warning) + } else { + processedItems = append(processedItems, *item) + } + } + + uniqueItems, duplicatedInRequest, alreadyExisted, err := s.removeDuplicated(processedItems) + if err != nil { + return nil, fmt.Errorf("failed to remove duplicated batches: %w", err) + } + + var warnings *InjectionWarnings + if len(parseWarnings) > 0 || len(duplicatedInRequest) > 0 || len(alreadyExisted) > 0 { + warnings = &InjectionWarnings{ + DuplicateServicesInBatch: parseWarnings, + DuplicateBatchesInRequest: duplicatedInRequest, + BatchesExistInDatabase: alreadyExisted, + } + } + + if len(req.Algorithms) > 0 { + refs := make([]*dto.ContainerRef, 0, len(req.Algorithms)) + for i := range req.Algorithms { + refs = append(refs, &req.Algorithms[i].ContainerRef) + } + + algorithmVersionsResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeAlgorithm, userID) + if err != nil { + return nil, fmt.Errorf("failed to map container refs to versions: %w", err) + } + + var algorithmVersionItems []dto.ContainerVersionItem + for i := range req.Algorithms { + spec := &req.Algorithms[i] + algorithmVersion, exists := algorithmVersionsResults[&spec.ContainerRef] + if !exists { + return nil, fmt.Errorf("algorithm version not found for %v", spec) + } + + algorithmVersionItem := dto.NewContainerVersionItem(&algorithmVersion) + envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.EnvVars, &algorithmVersion) + if err != nil { + return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) + } + + algorithmVersionItem.EnvVars = envVars + algorithmVersionItems = append(algorithmVersionItems, algorithmVersionItem) + } + + if len(algorithmVersionItems) > 0 { + if err := s.redis.SetHashField(ctx, consts.InjectionAlgorithmsKey, groupID, algorithmVersionItems); err != nil { + return nil, fmt.Errorf("failed to store injection algorithms: %w", err) + } + } + } + + injectionItems := make([]SubmitInjectionItem, 0, len(uniqueItems)) + for _, item := range uniqueItems { + payload := map[string]any{ + consts.RestartPedestal: pedestalItem, + consts.RestartHelmConfig: helmConfig, + consts.RestartIntarval: req.Interval, + consts.RestartFaultDuration: item.faultDuration, + consts.RestartInjectPayload: map[string]any{ + consts.InjectBenchmark: benchmarkVersionItem, + consts.InjectPreDuration: req.PreDuration, + consts.InjectNodes: item.nodes, + consts.InjectLabels: req.Labels, + consts.InjectSystem: chaos.SystemType(pedestalItem.ContainerName), + }, + } + + task := &dto.UnifiedTask{ + Type: consts.TaskTypeRestartPedestal, + Immediate: false, + ExecuteTime: item.executeTime.Unix(), + Payload: payload, + GroupID: groupID, + ProjectID: *projectID, + UserID: userID, + State: consts.TaskPending, + Extra: map[consts.TaskExtra]any{ + consts.TaskExtraInjectionAlgorithms: len(req.Algorithms), + }, + } + task.SetGroupCtx(ctx) + + if err := common.SubmitTaskWithDB(ctx, db, s.redis, task); err != nil { + return nil, fmt.Errorf("failed to submit fault injection task: %w", err) + } + + injectionItems = append(injectionItems, SubmitInjectionItem{ + Index: item.index, + TraceID: task.TraceID, + TaskID: task.TaskID, + }) + } + + sort.Slice(injectionItems, func(i, j int) bool { return injectionItems[i].Index < injectionItems[j].Index }) + return &SubmitInjectionResp{ + GroupID: groupID, + Items: injectionItems, + OriginalCount: len(processedItems), + Warnings: warnings, + }, nil +} + +func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*SubmitDatapackBuildingResp, error) { + if req == nil { + return nil, fmt.Errorf("submit datapack building request is nil") + } + db := s.repo.db + + if projectID == nil { + project, err := s.repo.ResolveProject(req.ProjectName) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + projectID = &project.ID + } + + refs := make([]*dto.ContainerRef, 0, len(req.Specs)) + for i := range req.Specs { + refs = append(refs, &req.Specs[i].Benchmark.ContainerRef) + } + + benchmarkVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeBenchmark, userID) + if err != nil { + return nil, fmt.Errorf("failed to map container refs to versions: %w", err) + } + + var allBuildingItems []SubmitBuildingItem + for idx, spec := range req.Specs { + datapacks, datasetVersionID, err := common.ExtractDatapacks(s.repo.db, spec.Datapack, spec.Dataset, userID, consts.TaskTypeBuildDatapack) + if err != nil { + return nil, fmt.Errorf("failed to extract datapacks: %w", err) + } + + benchmarkVersion, exists := benchmarkVersionResults[refs[idx]] + if !exists { + return nil, fmt.Errorf("benchmark version not found for %v", spec.Benchmark) + } + + benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) + envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.Benchmark.EnvVars, &benchmarkVersion) + if err != nil { + return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) + } + benchmarkVersionItem.EnvVars = envVars + + for _, datapack := range datapacks { + if datapack.StartTime == nil || datapack.EndTime == nil { + return nil, fmt.Errorf("datapack %s does not have valid start_time and end_time", datapack.Name) + } + + payload := map[string]any{ + consts.BuildBenchmark: benchmarkVersionItem, + consts.BuildDatapack: dto.NewInjectionItem(&datapack), + consts.BuildDatasetVersionID: datasetVersionID, + consts.BuildLabels: req.Labels, + } + + task := &dto.UnifiedTask{ + Type: consts.TaskTypeBuildDatapack, + Immediate: true, + Payload: payload, + GroupID: groupID, + ProjectID: *projectID, + UserID: userID, + State: consts.TaskPending, + } + task.SetGroupCtx(ctx) + + if err := common.SubmitTaskWithDB(ctx, db, s.redis, task); err != nil { + return nil, fmt.Errorf("failed to submit datapack building task: %w", err) + } + + allBuildingItems = append(allBuildingItems, SubmitBuildingItem{ + Index: idx, + TraceID: task.TraceID, + TaskID: task.TaskID, + }) + } + } + + return &SubmitDatapackBuildingResp{ + GroupID: groupID, + Items: allBuildingItems, + }, nil +} + +func (s *Service) ListInjections(_ context.Context, req *ListInjectionReq) (*dto.ListResp[InjectionResp], error) { + limit, offset := req.ToGormParams() + injections, total, err := s.repo.ListInjectionsView(limit, offset, req.ToFilterOptions()) + if err != nil { + return nil, fmt.Errorf("failed to list injections: %w", err) + } + + items := make([]InjectionResp, 0, len(injections)) + for _, injection := range injections { + items = append(items, *NewInjectionResp(&injection)) + } + + return &dto.ListResp[InjectionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetInjection(_ context.Context, id int) (*InjectionDetailResp, error) { + injection, err := s.repo.GetInjectionWithLabels(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get injection: %w", err) + } + return NewInjectionDetailResp(injection), nil +} + +func (s *Service) GetMetadata(_ context.Context) (*InjectionMetadataResp, error) { + return nil, nil +} + +func (s *Service) ManageLabels(_ context.Context, req *ManageInjectionLabelReq, id int) (*InjectionResp, error) { + var managedInjection *model.FaultInjection + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + injection, err := repo.LoadInjection(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return fmt.Errorf("failed to get injection: %w", err) + } + + if len(req.AddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + labelIDs := make([]int, 0, len(labels)) + for _, label := range labels { + labelIDs = append(labelIDs, label.ID) + } + if err := repo.AddInjectionLabels(injection.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add injection labels: %w", err) + } + } + + if len(req.RemoveLabels) > 0 { + labelIDs, err := repo.ListInjectionLabelIDsByKeys(injection.ID, req.RemoveLabels) + if err != nil { + return fmt.Errorf("failed to find label ids by keys: %w", err) + } + if len(labelIDs) > 0 { + if err := repo.ClearInjectionLabels([]int{id}, labelIDs); err != nil { + return fmt.Errorf("failed to clear injection labels: %w", err) + } + if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + return fmt.Errorf("failed to decrease label usage counts: %w", err) + } + } + } + + managedInjection, err = repo.GetInjectionWithLabels(id) + if err != nil { + return fmt.Errorf("failed to reload injection labels: %w", err) + } + return nil + }) + if err != nil { + return nil, err + } + return NewInjectionResp(managedInjection), nil +} + +func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjectionLabelReq) (*BatchManageInjectionLabelResp, error) { + resp := &BatchManageInjectionLabelResp{ + FailedItems: []string{}, + SuccessItems: []InjectionResp{}, + } + if len(req.Items) == 0 { + return resp, nil + } + + return resp, s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + allInjectionIDs := make([]int, 0, len(req.Items)) + operationMap := make(map[int]*InjectionLabelOperation, len(req.Items)) + for i := range req.Items { + item := &req.Items[i] + allInjectionIDs = append(allInjectionIDs, item.InjectionID) + operationMap[item.InjectionID] = item + } + + foundIDMap, err := repo.LoadExistingInjectionsByID(allInjectionIDs) + if err != nil { + return fmt.Errorf("failed to list injections: %w", err) + } + + validIDs := make([]int, 0, len(foundIDMap)) + for _, id := range allInjectionIDs { + if _, found := foundIDMap[id]; !found { + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d not found", id)) + resp.FailedCount++ + delete(operationMap, id) + } else { + validIDs = append(validIDs, id) + } + } + if len(validIDs) == 0 { + return fmt.Errorf("no valid injection IDs found") + } + + allAddLabels := make([]dto.LabelItem, 0) + allRemoveLabels := make([]dto.LabelItem, 0) + labelKeySet := make(map[string]bool) + for _, op := range operationMap { + for _, label := range op.AddLabels { + key := label.Key + ":" + label.Value + if !labelKeySet[key] { + labelKeySet[key] = true + allAddLabels = append(allAddLabels, label) + } + } + for _, label := range op.RemoveLabels { + key := label.Key + ":" + label.Value + if !labelKeySet[key] { + labelKeySet[key] = true + allRemoveLabels = append(allRemoveLabels, label) + } + } + } + + var labelMap map[string]int + if len(allAddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + labelMap = make(map[string]int, len(labels)) + for _, label := range labels { + labelMap[label.Key+":"+label.Value] = label.ID + } + } + + var removeLabelMap map[string]int + if len(allRemoveLabels) > 0 { + labelConditions := make([]map[string]string, 0, len(allRemoveLabels)) + for _, item := range allRemoveLabels { + labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) + } + removeLabelMap, err = repo.LoadInjectionLabelIDsByItems(labelConditions, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to find labels to remove: %w", err) + } + } + + for _, injectionID := range validIDs { + op := operationMap[injectionID] + if len(op.AddLabels) > 0 { + labelIDsToAdd := make([]int, 0, len(op.AddLabels)) + for _, label := range op.AddLabels { + if labelID, exists := labelMap[label.Key+":"+label.Value]; exists { + labelIDsToAdd = append(labelIDsToAdd, labelID) + } + } + if len(labelIDsToAdd) > 0 { + if err := repo.AddInjectionLabels(injectionID, labelIDsToAdd); err != nil { + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to add labels - %s", injectionID, err.Error())) + resp.FailedCount++ + delete(foundIDMap, injectionID) + continue + } + } + } + + if len(op.RemoveLabels) > 0 && removeLabelMap != nil { + labelIDsToRemove := make([]int, 0, len(op.RemoveLabels)) + for _, label := range op.RemoveLabels { + if labelID, exists := removeLabelMap[label.Key+":"+label.Value]; exists { + labelIDsToRemove = append(labelIDsToRemove, labelID) + } + } + if len(labelIDsToRemove) > 0 { + if err := repo.ClearInjectionLabels([]int{injectionID}, labelIDsToRemove); err != nil { + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to remove labels - %s", injectionID, err.Error())) + resp.FailedCount++ + delete(foundIDMap, injectionID) + continue + } + } + } + } + + if len(foundIDMap) > 0 { + successIDs := make([]int, 0, len(foundIDMap)) + for id := range foundIDMap { + successIDs = append(successIDs, id) + } + updatedInjections, err := repo.ListFaultInjectionsByIDWithLabels(successIDs) + if err != nil { + return fmt.Errorf("failed to fetch updated injections: %w", err) + } + for i := range updatedInjections { + injection := &updatedInjections[i] + resp.SuccessItems = append(resp.SuccessItems, *NewInjectionResp(injection)) + resp.SuccessCount++ + } + } + + return nil + }) +} + +func (s *Service) BatchDelete(ctx context.Context, req *BatchDeleteInjectionReq) error { + if len(req.IDs) > 0 { + return s.batchDeleteByIDs(req.IDs) + } + return s.batchDeleteByLabels(req.Labels) +} + +func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*InjectionDetailResp, error) { + original, err := s.repo.LoadInjection(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get injection: %w", err) + } + + cloned := &model.FaultInjection{ + Name: req.Name, + FaultType: original.FaultType, + Category: original.Category, + Description: original.Description, + DisplayConfig: original.DisplayConfig, + EngineConfig: original.EngineConfig, + Groundtruths: original.Groundtruths, + PreDuration: original.PreDuration, + StartTime: original.StartTime, + EndTime: original.EndTime, + BenchmarkID: original.BenchmarkID, + PedestalID: original.PedestalID, + State: consts.DatapackInitial, + Status: consts.CommonEnabled, + } + + err = s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.CreateInjectionRecord(cloned); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, cloned.Name) + } + return fmt.Errorf("failed to create injection: %w", err) + } + if len(req.Labels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + labelIDs := make([]int, 0, len(labels)) + for _, label := range labels { + labelIDs = append(labelIDs, label.ID) + } + if err := repo.AddInjectionLabels(cloned.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add injection labels: %w", err) + } + } + return nil + }) + if err != nil { + return nil, err + } + + cloned, err = s.repo.GetInjectionWithLabels(cloned.ID) + if err != nil { + return nil, fmt.Errorf("failed to get cloned injection labels: %w", err) + } + return NewInjectionDetailResp(cloned), nil +} + +func (s *Service) GetLogs(ctx context.Context, id int) (*InjectionLogsResp, error) { + injection, err := s.repo.LoadInjection(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get injection: %w", err) + } + + resp := &InjectionLogsResp{InjectionID: id, Logs: []string{}} + if injection.TaskID == nil { + return resp, nil + } + + resp.TaskID = *injection.TaskID + task, taskErr := s.repo.LoadTask(*injection.TaskID) + if taskErr != nil { + return resp, nil + } + + lokiCtx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + logEntries, lokiErr := s.lokiClient.QueryJobLogs(lokiCtx, *injection.TaskID, lokiinfra.QueryOpts{ + Start: task.CreatedAt, + Direction: "forward", + }) + if lokiErr != nil { + return resp, nil + } + for _, entry := range logEntries { + resp.Logs = append(resp.Logs, entry.Line) + } + return resp, nil +} + +func (s *Service) GetDatapackFilename(_ context.Context, id int) (string, error) { + injection, err := s.repo.LoadInjection(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return "", fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return "", fmt.Errorf("failed to get injection: %w", err) + } + if injection.State < consts.DatapackBuildSuccess { + return "", fmt.Errorf("datapack for injection id %d is not ready for download", id) + } + return injection.Name, nil +} + +func (s *Service) DownloadDatapack(_ context.Context, zipWriter *zip.Writer, excludeRules []utils.ExculdeRule, id int) error { + if zipWriter == nil { + return fmt.Errorf("zip writer cannot be nil") + } + injection, err := s.getReadyDatapack(id) + if err != nil { + return err + } + if err := s.store.Package(zipWriter, injection.Name, excludeRules); err != nil { + return fmt.Errorf("failed to package injection to zip: %w", err) + } + return nil +} + +func (s *Service) GetDatapackFiles(_ context.Context, id int, baseURL string) (*DatapackFilesResp, error) { + injection, err := s.getReadyDatapack(id) + if err != nil { + return nil, err + } + resp, err := s.store.BuildFileTree(injection.Name, baseURL, id) + if err != nil { + return nil, fmt.Errorf("failed to build file tree: %w", err) + } + return resp, nil +} + +func (s *Service) DownloadDatapackFile(_ context.Context, id int, filePath string) (string, string, int64, io.ReadSeekCloser, error) { + injection, err := s.getReadyDatapack(id) + if err != nil { + return "", "", 0, nil, err + } + return s.store.OpenFile(injection.Name, filePath) +} + +func (s *Service) QueryDatapackFile(ctx context.Context, id int, filePath string) (string, int64, io.ReadCloser, error) { + return s.queryDatapackFileContent(ctx, id, filePath) +} + +func (s *Service) UpdateGroundtruth(_ context.Context, id int, req *UpdateGroundtruthReq) error { + if _, err := s.repo.LoadInjection(id); err != nil { + return err + } + return s.repo.UpdateGroundtruth(id, req.Groundtruths, consts.GroundtruthSourceManual) +} + +func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file io.Reader, fileSize int64) (*UploadDatapackResp, error) { + _ = fileSize + + labels, err := req.ParseLabels() + if err != nil { + return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) + } + + groundtruths, err := req.ParseGroundtruths() + if err != nil { + return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) + } + + existing, _ := s.repo.FindInjectionByName(req.Name, false) + if existing != nil { + return nil, fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, req.Name) + } + + tmpFile, err := s.store.CreateUploadTempFile() + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %w", err) + } + tmpPath := tmpFile.Name() + defer func() { _ = s.store.Remove(tmpPath) }() + + if _, err := io.Copy(tmpFile, file); err != nil { + _ = tmpFile.Close() + return nil, fmt.Errorf("failed to save uploaded file: %w", err) + } + if err := tmpFile.Close(); err != nil { + return nil, fmt.Errorf("failed to close uploaded file: %w", err) + } + + if err := s.store.ValidateArchive(tmpPath); err != nil { + return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) + } + + targetDir, err := s.store.EnsureDatapackDirAvailable(req.Name) + if err != nil { + return nil, err + } + if err := s.store.ExtractArchive(tmpPath, targetDir); err != nil { + _ = s.store.RemoveAll(targetDir) + return nil, fmt.Errorf("failed to extract archive: %w", err) + } + + groundtruthSource := "" + if len(groundtruths) > 0 { + groundtruthSource = consts.GroundtruthSourceManual + } else { + groundtruths = s.store.ExtractGroundtruths(targetDir) + if len(groundtruths) > 0 { + groundtruthSource = consts.GroundtruthSourceImported + } + } + + category := chaos.SystemType("") + if req.Category != "" { + category = chaos.SystemType(req.Category) + } + + injection := &model.FaultInjection{ + Name: req.Name, + Source: consts.DatapackSourceManual, + FaultType: chaos.ChaosType(0), + Category: category, + Description: req.Description, + EngineConfig: "", + Groundtruths: groundtruths, + GroundtruthSource: groundtruthSource, + PreDuration: 0, + BenchmarkID: nil, + PedestalID: nil, + State: consts.DatapackBuildSuccess, + Status: consts.CommonEnabled, + } + + err = s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.CreateInjectionRecord(injection); err != nil { + return err + } + + if len(labels) > 0 { + createdLabels, err := common.CreateOrUpdateLabelsFromItems(tx, labels, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + labelIDs := make([]int, 0, len(createdLabels)) + for _, label := range createdLabels { + labelIDs = append(labelIDs, label.ID) + } + + if err := repo.AddInjectionLabels(injection.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add injection labels: %w", err) + } + } + return nil + }) + if err != nil { + _ = s.store.RemoveAll(targetDir) + return nil, err + } + + return &UploadDatapackResp{ + ID: injection.ID, + Name: injection.Name, + }, nil +} + +func (s *Service) getReadyDatapack(id int) (*model.FaultInjection, error) { + injection, err := s.repo.LoadInjection(id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get injection: %w", err) + } + if injection.State < consts.DatapackBuildSuccess { + return nil, fmt.Errorf("datapack %d is not ready", id) + } + return injection, nil +} + +func (s *Service) batchDeleteByIDs(injectionIDs []int) error { + if len(injectionIDs) == 0 { + return nil + } + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + return repo.DeleteInjectionsCascade(injectionIDs) + }) +} + +func (s *Service) batchDeleteByLabels(labelItems []dto.LabelItem) error { + if len(labelItems) == 0 { + return nil + } + labelConditions := make([]map[string]string, 0, len(labelItems)) + for _, item := range labelItems { + labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) + } + injectionIDs, err := s.repo.ListInjectionIDsByLabelConditions(labelConditions) + if err != nil { + return fmt.Errorf("failed to list injection ids by labels: %w", err) + } + return s.batchDeleteByIDs(injectionIDs) +} + +func splitLabelCondition(item string) [2]string { + parts := strings.SplitN(item, ":", 2) + if len(parts) == 1 { + return [2]string{parts[0], ""} + } + return [2]string{parts[0], parts[1]} +} diff --git a/src/module/injection/service_test.go b/src/module/injection/service_test.go new file mode 100644 index 00000000..c43da5eb --- /dev/null +++ b/src/module/injection/service_test.go @@ -0,0 +1,146 @@ +package injectionmodule + +import ( + "regexp" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + "aegis/testutil" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newInjectionService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + addr, cleanupRedis := testutil.StartRedisStub(t) + viper.Set("redis.host", addr) + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db), nil, nil, redisinfra.NewGateway(nil)), mock, func() { + cleanupRedis() + _ = sqlDB.Close() + } +} + +func TestServiceSearchNilRequest(t *testing.T) { + service := NewService(nil, nil, nil, nil) + + _, err := service.Search(t.Context(), nil, nil) + + require.Error(t, err) + require.ErrorContains(t, err, "search injection request is nil") +} + +func TestServiceListNoIssuesEmptyLabelsSucceeds(t *testing.T) { + service := NewService(nil, nil, nil, nil) + + resp, err := service.ListNoIssues(t.Context(), &ListInjectionNoIssuesReq{}, nil) + + require.NoError(t, err) + require.Nil(t, resp) +} + +func TestServiceListProjectInjectionsSuccess(t *testing.T) { + service, mock, cleanup := newInjectionService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `projects` WHERE id = ? ORDER BY `projects`.`id` LIMIT ?")). + WithArgs(7, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", + }).AddRow(7, "demo-project", "demo", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery("SELECT count\\(\\*\\) FROM `fault_injections` JOIN tasks ON tasks\\.id = fault_injections\\.task_id JOIN traces on traces\\.id = tasks\\.trace_id WHERE traces\\.project_id = \\? AND fault_injections\\.status != \\?"). + WithArgs(7, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery("SELECT `fault_injections`\\.`id`,`fault_injections`\\.`name`.*FROM `fault_injections` JOIN tasks ON tasks\\.id = fault_injections\\.task_id JOIN traces on traces\\.id = tasks\\.trace_id WHERE traces\\.project_id = \\? AND fault_injections\\.status != \\? ORDER BY fault_injections\\.updated_at DESC LIMIT \\?"). + WithArgs(7, consts.CommonDeleted, 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "source", "fault_type", "category", "description", "display_config", "engine_config", "groundtruths", "groundtruth_source", "pre_duration", "start_time", "end_time", "benchmark_id", "pedestal_id", "task_id", "state", "status", "created_at", "updated_at", + })) + + resp, err := service.ListProjectInjections(t.Context(), &ListInjectionReq{}, 7) + + require.NoError(t, err) + require.Empty(t, resp.Items) + require.Equal(t, int64(0), resp.Pagination.Total) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceSubmitDatapackBuildingSuccess(t *testing.T) { + addr, cleanupRedis := testutil.StartRedisStub(t) + defer cleanupRedis() + viper.Set("redis.host", addr) + + service, mock, cleanup := newInjectionService(t) + defer cleanup() + + mock.MatchExpectationsInOrder(false) + + now := time.Now() + start := now.Add(-10 * time.Minute) + end := now.Add(-2 * time.Minute) + projectID := 9 + datapackName := "dp-build" + + mock.ExpectQuery("SELECT .* FROM container_versions cv .*"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "name_major", "name_minor", "name_patch", "github_link", "registry", "namespace", "repository", "tag", "command", "usage_count", "container_id", "user_id", "status", "created_at", "updated_at", + }).AddRow(4, "1.0.0", 1, 0, 0, "", "docker.io", "", "bench", "latest", "", 0, 6, 1, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `containers` WHERE `containers`.`id` = ?")). + WithArgs(6). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "type", "readme", "is_public", "status", "created_at", "updated_at", + }).AddRow(6, "bench", consts.ContainerTypeBenchmark, "", true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fault_injections` WHERE name = ? AND status != ? ORDER BY `fault_injections`.`id` LIMIT ?")). + WithArgs(datapackName, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "source", "fault_type", "category", "description", "display_config", "engine_config", "groundtruths", "groundtruth_source", "pre_duration", "start_time", "end_time", "benchmark_id", "pedestal_id", "task_id", "state", "status", "created_at", "updated_at", + }).AddRow(11, datapackName, consts.DatapackSourceInjection, 0, "ts", "", nil, "{}", "[]", "auto", 5, start, end, nil, nil, nil, consts.DatapackInjectSuccess, consts.CommonEnabled, now, now)) + mock.ExpectQuery("SELECT .* FROM `fault_injection_labels` .*"). + WillReturnRows(sqlmock.NewRows([]string{"fault_injection_id", "label_id"})) + mock.ExpectQuery("SELECT .* FROM `parameter_configs` JOIN container_version_env_vars .*"). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "config_key", "type", "category", "value_type", "description", "default_value", "template_string", "required", "overridable", + })) + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `traces`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `tasks`")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + resp, err := service.SubmitDatapackBuilding(t.Context(), &SubmitDatapackBuildingReq{ + Specs: []BuildingSpec{ + { + Benchmark: dto.ContainerSpec{ + ContainerRef: dto.ContainerRef{Name: "bench", Version: "1.0.0"}, + }, + Datapack: &datapackName, + }, + }, + }, "group-build", 1, &projectID) + + require.NoError(t, err) + require.Equal(t, "group-build", resp.GroupID) + require.Len(t, resp.Items, 1) + require.NotEmpty(t, resp.Items[0].TaskID) + require.NotEmpty(t, resp.Items[0].TraceID) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/module/injection/submit.go b/src/module/injection/submit.go new file mode 100644 index 00000000..2c64fc3b --- /dev/null +++ b/src/module/injection/submit.go @@ -0,0 +1,205 @@ +package injectionmodule + +import ( + "aegis/consts" + "aegis/dto" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" + "github.com/sirupsen/logrus" +) + +type injectionProcessItem struct { + index int + faultDuration int + nodes []chaos.Node + executeTime time.Time +} + +func parseBatchInjectionSpecs(pedestal string, batchIndex int, specs []chaos.Node) (*injectionProcessItem, string, error) { + if len(specs) == 0 { + return nil, "", fmt.Errorf("empty fault injection batch at index %d", batchIndex) + } + + maxDuration := 0 + nodes := make([]chaos.Node, 0, len(specs)) + for idx, spec := range specs { + childNode, exists := spec.Children[strconv.Itoa(spec.Value)] + if !exists { + return nil, "", fmt.Errorf("failed to find key %d in the children at index %d", spec.Value, idx) + } + if len(childNode.Children) < 3 { + return nil, "", fmt.Errorf("no child nodes found for fault spec at index %d", idx) + } + + faultDuration := childNode.Children[consts.DurationNodeKey].Value + if faultDuration > maxDuration { + maxDuration = faultDuration + } + + systemIdx := childNode.Children[consts.SystemNodeKey].Value + system := chaos.GetAllSystemTypes()[systemIdx] + if pedestal != system.String() { + return nil, "", fmt.Errorf("mismatched system type %s for pedestal %s at index %d", system.String(), pedestal, idx) + } + + nodes = append(nodes, spec) + } + + uniqueServices := make(map[string]int, len(nodes)) + var duplicateServiceWarnings []string + for idx, node := range nodes { + conf, err := chaos.NodeToStruct[chaos.InjectionConf](&node) + if err != nil { + return nil, "", fmt.Errorf("failed to convert node to InjectionConf at index %d: %w", idx, err) + } + + groundtruth, err := conf.GetGroundtruth() + if err != nil { + return nil, "", fmt.Errorf("failed to get groundtruth from InjectionConf at index %d: %w", idx, err) + } + + for _, service := range groundtruth.Service { + if service == "" { + continue + } + if oldIdx, exists := uniqueServices[service]; exists { + duplicateServiceWarnings = append(duplicateServiceWarnings, fmt.Sprintf("service '%s' at positions %d and %d", service, oldIdx, idx)) + continue + } + uniqueServices[service] = idx + } + } + + nodes = sortNodes(nodes) + + var warning string + if len(duplicateServiceWarnings) > 0 { + warning = fmt.Sprintf("Batch %d contains duplicate service injections: %s", batchIndex, strings.Join(duplicateServiceWarnings, "; ")) + } + + return &injectionProcessItem{ + index: batchIndex, + faultDuration: maxDuration, + nodes: nodes, + }, warning, nil +} + +func flattenYAMLToParameters(data map[string]any, prefix string) []dto.ParameterSpec { + var params []dto.ParameterSpec + for key, value := range data { + fullKey := key + if prefix != "" { + fullKey = prefix + "." + key + } + + switch v := value.(type) { + case map[string]any: + params = append(params, flattenYAMLToParameters(v, fullKey)...) + case []any: + jsonBytes, err := json.Marshal(v) + if err != nil { + logrus.Warnf("Failed to marshal array for key %s: %v", fullKey, err) + continue + } + params = append(params, dto.ParameterSpec{Key: fullKey, Value: string(jsonBytes)}) + default: + params = append(params, dto.ParameterSpec{Key: fullKey, Value: v}) + } + } + return params +} + +func (s *Service) removeDuplicated(items []injectionProcessItem) ([]injectionProcessItem, []int, []int, error) { + engineConfigStrs := make([]string, len(items)) + for i, item := range items { + if len(item.nodes) == 0 { + continue + } + + b, err := json.Marshal(item.nodes) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to marshal engine config at batch index %d: %w", i, err) + } + engineConfigStrs[i] = string(b) + } + + orderedUniqueIdx := make([]int, 0, len(engineConfigStrs)) + seen := make(map[string]struct{}, len(engineConfigStrs)) + duplicatedInRequest := make([]int, 0) + for i, key := range engineConfigStrs { + if key == "" { + orderedUniqueIdx = append(orderedUniqueIdx, i) + continue + } + if _, ok := seen[key]; ok { + duplicatedInRequest = append(duplicatedInRequest, items[i].index) + continue + } + seen[key] = struct{}{} + orderedUniqueIdx = append(orderedUniqueIdx, i) + } + + keys := make([]string, 0, len(seen)) + for k := range seen { + keys = append(keys, k) + } + + existed := make(map[string]struct{}) + for start := 0; start < len(keys); start += 100 { + end := min(start+100, len(keys)) + existing, err := s.repo.ListExistingEngineConfigs(keys[start:end]) + if err != nil { + return nil, nil, nil, err + } + for _, v := range existing { + existed[v] = struct{}{} + } + } + + out := make([]injectionProcessItem, 0, len(orderedUniqueIdx)) + alreadyExisted := make([]int, 0) + for _, idx := range orderedUniqueIdx { + key := engineConfigStrs[idx] + if key != "" { + if _, ok := existed[key]; ok { + alreadyExisted = append(alreadyExisted, items[idx].index) + continue + } + } + + items[idx].executeTime = time.Now().Add(time.Duration(idx*2) * time.Second) + out = append(out, items[idx]) + } + + return out, duplicatedInRequest, alreadyExisted, nil +} + +func sortNodes(nodes []chaos.Node) []chaos.Node { + if len(nodes) <= 1 { + return nodes + } + + sortedNodes := make([]chaos.Node, len(nodes)) + copy(sortedNodes, nodes) + for i := 0; i < len(sortedNodes)-1; i++ { + for j := i + 1; j < len(sortedNodes); j++ { + if sortedNodes[i].Value > sortedNodes[j].Value { + sortedNodes[i], sortedNodes[j] = sortedNodes[j], sortedNodes[i] + continue + } + if sortedNodes[i].Value == sortedNodes[j].Value { + iJSON, _ := json.Marshal(sortedNodes[i]) + jJSON, _ := json.Marshal(sortedNodes[j]) + if string(iJSON) > string(jJSON) { + sortedNodes[i], sortedNodes[j] = sortedNodes[j], sortedNodes[i] + } + } + } + } + return sortedNodes +} diff --git a/src/dto/request.go b/src/module/injection/time_range.go similarity index 56% rename from src/dto/request.go rename to src/module/injection/time_range.go index 02592b06..5f8b3872 100644 --- a/src/dto/request.go +++ b/src/module/injection/time_range.go @@ -1,4 +1,4 @@ -package dto +package injectionmodule import ( "fmt" @@ -23,36 +23,28 @@ type TimeFilterOptions struct { } func (req *TimeRangeQuery) Convert() (*TimeFilterOptions, error) { - opts := &TimeFilterOptions{ - Lookback: 0, - UseCustomRange: false, - CustomStartTime: time.Time{}, - CustomEndTime: time.Time{}, - } - + opts := &TimeFilterOptions{} if req.Lookback != "custom" { duration, err := parseLookbackDuration(req.Lookback) if err != nil { return nil, fmt.Errorf("invalid lookback value: %v", err) } - opts.Lookback = duration - } else { - customStart, err := time.Parse(time.RFC3339, req.CustomStartStr) - if err != nil { - return nil, fmt.Errorf("invalid custom start time: %v", err) - } - - customEnd, err := time.Parse(time.RFC3339, req.CustomEndStr) - if err != nil { - return nil, fmt.Errorf("invalid custom end time: %v", err) - } + return opts, nil + } - opts.UseCustomRange = true - opts.CustomStartTime = customStart - opts.CustomEndTime = customEnd + customStart, err := time.Parse(time.RFC3339, req.CustomStartStr) + if err != nil { + return nil, fmt.Errorf("invalid custom start time: %v", err) + } + customEnd, err := time.Parse(time.RFC3339, req.CustomEndStr) + if err != nil { + return nil, fmt.Errorf("invalid custom end time: %v", err) } + opts.UseCustomRange = true + opts.CustomStartTime = customStart + opts.CustomEndTime = customEnd return opts, nil } @@ -61,44 +53,36 @@ func (req *TimeRangeQuery) Validate() error { if _, err := parseLookbackDuration(req.Lookback); err != nil { return fmt.Errorf("invalid lookback value: %s", req.Lookback) } - } else { - if req.CustomStartStr == "" || req.CustomEndStr == "" { - return fmt.Errorf("custom start and end times are required for custom lookback") - } - - startTime, err := time.Parse(time.RFC3339, req.CustomStartStr) - if err != nil { - return fmt.Errorf("invalid custom start time: %v", err) - } - - endTime, err := time.Parse(time.RFC3339, req.CustomEndStr) - if err != nil { - return fmt.Errorf("invalid custom end time: %v", err) - } + return nil + } - if startTime.After(endTime) { - return fmt.Errorf("custom start time cannot be after custom end time") - } + if req.CustomStartStr == "" || req.CustomEndStr == "" { + return fmt.Errorf("custom start and end times are required for custom lookback") } + startTime, err := time.Parse(time.RFC3339, req.CustomStartStr) + if err != nil { + return fmt.Errorf("invalid custom start time: %v", err) + } + endTime, err := time.Parse(time.RFC3339, req.CustomEndStr) + if err != nil { + return fmt.Errorf("invalid custom end time: %v", err) + } + if startTime.After(endTime) { + return fmt.Errorf("custom start time cannot be after custom end time") + } return nil } func (opts *TimeFilterOptions) GetTimeRange() (time.Time, time.Time) { now := time.Now() - var startTime, endTime time.Time if opts.UseCustomRange { - startTime = opts.CustomStartTime - endTime = opts.CustomEndTime - } else if opts.Lookback != 0 { - endTime = now - startTime = now.Add(-opts.Lookback) - } else { - endTime = now - startTime = time.Time{} + return opts.CustomStartTime, opts.CustomEndTime } - - return startTime, endTime + if opts.Lookback != 0 { + return now.Add(-opts.Lookback), now + } + return time.Time{}, now } func (opts *TimeFilterOptions) AddTimeFilter(query *gorm.DB, column string) *gorm.DB { @@ -106,17 +90,13 @@ func (opts *TimeFilterOptions) AddTimeFilter(query *gorm.DB, column string) *gor return query.Where(fmt.Sprintf("%s >= ? AND %s <= ?", column, column), startTime, endTime) } -// parseLookbackDuration parses a duration string with format like "5m", "2h", "1d" -// Supports: m (minutes), h (hours), d (days) func parseLookbackDuration(lookback string) (time.Duration, error) { if lookback == "" { return 0, nil } - // Use regex to match patterns like "5m", "2h", "1d" re := regexp.MustCompile(`^(\d+)([mhd])$`) matches := re.FindStringSubmatch(lookback) - if len(matches) != 3 { return 0, fmt.Errorf("invalid duration format: %s (expected format: 5m, 2h, 1d)", lookback) } @@ -125,13 +105,11 @@ func parseLookbackDuration(lookback string) (time.Duration, error) { if err != nil { return 0, fmt.Errorf("invalid duration value: %s", matches[1]) } - if value <= 0 { return 0, fmt.Errorf("duration value must be a positive integer: %s", matches[1]) } - unit := matches[2] - switch unit { + switch matches[2] { case "m": return time.Duration(value) * time.Minute, nil case "h": @@ -139,6 +117,6 @@ func parseLookbackDuration(lookback string) (time.Duration, error) { case "d": return time.Duration(value) * 24 * time.Hour, nil default: - return 0, fmt.Errorf("invalid duration unit: %s (supported: m, h, d)", unit) + return 0, fmt.Errorf("invalid duration unit: %s (supported: m, h, d)", matches[2]) } } diff --git a/src/module/label/api_types.go b/src/module/label/api_types.go new file mode 100644 index 00000000..77fc19e3 --- /dev/null +++ b/src/module/label/api_types.go @@ -0,0 +1,219 @@ +package labelmodule + +import ( + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/utils" +) + +// BatchDeleteLabelReq represents the request to batch delete labels. +type BatchDeleteLabelReq struct { + IDs []int `json:"ids" binding:"omitempty"` +} + +func (req *BatchDeleteLabelReq) Validate() error { + if len(req.IDs) == 0 { + return fmt.Errorf("ids cannot be empty") + } + for i, id := range req.IDs { + if id <= 0 { + return fmt.Errorf("invalid id at index %d: %d", i, id) + } + } + return nil +} + +// CreateLabelReq represents label creation request. +type CreateLabelReq struct { + Key string `json:"key" binding:"required"` + Value string `json:"value" binding:"required"` + Category consts.LabelCategory `json:"category" bindging:"required"` + Description string `json:"description" binding:"omitempty"` + Color *string `json:"color" binding:"omitempty"` +} + +func (req *CreateLabelReq) Validate() error { + if err := validateKeyAndValue(req.Key, req.Value); err != nil { + return err + } + if err := validateLabelCategory(req.Category); err != nil { + return err + } + if err := validateColor(req.Color); err != nil { + return err + } + return nil +} + +func (req *CreateLabelReq) ConvertToLabel() *model.Label { + return &model.Label{ + Key: req.Key, + Value: req.Value, + Category: req.Category, + Description: req.Description, + Color: utils.GetStringValue(req.Color, "#1890ff"), + IsSystem: false, + Usage: consts.DefaultLabelUsage, + } +} + +// ListLabelReq is the list-label query contract for the label module. +type ListLabelReq struct { + dto.PaginationReq + + Key string `form:"key" binding:"omitempty"` + Value string `form:"value" binding:"omitempty"` + Category *consts.LabelCategory `form:"category" binding:"omitempty"` + IsSystem *bool `form:"is_system" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +type ListLabelFilters struct { + Key string + Value string + Category *consts.LabelCategory + IsSystem *bool + Status *consts.StatusType +} + +func (req *ListLabelReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if err := validateKeyAndValue(req.Key, req.Value); err != nil { + return err + } + if req.Category != nil { + if err := validateLabelCategory(*req.Category); err != nil { + return err + } + } + return validateStatus(req.Status, false) +} + +func (req *ListLabelReq) ToFilterOptions() *ListLabelFilters { + return &ListLabelFilters{ + Key: req.Key, + Value: req.Value, + Category: req.Category, + IsSystem: req.IsSystem, + Status: req.Status, + } +} + +// UpdateLabelReq represents label update request. +type UpdateLabelReq struct { + Description *string `json:"description" binding:"omitempty"` + Color *string `json:"color" binding:"omitempty"` + Status *consts.StatusType `json:"status,omitempty"` +} + +func (req *UpdateLabelReq) Validate() error { + if err := validateColor(req.Color); err != nil { + return err + } + return validateStatus(req.Status, true) +} + +func (req *UpdateLabelReq) PatchLabelModel(target *model.Label) { + if req.Description != nil { + target.Description = *req.Description + } + if req.Color != nil { + target.Color = *req.Color + } + if req.Status != nil { + target.Status = *req.Status + } +} + +// LabelResp represents a label response. +type LabelResp struct { + ID int `json:"id"` + Key string `json:"key"` + Value string `json:"value"` + Category string `json:"category"` + Color string `json:"color"` + Usage int `json:"usage"` + IsSystem bool `json:"is_system"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewLabelResp(label *model.Label) *LabelResp { + return &LabelResp{ + ID: label.ID, + Key: label.Key, + Value: label.Value, + Category: consts.GetLabelCategoryName(label.Category), + Color: label.Color, + Usage: label.Usage, + IsSystem: label.IsSystem, + Status: consts.GetStatusTypeName(label.Status), + CreatedAt: label.CreatedAt, + UpdatedAt: label.UpdatedAt, + } +} + +// LabelDetailResp represents a detailed label response. +type LabelDetailResp struct { + LabelResp + + Description string `json:"description"` +} + +func NewLabelDetailResp(label *model.Label) *LabelDetailResp { + return &LabelDetailResp{ + LabelResp: *NewLabelResp(label), + Description: label.Description, + } +} + +func validateColor(color *string) error { + if color == nil { + return nil + } + if !utils.IsValidHexColor(*color) { + return fmt.Errorf("invalid color format: %s", *color) + } + return nil +} + +func validateKeyAndValue(key, value string) error { + if key == "" && value == "" { + return nil + } + if key == "" { + return fmt.Errorf("label key cannot be empty when value is provided") + } + if value == "" { + return fmt.Errorf("label value cannot be empty when key is provided") + } + return nil +} + +func validateLabelCategory(category consts.LabelCategory) error { + if _, exists := consts.ValidLabelCategories[category]; !exists { + return fmt.Errorf("invalid label category: %d", category) + } + return nil +} + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} diff --git a/src/module/label/core.go b/src/module/label/core.go new file mode 100644 index 00000000..ba7d03a1 --- /dev/null +++ b/src/module/label/core.go @@ -0,0 +1,40 @@ +package labelmodule + +import ( + "aegis/consts" + "aegis/model" + "errors" + "fmt" + + "gorm.io/gorm" +) + +func CreateLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { + query := db.Where("label_key = ? AND label_value = ?", label.Key, label.Value). + Where("status != ?", consts.CommonDeleted) + + var existingLabel model.Label + err := query.First(&existingLabel).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("failed to check existing label: %w", err) + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + if err := db.Omit(labelKeyOmitFields).Create(label).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return nil, fmt.Errorf("%w: label with key %s and value %s already exists", consts.ErrAlreadyExists, label.Key, label.Value) + } + return nil, fmt.Errorf("failed to create label: %w", err) + } + return label, nil + } + + existingLabel.Category = label.Category + existingLabel.Description = label.Description + existingLabel.Color = label.Color + existingLabel.Status = consts.CommonEnabled + if err := db.Omit(labelKeyOmitFields).Save(&existingLabel).Error; err != nil { + return nil, fmt.Errorf("failed to update existing label: %w", err) + } + return &existingLabel, nil +} diff --git a/src/handlers/v2/labels.go b/src/module/label/handler.go similarity index 74% rename from src/handlers/v2/labels.go rename to src/module/label/handler.go index 6a73af99..b35516d7 100644 --- a/src/handlers/v2/labels.go +++ b/src/module/label/handler.go @@ -1,17 +1,22 @@ -package v2 +package labelmodule import ( - "aegis/consts" + "aegis/httpx" "net/http" "strconv" + "aegis/consts" "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { return &Handler{service: service} } + // BatchDeleteLabels handles batch deletion of labels // // @Summary Batch delete labels @@ -21,31 +26,27 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.BatchDeleteLabelReq true "Batch delete request" +// @Param request body BatchDeleteLabelReq true "Batch delete request" // @Success 200 {object} dto.GenericResponse[any] "Labels deleted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/batch-delete [post] -// @x-api-type {"sdk":"true"} -func BatchDeleteLabels(c *gin.Context) { - var req dto.BatchDeleteLabelReq +// @x-api-type {"portal":"true"} +func (h *Handler) BatchDeleteLabels(c *gin.Context) { + var req BatchDeleteLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - err := producer.BatchDeleteLabels(req.IDs) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.BatchDelete(c.Request.Context(), req.IDs)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Labels deleted successfully", nil) } @@ -58,32 +59,29 @@ func BatchDeleteLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param label body dto.CreateLabelReq true "Label creation request" -// @Success 201 {object} dto.GenericResponse[dto.LabelResp] "Label created successfully" +// @Param label body CreateLabelReq true "Label creation request" +// @Success 201 {object} dto.GenericResponse[LabelResp] "Label created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Label already exists" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels [post] -// @x-api-type {"sdk":"true"} -func CreateLabel(c *gin.Context) { - var req dto.CreateLabelReq +// @x-api-type {"portal":"true"} +func (h *Handler) CreateLabel(c *gin.Context) { + var req CreateLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format:"+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.CreateLabel(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.Create(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusCreated, "Label created successfully", resp) } @@ -103,20 +101,15 @@ func CreateLabel(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Label not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/{label_id} [delete] -// @x-api-type {"sdk":"true"} -func DeleteLabel(c *gin.Context) { - labelIdStr := c.Param(consts.URLPathLabelID) - labelID, err := strconv.Atoi(labelIdStr) - if err != nil || labelID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid label ID") +// @x-api-type {"portal":"true"} +func (h *Handler) DeleteLabel(c *gin.Context) { + id, ok := parseLabelID(c) + if !ok { return } - - err = producer.DeleteLabel(labelID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.Delete(c.Request.Context(), id)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Label deleted successfully", nil) } @@ -129,27 +122,23 @@ func DeleteLabel(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param label_id path int true "Label ID" -// @Success 200 {object} dto.GenericResponse[dto.LabelDetailResp] "Label retrieved successfully" +// @Success 200 {object} dto.GenericResponse[LabelDetailResp] "Label retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Label not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/{label_id} [get] -// @x-api-type {"sdk":"true"} -func GetLabelDetail(c *gin.Context) { - labelIdStr := c.Param(consts.URLPathLabelID) - labelID, err := strconv.Atoi(labelIdStr) - if err != nil || labelID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid label ID") +// @x-api-type {"portal":"true"} +func (h *Handler) GetLabelDetail(c *gin.Context) { + id, ok := parseLabelID(c) + if !ok { return } - - resp, err := producer.GetLabelDetail(labelID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetDetail(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -168,30 +157,27 @@ func GetLabelDetail(c *gin.Context) { // @Param category query consts.LabelCategory false "Filter by category" // @Param is_system query bool false "Filter by system label" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.LabelResp]] "Labels retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[LabelResp]] "Labels retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels [get] -// @x-api-type {"sdk":"true"} -func ListLabels(c *gin.Context) { - var req dto.ListLabelReq +// @x-api-type {"portal":"true"} +func (h *Handler) ListLabels(c *gin.Context) { + var req ListLabelReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListLabels(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.List(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -205,38 +191,42 @@ func ListLabels(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param label_id path int true "Label ID" -// @Param request body dto.UpdateLabelReq true "Label update request" -// @Success 202 {object} dto.GenericResponse[dto.LabelResp] "Label updated successfully" +// @Param request body UpdateLabelReq true "Label update request" +// @Success 202 {object} dto.GenericResponse[LabelResp] "Label updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Label not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/{label_id} [patch] -// @x-api-type {"sdk":"true"} -func UpdateLabel(c *gin.Context) { - labelIdStr := c.Param(consts.URLPathLabelID) - labelID, err := strconv.Atoi(labelIdStr) - if err != nil || labelID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid label ID") +// @x-api-type {"portal":"true"} +func (h *Handler) UpdateLabel(c *gin.Context) { + id, ok := parseLabelID(c) + if !ok { return } - - var req dto.UpdateLabelReq + var req UpdateLabelReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.UpdateLabel(&req, labelID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.Update(c.Request.Context(), &req, id) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusAccepted, "Label updated successfully", resp) } + +func parseLabelID(c *gin.Context) (int, bool) { + v := c.Param(consts.URLPathLabelID) + id, err := strconv.Atoi(v) + if err != nil || id <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid label ID") + return 0, false + } + return id, true +} diff --git a/src/module/label/module.go b/src/module/label/module.go new file mode 100644 index 00000000..2f705aa4 --- /dev/null +++ b/src/module/label/module.go @@ -0,0 +1,9 @@ +package labelmodule + +import "go.uber.org/fx" + +var Module = fx.Module("label", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/label/repository.go b/src/module/label/repository.go new file mode 100644 index 00000000..e38e816c --- /dev/null +++ b/src/module/label/repository.go @@ -0,0 +1,275 @@ +package labelmodule + +import ( + "aegis/consts" + "aegis/model" + "errors" + "fmt" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const labelKeyOmitFields = "active_key_value" + +type Repository struct { + db *gorm.DB +} + +type labelCountResult struct { + LabelID int `gorm:"column:label_id"` + Count int64 `gorm:"column:count"` +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) ListLabelsByID(db *gorm.DB, labelIDs []int) ([]model.Label, error) { + if len(labelIDs) == 0 { + return []model.Label{}, nil + } + + var labels []model.Label + if err := r.useDB(db). + Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels by IDs: %w", err) + } + return labels, nil +} + +func (r *Repository) BatchUpdateLabels(db *gorm.DB, labels []model.Label) error { + if len(labels) == 0 { + return fmt.Errorf("no labels to update") + } + + if err := r.useDB(db).Omit(labelKeyOmitFields).Save(&labels).Error; err != nil { + return fmt.Errorf("failed to batch update labels: %w", err) + } + return nil +} + +func (r *Repository) BatchDeleteLabels(db *gorm.DB, labelIDs []int) error { + if len(labelIDs) == 0 { + return nil + } + + if err := r.useDB(db).Model(&model.Label{}). + Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return fmt.Errorf("failed to batch delete labels: %w", err) + } + return nil +} + +func (r *Repository) GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.StatusType) (*model.Label, error) { + query := r.useDB(db).Where("label_key = ? AND label_value = ?", key, value) + if len(status) == 0 { + query = query.Where("status != ?", consts.CommonDeleted) + } else if len(status) == 1 { + query = query.Where("status = ?", status[0]) + } else { + query = query.Where("status IN (?)", status) + } + + var label model.Label + if err := query.First(&label).Error; err != nil { + return nil, fmt.Errorf("failed to get label: %w", err) + } + return &label, nil +} + +func (r *Repository) CreateLabel(db *gorm.DB, label *model.Label) error { + if err := r.useDB(db).Omit(labelKeyOmitFields).Create(label).Error; err != nil { + return fmt.Errorf("failed to create label: %w", err) + } + return nil +} + +func (r *Repository) UpdateLabel(db *gorm.DB, label *model.Label) error { + if err := r.useDB(db).Omit(labelKeyOmitFields).Save(label).Error; err != nil { + return fmt.Errorf("failed to update label: %w", err) + } + return nil +} + +func (r *Repository) GetLabelByID(db *gorm.DB, id int) (*model.Label, error) { + var label model.Label + if err := r.useDB(db).First(&label, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("label with id %d not found", id) + } + return nil, fmt.Errorf("failed to get label: %w", err) + } + return &label, nil +} + +func (r *Repository) RemoveContainersFromLabel(db *gorm.DB, labelID int) (int64, error) { + return r.removeAssociationsFromLabel(db, &model.ContainerLabel{}, labelID, "containers") +} + +func (r *Repository) RemoveDatasetsFromLabel(db *gorm.DB, labelID int) (int64, error) { + return r.removeAssociationsFromLabel(db, &model.DatasetLabel{}, labelID, "datasets") +} + +func (r *Repository) RemoveProjectsFromLabel(db *gorm.DB, labelID int) (int64, error) { + return r.removeAssociationsFromLabel(db, &model.ProjectLabel{}, labelID, "projects") +} + +func (r *Repository) RemoveInjectionsFromLabel(db *gorm.DB, labelID int) (int64, error) { + return r.removeAssociationsFromLabel(db, &model.FaultInjectionLabel{}, labelID, "injection-label associations") +} + +func (r *Repository) RemoveExecutionsFromLabel(db *gorm.DB, labelID int) (int64, error) { + return r.removeAssociationsFromLabel(db, &model.ExecutionInjectionLabel{}, labelID, "execution-label associations") +} + +func (r *Repository) ListContainerLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return r.listAssociationCounts(db, &model.ContainerLabel{}, labelIDs) +} + +func (r *Repository) RemoveContainersFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { + return r.removeAssociationsFromLabels(db, &model.ContainerLabel{}, labelIDs, "containers") +} + +func (r *Repository) ListDatasetLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return r.listAssociationCounts(db, &model.DatasetLabel{}, labelIDs) +} + +func (r *Repository) RemoveDatasetsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { + return r.removeAssociationsFromLabels(db, &model.DatasetLabel{}, labelIDs, "datasets") +} + +func (r *Repository) ListProjectLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return r.listAssociationCounts(db, &model.ProjectLabel{}, labelIDs) +} + +func (r *Repository) RemoveProjectsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { + return r.removeAssociationsFromLabels(db, &model.ProjectLabel{}, labelIDs, "projects") +} + +func (r *Repository) ListInjectionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return r.listAssociationCounts(db, &model.FaultInjectionLabel{}, labelIDs) +} + +func (r *Repository) RemoveInjectionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { + return r.removeAssociationsFromLabels(db, &model.FaultInjectionLabel{}, labelIDs, "injection-label associations") +} + +func (r *Repository) ListExecutionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return r.listAssociationCounts(db, &model.ExecutionInjectionLabel{}, labelIDs) +} + +func (r *Repository) RemoveExecutionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { + return r.removeAssociationsFromLabels(db, &model.ExecutionInjectionLabel{}, labelIDs, "execution-label associations") +} + +func (r *Repository) BatchDecreaseLabelUsages(db *gorm.DB, labelIDs []int, decrement int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) + if err := r.useDB(db).Model(&model.Label{}). + Where("id IN (?)", labelIDs). + Clauses(clause.Returning{}). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch decrease label usages: %w", err) + } + return nil +} + +func (r *Repository) DeleteLabel(db *gorm.DB, labelID int) (int64, error) { + result := r.useDB(db).Model(&model.Label{}). + Where("id = ? AND status != ?", labelID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to soft delete label %d: %w", labelID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) ListLabels(limit, offset int, filterOptions *ListLabelFilters) ([]model.Label, int64, error) { + var ( + labels []model.Label + total int64 + ) + + query := r.db.Model(&model.Label{}) + if filterOptions.Key != "" { + query = query.Where("label_key = ?", filterOptions.Key) + } + if filterOptions.Value != "" { + query = query.Where("label_value = ?", filterOptions.Value) + } + if filterOptions.Category != nil { + query = query.Where("category = ?", *filterOptions.Category) + } + if filterOptions.IsSystem != nil { + query = query.Where("is_system = ?", *filterOptions.IsSystem) + } + if filterOptions.Status != nil { + query = query.Where("status = ?", *filterOptions.Status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count labels: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("usage_count DESC, created_at DESC").Find(&labels).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list labels: %w", err) + } + return labels, total, nil +} + +func (r *Repository) useDB(db *gorm.DB) *gorm.DB { + if db != nil { + return db + } + return r.db +} + +func (r *Repository) removeAssociationsFromLabel(db *gorm.DB, model any, labelID int, target string) (int64, error) { + result := r.useDB(db).Where("label_id = ?", labelID).Delete(model) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to remove %s from label %d: %w", target, labelID, err) + } + return result.RowsAffected, nil +} + +func (r *Repository) removeAssociationsFromLabels(db *gorm.DB, model any, labelIDs []int, target string) (int64, error) { + if len(labelIDs) == 0 { + return 0, nil + } + + result := r.useDB(db).Where("label_id IN (?)", labelIDs).Delete(model) + if err := result.Error; err != nil { + return 0, fmt.Errorf("failed to remove %s from labels %v: %w", target, labelIDs, err) + } + return result.RowsAffected, nil +} + +func (r *Repository) listAssociationCounts(db *gorm.DB, model any, labelIDs []int) (map[int]int64, error) { + if len(labelIDs) == 0 { + return map[int]int64{}, nil + } + + var results []labelCountResult + if err := r.useDB(db).Model(model). + Select("label_id, COUNT(label_id) AS count"). + Where("label_id IN (?)", labelIDs). + Group("label_id"). + Scan(&results).Error; err != nil { + return nil, fmt.Errorf("failed to count associations: %w", err) + } + + countMap := make(map[int]int64, len(results)) + for _, result := range results { + countMap[result.LabelID] = result.Count + } + return countMap, nil +} diff --git a/src/module/label/service.go b/src/module/label/service.go new file mode 100644 index 00000000..227f1658 --- /dev/null +++ b/src/module/label/service.go @@ -0,0 +1,283 @@ +package labelmodule + +import ( + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) BatchDelete(_ context.Context, ids []int) error { + if len(ids) == 0 { + return nil + } + + return s.repo.Transaction(func(tx *gorm.DB) error { + labels, err := s.repo.ListLabelsByID(tx, ids) + if err != nil { + return fmt.Errorf("failed to list labels by IDs: %w", err) + } + if len(labels) == 0 { + return fmt.Errorf("no labels found for the provided IDs") + } + if len(labels) != len(ids) { + return fmt.Errorf("some labels not found for the provided IDs") + } + + labelMap := make(map[int]*model.Label, len(labels)) + for _, label := range labels { + labelMap[label.ID] = &label + } + + containerCountMap, err := s.removeContainersFromLabels(tx, ids) + if err != nil { + return fmt.Errorf("failed to delete container-label associations: %v", err) + } + datasetCountMap, err := s.removeDatasetsFromLabels(tx, ids) + if err != nil { + return fmt.Errorf("failed to delete dataset-label associations: %v", err) + } + projectCountMap, err := s.removeProjectsFromLabels(tx, ids) + if err != nil { + return fmt.Errorf("failed to delete project-label associations: %v", err) + } + injectionCountMap, err := s.removeInjectionsFromLabels(tx, ids) + if err != nil { + return fmt.Errorf("failed to delete injection-label associations: %v", err) + } + executionCountMap, err := s.removeExecutionsFromLabels(tx, ids) + if err != nil { + return fmt.Errorf("failed to delete execution-label associations: %v", err) + } + + toUpdatedLabels := make([]model.Label, 0, len(ids)) + for labelID, label := range labelMap { + totalDecrement := int64(0) + totalDecrement += containerCountMap[labelID] + totalDecrement += datasetCountMap[labelID] + totalDecrement += projectCountMap[labelID] + totalDecrement += injectionCountMap[labelID] + totalDecrement += executionCountMap[labelID] + label.Usage = max(label.Usage-int(totalDecrement), 0) + toUpdatedLabels = append(toUpdatedLabels, *label) + } + + if err := s.repo.BatchUpdateLabels(tx, toUpdatedLabels); err != nil { + return fmt.Errorf("failed to update label usages: %v", err) + } + if err := s.repo.BatchDeleteLabels(tx, ids); err != nil { + return fmt.Errorf("failed to batch delete labels: %v", err) + } + return nil + }) +} + +func (s *Service) Create(_ context.Context, req *CreateLabelReq) (*LabelResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("label validation failed: %w", err) + } + + label := req.ConvertToLabel() + var createdLabel *model.Label + err := s.repo.Transaction(func(tx *gorm.DB) error { + item, err := s.createLabelCore(tx, label) + if err != nil { + return fmt.Errorf("failed to create label: %w", err) + } + createdLabel = item + return nil + }) + if err != nil { + return nil, err + } + + return NewLabelResp(createdLabel), nil +} + +func (s *Service) Delete(_ context.Context, id int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + label, err := s.repo.GetLabelByID(tx, id) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: label with id %d not found", consts.ErrNotFound, id) + } + return fmt.Errorf("failed to get label: %v", err) + } + + containerRows, err := s.repo.RemoveContainersFromLabel(tx, label.ID) + if err != nil { + return fmt.Errorf("failed to delete container-label associations: %v", err) + } + datasetRows, err := s.repo.RemoveDatasetsFromLabel(tx, label.ID) + if err != nil { + return fmt.Errorf("failed to delete dataset-label associations: %v", err) + } + projectRows, err := s.repo.RemoveProjectsFromLabel(tx, label.ID) + if err != nil { + return fmt.Errorf("failed to delete project-label associations: %v", err) + } + injectionRows, err := s.repo.RemoveInjectionsFromLabel(tx, label.ID) + if err != nil { + return fmt.Errorf("failed to delete injection-label associations: %v", err) + } + executionRows, err := s.repo.RemoveExecutionsFromLabel(tx, label.ID) + if err != nil { + return fmt.Errorf("failed to delete execution-label associations: %v", err) + } + + totalRows := int(containerRows + datasetRows + projectRows + injectionRows + executionRows) + if err := s.repo.BatchDecreaseLabelUsages(tx, []int{label.ID}, totalRows); err != nil { + return fmt.Errorf("failed to decrease label usage: %v", err) + } + + rows, err := s.repo.DeleteLabel(tx, id) + if err != nil { + return fmt.Errorf("failed to delete label: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: label id %d not found", consts.ErrNotFound, id) + } + return nil + }) +} + +func (s *Service) GetDetail(_ context.Context, id int) (*LabelDetailResp, error) { + label, err := s.repo.GetLabelByID(s.repo.db, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: label with ID %d not found", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get label: %w", err) + } + return NewLabelDetailResp(label), nil +} + +func (s *Service) List(_ context.Context, req *ListLabelReq) (*dto.ListResp[LabelResp], error) { + limit, offset := req.ToGormParams() + filterOptions := req.ToFilterOptions() + labels, total, err := s.repo.ListLabels(limit, offset, filterOptions) + if err != nil { + return nil, fmt.Errorf("failed to list labels: %w", err) + } + items := make([]LabelResp, 0, len(labels)) + for i := range labels { + items = append(items, *NewLabelResp(&labels[i])) + } + return &dto.ListResp[LabelResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) Update(_ context.Context, req *UpdateLabelReq, id int) (*LabelResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + var updatedLabel *model.Label + err := s.repo.Transaction(func(tx *gorm.DB) error { + existingLabel, err := s.repo.GetLabelByID(tx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: label with ID %d not found", consts.ErrNotFound, id) + } + return fmt.Errorf("failed to get label: %w", err) + } + + req.PatchLabelModel(existingLabel) + if err := s.repo.UpdateLabel(tx, existingLabel); err != nil { + return fmt.Errorf("failed to update label: %w", err) + } + updatedLabel = existingLabel + return nil + }) + if err != nil { + return nil, err + } + + return NewLabelResp(updatedLabel), nil +} + +type labelRemovalOps struct { + countFunc func(*gorm.DB, []int) (map[int]int64, error) + removeFunc func(*gorm.DB, []int) (int64, error) + entityName string +} + +func (s *Service) createLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { + return CreateLabelCore(db, label) +} + +func (s *Service) removeAssociationsFromLabels(db *gorm.DB, labelIDs []int, ops labelRemovalOps) (map[int]int64, error) { + if len(labelIDs) == 0 { + return nil, nil + } + countsMap, err := ops.countFunc(db, labelIDs) + if err != nil { + return nil, fmt.Errorf("failed to get %s-label counts: %w", ops.entityName, err) + } + if len(countsMap) == 0 { + return nil, nil + } + rows, err := ops.removeFunc(db, labelIDs) + if err != nil { + return nil, fmt.Errorf("failed to remove %ss from labels: %w", ops.entityName, err) + } + if rows == 0 { + return nil, nil + } + return countsMap, nil +} + +func (s *Service) removeContainersFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return s.removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ + countFunc: s.repo.ListContainerLabelCounts, + removeFunc: s.repo.RemoveContainersFromLabels, + entityName: "container", + }) +} + +func (s *Service) removeDatasetsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return s.removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ + countFunc: s.repo.ListDatasetLabelCounts, + removeFunc: s.repo.RemoveDatasetsFromLabels, + entityName: "dataset", + }) +} + +func (s *Service) removeProjectsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return s.removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ + countFunc: s.repo.ListProjectLabelCounts, + removeFunc: s.repo.RemoveProjectsFromLabels, + entityName: "project", + }) +} + +func (s *Service) removeInjectionsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return s.removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ + countFunc: s.repo.ListInjectionLabelCounts, + removeFunc: s.repo.RemoveInjectionsFromLabels, + entityName: "injection", + }) +} + +func (s *Service) removeExecutionsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { + return s.removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ + countFunc: s.repo.ListExecutionLabelCounts, + removeFunc: s.repo.RemoveExecutionsFromLabels, + entityName: "execution", + }) +} diff --git a/src/module/metric/api_types.go b/src/module/metric/api_types.go new file mode 100644 index 00000000..d553d05a --- /dev/null +++ b/src/module/metric/api_types.go @@ -0,0 +1,65 @@ +package metricmodule + +import ( + "fmt" + "time" +) + +// GetMetricsReq represents the request to get metrics with time range and filters. +type GetMetricsReq struct { + StartTime *time.Time `form:"start_time" binding:"omitempty"` + EndTime *time.Time `form:"end_time" binding:"omitempty"` + FaultType *string `form:"fault_type" binding:"omitempty"` + AlgorithmID *int `form:"algorithm_id" binding:"omitempty"` +} + +func (req *GetMetricsReq) Validate() error { + if req.StartTime != nil && req.EndTime != nil && req.EndTime.Before(*req.StartTime) { + return fmt.Errorf("end_time must be after start_time") + } + if req.AlgorithmID != nil && *req.AlgorithmID <= 0 { + return fmt.Errorf("algorithm_id must be positive") + } + return nil +} + +// InjectionMetrics represents aggregated metrics for injections. +type InjectionMetrics struct { + TotalCount int `json:"total_count"` + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + SuccessRate float64 `json:"success_rate"` + AvgDuration float64 `json:"avg_duration"` + MinDuration float64 `json:"min_duration"` + MaxDuration float64 `json:"max_duration"` + StateDistrib map[string]int `json:"state_distribution" swaggertype:"object"` + FaultTypeDistrib map[string]int `json:"fault_type_distribution" swaggertype:"object"` +} + +// ExecutionMetrics represents aggregated metrics for algorithm executions. +type ExecutionMetrics struct { + TotalCount int `json:"total_count"` + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + SuccessRate float64 `json:"success_rate"` + AvgDuration float64 `json:"avg_duration"` + MinDuration float64 `json:"min_duration"` + MaxDuration float64 `json:"max_duration"` + StateDistrib map[string]int `json:"state_distribution" swaggertype:"object"` +} + +// AlgorithmMetrics represents comparative metrics across different algorithms. +type AlgorithmMetrics struct { + Algorithms []AlgorithmMetricItem `json:"algorithms"` +} + +// AlgorithmMetricItem represents metrics for a single algorithm. +type AlgorithmMetricItem struct { + AlgorithmID int `json:"algorithm_id"` + AlgorithmName string `json:"algorithm_name"` + ExecutionCount int `json:"execution_count"` + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + SuccessRate float64 `json:"success_rate"` + AvgDuration float64 `json:"avg_duration"` +} diff --git a/src/handlers/v2/metrics.go b/src/module/metric/handler.go similarity index 75% rename from src/handlers/v2/metrics.go rename to src/module/metric/handler.go index d05dc0b1..7ff39f80 100644 --- a/src/handlers/v2/metrics.go +++ b/src/module/metric/handler.go @@ -1,14 +1,22 @@ -package v2 +package metricmodule import ( - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" + "aegis/httpx" "net/http" + "aegis/dto" + "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // GetInjectionMetrics handles retrieval of injection metrics // // @Summary Get injection metrics @@ -20,21 +28,21 @@ import ( // @Param start_time query string false "Start time (RFC3339)" // @Param end_time query string false "End time (RFC3339)" // @Param fault_type query string false "Filter by fault type" -// @Success 200 {object} dto.GenericResponse[dto.InjectionMetrics] "Injection metrics" +// @Success 200 {object} dto.GenericResponse[InjectionMetrics] "Injection metrics" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/injections [get] -// @x-api-type {"sdk":"true"} -func GetInjectionMetrics(c *gin.Context) { - var req dto.GetMetricsReq +// @x-api-type {} +func (h *Handler) GetInjectionMetrics(c *gin.Context) { + var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return } - metrics, err := producer.GetInjectionMetrics(&req) - if handlers.HandleServiceError(c, err) { + metrics, err := h.service.GetInjectionMetrics(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -52,21 +60,21 @@ func GetInjectionMetrics(c *gin.Context) { // @Param start_time query string false "Start time (RFC3339)" // @Param end_time query string false "End time (RFC3339)" // @Param algorithm_id query int false "Filter by algorithm ID" -// @Success 200 {object} dto.GenericResponse[dto.ExecutionMetrics] "Execution metrics" +// @Success 200 {object} dto.GenericResponse[ExecutionMetrics] "Execution metrics" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/executions [get] -// @x-api-type {"sdk":"true"} -func GetExecutionMetrics(c *gin.Context) { - var req dto.GetMetricsReq +// @x-api-type {} +func (h *Handler) GetExecutionMetrics(c *gin.Context) { + var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return } - metrics, err := producer.GetExecutionMetrics(&req) - if handlers.HandleServiceError(c, err) { + metrics, err := h.service.GetExecutionMetrics(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -84,21 +92,21 @@ func GetExecutionMetrics(c *gin.Context) { // @Param algorithm_ids query string false "Comma-separated algorithm IDs" // @Param start_time query string false "Start time (RFC3339)" // @Param end_time query string false "End time (RFC3339)" -// @Success 200 {object} dto.GenericResponse[dto.AlgorithmMetrics] "Algorithm metrics" +// @Success 200 {object} dto.GenericResponse[AlgorithmMetrics] "Algorithm metrics" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/algorithms [get] -// @x-api-type {"sdk":"true"} -func GetAlgorithmMetrics(c *gin.Context) { - var req dto.GetMetricsReq +// @x-api-type {} +func (h *Handler) GetAlgorithmMetrics(c *gin.Context) { + var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) return } - metrics, err := producer.GetAlgorithmMetrics(&req) - if handlers.HandleServiceError(c, err) { + metrics, err := h.service.GetAlgorithmMetrics(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } diff --git a/src/module/metric/module.go b/src/module/metric/module.go new file mode 100644 index 00000000..085fd509 --- /dev/null +++ b/src/module/metric/module.go @@ -0,0 +1,9 @@ +package metricmodule + +import "go.uber.org/fx" + +var Module = fx.Module("metric", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/metric/repository.go b/src/module/metric/repository.go new file mode 100644 index 00000000..99039fa3 --- /dev/null +++ b/src/module/metric/repository.go @@ -0,0 +1,39 @@ +package metricmodule + +import ( + "aegis/model" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) ListFaultInjections(query func(*gorm.DB) *gorm.DB) ([]model.FaultInjection, error) { + var items []model.FaultInjection + if err := query(r.db).Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListExecutions(query func(*gorm.DB) *gorm.DB) ([]model.Execution, error) { + var items []model.Execution + if err := query(r.db).Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} + +func (r *Repository) ListAlgorithmContainers() ([]model.Container, error) { + var items []model.Container + if err := r.db.Where("type = ?", 2).Find(&items).Error; err != nil { + return nil, err + } + return items, nil +} diff --git a/src/service/producer/metrics.go b/src/module/metric/service.go similarity index 52% rename from src/service/producer/metrics.go rename to src/module/metric/service.go index 48ac554a..f8837dd9 100644 --- a/src/service/producer/metrics.go +++ b/src/module/metric/service.go @@ -1,15 +1,24 @@ -package producer +package metricmodule import ( - "aegis/database" - "aegis/dto" + "context" "fmt" + "aegis/model" + "github.com/sirupsen/logrus" + "gorm.io/gorm" ) -// GetInjectionMetrics retrieves aggregated metrics for fault injections -func GetInjectionMetrics(req *dto.GetMetricsReq) (*dto.InjectionMetrics, error) { +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) GetInjectionMetrics(_ context.Context, req *GetMetricsReq) (*InjectionMetrics, error) { if err := req.Validate(); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } @@ -20,28 +29,107 @@ func GetInjectionMetrics(req *dto.GetMetricsReq) (*dto.InjectionMetrics, error) "fault_type": req.FaultType, }).Info("GetInjectionMetrics: starting") - var injections []database.FaultInjection - query := database.DB + injections, err := s.repo.ListFaultInjections(func(db *gorm.DB) *gorm.DB { + query := db + if req.StartTime != nil { + query = query.Where("created_at >= ?", req.StartTime) + } + if req.EndTime != nil { + query = query.Where("created_at <= ?", req.EndTime) + } + if req.FaultType != nil { + query = query.Where("fault_type = ?", *req.FaultType) + } + return query + }) + if err != nil { + return nil, fmt.Errorf("failed to query injections: %w", err) + } + + metrics := buildInjectionMetrics(injections) + logrus.WithField("metrics", metrics).Info("GetInjectionMetrics: completed") + return metrics, nil +} - // Apply time range filter - if req.StartTime != nil { - query = query.Where("created_at >= ?", req.StartTime) +func (s *Service) GetExecutionMetrics(_ context.Context, req *GetMetricsReq) (*ExecutionMetrics, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) } - if req.EndTime != nil { - query = query.Where("created_at <= ?", req.EndTime) + + logrus.WithFields(map[string]interface{}{ + "start_time": req.StartTime, + "end_time": req.EndTime, + "algorithm_id": req.AlgorithmID, + }).Info("GetExecutionMetrics: starting") + + executions, err := s.repo.ListExecutions(func(db *gorm.DB) *gorm.DB { + query := db + if req.StartTime != nil { + query = query.Where("created_at >= ?", req.StartTime) + } + if req.EndTime != nil { + query = query.Where("created_at <= ?", req.EndTime) + } + if req.AlgorithmID != nil { + query = query.Where("algorithm_id = ?", *req.AlgorithmID) + } + return query + }) + if err != nil { + return nil, fmt.Errorf("failed to query executions: %w", err) } - // Apply fault type filter - if req.FaultType != nil { - query = query.Where("fault_type = ?", *req.FaultType) + metrics := buildExecutionMetrics(executions) + logrus.WithField("metrics", metrics).Info("GetExecutionMetrics: completed") + return metrics, nil +} + +func (s *Service) GetAlgorithmMetrics(_ context.Context, req *GetMetricsReq) (*AlgorithmMetrics, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("invalid request: %w", err) } - if err := query.Find(&injections).Error; err != nil { - return nil, fmt.Errorf("failed to query injections: %w", err) + logrus.WithFields(map[string]interface{}{ + "start_time": req.StartTime, + "end_time": req.EndTime, + }).Info("GetAlgorithmMetrics: starting") + + algorithms, err := s.repo.ListAlgorithmContainers() + if err != nil { + return nil, fmt.Errorf("failed to query algorithms: %w", err) + } + + metrics := &AlgorithmMetrics{ + Algorithms: make([]AlgorithmMetricItem, 0, len(algorithms)), } - // Calculate metrics - metrics := &dto.InjectionMetrics{ + for _, algo := range algorithms { + executions, err := s.repo.ListExecutions(func(db *gorm.DB) *gorm.DB { + query := db.Where("algorithm_id = ?", algo.ID) + if req.StartTime != nil { + query = query.Where("created_at >= ?", req.StartTime) + } + if req.EndTime != nil { + query = query.Where("created_at <= ?", req.EndTime) + } + return query + }) + if err != nil { + logrus.WithError(err).Warnf("failed to query executions for algorithm %d", algo.ID) + continue + } + item, ok := buildAlgorithmMetricItem(algo, executions) + if ok { + metrics.Algorithms = append(metrics.Algorithms, item) + } + } + + logrus.WithField("algorithm_count", len(metrics.Algorithms)).Info("GetAlgorithmMetrics: completed") + return metrics, nil +} + +func buildInjectionMetrics(injections []model.FaultInjection) *InjectionMetrics { + metrics := &InjectionMetrics{ TotalCount: len(injections), StateDistrib: make(map[string]int), FaultTypeDistrib: make(map[string]int), @@ -52,19 +140,15 @@ func GetInjectionMetrics(req *dto.GetMetricsReq) (*dto.InjectionMetrics, error) failedCount := 0 for _, inj := range injections { - // Count by state stateName := fmt.Sprintf("%d", inj.State) metrics.StateDistrib[stateName]++ - // Count by fault type faultTypeName := fmt.Sprintf("%d", inj.FaultType) metrics.FaultTypeDistrib[faultTypeName]++ - // Calculate duration stats if inj.StartTime != nil && inj.EndTime != nil { duration := inj.EndTime.Sub(*inj.StartTime).Seconds() totalDuration += duration - if metrics.MinDuration == 0 || duration < metrics.MinDuration { metrics.MinDuration = duration } @@ -73,61 +157,25 @@ func GetInjectionMetrics(req *dto.GetMetricsReq) (*dto.InjectionMetrics, error) } } - // Count success/failed switch inj.State { - case 2: // success state + case 2: successCount++ - case 3: // failed state + case 3: failedCount++ } } metrics.SuccessCount = successCount metrics.FailedCount = failedCount - if metrics.TotalCount > 0 { metrics.SuccessRate = float64(successCount) / float64(metrics.TotalCount) * 100 metrics.AvgDuration = totalDuration / float64(metrics.TotalCount) } - - logrus.WithField("metrics", metrics).Info("GetInjectionMetrics: completed") - return metrics, nil + return metrics } -// GetExecutionMetrics retrieves aggregated metrics for algorithm executions -func GetExecutionMetrics(req *dto.GetMetricsReq) (*dto.ExecutionMetrics, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("invalid request: %w", err) - } - - logrus.WithFields(map[string]interface{}{ - "start_time": req.StartTime, - "end_time": req.EndTime, - "algorithm_id": req.AlgorithmID, - }).Info("GetExecutionMetrics: starting") - - var executions []database.Execution - query := database.DB - - // Apply time range filter - if req.StartTime != nil { - query = query.Where("created_at >= ?", req.StartTime) - } - if req.EndTime != nil { - query = query.Where("created_at <= ?", req.EndTime) - } - - // Apply algorithm filter - if req.AlgorithmID != nil { - query = query.Where("algorithm_id = ?", *req.AlgorithmID) - } - - if err := query.Find(&executions).Error; err != nil { - return nil, fmt.Errorf("failed to query executions: %w", err) - } - - // Calculate metrics - metrics := &dto.ExecutionMetrics{ +func buildExecutionMetrics(executions []model.Execution) *ExecutionMetrics { + metrics := &ExecutionMetrics{ TotalCount: len(executions), StateDistrib: make(map[string]int), } @@ -137,14 +185,10 @@ func GetExecutionMetrics(req *dto.GetMetricsReq) (*dto.ExecutionMetrics, error) failedCount := 0 for _, exec := range executions { - // Count by state stateName := fmt.Sprintf("%d", exec.State) metrics.StateDistrib[stateName]++ - - // Calculate duration stats if exec.Duration > 0 { totalDuration += exec.Duration - if metrics.MinDuration == 0 || exec.Duration < metrics.MinDuration { metrics.MinDuration = exec.Duration } @@ -152,108 +196,52 @@ func GetExecutionMetrics(req *dto.GetMetricsReq) (*dto.ExecutionMetrics, error) metrics.MaxDuration = exec.Duration } } - - // Count success/failed switch exec.State { - case 2: // success state + case 2: successCount++ - case 3: // failed state + case 3: failedCount++ } } metrics.SuccessCount = successCount metrics.FailedCount = failedCount - if metrics.TotalCount > 0 { metrics.SuccessRate = float64(successCount) / float64(metrics.TotalCount) * 100 metrics.AvgDuration = totalDuration / float64(metrics.TotalCount) } - - logrus.WithField("metrics", metrics).Info("GetExecutionMetrics: completed") - return metrics, nil + return metrics } -// GetAlgorithmMetrics retrieves comparative metrics across different algorithms -func GetAlgorithmMetrics(req *dto.GetMetricsReq) (*dto.AlgorithmMetrics, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("invalid request: %w", err) - } - - logrus.WithFields(map[string]interface{}{ - "start_time": req.StartTime, - "end_time": req.EndTime, - }).Info("GetAlgorithmMetrics: starting") - - // Get all algorithms - var algorithms []database.Container - query := database.DB.Where("type = ?", 2) // Assuming 2 is algorithm type - - if err := query.Find(&algorithms).Error; err != nil { - return nil, fmt.Errorf("failed to query algorithms: %w", err) +func buildAlgorithmMetricItem(algo model.Container, executions []model.Execution) (AlgorithmMetricItem, bool) { + if len(executions) == 0 { + return AlgorithmMetricItem{}, false } - metrics := &dto.AlgorithmMetrics{ - Algorithms: make([]dto.AlgorithmMetricItem, 0, len(algorithms)), + item := AlgorithmMetricItem{ + AlgorithmID: algo.ID, + AlgorithmName: algo.Name, + ExecutionCount: len(executions), } - // Calculate metrics for each algorithm - for _, algo := range algorithms { - var executions []database.Execution - execQuery := database.DB.Where("algorithm_id = ?", algo.ID) - - // Apply time range filter - if req.StartTime != nil { - execQuery = execQuery.Where("created_at >= ?", req.StartTime) - } - if req.EndTime != nil { - execQuery = execQuery.Where("created_at <= ?", req.EndTime) - } - - if err := execQuery.Find(&executions).Error; err != nil { - logrus.WithError(err).Warnf("failed to query executions for algorithm %d", algo.ID) - continue - } - - if len(executions) == 0 { - continue - } - - item := dto.AlgorithmMetricItem{ - AlgorithmID: algo.ID, - AlgorithmName: algo.Name, - ExecutionCount: len(executions), - } - - var totalDuration float64 - successCount := 0 - failedCount := 0 - - for _, exec := range executions { - // Calculate duration stats - if exec.Duration > 0 { - totalDuration += exec.Duration - } - - // Count success/failed - switch exec.State { - case 2: // success state - successCount++ - case 3: // failed state - failedCount++ - } + var totalDuration float64 + successCount := 0 + failedCount := 0 + for _, exec := range executions { + if exec.Duration > 0 { + totalDuration += exec.Duration } - - item.SuccessCount = successCount - item.FailedCount = failedCount - item.SuccessRate = float64(successCount) / float64(item.ExecutionCount) * 100 - if item.ExecutionCount > 0 { - item.AvgDuration = totalDuration / float64(item.ExecutionCount) + switch exec.State { + case 2: + successCount++ + case 3: + failedCount++ } - - metrics.Algorithms = append(metrics.Algorithms, item) } - logrus.WithField("algorithm_count", len(metrics.Algorithms)).Info("GetAlgorithmMetrics: completed") - return metrics, nil + item.SuccessCount = successCount + item.FailedCount = failedCount + item.SuccessRate = float64(successCount) / float64(item.ExecutionCount) * 100 + item.AvgDuration = totalDuration / float64(item.ExecutionCount) + return item, true } diff --git a/src/dto/notification.go b/src/module/notification/api_types.go similarity index 93% rename from src/dto/notification.go rename to src/module/notification/api_types.go index e80df471..dd44e524 100644 --- a/src/dto/notification.go +++ b/src/module/notification/api_types.go @@ -1,4 +1,4 @@ -package dto +package notificationmodule import "time" diff --git a/src/handlers/v2/notifications.go b/src/module/notification/handler.go similarity index 73% rename from src/handlers/v2/notifications.go rename to src/module/notification/handler.go index 0c09fc0a..168536de 100644 --- a/src/handlers/v2/notifications.go +++ b/src/module/notification/handler.go @@ -1,21 +1,29 @@ -package v2 +package notificationmodule import ( - "aegis/consts" - "aegis/dto" - producer "aegis/service/producer" "context" "errors" "fmt" "net/http" "time" + "aegis/consts" + "aegis/dto" + "github.com/gin-contrib/sse" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // GetNotificationStream handles streaming of global workflow notifications via Server-Sent Events (SSE) // // @Summary Stream global notifications in real-time @@ -31,13 +39,13 @@ import ( // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/notifications/stream [get] // @x-request-type {"stream":"true"} -func GetNotificationStream(c *gin.Context) { - var req dto.GetNotificationStreamReq +// @x-api-type {} +func (h *Handler) GetStream(c *gin.Context) { + var req GetNotificationStreamReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format") return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return @@ -45,18 +53,14 @@ func GetNotificationStream(c *gin.Context) { ctx, cancel := context.WithCancel(c.Request.Context()) defer cancel() - if c.IsAborted() { return } streamKey := consts.NotificationStreamKey - logEntry := logrus.WithFields(logrus.Fields{ - "stream_key": streamKey, - }) + logEntry := logrus.WithField("stream_key", streamKey) - logEntry.Infof("Reading historical notifications from Stream") - historicalMessages, err := producer.ReadNotificationStreamMessages(ctx, streamKey, req.LastID, 100, 0) + historicalMessages, err := h.service.ReadStreamMessages(ctx, streamKey, req.LastID, 100, 0) if err != nil { logEntry.Errorf("failed to read historical notifications from redis: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, "failed to read notification history") @@ -73,44 +77,33 @@ func GetNotificationStream(c *gin.Context) { req.LastID = lastID } - logEntry.Infof("Switching to real-time notification monitoring from ID: %s", req.LastID) for { select { case <-c.Done(): - logEntry.Info("Request context done") return - default: - newMessages, err := producer.ReadNotificationStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) + newMessages, err := h.service.ReadStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - logEntry.Infof("Context done while reading stream: %v", err) return } - logEntry.Errorf("Error reading notification stream: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, "failed to read notification events") return } - if len(newMessages) == 0 { - logEntry.Debug("No new notifications, continuing") continue } - lastID, err := sendNotificationSSEEvents(c, newMessages) if err != nil { logEntry.Errorf("failed to send notification events of ID %s: %v", lastID, err) return } - req.LastID = lastID - logrus.Info("Sent notification SSE messages, lastID:", lastID) } } } -// sendNotificationSSEEvents processes and sends notification messages as SSE events func sendNotificationSSEEvents(c *gin.Context, streams []redis.XStream) (string, error) { if len(streams) == 0 || len(streams[0].Messages) == 0 { return "", fmt.Errorf("no messages to process") @@ -119,27 +112,15 @@ func sendNotificationSSEEvents(c *gin.Context, streams []redis.XStream) (string, var lastID string for _, msg := range streams[0].Messages { lastID = msg.ID - - // Parse notification event from message notification := parseNotificationMessage(msg) - - c.Render(-1, sse.Event{ - Id: lastID, - Event: "notification", - Data: notification, - }) + c.Render(-1, sse.Event{Id: lastID, Event: "notification", Data: notification}) c.Writer.Flush() } - return lastID, nil } -// parseNotificationMessage converts a Redis stream message to a notification event -func parseNotificationMessage(msg redis.XMessage) dto.NotificationEvent { - notification := dto.NotificationEvent{ - Timestamp: time.Now(), - } - +func parseNotificationMessage(msg redis.XMessage) NotificationEvent { + notification := NotificationEvent{Timestamp: time.Now()} for key, val := range msg.Values { switch key { case "type": @@ -152,6 +133,5 @@ func parseNotificationMessage(msg redis.XMessage) dto.NotificationEvent { notification.Status = val.(string) } } - return notification } diff --git a/src/module/notification/module.go b/src/module/notification/module.go new file mode 100644 index 00000000..b081c0cf --- /dev/null +++ b/src/module/notification/module.go @@ -0,0 +1,9 @@ +package notificationmodule + +import "go.uber.org/fx" + +var Module = fx.Module("notification", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/notification/repository.go b/src/module/notification/repository.go new file mode 100644 index 00000000..6188f578 --- /dev/null +++ b/src/module/notification/repository.go @@ -0,0 +1,11 @@ +package notificationmodule + +import "gorm.io/gorm" + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} diff --git a/src/module/notification/service.go b/src/module/notification/service.go new file mode 100644 index 00000000..a76a5142 --- /dev/null +++ b/src/module/notification/service.go @@ -0,0 +1,32 @@ +package notificationmodule + +import ( + "context" + "fmt" + "time" + + redisinfra "aegis/infra/redis" + + "github.com/redis/go-redis/v9" +) + +type Service struct { + repo *Repository + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, redis: redis} +} + +func (s *Service) ReadStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if lastID == "" { + lastID = "0" + } + + messages, err := s.redis.XRead(ctx, []string{streamKey, lastID}, count, block) + if err != nil { + return nil, fmt.Errorf("failed to read notification stream messages: %w", err) + } + return messages, nil +} diff --git a/src/module/project/api_types.go b/src/module/project/api_types.go new file mode 100644 index 00000000..913f604e --- /dev/null +++ b/src/module/project/api_types.go @@ -0,0 +1,183 @@ +package projectmodule + +import ( + "fmt" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + injectionmodule "aegis/module/injection" +) + +type ProjectContainerItem = containermodule.ContainerResp +type ProjectDatasetItem = datasetmodule.DatasetResp + +// CreateProjectReq represents project creation request. +type CreateProjectReq struct { + Name string `json:"name" binding:"required"` + Description string `json:"description" binding:"omitempty"` + IsPublic *bool `json:"is_public" binding:"omitempty"` +} + +func (req *CreateProjectReq) Validate() error { + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + return fmt.Errorf("project name cannot be empty") + } + if req.IsPublic == nil { + defaultPublic := true + req.IsPublic = &defaultPublic + } + return nil +} + +func (req *CreateProjectReq) ConvertToProject() *model.Project { + return &model.Project{ + Name: req.Name, + Description: req.Description, + IsPublic: *req.IsPublic, + Status: consts.CommonEnabled, + } +} + +// ListProjectReq represents project list query parameters. +type ListProjectReq struct { + dto.PaginationReq + IsPublic *bool `form:"is_public" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListProjectReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +// UpdateProjectReq represents project update request. +type UpdateProjectReq struct { + Description *string `json:"description,omitempty"` + IsPublic *bool `json:"is_public,omitempty"` + Status *consts.StatusType `json:"status,omitempty"` +} + +func (req *UpdateProjectReq) Validate() error { + return validateStatus(req.Status, true) +} + +func (req *UpdateProjectReq) PatchProjectModel(target *model.Project) { + if req.Description != nil { + target.Description = *req.Description + } + if req.IsPublic != nil { + target.IsPublic = *req.IsPublic + } + if req.Status != nil { + target.Status = *req.Status + } +} + +// ManageProjectLabelReq represents project label management request. +type ManageProjectLabelReq struct { + AddLabels []dto.LabelItem `json:"add_labels" binding:"omitempty"` + RemoveLabels []string `json:"remove_labels" binding:"omitempty"` +} + +func (req *ManageProjectLabelReq) Validate() error { + if len(req.AddLabels) == 0 && len(req.RemoveLabels) == 0 { + return fmt.Errorf("at least one of add_labels or remove_labels must be provided") + } + + for i, label := range req.AddLabels { + if strings.TrimSpace(label.Key) == "" { + return fmt.Errorf("empty label key at index %d in add_labels", i) + } + if strings.TrimSpace(label.Value) == "" { + return fmt.Errorf("empty label value at index %d in add_labels", i) + } + } + + for i, key := range req.RemoveLabels { + if strings.TrimSpace(key) == "" { + return fmt.Errorf("empty label key at index %d in remove_labels", i) + } + } + + return nil +} + +// ProjectResp represents basic project response. +type ProjectResp struct { + ID int `json:"id"` + Name string `json:"name"` + IsPublic bool `json:"is_public"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + LastInjectionAt *time.Time `json:"last_injection_at,omitempty"` + LastExecutionAt *time.Time `json:"last_execution_at,omitempty"` + InjectionCount int `json:"injection_count"` + ExecutionCount int `json:"execution_count"` + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +func NewProjectResp(project *model.Project, stats *dto.ProjectStatistics) *ProjectResp { + resp := &ProjectResp{ + ID: project.ID, + Name: project.Name, + IsPublic: project.IsPublic, + Status: consts.GetStatusTypeName(project.Status), + CreatedAt: project.CreatedAt, + UpdatedAt: project.UpdatedAt, + } + + if stats != nil { + resp.LastInjectionAt = stats.LastInjectionAt + resp.LastExecutionAt = stats.LastExecutionAt + resp.InjectionCount = stats.InjectionCount + resp.ExecutionCount = stats.ExecutionCount + } + + if project.Labels != nil { + resp.Labels = make([]dto.LabelItem, len(project.Labels)) + for i, label := range project.Labels { + resp.Labels[i] = dto.LabelItem{Key: label.Key, Value: label.Value} + } + } + return resp +} + +// ProjectDetailResp represents detailed project response. +type ProjectDetailResp struct { + ProjectResp + + Containers []ProjectContainerItem `json:"containers,omitempty"` + Datapacks []injectionmodule.InjectionResp `json:"datapacks,omitempty"` + Datasets []ProjectDatasetItem `json:"datasets,omitempty"` + UserCount int `json:"user_count"` +} + +func NewProjectDetailResp(project *model.Project, stats *dto.ProjectStatistics) *ProjectDetailResp { + return &ProjectDetailResp{ + ProjectResp: *NewProjectResp(project, stats), + } +} + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} diff --git a/src/module/project/handler.go b/src/module/project/handler.go new file mode 100644 index 00000000..537f1f79 --- /dev/null +++ b/src/module/project/handler.go @@ -0,0 +1,264 @@ +package projectmodule + +import ( + "aegis/httpx" + "net/http" + "strconv" + + "aegis/consts" + "aegis/dto" + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// CreateProject handles project creation +// +// @Summary Create a new project +// @Description Create a new project with specified details +// @Tags Projects +// @ID create_project +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateProjectReq true "Project creation request" +// @Success 201 {object} dto.GenericResponse[ProjectResp] "Project created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Project already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects [post] +// @x-api-type {"portal":"true"} +func (h *Handler) CreateProject(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + var req CreateProjectReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.CreateProject(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse(c, http.StatusCreated, "Project created successfully", resp) +} + +// DeleteProject handles project deletion +// +// @Summary Delete project +// @Description Delete a project +// @Tags Projects +// @ID delete_project +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Success 204 {object} dto.GenericResponse[any] "Project deleted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id} [delete] +// @x-api-type {"portal":"true"} +func (h *Handler) DeleteProject(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } + + err := h.service.DeleteProject(c.Request.Context(), projectID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse[any](c, http.StatusNoContent, "Project deleted successfully", nil) +} + +// GetProjectDetail handles getting a single project by ID +// +// @Summary Get project by ID +// @Description Get detailed information about a specific project +// @Tags Projects +// @ID get_project_by_id +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Success 200 {object} dto.GenericResponse[ProjectDetailResp] "Project retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id} [get] +// @x-api-type {"portal":"true"} +func (h *Handler) GetProjectDetail(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } + + resp, err := h.service.GetProjectDetail(c.Request.Context(), projectID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} + +// ListProjects handles listing projects with pagination and filtering +// +// @Summary List projects +// @Description Get paginated list of projects with filtering +// @Tags Projects +// @ID list_projects +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param is_public query bool false "Filter by public status" +// @Param status query consts.StatusType false "Filter by status" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ProjectResp]] "Projects retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects [get] +// @x-api-type {"portal":"true"} +func (h *Handler) ListProjects(c *gin.Context) { + var req ListProjectReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ListProjects(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} + +// UpdateProject handles project updates +// +// @Summary Update project +// @Description Update an existing project's information +// @Tags Projects +// @ID update_project +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Param request body UpdateProjectReq true "Project update request" +// @Success 202 {object} dto.GenericResponse[ProjectResp] "Project updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id} [patch] +// @x-api-type {"portal":"true"} +func (h *Handler) UpdateProject(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } + + var req UpdateProjectReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.UpdateProject(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.JSONResponse[any](c, http.StatusAccepted, "Project updated successfully", resp) +} + +// ManageProjectCustomLabels manages project custom labels (key-value pairs) +// +// @Summary Manage project custom labels +// @Description Add or remove custom labels (key-value pairs) for a project +// @Tags Projects +// @ID update_project_labels +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param project_id path int true "Project ID" +// @Param manage body ManageProjectLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[ProjectResp] "Labels managed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/projects/{project_id}/labels [patch] +// @x-api-type {"portal":"true"} +func (h *Handler) ManageProjectCustomLabels(c *gin.Context) { + projectID, ok := parseProjectID(c) + if !ok { + return + } + + var req ManageProjectLabelReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ManageProjectLabels(c.Request.Context(), &req, projectID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) +} + +func parseProjectID(c *gin.Context) (int, bool) { + projectIDStr := c.Param(consts.URLPathProjectID) + projectID, err := strconv.Atoi(projectIDStr) + if err != nil || projectID <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") + return 0, false + } + return projectID, true +} diff --git a/src/module/project/module.go b/src/module/project/module.go new file mode 100644 index 00000000..3187e816 --- /dev/null +++ b/src/module/project/module.go @@ -0,0 +1,13 @@ +package projectmodule + +import ( + "go.uber.org/fx" +) + +var Module = fx.Module("project", + fx.Provide( + NewRepository, + NewService, + NewHandler, + ), +) diff --git a/src/module/project/repository.go b/src/module/project/repository.go new file mode 100644 index 00000000..57204569 --- /dev/null +++ b/src/module/project/repository.go @@ -0,0 +1,322 @@ +package projectmodule + +import ( + "aegis/consts" + "aegis/dto" + "aegis/model" + "fmt" + "time" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) createProjectWithOwner(project *model.Project, userID int) error { + var role model.Role + if err := r.db.Where("name = ? AND status != ?", consts.RoleProjectAdmin.String(), consts.CommonDeleted). + First(&role).Error; err != nil { + return fmt.Errorf("failed to get project owner role: %w", err) + } + + if err := r.db.Omit("ActiveName").Create(project).Error; err != nil { + return fmt.Errorf("failed to create project: %w", err) + } + + if err := r.db.Create(&model.UserProject{ + UserID: userID, + ProjectID: project.ID, + RoleID: role.ID, + Status: consts.CommonEnabled, + }).Error; err != nil { + return fmt.Errorf("failed to create user-project association: %w", err) + } + return nil +} + +func (r *Repository) deleteProjectCascade(projectID int) (int64, error) { + if err := r.db.Model(&model.UserProject{}). + Where("project_id = ? AND status != ?", projectID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove users from project: %w", err) + } + + result := r.db.Model(&model.Project{}). + Where("id = ? AND status != ?", projectID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to soft delete project %d: %w", projectID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) loadProjectDetail(projectID int) (*model.Project, *dto.ProjectStatistics, int, error) { + project, err := r.loadProjectRecord(projectID) + if err != nil { + return nil, nil, 0, err + } + + statsMap, err := r.listProjectStatistics([]int{project.ID}) + if err != nil { + return nil, nil, 0, err + } + + userCount, err := r.countProjectUsers(project.ID) + if err != nil { + return nil, nil, 0, err + } + + return project, statsMap[project.ID], userCount, nil +} + +func (r *Repository) listProjectViews(limit, offset int, isPublic *bool, status *consts.StatusType) ([]model.Project, map[int]*dto.ProjectStatistics, int64, error) { + var ( + projects []model.Project + total int64 + ) + + query := r.db.Model(&model.Project{}) + if isPublic != nil { + query = query.Where("is_public = ?", *isPublic) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, nil, 0, fmt.Errorf("failed to count projects: %w", err) + } + if err := query.Limit(limit).Offset(offset).Find(&projects).Error; err != nil { + return nil, nil, 0, fmt.Errorf("failed to list projects: %w", err) + } + + projectIDs := make([]int, 0, len(projects)) + for _, project := range projects { + projectIDs = append(projectIDs, project.ID) + } + + labelsMap, err := r.listProjectLabels(projectIDs) + if err != nil { + return nil, nil, 0, err + } + + statsMap, err := r.listProjectStatistics(projectIDs) + if err != nil { + return nil, nil, 0, err + } + + for i := range projects { + projects[i].Labels = labelsMap[projects[i].ID] + } + + return projects, statsMap, total, nil +} + +func (r *Repository) updateMutableProject(projectID int, patch func(*model.Project)) (*model.Project, error) { + var project model.Project + if err := r.db.Where("id = ?", projectID).First(&project).Error; err != nil { + return nil, fmt.Errorf("failed to find project with id %d: %w", projectID, err) + } + patch(&project) + if err := r.db.Omit("ActiveName").Save(&project).Error; err != nil { + return nil, fmt.Errorf("failed to update project: %w", err) + } + return &project, nil +} + +func (r *Repository) manageProjectLabels(projectID int, addLabelIDs []int, removeKeys []string) (*model.Project, error) { + project, err := r.loadProjectRecord(projectID) + if err != nil { + return nil, err + } + if err := r.addProjectLabels(projectID, addLabelIDs); err != nil { + return nil, err + } + if err := r.removeProjectLabelsByKeys(projectID, removeKeys); err != nil { + return nil, err + } + labels, err := r.listLabelsByProjectID(project.ID) + if err != nil { + return nil, err + } + project.Labels = labels + return project, nil +} + +func (r *Repository) addProjectLabels(projectID int, labelIDs []int) error { + if len(labelIDs) == 0 { + return nil + } + + projectLabels := make([]model.ProjectLabel, 0, len(labelIDs)) + for _, labelID := range labelIDs { + projectLabels = append(projectLabels, model.ProjectLabel{ + ProjectID: projectID, + LabelID: labelID, + }) + } + if err := r.db.Create(&projectLabels).Error; err != nil { + return fmt.Errorf("failed to add project-label associations: %w", err) + } + return nil +} + +func (r *Repository) removeProjectLabelsByKeys(projectID int, keys []string) error { + if len(keys) == 0 { + return nil + } + + labelIDs, err := r.listProjectLabelIDsByKeys(projectID, keys) + if err != nil { + return fmt.Errorf("failed to find label ids by keys: %w", err) + } + if len(labelIDs) == 0 { + return nil + } + + if err := r.db.Table("project_labels"). + Where("project_id = ? AND label_id IN (?)", projectID, labelIDs). + Delete(nil).Error; err != nil { + return fmt.Errorf("failed to clear project labels: %w", err) + } + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + UpdateColumn("usage_count", gorm.Expr("GREATEST(0, usage_count - ?)", 1)).Error; err != nil { + return fmt.Errorf("failed to decrease label usage counts: %w", err) + } + return nil +} + +func (r *Repository) loadProjectRecord(projectID int) (*model.Project, error) { + var project model.Project + if err := r.db.Where("id = ?", projectID).First(&project).Error; err != nil { + return nil, fmt.Errorf("failed to find project with id %d: %w", projectID, err) + } + return &project, nil +} + +func (r *Repository) countProjectUsers(projectID int) (int, error) { + var userCount int64 + if err := r.db.Model(&model.UserProject{}). + Where("project_id = ? AND status = ?", projectID, consts.CommonEnabled). + Count(&userCount).Error; err != nil { + return 0, fmt.Errorf("failed to count project users: %w", err) + } + return int(userCount), nil +} + +func (r *Repository) listProjectStatistics(projectIDs []int) (map[int]*dto.ProjectStatistics, error) { + statsMap := make(map[int]*dto.ProjectStatistics, len(projectIDs)) + for _, projectID := range projectIDs { + statsMap[projectID] = &dto.ProjectStatistics{} + } + if len(projectIDs) == 0 { + return statsMap, nil + } + + var injectionStats []struct { + ProjectID int + Count int64 + LastAt *time.Time + } + if err := r.db.Table("fault_injections fi"). + Select("tr.project_id, COUNT(*) as count, MAX(fi.updated_at) as last_at"). + Joins("JOIN tasks t ON fi.task_id = t.id"). + Joins("JOIN traces tr ON t.trace_id = tr.id"). + Where("tr.project_id IN (?)", projectIDs). + Group("tr.project_id"). + Scan(&injectionStats).Error; err != nil { + return nil, fmt.Errorf("failed to batch get injection statistics: %w", err) + } + for _, stat := range injectionStats { + statsMap[stat.ProjectID].InjectionCount = int(stat.Count) + statsMap[stat.ProjectID].LastInjectionAt = stat.LastAt + } + + var executionStats []struct { + ProjectID int + Count int64 + LastAt *time.Time + } + if err := r.db.Table("executions e"). + Select("tr.project_id, COUNT(*) as count, MAX(e.updated_at) as last_at"). + Joins("JOIN tasks t ON e.task_id = t.id"). + Joins("JOIN traces tr ON t.trace_id = tr.id"). + Where("tr.project_id IN (?)", projectIDs). + Group("tr.project_id"). + Scan(&executionStats).Error; err != nil { + return nil, fmt.Errorf("failed to batch get execution statistics: %w", err) + } + for _, stat := range executionStats { + statsMap[stat.ProjectID].ExecutionCount = int(stat.Count) + statsMap[stat.ProjectID].LastExecutionAt = stat.LastAt + } + + return statsMap, nil +} + +func (r *Repository) listProjectLabels(projectIDs []int) (map[int][]model.Label, error) { + labelsMap := make(map[int][]model.Label, len(projectIDs)) + for _, projectID := range projectIDs { + labelsMap[projectID] = []model.Label{} + } + if len(projectIDs) == 0 { + return labelsMap, nil + } + + type projectLabelResult struct { + model.Label + ProjectID int `gorm:"column:project_id"` + } + + var flatResults []projectLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN project_labels pl ON pl.label_id = labels.id"). + Where("pl.project_id IN (?)", projectIDs). + Select("labels.*, pl.project_id"). + Find(&flatResults).Error; err != nil { + return nil, fmt.Errorf("failed to batch query project labels: %w", err) + } + + for _, result := range flatResults { + labelsMap[result.ProjectID] = append(labelsMap[result.ProjectID], result.Label) + } + return labelsMap, nil +} + +func (r *Repository) listLabelsByProjectID(projectID int) ([]model.Label, error) { + var labels []model.Label + if err := r.db.Model(&model.Label{}). + Joins("JOIN project_labels pl ON pl.label_id = labels.id"). + Where("pl.project_id = ?", projectID). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels for project %d: %w", projectID, err) + } + return labels, nil +} + +func (r *Repository) listProjectLabelIDsByKeys(projectID int, keys []string) ([]int, error) { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN project_labels pl ON pl.label_id = l.id"). + Where("pl.project_id = ? AND l.label_key IN (?)", projectID, keys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by key '%v': %w", keys, err) + } + return labelIDs, nil +} diff --git a/src/module/project/service.go b/src/module/project/service.go new file mode 100644 index 00000000..7759cf44 --- /dev/null +++ b/src/module/project/service.go @@ -0,0 +1,173 @@ +package projectmodule + +import ( + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/service/common" + + "gorm.io/gorm" +) + +type Service struct { + repository *Repository +} + +func NewService(repository *Repository) *Service { + return &Service{repository: repository} +} + +func (s *Service) CreateProject(ctx context.Context, req *CreateProjectReq, userID int) (*ProjectResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + project := req.ConvertToProject() + + var createdProject *model.Project + err := s.repository.Transaction(func(tx *gorm.DB) error { + if err := s.repository.withDB(tx).createProjectWithOwner(project, userID); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: project with name %s already exists", consts.ErrAlreadyExists, project.Name) + } + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: role %v not found", err, consts.RoleProjectAdmin) + } + return err + } + createdProject = project + return nil + }) + if err != nil { + return nil, err + } + + return NewProjectResp(createdProject, nil), nil +} + +func (s *Service) DeleteProject(ctx context.Context, projectID int) error { + return s.repository.Transaction(func(tx *gorm.DB) error { + rows, err := s.repository.withDB(tx).deleteProjectCascade(projectID) + if err != nil { + return err + } + if rows == 0 { + return fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) + } + + return nil + }) +} + +func (s *Service) GetProjectDetail(ctx context.Context, projectID int) (*ProjectDetailResp, error) { + project, stats, userCount, err := s.repository.loadProjectDetail(projectID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: project with ID %d not found", consts.ErrNotFound, projectID) + } + return nil, fmt.Errorf("failed to get project: %w", err) + } + resp := NewProjectDetailResp(project, stats) + resp.UserCount = userCount + + return resp, nil +} + +func (s *Service) ListProjects(ctx context.Context, req *ListProjectReq) (*dto.ListResp[ProjectResp], error) { + if req == nil { + return nil, fmt.Errorf("list project request is nil") + } + + limit, offset := req.ToGormParams() + + projects, statsMap, total, err := s.repository.listProjectViews(limit, offset, req.IsPublic, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list projects: %w", err) + } + + projectResps := make([]ProjectResp, 0, len(projects)) + for i := range projects { + var stats *dto.ProjectStatistics + if repoStats, exists := statsMap[projects[i].ID]; exists { + stats = &dto.ProjectStatistics{ + InjectionCount: repoStats.InjectionCount, + ExecutionCount: repoStats.ExecutionCount, + LastInjectionAt: repoStats.LastInjectionAt, + LastExecutionAt: repoStats.LastExecutionAt, + } + } + + projectResps = append(projectResps, *NewProjectResp(&projects[i], stats)) + } + + resp := dto.ListResp[ProjectResp]{ + Items: projectResps, + Pagination: req.ConvertToPaginationInfo(total), + } + return &resp, nil +} + +func (s *Service) UpdateProject(ctx context.Context, req *UpdateProjectReq, projectID int) (*ProjectResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + var updatedProject *model.Project + + err := s.repository.Transaction(func(tx *gorm.DB) error { + project, err := s.repository.withDB(tx).updateMutableProject(projectID, func(existingProject *model.Project) { + req.PatchProjectModel(existingProject) + }) + if err != nil { + return fmt.Errorf("failed to get project: %w", err) + } + updatedProject = project + return nil + }) + if err != nil { + return nil, err + } + + return NewProjectResp(updatedProject, nil), nil +} + +func (s *Service) ManageProjectLabels(ctx context.Context, req *ManageProjectLabelReq, projectID int) (*ProjectResp, error) { + if req == nil { + return nil, fmt.Errorf("manage project labels request is nil") + } + + var managedProject *model.Project + err := s.repository.Transaction(func(tx *gorm.DB) error { + repo := s.repository.withDB(tx) + addLabelIDs := make([]int, 0, len(req.AddLabels)) + if len(req.AddLabels) > 0 { + labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + for _, label := range labels { + addLabelIDs = append(addLabelIDs, label.ID) + } + } + + project, err := repo.manageProjectLabels(projectID, addLabelIDs, req.RemoveLabels) + if err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: project id: %d", consts.ErrNotFound, projectID) + } + return fmt.Errorf("failed to manage project labels: %w", err) + } + managedProject = project + return nil + }) + if err != nil { + return nil, err + } + + return NewProjectResp(managedProject, nil), nil +} diff --git a/src/module/project/service_test.go b/src/module/project/service_test.go new file mode 100644 index 00000000..dbfccd2a --- /dev/null +++ b/src/module/project/service_test.go @@ -0,0 +1,194 @@ +package projectmodule + +import ( + "regexp" + "testing" + "time" + + "aegis/consts" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newProjectService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db)), mock, func() { + _ = sqlDB.Close() + } +} + +func TestProjectServiceListProjectsSuccess(t *testing.T) { + service, mock, cleanup := newProjectService(t) + defer cleanup() + + now := time.Now() + isPublic := true + status := consts.CommonEnabled + + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `projects` WHERE is_public = ? AND status = ?")). + WithArgs(isPublic, status). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `projects` WHERE is_public = ? AND status = ? LIMIT ?")). + WithArgs(isPublic, status, 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", + }).AddRow(1, "demo-project", "demo", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT labels.*, pl.project_id FROM `labels` JOIN project_labels pl ON pl.label_id = labels.id WHERE pl.project_id IN (?)")). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "label_key", "label_value", "category", "description", "color", "usage_count", "is_system", "status", "created_at", "updated_at", "project_id", + }).AddRow(10, "env", "prod", consts.ProjectCategory, "", "#1890ff", 1, false, consts.CommonEnabled, now, now, 1)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 2, now)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 3, now)) + + resp, err := service.ListProjects(t.Context(), &ListProjectReq{ + IsPublic: &isPublic, + Status: &status, + }) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "demo-project", resp.Items[0].Name) + require.Len(t, resp.Items[0].Labels, 1) + require.Equal(t, 2, resp.Items[0].InjectionCount) + require.Equal(t, 3, resp.Items[0].ExecutionCount) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestProjectServiceCreateProjectSuccess(t *testing.T) { + service, mock, cleanup := newProjectService(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `roles` WHERE name = ? AND status != ? ORDER BY `roles`.`id` LIMIT ?")). + WithArgs(consts.RoleProjectAdmin.String(), consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", + }).AddRow(3, consts.RoleProjectAdmin.String(), "Project Admin", "", true, consts.CommonEnabled, time.Now(), time.Now())) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `projects` (`name`,`description`,`team_id`,`is_public`,`status`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?)")). + WithArgs("demo-project", "demo", nil, true, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(11, 1)) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `user_projects` (`user_id`,`project_id`,`role_id`,`workspace_config`,`status`,`created_at`,`updated_at`,`active_user_project`) VALUES (?,?,?,?,?,?,?,?)")). + WithArgs(7, 11, 3, "", consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). + WillReturnResult(sqlmock.NewResult(21, 1)) + mock.ExpectCommit() + + isPublic := true + resp, err := service.CreateProject(t.Context(), &CreateProjectReq{ + Name: "demo-project", + Description: "demo", + IsPublic: &isPublic, + }, 7) + + require.NoError(t, err) + require.Equal(t, 11, resp.ID) + require.Equal(t, "demo-project", resp.Name) + require.True(t, resp.IsPublic) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestProjectServiceGetProjectDetailSuccess(t *testing.T) { + service, mock, cleanup := newProjectService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `projects` WHERE id = ? ORDER BY `projects`.`id` LIMIT ?")). + WithArgs(1, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", + }).AddRow(1, "demo-project", "demo", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 2, now)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). + WithArgs(1). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 3, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `user_projects` WHERE project_id = ? AND status = ?")). + WithArgs(1, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(4)) + + resp, err := service.GetProjectDetail(t.Context(), 1) + + require.NoError(t, err) + require.Equal(t, "demo-project", resp.Name) + require.Equal(t, 4, resp.UserCount) + require.Equal(t, 2, resp.InjectionCount) + require.Equal(t, 3, resp.ExecutionCount) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestProjectServiceUpdateProjectSuccess(t *testing.T) { + service, mock, cleanup := newProjectService(t) + defer cleanup() + + now := time.Now() + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `projects` WHERE id = ? ORDER BY `projects`.`id` LIMIT ?")). + WithArgs(1, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", + }).AddRow(1, "demo-project", "old-desc", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `projects` SET `name`=?,`description`=?,`team_id`=?,`is_public`=?,`status`=?,`created_at`=?,`updated_at`=? WHERE `id` = ?")). + WithArgs("demo-project", "new-desc", nil, false, consts.CommonDisabled, sqlmock.AnyArg(), sqlmock.AnyArg(), 1). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + description := "new-desc" + isPublic := false + status := consts.CommonDisabled + resp, err := service.UpdateProject(t.Context(), &UpdateProjectReq{ + Description: &description, + IsPublic: &isPublic, + Status: &status, + }, 1) + + require.NoError(t, err) + require.Equal(t, "demo-project", resp.Name) + require.False(t, resp.IsPublic) + require.Equal(t, consts.GetStatusTypeName(consts.CommonDisabled), resp.Status) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestProjectServiceDeleteProjectSuccess(t *testing.T) { + service, mock, cleanup := newProjectService(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_projects` SET `status`=?,`updated_at`=? WHERE project_id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 2)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `projects` SET `status`=?,`updated_at`=? WHERE id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + err := service.DeleteProject(t.Context(), 1) + + require.NoError(t, err) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestProjectServiceManageLabelsNilRequest(t *testing.T) { + service := NewService(nil) + + _, err := service.ManageProjectLabels(t.Context(), nil, 1) + + require.Error(t, err) + require.ErrorContains(t, err, "manage project labels request is nil") +} diff --git a/src/module/rbac/api_types.go b/src/module/rbac/api_types.go new file mode 100644 index 00000000..92cc57ba --- /dev/null +++ b/src/module/rbac/api_types.go @@ -0,0 +1,326 @@ +package rbacmodule + +import ( + "fmt" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" +) + +// CreateRoleReq represents role creation request. +type CreateRoleReq struct { + Name string `json:"name" binding:"required"` + DisplayName string `json:"display_name" binding:"required"` + Description string `json:"description,omitempty" binding:"omitempty"` +} + +func (req *CreateRoleReq) ConvertToRole() *model.Role { + return &model.Role{ + Name: req.Name, + DisplayName: req.DisplayName, + Description: req.Description, + IsSystem: false, + Status: consts.CommonEnabled, + } +} + +// ListRoleReq represents role list query parameters. +type ListRoleReq struct { + dto.PaginationReq + IsSystem *bool `form:"is_system" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListRoleReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateStatus(req.Status, false) +} + +// UpdateRoleReq represents role update request. +type UpdateRoleReq struct { + DisplayName *string `json:"display_name" binding:"omitempty"` + Description *string `json:"description" binding:"omitempty"` + Status *consts.StatusType `json:"status" binding:"omitempty"` +} + +func (req *UpdateRoleReq) Validate() error { + if req.DisplayName != nil && *req.DisplayName != "" { + *req.DisplayName = strings.TrimSpace(*req.DisplayName) + } + return validateStatus(req.Status, true) +} + +func (req *UpdateRoleReq) PatchRoleModel(target *model.Role) { + if req.DisplayName != nil { + target.DisplayName = *req.DisplayName + } + if req.Description != nil { + target.Description = *req.Description + } + if req.Status != nil { + target.Status = *req.Status + } +} + +// AssignRolePermissionReq represents request to assign permissions to a role. +type AssignRolePermissionReq struct { + PermissionIDs []int `json:"permission_ids" binding:"required,min=1,non_zero_int_slice"` +} + +// RemoveRolePermissionReq represents request to remove permissions from a role. +type RemoveRolePermissionReq struct { + PermissionIDs []int `json:"permission_ids" binding:"required,min=1,non_zero_int_slice"` +} + +// ListResourceReq represents request for listing resources. +type ListResourceReq struct { + dto.PaginationReq + + Type *consts.ResourceType `form:"type" binding:"omitempty"` + Category *consts.ResourceCategory `form:"category" binding:"omitempty"` +} + +func (req *ListResourceReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if req.Type != nil { + if _, exists := consts.ValidResourceTypes[*req.Type]; !exists { + return fmt.Errorf("invalid resource type: %d", *req.Type) + } + } + if req.Category != nil { + if _, exists := consts.ValidResourceCategories[*req.Category]; !exists { + return fmt.Errorf("invalid resource category: %d", *req.Category) + } + } + return nil +} + +// ResourceResp represents an RBAC resource response. +type ResourceResp struct { + ID int `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Type string `json:"type"` + Category string `json:"category"` + ParentID *int `json:"parent_id,omitempty"` +} + +func NewResourceResp(resource *model.Resource) *ResourceResp { + return &ResourceResp{ + ID: resource.ID, + Name: resource.Name.String(), + DisplayName: resource.DisplayName, + Type: consts.GetResourceTypeName(resource.Type), + Category: consts.GetResourceCategoryName(resource.Category), + ParentID: resource.ParentID, + } +} + +// ResourceDetailResp represents a detailed RBAC resource response. +type ResourceDetailResp struct { + ResourceResp + + Description string `json:"description,omitempty"` +} + +func NewResourceDetailResp(resource *model.Resource) *ResourceDetailResp { + return &ResourceDetailResp{ + ResourceResp: *NewResourceResp(resource), + Description: resource.Description, + } +} + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} + +// RoleResp represents role response. +type RoleResp struct { + ID int `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Type string `json:"type"` + IsSystem bool `json:"is_system"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewRoleResp(role *model.Role) *RoleResp { + return &RoleResp{ + ID: role.ID, + Name: role.Name, + DisplayName: role.DisplayName, + IsSystem: role.IsSystem, + Status: consts.GetStatusTypeName(role.Status), + UpdatedAt: role.UpdatedAt, + } +} + +// RoleDetailResp represents role detail response. +type RoleDetailResp struct { + RoleResp + + Description string `json:"description"` + CreatedAt time.Time `json:"created_at"` + UserCount int64 `json:"user_count"` + Permissions []PermissionResp `json:"permissions"` +} + +func NewRoleDetailResp(role *model.Role) *RoleDetailResp { + return &RoleDetailResp{ + RoleResp: *NewRoleResp(role), + Description: role.Description, + CreatedAt: role.CreatedAt, + } +} + +// ListPermissionReq represents permission list query parameters. +type ListPermissionReq struct { + dto.PaginationReq + Action consts.ActionName `form:"action" binding:"omitempty"` + IsSystem *bool `form:"is_system" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListPermissionReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if req.Action != "" { + if _, exists := consts.ValidActions[req.Action]; !exists { + return fmt.Errorf("invalid action: %s", req.Action) + } + } + return validateStatus(req.Status, false) +} + +// PermissionBaseResp contains common fields for permission responses. +type PermissionBaseResp struct { + ID int `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Action consts.ActionName `json:"action"` + Scope consts.ResourceScope `json:"scope"` + IsSystem bool `json:"is_system"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewPermissionBaseResp(perm *model.Permission) *PermissionBaseResp { + return &PermissionBaseResp{ + ID: perm.ID, + Name: perm.Name, + DisplayName: perm.DisplayName, + Action: perm.Action, + Scope: perm.Scope, + IsSystem: perm.IsSystem, + Status: consts.GetStatusTypeName(perm.Status), + UpdatedAt: perm.UpdatedAt, + } +} + +// PermissionResp represents permission summary information. +type PermissionResp struct { + PermissionBaseResp + Resource string `json:"resource_name"` +} + +func NewPermissionResp(perm *model.Permission) *PermissionResp { + resp := &PermissionResp{ + PermissionBaseResp: *NewPermissionBaseResp(perm), + } + if perm.Resource != nil { + resp.Resource = perm.Resource.Name.String() + } + return resp +} + +// PermissionDetailResp represents permission detail information. +type PermissionDetailResp struct { + PermissionBaseResp + Description string `json:"description"` + Resource *PermissionResourceResp `json:"resource,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func NewPermissionDetailResp(perm *model.Permission) *PermissionDetailResp { + resp := &PermissionDetailResp{ + PermissionBaseResp: *NewPermissionBaseResp(perm), + Description: perm.Description, + CreatedAt: perm.CreatedAt, + } + if perm.Resource != nil { + resp.Resource = NewPermissionResourceResp(perm.Resource) + } + return resp +} + +// PermissionResourceResp keeps the resource snapshot embedded in permission detail responses. +type PermissionResourceResp struct { + ID int `json:"id"` + Name string `json:"name"` + DisplayName string `json:"display_name"` + Type string `json:"type"` + Category string `json:"category"` + ParentID *int `json:"parent_id,omitempty"` +} + +func NewPermissionResourceResp(resource *model.Resource) *PermissionResourceResp { + return &PermissionResourceResp{ + ID: resource.ID, + Name: resource.Name.String(), + DisplayName: resource.DisplayName, + Type: consts.GetResourceTypeName(resource.Type), + Category: consts.GetResourceCategoryName(resource.Category), + ParentID: resource.ParentID, + } +} + +// UserListItem is the RBAC-facing user summary contract for role membership queries. +type UserListItem struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FullName string `json:"full_name"` + Avatar string `json:"avatar,omitempty"` + Phone string `json:"phone,omitempty"` + IsActive bool `json:"is_active"` + Status string `json:"status"` + LastLoginAt *time.Time `json:"last_login_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewUserListItem(user *model.User) *UserListItem { + return &UserListItem{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + FullName: user.FullName, + Avatar: user.Avatar, + Phone: user.Phone, + IsActive: user.IsActive, + Status: consts.GetStatusTypeName(user.Status), + LastLoginAt: user.LastLoginAt, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } +} diff --git a/src/module/rbac/handler.go b/src/module/rbac/handler.go new file mode 100644 index 00000000..7a506e53 --- /dev/null +++ b/src/module/rbac/handler.go @@ -0,0 +1,478 @@ +package rbacmodule + +import ( + "aegis/httpx" + "net/http" + "strconv" + + "aegis/consts" + "aegis/dto" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// CreateRole handles role creation +// +// @Summary Create a new role +// @Description Create a new role with specified permissions +// @Tags Roles +// @ID create_role +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body CreateRoleReq true "Role creation request" +// @Success 201 {object} dto.GenericResponse[RoleResp] "Role created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Role already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles [post] +// @x-api-type {"admin":"true"} +func (h *Handler) CreateRole(c *gin.Context) { + var req CreateRoleReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + resp, err := h.service.CreateRole(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusCreated, "Role created successfully", resp) +} + +// DeleteRole handles role deletion +// +// @Summary Delete role +// @Description Delete a role (soft delete by setting status to -1) +// @Tags Roles +// @ID delete_role +// @Produce json +// @Security BearerAuth +// @Param id path int true "Role ID" +// @Success 200 {object} dto.GenericResponse[any] "Role deleted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied or cannot delete system role" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{id} [delete] +// @x-api-type {"admin":"true"} +func (h *Handler) DeleteRole(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + if httpx.HandleServiceError(c, h.service.DeleteRole(c.Request.Context(), roleID)) { + return + } + dto.JSONResponse[any](c, http.StatusNoContent, "Role deleted successfully", nil) +} + +// GetRole handles getting a single role by ID +// +// @Summary Get role by ID +// @Description Get detailed information about a specific role +// @Tags Roles +// @ID get_role_by_id +// @Produce json +// @Security BearerAuth +// @Param id path int true "Role ID" +// @Success 200 {object} dto.GenericResponse[RoleDetailResp] "Role retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{id} [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetRole(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + resp, err := h.service.GetRole(c.Request.Context(), roleID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListRoles handles listing roles with pagination and filtering +// +// @Summary List roles +// @Description Get paginated list of roles with optional filtering +// @Tags Roles +// @ID list_roles +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param is_system query bool false "Filter by system role" +// @Param status query consts.StatusType false "Filter by status" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[RoleResp]] "Roles retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListRoles(c *gin.Context) { + var req ListRoleReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + resp, err := h.service.ListRoles(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// UpdateRole handles role updates +// +// @Summary Update role +// @Description Update role information (partial update supported) +// @Tags Roles +// @ID update_role +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "Role ID" +// @Param request body UpdateRoleReq true "Role update request" +// @Success 202 {object} dto.GenericResponse[RoleResp] "Role updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{id} [patch] +// @x-api-type {"admin":"true"} +func (h *Handler) UpdateRole(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + var req UpdateRoleReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + resp, err := h.service.UpdateRole(c.Request.Context(), &req, roleID) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse[any](c, http.StatusAccepted, "Role updated successfully", resp) +} + +// AssignRolePermission handles role-permission assignment +// +// @Summary Assign permissions to role +// @Description Assign multiple permissions to a role +// @Tags Roles +// @ID grant_permissions_to_role +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param role_id path int true "Role ID" +// @Param request body AssignRolePermissionReq true "Permission assignment request" +// @Success 200 {object} dto.GenericResponse[any] "Permissions assigned successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID or request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{role_id}/permissions/assign [post] +// @x-api-type {"admin":"true"} +func (h *Handler) AssignRolePermissions(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + var req AssignRolePermissionReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if httpx.HandleServiceError(c, h.service.AssignRolePermissions(c.Request.Context(), req.PermissionIDs, roleID)) { + return + } + dto.JSONResponse[any](c, http.StatusOK, "Permissions assigned successfully", nil) +} + +// RemovePermissionsFromRole handles permission removal from role +// +// @Summary Remove permissions from role +// @Description Remove multiple permissions from a role +// @Tags Roles +// @ID revoke_permissions_from_role +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param role_id path int true "Role ID" +// @Param request body RemoveRolePermissionReq true "Permission removal request" +// @Success 200 {object} dto.GenericResponse[any] "Permissions removed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID or request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{role_id}/permissions/remove [post] +// @x-api-type {"admin":"true"} +func (h *Handler) RemoveRolePermissions(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + var req RemoveRolePermissionReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if httpx.HandleServiceError(c, h.service.RemoveRolePermissions(c.Request.Context(), req.PermissionIDs, roleID)) { + return + } + dto.JSONResponse[any](c, http.StatusOK, "Permissions removed successfully", nil) +} + +// ListUsersFromRole handles listing users assigned to a role +// +// @Summary List users from role +// @Description Get list of users assigned to a specific role +// @Tags Roles +// @ID list_users_by_role +// @Produce json +// @Security BearerAuth +// @Param role_id path int true "Role ID" +// @Success 200 {object} dto.GenericResponse[[]UserListItem] "Users retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/roles/{role_id}/users [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListUsersFromRole(c *gin.Context) { + roleID, ok := parseID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return + } + resp, err := h.service.ListUsersFromRole(c.Request.Context(), roleID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// GetPermission handles getting a single permission by ID +// +// @Summary Get permission by ID +// @Description Get detailed information about a specific permission +// @Tags Permissions +// @ID get_permission_by_id +// @Produce json +// @Security BearerAuth +// @Param id path int true "Permission ID" +// @Success 200 {object} dto.GenericResponse[PermissionDetailResp] "Permission retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/permissions/{id} [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetPermission(c *gin.Context) { + permissionID, ok := parseID(c, consts.URLPathPermissionID, "Invalid permission ID") + if !ok { + return + } + resp, err := h.service.GetPermission(c.Request.Context(), permissionID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListPermissions handles listing permissions with pagination and filtering +// +// @Summary List permissions +// @Description Get paginated list of permissions with optional filtering +// @Tags Permissions +// @ID list_permissions +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param action query string false "Filter by action" +// @Param is_system query bool false "Filter by system permission" +// @Param status query consts.StatusType false "Filter by status" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[PermissionResp]] "Permissions retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/permissions [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListPermissions(c *gin.Context) { + var req ListPermissionReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + resp, err := h.service.ListPermissions(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListRolesFromPermission handles listing roles assigned to a permission +// +// @Summary List roles from permission +// @Description Get list of roles assigned to a specific permission +// @Tags Permissions +// @ID list_roles_with_permission +// @Produce json +// @Security BearerAuth +// @Param permission_id path int true "Permission ID" +// @Success 200 {object} dto.GenericResponse[[]RoleResp] "Roles retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/permissions/{permission_id}/roles [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListRolesFromPermission(c *gin.Context) { + permissionID, ok := parseID(c, consts.URLPathPermissionID, "Invalid permission ID") + if !ok { + return + } + resp, err := h.service.ListRolesFromPermission(c.Request.Context(), permissionID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// GetResourceDetail handles getting a single resource by ID +// +// @Summary Get resource by ID +// @Description Get detailed information about a specific resource +// @Tags Resources +// @ID get_resource_by_id +// @Produce json +// @Security BearerAuth +// @Param id path int true "Resource ID" +// @Success 200 {object} dto.GenericResponse[ResourceResp] "Resource retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/resources/{id} [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetResource(c *gin.Context) { + resourceID, ok := parseID(c, consts.URLPathResourceID, "Invalid resource ID") + if !ok { + return + } + resp, err := h.service.GetResource(c.Request.Context(), resourceID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListResources handles listing resources with pagination and filtering +// +// @Summary List resources +// @Description Get paginated list of resources with filtering +// @Tags Resources +// @ID list_resources +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param type query consts.ResourceType false "Filter by resource type" +// @Param category query consts.ResourceCategory false "Filter by resource category" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ResourceResp]] "Resources retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/resources [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListResources(c *gin.Context) { + var req ListResourceReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + resp, err := h.service.ListResources(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListResourcePermissions handles listing permissions by resource +// +// @Summary List permissions from resource +// @Description Get list of permissions assigned to a specific resource +// @Tags Resources +// @ID list_resource_permissions +// @Produce json +// @Security BearerAuth +// @Param id path int true "Resource ID" +// @Success 200 {object} dto.GenericResponse[[]PermissionResp] "Permissions retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID or request form" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/resources/{id}/permissions [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListResourcePermissions(c *gin.Context) { + resourceID, ok := parseID(c, consts.URLPathResourceID, "Invalid resource ID") + if !ok { + return + } + resp, err := h.service.ListResourcePermissions(c.Request.Context(), resourceID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +func parseID(c *gin.Context, param, message string) (int, bool) { + value := c.Param(param) + id, err := strconv.Atoi(value) + if err != nil || id <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, message) + return 0, false + } + return id, true +} diff --git a/src/module/rbac/module.go b/src/module/rbac/module.go new file mode 100644 index 00000000..68d583f0 --- /dev/null +++ b/src/module/rbac/module.go @@ -0,0 +1,9 @@ +package rbacmodule + +import "go.uber.org/fx" + +var Module = fx.Module("rbac", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/rbac/repository.go b/src/module/rbac/repository.go new file mode 100644 index 00000000..ceb74125 --- /dev/null +++ b/src/module/rbac/repository.go @@ -0,0 +1,358 @@ +package rbacmodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) createRoleRecord(role *model.Role) error { + if err := r.db.Create(role).Error; err != nil { + return fmt.Errorf("failed to create role: %w", err) + } + return nil +} + +func (r *Repository) deleteRoleCascade(roleID int) (int64, error) { + role, err := r.loadRole(roleID) + if err != nil { + return 0, err + } + if role.IsSystem { + return 0, fmt.Errorf("%w: cannot delete system role", consts.ErrPermissionDenied) + } + + if err := r.db.Model(&model.UserContainer{}). + Where("role_id = ? AND status != ?", role.ID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove containers with role: %w", err) + } + if err := r.db.Model(&model.UserDataset{}). + Where("role_id = ? AND status != ?", role.ID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove datasets with role: %w", err) + } + if err := r.db.Model(&model.UserProject{}). + Where("role_id = ? AND status != ?", role.ID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove projects with role: %w", err) + } + if err := r.db.Where("role_id = ?", role.ID).Delete(&model.RolePermission{}).Error; err != nil { + return 0, fmt.Errorf("failed to remove permissions with role: %w", err) + } + if err := r.db.Where("role_id = ?", role.ID).Delete(&model.UserRole{}).Error; err != nil { + return 0, fmt.Errorf("failed to remove users with role: %w", err) + } + + result := r.db.Model(&model.Role{}). + Where("id = ? AND status != ?", role.ID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete role %d: %w", role.ID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) loadRoleDetail(roleID int) (*model.Role, int64, []model.Permission, error) { + role, err := r.loadRole(roleID) + if err != nil { + return nil, 0, nil, err + } + + var userCount int64 + if err := r.db.Table("users"). + Joins("JOIN user_roles ON users.id = user_roles.user_id"). + Where("user_roles.role_id = ? AND users.status = ?", role.ID, consts.CommonEnabled). + Count(&userCount).Error; err != nil { + return nil, 0, nil, fmt.Errorf("failed to get role user count: %w", err) + } + + var permissions []model.Permission + if err := r.db.Table("permissions"). + Joins("JOIN role_permissions ON permissions.id = role_permissions.permission_id"). + Where("role_permissions.role_id = ? AND permissions.status = ?", role.ID, consts.CommonEnabled). + Find(&permissions).Error; err != nil { + return nil, 0, nil, fmt.Errorf("failed to get role permissions: %w", err) + } + + return role, userCount, permissions, nil +} + +func (r *Repository) listRoleViews(limit, offset int, isSystem *bool, status *consts.StatusType) ([]model.Role, int64, error) { + var roles []model.Role + var total int64 + + query := r.db.Model(&model.Role{}) + if isSystem != nil { + query = query.Where("is_system = ?", *isSystem) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count roles: %v", err) + } + if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&roles).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list roles: %v", err) + } + return roles, total, nil +} + +func (r *Repository) updateMutableRole(roleID int, patch func(*model.Role)) (*model.Role, error) { + role, err := r.loadRole(roleID) + if err != nil { + return nil, err + } + if role.IsSystem { + return nil, fmt.Errorf("%w: cannot update system role", consts.ErrPermissionDenied) + } + + patch(role) + if err := r.db.Omit("ActiveName").Save(role).Error; err != nil { + return nil, fmt.Errorf("failed to update role: %w", err) + } + return role, nil +} + +func (r *Repository) loadAssignablePermissions(permissionIDs []int) (map[int]model.Permission, error) { + if len(permissionIDs) == 0 { + return map[int]model.Permission{}, nil + } + + unique := make(map[int]struct{}, len(permissionIDs)) + for _, id := range permissionIDs { + unique[id] = struct{}{} + } + + deduplicatedIDs := make([]int, 0, len(unique)) + for id := range unique { + deduplicatedIDs = append(deduplicatedIDs, id) + } + + permissions, err := r.listPermissionsByIDs(deduplicatedIDs) + if err != nil { + return nil, fmt.Errorf("failed to list permissions by ids: %w", err) + } + + result := make(map[int]model.Permission, len(permissions)) + for _, permission := range permissions { + result[permission.ID] = permission + } + return result, nil +} + +func (r *Repository) AssignRolePermissions(roleID int, permissionIDs []int) error { + role, err := r.loadRole(roleID) + if err != nil { + return err + } + if role.IsSystem { + return fmt.Errorf("%w: cannot assign permissions to system role", consts.ErrPermissionDenied) + } + + permissionMap, err := r.loadAssignablePermissions(permissionIDs) + if err != nil { + return err + } + + rolePermissions := make([]model.RolePermission, 0, len(permissionIDs)) + for _, permissionID := range permissionIDs { + if _, exists := permissionMap[permissionID]; !exists { + return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, permissionID) + } + rolePermissions = append(rolePermissions, model.RolePermission{ + RoleID: role.ID, + PermissionID: permissionID, + }) + } + + if len(rolePermissions) == 0 { + return nil + } + if err := r.db.Create(&rolePermissions).Error; err != nil { + return fmt.Errorf("failed to batch create role permissions: %w", err) + } + return nil +} + +func (r *Repository) RemoveRolePermissions(roleID int, permissionIDs []int) error { + role, err := r.loadRole(roleID) + if err != nil { + return err + } + if role.IsSystem { + return fmt.Errorf("%w: cannot remove permissions of system role", consts.ErrPermissionDenied) + } + + permissionMap, err := r.loadAssignablePermissions(permissionIDs) + if err != nil { + return err + } + for _, permissionID := range permissionIDs { + if _, exists := permissionMap[permissionID]; !exists { + return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, permissionID) + } + } + + if len(permissionIDs) == 0 { + return nil + } + if err := r.db.Where("role_id = ? AND permission_id IN (?)", role.ID, permissionIDs). + Delete(&model.RolePermission{}).Error; err != nil { + return fmt.Errorf("failed to batch delete role permissions: %w", err) + } + return nil +} + +func (r *Repository) listUsersFromRole(roleID int) (*model.Role, []model.User, error) { + role, err := r.loadRole(roleID) + if err != nil { + return nil, nil, err + } + + var users []model.User + if err := r.db.Table("users"). + Joins("JOIN user_roles ON users.id = user_roles.user_id"). + Where("user_roles.role_id = ? AND users.status = ?", role.ID, consts.CommonEnabled). + Find(&users).Error; err != nil { + return nil, nil, fmt.Errorf("failed to get role users: %w", err) + } + return role, users, nil +} + +func (r *Repository) getPermissionDetail(permissionID int) (*model.Permission, error) { + var permission model.Permission + if err := r.db.Preload("Resource"). + Where("id = ? and status != ?", permissionID, consts.CommonDeleted). + First(&permission).Error; err != nil { + return nil, fmt.Errorf("failed to find permission with id %d: %w", permissionID, err) + } + return &permission, nil +} + +func (r *Repository) listPermissionViews(limit, offset int, action consts.ActionName, isSystem *bool, status *consts.StatusType) ([]model.Permission, int64, error) { + var permissions []model.Permission + var total int64 + + query := r.db.Model(&model.Permission{}) + if action != "" { + query = query.Where("action = ?", action) + } + if isSystem != nil { + query = query.Where("is_system = ?", *isSystem) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count permissions: %v", err) + } + if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&permissions).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list permissions: %v", err) + } + return permissions, total, nil +} + +func (r *Repository) listRolesFromPermission(permissionID int) (*model.Permission, []model.Role, error) { + permission, err := r.getPermissionDetail(permissionID) + if err != nil { + return nil, nil, err + } + + var roles []model.Role + if err := r.db.Table("roles"). + Joins("JOIN role_permissions ON roles.id = role_permissions.role_id"). + Where("role_permissions.permission_id = ? AND roles.status != ?", permission.ID, consts.CommonDeleted). + Find(&roles).Error; err != nil { + return nil, nil, fmt.Errorf("failed to get permission roles: %w", err) + } + return permission, roles, nil +} + +func (r *Repository) getResourceDetail(resourceID int) (*model.Resource, error) { + var resource model.Resource + if err := r.db. + Where("id = ? and status != ?", resourceID, consts.CommonDeleted). + First(&resource).Error; err != nil { + return nil, fmt.Errorf("failed to find resource with id %d: %w", resourceID, err) + } + return &resource, nil +} + +func (r *Repository) listResourceViews(limit, offset int, resourceType *consts.ResourceType, category *consts.ResourceCategory) ([]model.Resource, int64, error) { + var resources []model.Resource + var total int64 + + query := r.db.Model(&model.Resource{}).Preload("Parent") + if resourceType != nil { + query = query.Where("type = ?", *resourceType) + } + if category != nil { + query = query.Where("category = ?", *category) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count resources: %v", err) + } + if err := query.Limit(limit).Offset(offset).Find(&resources).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list resources: %v", err) + } + return resources, total, nil +} + +func (r *Repository) listResourcePermissions(resourceID int) (*model.Resource, []model.Permission, error) { + resource, err := r.getResourceDetail(resourceID) + if err != nil { + return nil, nil, err + } + + var permissions []model.Permission + if err := r.db. + Where("resource_id = ? AND status = ?", resource.ID, consts.CommonEnabled). + Order("action"). + Find(&permissions).Error; err != nil { + return nil, nil, fmt.Errorf("failed to get permissions by resource: %w", err) + } + return resource, permissions, nil +} + +func (r *Repository) loadRole(roleID int) (*model.Role, error) { + var role model.Role + if err := r.db.Where("id = ? and status != ?", roleID, consts.CommonDeleted).First(&role).Error; err != nil { + return nil, fmt.Errorf("failed to find role with id %d: %w", roleID, err) + } + return &role, nil +} + +func (r *Repository) listPermissionsByIDs(permissionIDs []int) ([]model.Permission, error) { + if len(permissionIDs) == 0 { + return []model.Permission{}, nil + } + + var permissions []model.Permission + if err := r.db.Where("id IN (?) AND status = ?", permissionIDs, consts.CommonEnabled). + Find(&permissions).Error; err != nil { + return nil, fmt.Errorf("failed to query permissions: %w", err) + } + return permissions, nil +} diff --git a/src/module/rbac/service.go b/src/module/rbac/service.go new file mode 100644 index 00000000..83187667 --- /dev/null +++ b/src/module/rbac/service.go @@ -0,0 +1,243 @@ +package rbacmodule + +import ( + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) CreateRole(_ context.Context, req *CreateRoleReq) (*RoleResp, error) { + role := req.ConvertToRole() + + if err := s.repo.Transaction(func(tx *gorm.DB) error { + if err := s.repo.withDB(tx).createRoleRecord(role); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: role with name %s already exists", consts.ErrAlreadyExists, role.Name) + } + return err + } + return nil + }); err != nil { + return nil, err + } + + return NewRoleResp(role), nil +} + +func (s *Service) DeleteRole(_ context.Context, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + rows, err := s.repo.withDB(tx).deleteRoleCascade(roleID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: role not found", consts.ErrNotFound) + } + return err + } + if rows == 0 { + return fmt.Errorf("%w: role id %d not found", consts.ErrNotFound, roleID) + } + return nil + }) +} + +func (s *Service) GetRole(_ context.Context, roleID int) (*RoleDetailResp, error) { + role, userCount, permissions, err := s.repo.loadRoleDetail(roleID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: role with ID %d not found", consts.ErrNotFound, roleID) + } + return nil, fmt.Errorf("failed to get role: %w", err) + } + + resp := NewRoleDetailResp(role) + resp.UserCount = userCount + + resp.Permissions = make([]PermissionResp, 0, len(permissions)) + for _, permission := range permissions { + resp.Permissions = append(resp.Permissions, *NewPermissionResp(&permission)) + } + + return resp, nil +} + +func (s *Service) ListRoles(_ context.Context, req *ListRoleReq) (*dto.ListResp[RoleResp], error) { + limit, offset := req.ToGormParams() + roles, total, err := s.repo.listRoleViews(limit, offset, req.IsSystem, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list roles: %w", err) + } + + items := make([]RoleResp, len(roles)) + for i, role := range roles { + items[i] = *NewRoleResp(&role) + } + + return &dto.ListResp[RoleResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateRole(_ context.Context, req *UpdateRoleReq, roleID int) (*RoleResp, error) { + var updatedRole *model.Role + + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + role, err := repo.updateMutableRole(roleID, func(existingRole *model.Role) { + req.PatchRoleModel(existingRole) + }) + if err != nil { + return err + } + updatedRole = role + return nil + }) + if err != nil { + return nil, err + } + + return NewRoleResp(updatedRole), nil +} + +func (s *Service) AssignRolePermissions(_ context.Context, permissionIDs []int, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.AssignRolePermissions(roleID, permissionIDs); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: role already has one or more of these permissions", consts.ErrAlreadyExists) + } + return fmt.Errorf("failed to assign permissions to role: %w", err) + } + return nil + }) +} + +func (s *Service) RemoveRolePermissions(_ context.Context, permissionIDs []int, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.RemoveRolePermissions(roleID, permissionIDs); err != nil { + return fmt.Errorf("failed to remove permissions from role: %w", err) + } + return nil + }) +} + +func (s *Service) ListUsersFromRole(_ context.Context, roleID int) ([]UserListItem, error) { + _, users, err := s.repo.listUsersFromRole(roleID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: role not found", consts.ErrNotFound) + } + return nil, err + } + + userResps := make([]UserListItem, 0, len(users)) + for _, user := range users { + userResps = append(userResps, *NewUserListItem(&user)) + } + return userResps, nil +} + +func (s *Service) GetPermission(_ context.Context, permissionID int) (*PermissionDetailResp, error) { + permission, err := s.repo.getPermissionDetail(permissionID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: permission not found", consts.ErrNotFound) + } + return nil, fmt.Errorf("failed to get permission: %w", err) + } + return NewPermissionDetailResp(permission), nil +} + +func (s *Service) ListPermissions(_ context.Context, req *ListPermissionReq) (*dto.ListResp[PermissionResp], error) { + limit, offset := req.ToGormParams() + permissions, total, err := s.repo.listPermissionViews(limit, offset, req.Action, req.IsSystem, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list permissions: %w", err) + } + + items := make([]PermissionResp, len(permissions)) + for i, permission := range permissions { + items[i] = *NewPermissionResp(&permission) + } + + return &dto.ListResp[PermissionResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) ListRolesFromPermission(_ context.Context, permissionID int) ([]RoleResp, error) { + _, roles, err := s.repo.listRolesFromPermission(permissionID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: permission not found", consts.ErrNotFound) + } + return nil, err + } + + items := make([]RoleResp, 0, len(roles)) + for _, role := range roles { + items = append(items, *NewRoleResp(&role)) + } + return items, nil +} + +func (s *Service) GetResource(_ context.Context, resourceID int) (*ResourceResp, error) { + resource, err := s.repo.getResourceDetail(resourceID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: resource with ID %d not found", consts.ErrNotFound, resourceID) + } + return nil, fmt.Errorf("failed to get resource: %w", err) + } + return NewResourceResp(resource), nil +} + +func (s *Service) ListResources(_ context.Context, req *ListResourceReq) (*dto.ListResp[ResourceResp], error) { + limit, offset := req.ToGormParams() + resources, total, err := s.repo.listResourceViews(limit, offset, req.Type, req.Category) + if err != nil { + return nil, fmt.Errorf("failed to list resources: %w", err) + } + + items := make([]ResourceResp, 0, len(resources)) + for i := range resources { + items = append(items, *NewResourceResp(&resources[i])) + } + + return &dto.ListResp[ResourceResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) ListResourcePermissions(_ context.Context, resourceID int) ([]PermissionResp, error) { + _, permissions, err := s.repo.listResourcePermissions(resourceID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: resource with ID %d not found", consts.ErrNotFound, resourceID) + } + return nil, err + } + + items := make([]PermissionResp, 0, len(permissions)) + for _, permission := range permissions { + items = append(items, *NewPermissionResp(&permission)) + } + return items, nil +} diff --git a/src/module/rbac/service_test.go b/src/module/rbac/service_test.go new file mode 100644 index 00000000..5423e8ff --- /dev/null +++ b/src/module/rbac/service_test.go @@ -0,0 +1,66 @@ +package rbacmodule + +import ( + "regexp" + "testing" + "time" + + "aegis/consts" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newRBACService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db)), mock, func() { + _ = sqlDB.Close() + } +} + +func TestServiceListRolesSuccess(t *testing.T) { + service, mock, cleanup := newRBACService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `roles`")). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `roles` ORDER BY updated_at DESC LIMIT ?")). + WithArgs(20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", + }).AddRow(1, "admin", "Admin", "system admin", false, consts.CommonEnabled, now, now)) + + resp, err := service.ListRoles(t.Context(), &ListRoleReq{}) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "admin", resp.Items[0].Name) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceGetRoleNotFound(t *testing.T) { + service, mock, cleanup := newRBACService(t) + defer cleanup() + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `roles` WHERE id = ? and status != ? ORDER BY `roles`.`id` LIMIT ?")). + WithArgs(99, consts.CommonDeleted, 1). + WillReturnError(gorm.ErrRecordNotFound) + + _, err := service.GetRole(t.Context(), 99) + + require.Error(t, err) + require.ErrorContains(t, err, "failed to get role") + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/dto/sdk_evaluation.go b/src/module/sdk/api_types.go similarity index 91% rename from src/dto/sdk_evaluation.go rename to src/module/sdk/api_types.go index 2e5ac672..e655dfcb 100644 --- a/src/dto/sdk_evaluation.go +++ b/src/module/sdk/api_types.go @@ -1,10 +1,14 @@ -package dto +package sdkmodule -import "fmt" +import ( + "fmt" + + "aegis/dto" +) // ListSDKEvaluationReq represents the request for listing SDK evaluation samples. type ListSDKEvaluationReq struct { - PaginationReq + dto.PaginationReq ExpID string `form:"exp_id"` Stage string `form:"stage"` // "init", "rollout", "judged" } @@ -30,7 +34,7 @@ type SDKExperimentListResp struct { // ListSDKDatasetSampleReq represents the request for listing SDK dataset samples. type ListSDKDatasetSampleReq struct { - PaginationReq + dto.PaginationReq Dataset string `form:"dataset"` } diff --git a/src/handlers/v2/sdk_evaluations.go b/src/module/sdk/handler.go similarity index 69% rename from src/handlers/v2/sdk_evaluations.go rename to src/module/sdk/handler.go index 7ad4425e..a79f70d3 100644 --- a/src/handlers/v2/sdk_evaluations.go +++ b/src/module/sdk/handler.go @@ -1,16 +1,23 @@ -package v2 +package sdkmodule import ( + "aegis/httpx" "net/http" "aegis/consts" "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // ListSDKEvaluations handles listing SDK evaluation samples with pagination // // @Summary List SDK evaluation samples @@ -23,27 +30,25 @@ import ( // @Param stage query string false "Stage filter (init, rollout, judged)" // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[database.SDKEvaluationSample]] "SDK evaluations retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[SDKEvaluationSample]] "SDK evaluations retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations [get] -func ListSDKEvaluations(c *gin.Context) { - var req dto.ListSDKEvaluationReq +// @x-api-type {"sdk":"true"} +func (h *Handler) ListEvaluations(c *gin.Context) { + var req ListSDKEvaluationReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListSDKEvaluations(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListEvaluations(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -56,22 +61,21 @@ func ListSDKEvaluations(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "SDK Evaluation Sample ID" -// @Success 200 {object} dto.GenericResponse[database.SDKEvaluationSample] "SDK evaluation sample retrieved successfully" +// @Success 200 {object} dto.GenericResponse[SDKEvaluationSample] "SDK evaluation sample retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" // @Failure 404 {object} dto.GenericResponse[any] "SDK evaluation sample not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations/{id} [get] -func GetSDKEvaluation(c *gin.Context) { - id, ok := handlers.ParsePositiveID(c, c.Param(consts.URLPathID), "SDK evaluation ID") +// @x-api-type {"sdk":"true"} +func (h *Handler) GetEvaluation(c *gin.Context) { + id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "SDK evaluation ID") if !ok { return } - - resp, err := producer.GetSDKEvaluation(id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetEvaluation(c.Request.Context(), id) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -83,15 +87,15 @@ func GetSDKEvaluation(c *gin.Context) { // @ID list_sdk_experiments // @Produce json // @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.SDKExperimentListResp] "SDK experiments retrieved successfully" +// @Success 200 {object} dto.GenericResponse[SDKExperimentListResp] "SDK experiments retrieved successfully" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations/experiments [get] -func ListSDKExperiments(c *gin.Context) { - resp, err := producer.ListSDKExperiments() - if handlers.HandleServiceError(c, err) { +// @x-api-type {"sdk":"true"} +func (h *Handler) ListExperiments(c *gin.Context) { + resp, err := h.service.ListExperiments(c.Request.Context()) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -106,26 +110,24 @@ func ListSDKExperiments(c *gin.Context) { // @Param dataset query string false "Dataset name filter" // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[database.SDKDatasetSample]] "SDK dataset samples retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[SDKDatasetSample]] "SDK dataset samples retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/datasets [get] -func ListSDKDatasetSamples(c *gin.Context) { - var req dto.ListSDKDatasetSampleReq +// @x-api-type {"sdk":"true"} +func (h *Handler) ListDatasetSamples(c *gin.Context) { + var req ListSDKDatasetSampleReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListSDKDatasetSamples(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListDatasetSamples(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } diff --git a/src/database/sdk_entities.go b/src/module/sdk/models.go similarity index 94% rename from src/database/sdk_entities.go rename to src/module/sdk/models.go index 826340e9..0cdb7de2 100644 --- a/src/database/sdk_entities.go +++ b/src/module/sdk/models.go @@ -1,9 +1,9 @@ -package database +package sdkmodule import "time" // SDKDatasetSample maps to the Python SDK's `data` table (read-only from AegisLab). -// Do NOT add this to AutoMigrate — the SDK creates and manages this table. +// Do NOT add this to AutoMigrate - the SDK creates and manages this table. type SDKDatasetSample struct { ID int `gorm:"primaryKey;column:id" json:"id"` Dataset string `gorm:"column:dataset" json:"dataset"` @@ -22,7 +22,7 @@ type SDKDatasetSample struct { func (SDKDatasetSample) TableName() string { return "data" } // SDKEvaluationSample maps to the Python SDK's `evaluation_data` table (read-only from AegisLab). -// Do NOT add this to AutoMigrate — the SDK creates and manages this table. +// Do NOT add this to AutoMigrate - the SDK creates and manages this table. type SDKEvaluationSample struct { ID int `gorm:"primaryKey;column:id" json:"id"` CreatedAt *time.Time `gorm:"column:created_at" json:"created_at"` diff --git a/src/module/sdk/module.go b/src/module/sdk/module.go new file mode 100644 index 00000000..8f42df2f --- /dev/null +++ b/src/module/sdk/module.go @@ -0,0 +1,9 @@ +package sdkmodule + +import "go.uber.org/fx" + +var Module = fx.Module("sdk", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/repository/sdk_evaluation.go b/src/module/sdk/repository.go similarity index 54% rename from src/repository/sdk_evaluation.go rename to src/module/sdk/repository.go index 6fdd643d..f60d28b9 100644 --- a/src/repository/sdk_evaluation.go +++ b/src/module/sdk/repository.go @@ -1,33 +1,27 @@ -package repository +package sdkmodule import ( "fmt" "strings" - "aegis/database" - "gorm.io/gorm" ) -// isTableNotExistError checks if the error indicates the table does not exist. -// This handles the case where the SDK tables have not been created yet. -func isTableNotExistError(err error) bool { - if err == nil { - return false - } - msg := err.Error() - return strings.Contains(msg, "doesn't exist") || - strings.Contains(msg, "does not exist") || - strings.Contains(msg, "no such table") +type Repository struct { + db *gorm.DB } -// ListSDKEvaluations returns paginated SDK evaluation samples filtered by exp_id and stage. -func ListSDKEvaluations(db *gorm.DB, expID string, stage string, limit, offset int) ([]database.SDKEvaluationSample, int64, error) { - var items []database.SDKEvaluationSample - var total int64 +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} - query := db.Model(&database.SDKEvaluationSample{}) +func (r *Repository) ListSDKEvaluations(expID, stage string, limit, offset int) ([]SDKEvaluationSample, int64, error) { + var ( + items []SDKEvaluationSample + total int64 + ) + query := r.db.Model(&SDKEvaluationSample{}) if expID != "" { query = query.Where("exp_id = ?", expID) } @@ -37,14 +31,13 @@ func ListSDKEvaluations(db *gorm.DB, expID string, stage string, limit, offset i if err := query.Count(&total).Error; err != nil { if isTableNotExistError(err) { - return []database.SDKEvaluationSample{}, 0, nil + return []SDKEvaluationSample{}, 0, nil } return nil, 0, fmt.Errorf("failed to count SDK evaluation samples: %w", err) } - if err := query.Limit(limit).Offset(offset).Order("id DESC").Find(&items).Error; err != nil { if isTableNotExistError(err) { - return []database.SDKEvaluationSample{}, 0, nil + return []SDKEvaluationSample{}, 0, nil } return nil, 0, fmt.Errorf("failed to list SDK evaluation samples: %w", err) } @@ -52,10 +45,9 @@ func ListSDKEvaluations(db *gorm.DB, expID string, stage string, limit, offset i return items, total, nil } -// GetSDKEvaluationByID returns a single SDK evaluation sample by its ID. -func GetSDKEvaluationByID(db *gorm.DB, id int) (*database.SDKEvaluationSample, error) { - var item database.SDKEvaluationSample - if err := db.Where("id = ?", id).First(&item).Error; err != nil { +func (r *Repository) GetSDKEvaluationByID(id int) (*SDKEvaluationSample, error) { + var item SDKEvaluationSample + if err := r.db.Where("id = ?", id).First(&item).Error; err != nil { if isTableNotExistError(err) { return nil, fmt.Errorf("SDK evaluation sample with id %d not found (table does not exist)", id) } @@ -67,10 +59,9 @@ func GetSDKEvaluationByID(db *gorm.DB, id int) (*database.SDKEvaluationSample, e return &item, nil } -// ListSDKExperiments returns all distinct exp_id values from the evaluation_data table. -func ListSDKExperiments(db *gorm.DB) ([]string, error) { +func (r *Repository) ListSDKExperiments() ([]string, error) { var expIDs []string - if err := db.Model(&database.SDKEvaluationSample{}).Distinct("exp_id").Pluck("exp_id", &expIDs).Error; err != nil { + if err := r.db.Model(&SDKEvaluationSample{}).Distinct("exp_id").Pluck("exp_id", &expIDs).Error; err != nil { if isTableNotExistError(err) { return []string{}, nil } @@ -79,30 +70,39 @@ func ListSDKExperiments(db *gorm.DB) ([]string, error) { return expIDs, nil } -// ListSDKDatasetSamples returns paginated SDK dataset samples filtered by dataset name. -func ListSDKDatasetSamples(db *gorm.DB, dataset string, limit, offset int) ([]database.SDKDatasetSample, int64, error) { - var items []database.SDKDatasetSample - var total int64 - - query := db.Model(&database.SDKDatasetSample{}) +func (r *Repository) ListSDKDatasetSamples(dataset string, limit, offset int) ([]SDKDatasetSample, int64, error) { + var ( + items []SDKDatasetSample + total int64 + ) + query := r.db.Model(&SDKDatasetSample{}) if dataset != "" { query = query.Where("dataset = ?", dataset) } if err := query.Count(&total).Error; err != nil { if isTableNotExistError(err) { - return []database.SDKDatasetSample{}, 0, nil + return []SDKDatasetSample{}, 0, nil } return nil, 0, fmt.Errorf("failed to count SDK dataset samples: %w", err) } - if err := query.Limit(limit).Offset(offset).Order("id DESC").Find(&items).Error; err != nil { if isTableNotExistError(err) { - return []database.SDKDatasetSample{}, 0, nil + return []SDKDatasetSample{}, 0, nil } return nil, 0, fmt.Errorf("failed to list SDK dataset samples: %w", err) } return items, total, nil } + +func isTableNotExistError(err error) bool { + if err == nil { + return false + } + msg := err.Error() + return strings.Contains(msg, "doesn't exist") || + strings.Contains(msg, "does not exist") || + strings.Contains(msg, "no such table") +} diff --git a/src/module/sdk/service.go b/src/module/sdk/service.go new file mode 100644 index 00000000..92136a5d --- /dev/null +++ b/src/module/sdk/service.go @@ -0,0 +1,52 @@ +package sdkmodule + +import ( + "context" + "fmt" + + "aegis/dto" +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) ListEvaluations(_ context.Context, req *ListSDKEvaluationReq) (*dto.ListResp[SDKEvaluationSample], error) { + limit, offset := req.ToGormParams() + items, total, err := s.repo.ListSDKEvaluations(req.ExpID, req.Stage, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list SDK evaluations: %w", err) + } + return &dto.ListResp[SDKEvaluationSample]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetEvaluation(_ context.Context, id int) (*SDKEvaluationSample, error) { + return s.repo.GetSDKEvaluationByID(id) +} + +func (s *Service) ListExperiments(_ context.Context) (*SDKExperimentListResp, error) { + items, err := s.repo.ListSDKExperiments() + if err != nil { + return nil, fmt.Errorf("failed to list SDK experiments: %w", err) + } + return &SDKExperimentListResp{Experiments: items}, nil +} + +func (s *Service) ListDatasetSamples(_ context.Context, req *ListSDKDatasetSampleReq) (*dto.ListResp[SDKDatasetSample], error) { + limit, offset := req.ToGormParams() + items, total, err := s.repo.ListSDKDatasetSamples(req.Dataset, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to list SDK dataset samples: %w", err) + } + return &dto.ListResp[SDKDatasetSample]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} diff --git a/src/module/sdk/service_test.go b/src/module/sdk/service_test.go new file mode 100644 index 00000000..e4b6976e --- /dev/null +++ b/src/module/sdk/service_test.go @@ -0,0 +1,117 @@ +package sdkmodule + +import ( + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newSDKService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db)), mock, func() { + _ = sqlDB.Close() + } +} + +func TestSDKServiceListEvaluationsSuccess(t *testing.T) { + service, mock, cleanup := newSDKService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `evaluation_data` WHERE exp_id = ? AND stage = ?")). + WithArgs("exp-1", "judged"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `evaluation_data` WHERE exp_id = ? AND stage = ? ORDER BY id DESC LIMIT ?")). + WithArgs("exp-1", "judged", 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "created_at", "updated_at", "dataset", "dataset_index", "source", "raw_question", "level", + "augmented_question", "correct_answer", "file_name", "meta", "trace_id", "trace_url", "response", + "time_cost", "trajectories", "extracted_final_answer", "judged_response", "reasoning", "correct", + "confidence", "exp_id", "agent_type", "model_name", "stage", + }).AddRow(1, now, now, "demo", 0, "manual", "q", 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, true, 0.9, "exp-1", nil, nil, "judged")) + + resp, err := service.ListEvaluations(t.Context(), &ListSDKEvaluationReq{ + ExpID: "exp-1", + Stage: "judged", + }) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "exp-1", resp.Items[0].ExpID) + require.Equal(t, "judged", resp.Items[0].Stage) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestSDKServiceGetEvaluationSuccess(t *testing.T) { + service, mock, cleanup := newSDKService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `evaluation_data` WHERE id = ? ORDER BY `evaluation_data`.`id` LIMIT ?")). + WithArgs(3, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "created_at", "updated_at", "dataset", "dataset_index", "source", "raw_question", "level", + "augmented_question", "correct_answer", "file_name", "meta", "trace_id", "trace_url", "response", + "time_cost", "trajectories", "extracted_final_answer", "judged_response", "reasoning", "correct", + "confidence", "exp_id", "agent_type", "model_name", "stage", + }).AddRow(3, now, now, "demo", 0, "manual", "q", 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, false, 0.2, "exp-2", nil, nil, "rollout")) + + item, err := service.GetEvaluation(t.Context(), 3) + + require.NoError(t, err) + require.Equal(t, 3, item.ID) + require.Equal(t, "exp-2", item.ExpID) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestSDKServiceListExperimentsSuccess(t *testing.T) { + service, mock, cleanup := newSDKService(t) + defer cleanup() + + mock.ExpectQuery(regexp.QuoteMeta("SELECT DISTINCT `exp_id` FROM `evaluation_data`")). + WillReturnRows(sqlmock.NewRows([]string{"exp_id"}).AddRow("exp-1").AddRow("exp-2")) + + resp, err := service.ListExperiments(t.Context()) + + require.NoError(t, err) + require.Equal(t, []string{"exp-1", "exp-2"}, resp.Experiments) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestSDKServiceListDatasetSamplesSuccess(t *testing.T) { + service, mock, cleanup := newSDKService(t) + defer cleanup() + + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `data` WHERE dataset = ?")). + WithArgs("gsm8k"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `data` WHERE dataset = ? ORDER BY id DESC LIMIT ?")). + WithArgs("gsm8k", 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "dataset", "index", "source", "source_index", "question", "answer", "topic", "level", "file_name", "meta", "tags", + }).AddRow(2, "gsm8k", 1, "manual", 0, "question", "answer", "math", 2, "sample.json", nil, nil)) + + resp, err := service.ListDatasetSamples(t.Context(), &ListSDKDatasetSampleReq{ + Dataset: "gsm8k", + }) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "gsm8k", resp.Items[0].Dataset) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/module/system/api_types.go b/src/module/system/api_types.go new file mode 100644 index 00000000..805bfc07 --- /dev/null +++ b/src/module/system/api_types.go @@ -0,0 +1,431 @@ +package systemmodule + +import ( + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" +) + +// HealthCheckResp represents system health check response. +type HealthCheckResp struct { + Status string `json:"status"` + Timestamp time.Time `json:"timestamp"` + Version string `json:"version"` + Uptime string `json:"uptime"` + Services map[string]ServiceInfo `json:"services" swaggertype:"object"` +} + +// ServiceInfo represents individual service health information. +type ServiceInfo struct { + Status string `json:"status"` + LastChecked time.Time `json:"last_checked"` + ResponseTime string `json:"response_time"` + Error string `json:"error,omitempty"` + Details any `json:"details,omitempty"` +} + +// SystemInfo represents system information. +type SystemInfo struct { + CPUUsage float64 `json:"cpu_usage"` + MemoryUsage float64 `json:"memory_usage"` + DiskUsage float64 `json:"disk_usage"` + LoadAverage string `json:"load_average"` +} + +// MonitoringQueryReq represents monitoring query request. +type MonitoringQueryReq struct { + Query string `json:"query" binding:"required"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + Step string `json:"step,omitempty"` +} + +// MonitoringMetricsResp represents monitoring metrics response. +type MonitoringMetricsResp struct { + Timestamp time.Time `json:"timestamp"` + Metrics map[string]MetricValue `json:"metrics"` + Labels map[string]string `json:"labels,omitempty"` +} + +type MetricValue = systemmetricmodule.MetricValue +type ListNamespaceLockResp = systemmetricmodule.ListNamespaceLockResp +type QueuedTasksResp = taskmodule.QueuedTasksResp + +type ListAuditLogFilters struct { + Action string + IPAddress string + UserID int + ResourceID int + State *consts.AuditLogState + Status *consts.StatusType + StartTime *time.Time + EndTime *time.Time +} + +type ListAuditLogReq struct { + dto.PaginationReq + + Action string `form:"action" binding:"omitempty"` + IPAddress string `form:"ip_address" binding:"omitempty"` + UserID int `form:"user_id" binding:"omitempty"` + ResourceID int `form:"resource_id" binding:"omitempty"` + State *consts.AuditLogState `form:"state" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` + StartDate string `form:"start_date" binding:"omitempty"` + EndDate string `form:"end_date" binding:"omitempty"` +} + +func (req *ListAuditLogReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if err := validateDateField(req.StartDate); err != nil { + return fmt.Errorf("invalid start_time: %w", err) + } + if err := validateDateField(req.EndDate); err != nil { + return fmt.Errorf("invalid end_time: %w", err) + } + if req.State != nil { + if _, exists := consts.ValidAuditLogStates[*req.State]; !exists { + return fmt.Errorf("invalid state: %d", *req.State) + } + } + return validateStatusValue(req.Status, false) +} + +func (req *ListAuditLogReq) ToFilterOptions() *ListAuditLogFilters { + var startTimePtr, endTimePtr *time.Time + if req.StartDate != "" { + startTime, _ := time.Parse(time.DateOnly, req.StartDate) + startTimePtr = &startTime + } + if req.EndDate != "" { + endTime, _ := time.Parse(time.DateOnly, req.EndDate) + endTimePtr = &endTime + } + + return &ListAuditLogFilters{ + Action: req.Action, + IPAddress: req.IPAddress, + UserID: req.UserID, + ResourceID: req.ResourceID, + State: req.State, + Status: req.Status, + StartTime: startTimePtr, + EndTime: endTimePtr, + } +} + +type AuditLogResp struct { + ID int `json:"id"` + Action string `json:"action"` + IPAddress string `json:"ip_address"` + Duration int `json:"duration"` + UserAgent string `json:"user_agent"` + UserID int `json:"user_id,omitempty"` + Username string `json:"username,omitempty"` + ResourceID int `json:"resource_id,omitempty"` + Resource consts.ResourceName `json:"resource,omitempty"` + State string `json:"state"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +func NewAuditLogResp(log *model.AuditLog) *AuditLogResp { + resp := &AuditLogResp{ + ID: log.ID, + Action: log.Action, + IPAddress: log.IPAddress, + Duration: log.Duration, + UserAgent: log.UserAgent, + UserID: log.UserID, + ResourceID: log.ResourceID, + State: consts.GetAuditLogStateName(log.State), + Status: consts.GetStatusTypeName(log.Status), + CreatedAt: log.CreatedAt, + } + if log.User != nil { + resp.Username = log.User.Username + } + if log.Resource != nil { + resp.Resource = log.Resource.Name + } + return resp +} + +type AuditLogDetailResp struct { + AuditLogResp + Details string `json:"details"` + ErrorMsg string `json:"error_msg,omitempty"` +} + +func NewAuditLogDetailResp(log *model.AuditLog) *AuditLogDetailResp { + return &AuditLogDetailResp{ + AuditLogResp: *NewAuditLogResp(log), + Details: log.Details, + ErrorMsg: log.ErrorMsg, + } +} + +type ListConfigReq struct { + dto.PaginationReq + ValueType *consts.ConfigValueType `form:"value_type" binding:"omitempty"` + Category *string `form:"category" binding:"omitempty"` + IsSecret *bool `form:"is_secret" binding:"omitempty"` + UpdatedBy *int `form:"updated_by" binding:"omitempty,min_ptr=1"` +} + +func (req *ListConfigReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + return validateConfigValueType(req.ValueType) +} + +type RollbackConfigReq struct { + HistoryID int `json:"history_id" binding:"required,min=1"` + Reason string `json:"reason" binding:"required"` +} + +type UpdateConfigValueReq struct { + Value string `json:"value" binding:"required"` + Reason string `json:"reason" binding:"required"` +} + +type UpdateConfigMetadataReq struct { + DefaultValue *string `json:"default_value" binding:"omitempty"` + Description *string `json:"description" binding:"omitempty"` + MinValue *float64 `json:"min_value" binding:"omitempty"` + MaxValue *float64 `json:"max_value" binding:"omitempty"` + Pattern *string `json:"pattern" binding:"omitempty"` + Options *string `json:"options" binding:"omitempty"` + Reason string `json:"reason" binding:"required"` +} + +func (req *UpdateConfigMetadataReq) Validate() error { + fieldCount := 0 + if req.DefaultValue != nil { + fieldCount++ + } + if req.Description != nil { + fieldCount++ + } + if req.MinValue != nil { + fieldCount++ + } + if req.MaxValue != nil { + fieldCount++ + } + if req.Pattern != nil { + fieldCount++ + } + if req.Options != nil { + fieldCount++ + } + + if fieldCount == 0 { + return fmt.Errorf("at least one metadata field must be provided for update") + } + if fieldCount > 1 { + return fmt.Errorf("can only update one metadata field at a time") + } + return nil +} + +func (req *UpdateConfigMetadataReq) PatchConfigModel(target *model.DynamicConfig) (string, string) { + var oldValue string + var newValue string + + if req.DefaultValue != nil { + oldValue = target.DefaultValue + newValue = *req.DefaultValue + target.DefaultValue = *req.DefaultValue + } + if req.Description != nil { + oldValue = target.Description + newValue = *req.Description + target.Description = *req.Description + } + if req.MinValue != nil { + oldValue = fmt.Sprintf("%v", target.MinValue) + newValue = fmt.Sprintf("%v", req.MinValue) + target.MinValue = req.MinValue + } + if req.MaxValue != nil { + oldValue = fmt.Sprintf("%v", target.MaxValue) + newValue = fmt.Sprintf("%v", req.MaxValue) + target.MaxValue = req.MaxValue + } + if req.Pattern != nil { + oldValue = target.Pattern + newValue = *req.Pattern + target.Pattern = *req.Pattern + } + if req.Options != nil { + oldValue = target.Options + newValue = *req.Options + target.Options = *req.Options + } + + return oldValue, newValue +} + +func (req *UpdateConfigMetadataReq) GetChangeField() consts.ConfigHistoryChangeField { + if req.DefaultValue != nil { + return consts.ChangeFieldDefaultValue + } + if req.Description != nil { + return consts.ChangeFieldDescription + } + if req.MinValue != nil { + return consts.ChangeFieldMinValue + } + if req.MaxValue != nil { + return consts.ChangeFieldMaxValue + } + if req.Pattern != nil { + return consts.ChangeFieldPattern + } + if req.Options != nil { + return consts.ChangeFieldOptions + } + return consts.ChangeFieldValue +} + +type ListConfigHistoryReq struct { + dto.PaginationReq + ChangeType *consts.ConfigHistoryChangeType `form:"change_type" binding:"omitempty"` + OperatorID *int `form:"operator_id" binding:"omitempty,min_ptr=1"` +} + +func (req *ListConfigHistoryReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if req.ChangeType != nil { + if _, ok := consts.ValidConfigHistoryChanteTypes[*req.ChangeType]; !ok { + return fmt.Errorf("invalid change type: %v", req.ChangeType) + } + } + return nil +} + +type ConfigResp struct { + ID int `json:"id"` + Key string `json:"key"` + ValueType string `json:"value_type"` + Category string `json:"category"` + UpdatedAt time.Time `json:"updated_at"` + UpdatedByID int `json:"updated_by_id"` + UpdatedByName string `json:"updated_by_name"` +} + +func NewConfigResp(config *model.DynamicConfig) *ConfigResp { + resp := &ConfigResp{ + ID: config.ID, + Key: config.Key, + ValueType: consts.GetDynamicConfigTypeName(config.ValueType), + Category: config.Category, + UpdatedAt: config.UpdatedAt, + } + if config.UpdatedByUser != nil { + resp.UpdatedByName = config.UpdatedByUser.Username + } + return resp +} + +type ConfigDetailResp struct { + ConfigResp + DefaultValue string `json:"default_value"` + Description string `json:"description"` + MinValue *float64 `json:"min_value,omitempty"` + MaxValue *float64 `json:"max_value,omitempty"` + Pattern string `json:"pattern,omitempty"` + Options string `json:"options,omitempty"` + Histories []ConfigHistoryResp `json:"histories,omitempty"` +} + +func NewConfigDetailResp(config *model.DynamicConfig) *ConfigDetailResp { + return &ConfigDetailResp{ + ConfigResp: *NewConfigResp(config), + DefaultValue: config.DefaultValue, + Description: config.Description, + MinValue: config.MinValue, + MaxValue: config.MaxValue, + Pattern: config.Pattern, + Options: config.Options, + } +} + +type ConfigHistoryResp struct { + ID int `json:"id"` + ChangeType string `json:"change_type"` + OldValue string `json:"old_value"` + NewValue string `json:"new_value"` + Reason string `json:"reason"` + ConfigID int `json:"config_id"` + OperatorID *int `json:"operator_id"` + OperatorName string `json:"operator_name,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + UserAgent string `json:"user_agent,omitempty"` + RolledBackFromID *int `json:"rolled_back_from_id,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func NewConfigHistoryResp(history *model.ConfigHistory) *ConfigHistoryResp { + resp := &ConfigHistoryResp{ + ID: history.ID, + ChangeType: consts.GetConfigHistoryChangeTypeName(history.ChangeType), + ConfigID: history.ConfigID, + OldValue: history.OldValue, + NewValue: history.NewValue, + Reason: history.Reason, + OperatorID: history.OperatorID, + IPAddress: history.IPAddress, + UserAgent: history.UserAgent, + RolledBackFromID: history.RolledBackFromID, + CreatedAt: history.CreatedAt, + } + if history.Operator != nil { + resp.OperatorName = history.Operator.Username + } + return resp +} + +func validateDateField(value string) error { + if value == "" { + return nil + } + if _, err := time.Parse(time.DateOnly, value); err != nil { + return fmt.Errorf("invalid time format: %s", value) + } + return nil +} + +func validateStatusValue(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} + +func validateConfigValueType(valueType *consts.ConfigValueType) error { + if valueType != nil { + if _, ok := consts.ValidDynamicConfigTypes[*valueType]; !ok { + return fmt.Errorf("invalid value type: %v", valueType) + } + } + return nil +} diff --git a/src/module/system/handler.go b/src/module/system/handler.go new file mode 100644 index 00000000..4a4fd252 --- /dev/null +++ b/src/module/system/handler.go @@ -0,0 +1,500 @@ +package systemmodule + +import ( + "aegis/httpx" + "net/http" + "strconv" + + "aegis/consts" + "aegis/dto" + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// GetHealth handles system health check +// +// @Summary System health check +// @Description Get system health status and service information +// @Tags System +// @ID get_system_health +// @Produce json +// @Success 200 {object} dto.GenericResponse[HealthCheckResp] "Health check successful" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/health [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetHealth(c *gin.Context) { + resp, err := h.service.GetHealth(c.Request.Context()) + if err != nil { + dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get health status: "+err.Error()) + return + } + dto.SuccessResponse(c, resp) +} + +// GetMetrics handles monitoring metrics query +// +// @Summary Get monitoring metrics +// @Description Deprecated: This endpoint returns hardcoded/fabricated data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. +// @Deprecated +// @Tags System +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body MonitoringQueryReq true "Metrics query request" +// @Success 200 {object} dto.GenericResponse[MonitoringMetricsResp] "Metrics retrieved successfully" +// @Success 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/monitor/metrics [post] +// @x-api-type {"admin":"true"} +func (h *Handler) GetMetrics(c *gin.Context) { + var req MonitoringQueryReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + c.Header("Deprecation", "true") + c.Header("Link", `; rel="successor-version"`) + dto.SuccessResponse(c, h.service.GetMetrics()) +} + +// GetSystemInfo handles basic system information +// +// @Summary Get system information +// @Description Deprecated: This endpoint returns partially hardcoded data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. +// @Deprecated +// @Tags System +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.GenericResponse[SystemInfo] "System info retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/monitor/info [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetSystemInfo(c *gin.Context) { + c.Header("Deprecation", "true") + c.Header("Link", `; rel="successor-version"`) + dto.SuccessResponse(c, h.service.GetSystemInfo()) +} + +// ListNamespaceLocks handles listing of namespace locks +// +// @Summary List namespace locks +// @Description Retrieve the list of currently locked namespaces +// @Tags System +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.GenericResponse[ListNamespaceLockResp] "Successfully retrieved the list of locks" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal Server Error" +// @Router /system/monitor/namespaces/locks [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListNamespaceLocks(c *gin.Context) { + resp, err := h.service.ListNamespaceLocks(c.Request.Context()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Successfully retrieved the list of locks", resp) +} + +// ListQueuedTasks handles listing of queued tasks +// +// @Summary List queued tasks +// @Description List tasks in queue (ready and delayed) +// @Tags System +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.GenericResponse[QueuedTasksResp] "Queued tasks retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "No queued tasks found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/monitor/tasks/queue [post] +// @x-api-type {"admin":"true"} +func (h *Handler) ListQueuedTasks(c *gin.Context) { + resp, err := h.service.ListQueuedTasks(c.Request.Context()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Queued tasks retrieved successfully", resp) +} + +// GetAuditLog handles single audit log retrieval +// +// @Summary Get audit log by ID +// @Description Get a specific audit log entry by ID +// @Tags System +// @Produce json +// @Security BearerAuth +// @Param id path int true "Audit log ID" +// @Success 200 {object} dto.GenericResponse[AuditLogDetailResp] "Audit log retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Audit log not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/audit/{id} [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetAuditLog(c *gin.Context) { + id, ok := parseID(c, "id", "Invalid audit log ID") + if !ok { + return + } + + resp, err := h.service.GetAuditLog(id) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListAuditLogs handles audit log listing +// +// @Summary List audit logs +// @Description Get paginated list of audit logs with optional filtering +// @Tags System +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param action query string false "Filter by action" +// @Param user_id query int false "Filter by user ID" +// @Param resource_id query int false "Filter by resource ID" +// @Param state query int false "Filter by state" +// @Param status query int false "Filter by status" +// @Param start_date query string false "Filter from date (YYYY-MM-DD)" +// @Param end_date query string false "Filter to date (YYYY-MM-DD)" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[AuditLogResp]] "Audit logs retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/audit [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListAuditLogs(c *gin.Context) { + var req ListAuditLogReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid query parameters: "+err.Error()) + return + } + + resp, err := h.service.ListAuditLogs(&req) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Audit logs retrieved successfully", resp) +} + +// GetConfig retrieves a configuration by ID +// +// @Summary Get configuration +// @Description Get detailed information about a specific configuration +// @Tags Configurations +// @ID get_config_by_id +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Config not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id} [get] +// @x-api-type {"admin":"true"} +func (h *Handler) GetConfig(c *gin.Context) { + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + resp, err := h.service.GetConfig(configID) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// ListConfigs lists configurations with pagination and filtering +// +// @Summary List configurations +// @Description List configurations with pagination and optional filters +// @Tags Configurations +// @ID list_configs +// @Produce json +// @Security BearerAuth +// @Param page query int false "Page number" default(1) +// @Param page_size query int false "Page size" default(20) +// @Param category query string false "Filter by configuration category" +// @Param value_type query consts.ConfigValueType false "Filter by configuration value type" +// @Param is_secret query bool false "Filter by secret status" +// @Param updated_by query int false "Filter by ID of the user who last updated the config" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ConfigResp]] "Configurations retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListConfigs(c *gin.Context) { + var req ListConfigReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ListConfigs(&req) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) +} + +// RollbackConfigValue rolls back a configuration value to previous value from history +// +// @Summary Rollback configuration value +// @Description Rollback a configuration value to a previous value from history +// @Tags Configurations +// @ID rollback_config_value +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Param rollback body RollbackConfigReq true "Rollback request with history_id and reason" +// @Success 202 {object} dto.GenericResponse[any] "Configuration value rolled back successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is not a value change" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id}/value/rollback [post] +// @x-api-type {"admin":"true"} +func (h *Handler) RollbackConfigValue(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + var req RollbackConfigReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + err := h.service.RollbackConfigValue(c.Request.Context(), &req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse[any](c, http.StatusAccepted, "Configuration value rolled back successfully", nil) +} + +// RollbackConfigMetadata rolls back a configuration metadata field to previous value from history +// +// @Summary Rollback configuration metadata +// @Description Rollback a configuration metadata field (e.g., min_value, max_value, pattern) to a previous value from history +// @Tags Configurations +// @ID rollback_config_metadata +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Param rollback body RollbackConfigReq true "Rollback request with history_id and reason" +// @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration metadata rolled back successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is a value change" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id}/metadata/rollback [post] +// @x-api-type {"admin":"true"} +func (h *Handler) RollbackConfigMetadata(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + var req RollbackConfigReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + resp, err := h.service.RollbackConfigMetadata(&req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Configuration metadata rolled back successfully", resp) +} + +// UpdateConfigValue updates a configuration value (runtime operational change) +// +// @Summary Update configuration value +// @Description Update a configuration value with validation and history tracking. This is for frequent operational adjustments. +// @Tags Configurations +// @ID update_config_value +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Param request body UpdateConfigValueReq true "Configuration value update request" +// @Success 202 {object} dto.GenericResponse[any] "Configuration value updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id} [patch] +// @x-api-type {"admin":"true"} +func (h *Handler) UpdateConfigValue(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + var req UpdateConfigValueReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + + err := h.service.UpdateConfigValue(c.Request.Context(), &req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse[any](c, http.StatusAccepted, "Configuration value updated successfully", nil) +} + +// UpdateConfigMetadata updates configuration metadata (rare admin operation) +// +// @Summary Update configuration metadata +// @Description Update configuration metadata such as min/max values, validation rules, etc. This is a high-privilege operation. +// @Tags Configurations +// @ID update_config_metadata +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Param request body UpdateConfigMetadataReq true "Configuration metadata update request" +// @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration metadata updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id}/metadata [put] +// @x-api-type {"admin":"true"} +func (h *Handler) UpdateConfigMetadata(c *gin.Context) { + userID, exists := middleware.GetCurrentUserID(c) + if !exists || userID <= 0 { + dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") + return + } + + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + var req UpdateConfigMetadataReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.UpdateConfigMetadata(&req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Configuration metadata updated successfully", resp) +} + +// ListConfigHistories handles listing config histories with pagination and filtering +// +// @Summary List configuration histories +// @Description Get paginated list of config histories for a specific config +// @Tags Configurations +// @ID list_config_histories +// @Produce json +// @Security BearerAuth +// @Param config_id path int true "Configuration ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Success 200 {object} dto.GenericResponse[dto.ListResp[ConfigHistoryResp]] "Config histories retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/configs/{config_id}/histories [get] +// @x-api-type {"admin":"true"} +func (h *Handler) ListConfigHistories(c *gin.Context) { + configID, ok := parseID(c, consts.URLPathConfigID, "Invalid config ID") + if !ok { + return + } + + var req ListConfigHistoryReq + if err := c.ShouldBindQuery(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) + return + } + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) + return + } + + resp, err := h.service.ListConfigHistories(&req, configID) + if httpx.HandleServiceError(c, err) { + return + } + dto.JSONResponse(c, http.StatusOK, "Config historys retrieved successfully", resp) +} + +func parseID(c *gin.Context, param, message string) (int, bool) { + value := c.Param(param) + id, err := strconv.Atoi(value) + if err != nil || id <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, message) + return 0, false + } + return id, true +} diff --git a/src/module/system/handler_test.go b/src/module/system/handler_test.go new file mode 100644 index 00000000..f1251f50 --- /dev/null +++ b/src/module/system/handler_test.go @@ -0,0 +1,115 @@ +package systemmodule + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "aegis/utils" + + "github.com/gin-gonic/gin" +) + +func init() { + utils.InitValidator() +} + +func TestGetConfigRejectsInvalidID(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodGet, "/system/configs/abc", nil) + c.Request = req + c.Params = gin.Params{{Key: "config_id", Value: "abc"}} + + h.GetConfig(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestGetAuditLogRejectsInvalidID(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + req := httptest.NewRequest(http.MethodGet, "/system/audit/abc", nil) + c.Request = req + c.Params = gin.Params{{Key: "id", Value: "abc"}} + + h.GetAuditLog(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestListConfigsRejectsInvalidQuery(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/system/configs?size=999", nil) + + h.ListConfigs(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestListAuditLogsRejectsInvalidQuery(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/system/audit?start_date=not-a-date", nil) + + h.ListAuditLogs(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestRollbackConfigValueRequiresAuthentication(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, "/system/configs/1/rollback/value", bytes.NewBufferString(`{"history_id":1,"reason":"rollback"}`)) + c.Request.Header.Set("Content-Type", "application/json") + c.Params = gin.Params{{Key: "config_id", Value: "1"}} + + h.RollbackConfigValue(c) + + if w.Code != http.StatusUnauthorized { + t.Fatalf("expected status 401, got %d", w.Code) + } +} + +func TestUpdateConfigMetadataRejectsInvalidPayload(t *testing.T) { + gin.SetMode(gin.TestMode) + h := NewHandler(&Service{}) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPatch, "/system/configs/1/metadata", bytes.NewBufferString(`{"reason":"update"}`)) + c.Request.Header.Set("Content-Type", "application/json") + c.Set("user_id", 1) + c.Params = gin.Params{{Key: "config_id", Value: "1"}} + + h.UpdateConfigMetadata(c) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} diff --git a/src/module/system/module.go b/src/module/system/module.go new file mode 100644 index 00000000..def22914 --- /dev/null +++ b/src/module/system/module.go @@ -0,0 +1,9 @@ +package systemmodule + +import "go.uber.org/fx" + +var Module = fx.Module("system", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/system/repository.go b/src/module/system/repository.go new file mode 100644 index 00000000..0cd6a6ea --- /dev/null +++ b/src/module/system/repository.go @@ -0,0 +1,171 @@ +package systemmodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) GetAuditLogByID(id int) (*model.AuditLog, error) { + var auditLog model.AuditLog + if err := r.db.Where("id = ?", id).First(&auditLog).Error; err != nil { + return nil, fmt.Errorf("failed to get audit log: %w", err) + } + return &auditLog, nil +} + +func (r *Repository) ListAuditLogs(limit, offset int, filters *ListAuditLogFilters) ([]model.AuditLog, int64, error) { + var ( + logs []model.AuditLog + total int64 + ) + + query := r.db.Model(&model.AuditLog{}).Preload("User").Preload("Resource") + if filters != nil { + if filters.Action != "" { + query = query.Where("action = ?", filters.Action) + } + if filters.IPAddress != "" { + query = query.Where("ip_address = ?", filters.IPAddress) + } + if filters.UserID != 0 { + query = query.Where("user_id = ?", filters.UserID) + } + if filters.ResourceID != 0 { + query = query.Where("resource_id = ?", filters.ResourceID) + } + if filters.State != nil { + query = query.Where("state = ?", *filters.State) + } + if filters.Status != nil { + query = query.Where("status = ?", *filters.Status) + } + if filters.StartTime != nil { + query = query.Where("created_at >= ?", *filters.StartTime) + } + if filters.EndTime != nil { + query = query.Where("created_at <= ?", *filters.EndTime) + } + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count audit logs: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&logs).Error; err != nil { + return nil, 0, fmt.Errorf("failed to get audit logs: %w", err) + } + return logs, total, nil +} + +func (r *Repository) GetConfigByID(configID int, includeUser bool) (*model.DynamicConfig, error) { + query := r.db + if includeUser { + query = query.Preload("UpdatedByUser") + } + + var cfg model.DynamicConfig + if err := query.Where("id = ?", configID).First(&cfg).Error; err != nil { + return nil, fmt.Errorf("failed to find config with id %d: %w", configID, err) + } + return &cfg, nil +} + +func (r *Repository) ListConfigs(limit, offset int, valueType *consts.ConfigValueType, category *string, isSecret *bool, updatedBy *int) ([]model.DynamicConfig, int64, error) { + var ( + configs []model.DynamicConfig + total int64 + ) + + query := r.db.Model(&model.DynamicConfig{}) + if valueType != nil { + query = query.Where("value_type = ?", *valueType) + } + if category != nil { + query = query.Where("category = ?", *category) + } + if isSecret != nil { + query = query.Where("is_secret = ?", *isSecret) + } + if updatedBy != nil { + query = query.Where("updated_by = ?", *updatedBy) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count configs: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&configs).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list configs: %w", err) + } + return configs, total, nil +} + +func (r *Repository) UpdateConfig(config *model.DynamicConfig) error { + if err := r.db.Save(config).Error; err != nil { + return fmt.Errorf("failed to update config: %w", err) + } + return nil +} + +func (r *Repository) GetConfigHistory(historyID int) (*model.ConfigHistory, error) { + var history model.ConfigHistory + if err := r.db.Preload("Operator").Preload("Config").First(&history, historyID).Error; err != nil { + return nil, fmt.Errorf("failed to find config history with id %d: %w", historyID, err) + } + return &history, nil +} + +func (r *Repository) CreateConfigHistory(history *model.ConfigHistory) error { + if err := r.db.Create(history).Error; err != nil { + return fmt.Errorf("failed to create config history: %w", err) + } + return nil +} + +func (r *Repository) ListConfigHistories(limit, offset int, configID int, changeType *consts.ConfigHistoryChangeType, operatorID *int) ([]model.ConfigHistory, int64, error) { + var ( + histories []model.ConfigHistory + total int64 + ) + + query := r.db.Model(&model.ConfigHistory{}).Where("config_id = ?", configID) + if changeType != nil { + query = query.Where("change_type = ?", *changeType) + } + if operatorID != nil { + query = query.Where("operator_id = ?", *operatorID) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count config histories: %w", err) + } + if err := query.Preload("Operator").Limit(limit).Offset(offset).Order("created_at DESC").Find(&histories).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list config histories: %w", err) + } + return histories, total, nil +} + +func (r *Repository) ListConfigHistoriesByConfigID(configID int) ([]model.ConfigHistory, error) { + var histories []model.ConfigHistory + if err := r.db.Preload("Operator").Where("config_id = ?", configID).Order("created_at DESC").Find(&histories).Error; err != nil { + return nil, fmt.Errorf("failed to list config histories for config %d: %w", configID, err) + } + return histories, nil +} diff --git a/src/module/system/service.go b/src/module/system/service.go new file mode 100644 index 00000000..c1939a01 --- /dev/null +++ b/src/module/system/service.go @@ -0,0 +1,731 @@ +package systemmodule + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "runtime" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + buildkitinfra "aegis/infra/buildkit" + etcdinfra "aegis/infra/etcd" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + "aegis/model" + systemmetricmodule "aegis/module/systemmetric" + "aegis/service/common" + "aegis/utils" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type configUpdateContext struct { + ChangeField consts.ConfigHistoryChangeField + OldValue string + NewValue string + Reason string + OperatorID int + IpAddress string + UserAgent string +} + +type configHistoryParams struct { + ConfigID int + ChangeType consts.ConfigHistoryChangeType + RollbackFromID *int + + ConfigUpdateContext configUpdateContext +} + +type configHistoryWriter interface { + CreateConfigHistory(history *model.ConfigHistory) error +} + +type Service struct { + repo *Repository + buildkit *buildkitinfra.Gateway + etcd *etcdinfra.Gateway + k8s *k8sinfra.Gateway + redis *redisinfra.Gateway + systemMetric *systemmetricmodule.Service +} + +func NewService(repo *Repository, buildkit *buildkitinfra.Gateway, etcd *etcdinfra.Gateway, k8s *k8sinfra.Gateway, redis *redisinfra.Gateway, systemMetric *systemmetricmodule.Service) *Service { + return &Service{ + repo: repo, + buildkit: buildkit, + etcd: etcd, + k8s: k8s, + redis: redis, + systemMetric: systemMetric, + } +} + +func (s *Service) GetHealth(ctx context.Context) (*HealthCheckResp, error) { + start := time.Now() + services := make(map[string]ServiceInfo) + overallStatus := "healthy" + + buildkitInfo := s.checkBuildKitHealth(ctx) + services["buildkit"] = buildkitInfo + if buildkitInfo.Status != "healthy" { + overallStatus = "unhealthy" + } + + dbInfo := s.checkDatabaseHealth(ctx) + services["database"] = dbInfo + if dbInfo.Status != "healthy" { + overallStatus = "unhealthy" + } + + jaegerInfo := s.checkJaegerHealth(ctx) + services["jaeger"] = jaegerInfo + if jaegerInfo.Status != "healthy" { + overallStatus = "unhealthy" + } + + k8sInfo := s.checkKubernetesHealth(ctx) + services["kubernetes"] = k8sInfo + if k8sInfo.Status != "healthy" { + overallStatus = "unhealthy" + } + + redisInfo := s.checkRedisHealth(ctx) + services["redis"] = redisInfo + if redisInfo.Status != "healthy" { + overallStatus = "unhealthy" + } + + return &HealthCheckResp{ + Status: overallStatus, + Timestamp: time.Now(), + Version: config.GetString("version"), + Uptime: time.Since(start).String(), + Services: services, + }, nil +} + +func (s *Service) GetMetrics() *MonitoringMetricsResp { + return &MonitoringMetricsResp{ + Timestamp: time.Now(), + Metrics: map[string]MetricValue{ + "cpu_usage": {Value: 25.5, Timestamp: time.Now(), Unit: "percent"}, + "memory_usage": {Value: 60.2, Timestamp: time.Now(), Unit: "percent"}, + "disk_usage": {Value: 45.8, Timestamp: time.Now(), Unit: "percent"}, + "active_connections": {Value: 142, Timestamp: time.Now(), Unit: "count"}, + }, + Labels: map[string]string{ + "instance": "rcabench-01", + "version": config.GetString("version"), + }, + } +} + +func (s *Service) GetSystemInfo() *SystemInfo { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + return &SystemInfo{ + CPUUsage: 25.5, + MemoryUsage: float64(memStats.Alloc) / float64(memStats.Sys) * 100, + DiskUsage: 45.8, + LoadAverage: "1.2, 1.5, 1.8", + } +} + +func (s *Service) ListNamespaceLocks(ctx context.Context) (*ListNamespaceLockResp, error) { + return s.systemMetric.ListNamespaceLocks(ctx) +} + +func (s *Service) ListQueuedTasks(ctx context.Context) (*QueuedTasksResp, error) { + return s.systemMetric.ListQueuedTasks(ctx) +} + +func (s *Service) GetAuditLog(id int) (*AuditLogDetailResp, error) { + log, err := s.repo.GetAuditLogByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: audit log with ID %d not found", consts.ErrNotFound, id) + } + return nil, fmt.Errorf("failed to get audit log: %w", err) + } + + return NewAuditLogDetailResp(log), nil +} + +func (s *Service) ListAuditLogs(req *ListAuditLogReq) (*dto.ListResp[AuditLogResp], error) { + limit, offset := req.ToGormParams() + filterOptions := req.ToFilterOptions() + + logs, total, err := s.repo.ListAuditLogs(limit, offset, filterOptions) + if err != nil { + return nil, fmt.Errorf("failed to list audit logs: %w", err) + } + + return buildAuditLogListResp(logs, req, total), nil +} + +func (s *Service) GetConfig(configID int) (*ConfigDetailResp, error) { + cfg, err := s.repo.GetConfigByID(configID, true) + if err != nil { + return nil, fmt.Errorf("failed to get config detail: %w", err) + } + + histories, err := s.repo.ListConfigHistoriesByConfigID(cfg.ID) + if err != nil { + return nil, fmt.Errorf("failed to get config histories: %w", err) + } + + return buildConfigDetailResp(cfg, histories), nil +} + +func (s *Service) ListConfigs(req *ListConfigReq) (*dto.ListResp[ConfigResp], error) { + limit, offset := req.ToGormParams() + + configs, total, err := s.repo.ListConfigs(limit, offset, req.ValueType, req.Category, req.IsSecret, req.UpdatedBy) + if err != nil { + return nil, fmt.Errorf("failed to list configs: %w", err) + } + + return buildConfigListResp(configs, req, total), nil +} + +func (s *Service) RollbackConfigValue(ctx context.Context, req *RollbackConfigReq, configID, userID int, ipAddress, userAgent string) error { + history, err := s.repo.GetConfigHistory(req.HistoryID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) + } + return fmt.Errorf("failed to get config history: %w", err) + } + + if history.ChangeField != consts.ChangeFieldValue { + return fmt.Errorf("history entry %d is not a value change (field: %v)", req.HistoryID, history.ChangeField) + } + + existingConfig, err := s.repo.GetConfigByID(configID, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) + } + return fmt.Errorf("failed to get config: %w", err) + } + + oldValue, err := s.etcd.Get(ctx, fmt.Sprintf("%s%s", etcdPrefixForScope(existingConfig.Scope), existingConfig.Key)) + if err != nil { + return fmt.Errorf("failed to get current config value from etcd: %w", err) + } + + newValue := history.OldValue + if err := common.ValidateConfig(existingConfig, newValue); err != nil { + return fmt.Errorf("invalid config after rollback: %w", err) + } + + if err := setViperIfNeeded(existingConfig, newValue); err != nil { + return fmt.Errorf("failed to set config value in viper: %w", err) + } + + if _, err := s.createConfigRollback(existingConfig, utils.IntPtr(history.ID), configUpdateContext{ + ChangeField: consts.ChangeFieldValue, + OldValue: oldValue, + NewValue: newValue, + Reason: req.Reason, + OperatorID: userID, + IpAddress: ipAddress, + UserAgent: userAgent, + }); err != nil { + return err + } + + return s.propagateValueChange(ctx, existingConfig, newValue, "rollback") +} + +func (s *Service) RollbackConfigMetadata(req *RollbackConfigReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { + history, err := s.repo.GetConfigHistory(req.HistoryID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) + } + return nil, fmt.Errorf("failed to get config history: %w", err) + } + + if history.ChangeField == consts.ChangeFieldValue { + return nil, fmt.Errorf("history entry %d is a value change, use RollbackConfigValue instead", req.HistoryID) + } + + existingConfig, err := s.repo.GetConfigByID(configID, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) + } + return nil, fmt.Errorf("failed to get config: %w", err) + } + + oldValue, newValue, err := rollbackMetaFieldValue(existingConfig, history.ChangeField, history.OldValue) + if err != nil { + return nil, fmt.Errorf("failed to rollback metadata field: %w", err) + } + + if err := common.ValidateConfigMetadataConstraints(existingConfig); err != nil { + return nil, fmt.Errorf("invalid config after metadata rollback: %w", err) + } + + updatedConfig, err := s.createConfigRollback(existingConfig, utils.IntPtr(history.ID), configUpdateContext{ + ChangeField: history.ChangeField, + OldValue: oldValue, + NewValue: newValue, + Reason: req.Reason, + OperatorID: userID, + IpAddress: ipAddress, + UserAgent: userAgent, + }) + if err != nil { + return nil, err + } + + return NewConfigResp(updatedConfig), nil +} + +func (s *Service) UpdateConfigValue(ctx context.Context, req *UpdateConfigValueReq, configID, userID int, ipAddress, userAgent string) error { + existingConfig, err := s.repo.GetConfigByID(configID, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) + } + return fmt.Errorf("failed to get config: %w", err) + } + + oldValue, err := s.etcd.Get(ctx, fmt.Sprintf("%s%s", etcdPrefixForScope(existingConfig.Scope), existingConfig.Key)) + if err != nil { + return fmt.Errorf("failed to get current config value from etcd: %w", err) + } + + newValue := req.Value + if err := common.ValidateConfig(existingConfig, newValue); err != nil { + return fmt.Errorf("invalid config value: %w", err) + } + + if err := setViperIfNeeded(existingConfig, newValue); err != nil { + return fmt.Errorf("failed to set config value in viper: %w", err) + } + + if err := s.createConfigHistory(s.repo, configHistoryParams{ + ConfigID: existingConfig.ID, + ChangeType: consts.ChangeTypeUpdate, + ConfigUpdateContext: configUpdateContext{ + ChangeField: consts.ChangeFieldValue, + OldValue: oldValue, + NewValue: newValue, + Reason: req.Reason, + OperatorID: userID, + IpAddress: ipAddress, + UserAgent: userAgent, + }, + }); err != nil { + return fmt.Errorf("failed to create config history: %w", err) + } + + return s.propagateValueChange(ctx, existingConfig, newValue, "update") +} + +func (s *Service) UpdateConfigMetadata(req *UpdateConfigMetadataReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { + existingConfig, err := s.repo.GetConfigByID(configID, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) + } + return nil, fmt.Errorf("failed to get config: %w", err) + } + + oldValue, newValue := req.PatchConfigModel(existingConfig) + if err := common.ValidateConfigMetadataConstraints(existingConfig); err != nil { + return nil, fmt.Errorf("invalid config after metadata update: %w", err) + } + + var updatedConfig *model.DynamicConfig + err = s.repo.Transaction(func(tx *gorm.DB) error { + txRepo := s.repo.withDB(tx) + existingConfig.UpdatedBy = utils.IntPtr(userID) + + if err := txRepo.UpdateConfig(existingConfig); err != nil { + return fmt.Errorf("failed to update config: %w", err) + } + + updatedConfig = existingConfig + if err := s.createConfigHistory(txRepo, configHistoryParams{ + ConfigID: updatedConfig.ID, + ChangeType: consts.ChangeTypeUpdate, + ConfigUpdateContext: configUpdateContext{ + ChangeField: req.GetChangeField(), + OldValue: oldValue, + NewValue: newValue, + Reason: req.Reason, + OperatorID: userID, + IpAddress: ipAddress, + UserAgent: userAgent, + }, + }); err != nil { + return fmt.Errorf("failed to create config history: %w", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + return NewConfigResp(updatedConfig), nil +} + +func (s *Service) ListConfigHistories(req *ListConfigHistoryReq, configID int) (*dto.ListResp[ConfigHistoryResp], error) { + limit, offset := req.ToGormParams() + + histories, total, err := s.repo.ListConfigHistories(limit, offset, configID, req.ChangeType, req.OperatorID) + if err != nil { + return nil, fmt.Errorf("failed to list config histories: %w", err) + } + + return buildConfigHistoryListResp(histories, req, total), nil +} + +func etcdPrefixForScope(scope consts.ConfigScope) string { + switch scope { + case consts.ConfigScopeProducer: + return consts.ConfigEtcdProducerPrefix + case consts.ConfigScopeConsumer: + return consts.ConfigEtcdConsumerPrefix + case consts.ConfigScopeGlobal: + return consts.ConfigEtcdGlobalPrefix + } + return "" +} + +func buildAuditLogListResp(logs []model.AuditLog, req *ListAuditLogReq, total int64) *dto.ListResp[AuditLogResp] { + logResps := make([]AuditLogResp, 0, len(logs)) + for i := range logs { + logResps = append(logResps, *NewAuditLogResp(&logs[i])) + } + + return &dto.ListResp[AuditLogResp]{ + Items: logResps, + Pagination: req.ConvertToPaginationInfo(total), + } +} + +func buildConfigDetailResp(cfg *model.DynamicConfig, histories []model.ConfigHistory) *ConfigDetailResp { + resp := NewConfigDetailResp(cfg) + for _, history := range histories { + resp.Histories = append(resp.Histories, *NewConfigHistoryResp(&history)) + } + return resp +} + +func buildConfigListResp(configs []model.DynamicConfig, req *ListConfigReq, total int64) *dto.ListResp[ConfigResp] { + configResps := make([]ConfigResp, 0, len(configs)) + for _, cfg := range configs { + configResps = append(configResps, *NewConfigResp(&cfg)) + } + + return &dto.ListResp[ConfigResp]{ + Items: configResps, + Pagination: req.ConvertToPaginationInfo(total), + } +} + +func buildConfigHistoryListResp(histories []model.ConfigHistory, req *ListConfigHistoryReq, total int64) *dto.ListResp[ConfigHistoryResp] { + historyResps := make([]ConfigHistoryResp, 0, len(histories)) + for _, history := range histories { + historyResps = append(historyResps, *NewConfigHistoryResp(&history)) + } + + return &dto.ListResp[ConfigHistoryResp]{ + Items: historyResps, + Pagination: req.ConvertToPaginationInfo(total), + } +} + +func (s *Service) createConfigHistory(repo configHistoryWriter, params configHistoryParams) error { + entry := &model.ConfigHistory{ + ChangeType: params.ChangeType, + OldValue: params.ConfigUpdateContext.OldValue, + NewValue: params.ConfigUpdateContext.NewValue, + Reason: params.ConfigUpdateContext.Reason, + ConfigID: params.ConfigID, + OperatorID: utils.IntPtr(params.ConfigUpdateContext.OperatorID), + IPAddress: params.ConfigUpdateContext.IpAddress, + UserAgent: params.ConfigUpdateContext.UserAgent, + RolledBackFromID: params.RollbackFromID, + ChangeField: params.ConfigUpdateContext.ChangeField, + } + if err := repo.CreateConfigHistory(entry); err != nil { + return fmt.Errorf("failed to create config history: %w", err) + } + return nil +} + +func (s *Service) createConfigRollback(cfg *model.DynamicConfig, historyID *int, updateContext configUpdateContext) (*model.DynamicConfig, error) { + var updatedConfig *model.DynamicConfig + + err := s.repo.Transaction(func(tx *gorm.DB) error { + txRepo := s.repo.withDB(tx) + if err := txRepo.UpdateConfig(cfg); err != nil { + return fmt.Errorf("failed to update config: %w", err) + } + + updatedConfig = cfg + if err := s.createConfigHistory(txRepo, configHistoryParams{ + ConfigID: cfg.ID, + ChangeType: consts.ChangeTypeRollback, + ConfigUpdateContext: updateContext, + RollbackFromID: historyID, + }); err != nil { + return fmt.Errorf("failed to create rollback history: %w", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + return updatedConfig, nil +} + +func rollbackMetaFieldValue(cfg *model.DynamicConfig, changeField consts.ConfigHistoryChangeField, targetValue string) (string, string, error) { + newValue := targetValue + oldValue := "" + + switch changeField { + case consts.ChangeFieldDefaultValue: + oldValue = cfg.DefaultValue + cfg.DefaultValue = newValue + case consts.ChangeFieldDescription: + oldValue = cfg.Description + cfg.Description = newValue + case consts.ChangeFieldMinValue: + if cfg.MinValue != nil { + oldValue = fmt.Sprintf("%f", *cfg.MinValue) + } + if newValue == "" { + cfg.MinValue = nil + } else { + var minVal float64 + if _, err := fmt.Sscanf(newValue, "%f", &minVal); err != nil { + return "", "", fmt.Errorf("failed to parse min value: %w", err) + } + cfg.MinValue = &minVal + } + case consts.ChangeFieldMaxValue: + if cfg.MaxValue != nil { + oldValue = fmt.Sprintf("%f", *cfg.MaxValue) + } + if newValue == "" { + cfg.MaxValue = nil + } else { + var maxVal float64 + if _, err := fmt.Sscanf(newValue, "%f", &maxVal); err != nil { + return "", "", fmt.Errorf("failed to parse max value: %w", err) + } + cfg.MaxValue = &maxVal + } + case consts.ChangeFieldPattern: + oldValue = cfg.Pattern + cfg.Pattern = newValue + case consts.ChangeFieldOptions: + oldValue = cfg.Options + cfg.Options = newValue + default: + return "", "", fmt.Errorf("unknown change field: %d", changeField) + } + + return oldValue, newValue, nil +} + +func setViperIfNeeded(cfg *model.DynamicConfig, newValue string) error { + if cfg.Scope == consts.ConfigScopeConsumer { + return nil + } + return config.SetViperValue(cfg.Key, newValue, cfg.ValueType) +} + +func (s *Service) propagateValueChange(ctx context.Context, cfg *model.DynamicConfig, newValue, opDesc string) error { + if cfg.Scope != consts.ConfigScopeGlobal && cfg.Scope != consts.ConfigScopeConsumer { + return nil + } + + etcdKey := fmt.Sprintf("%s%s", etcdPrefixForScope(cfg.Scope), cfg.Key) + if err := s.publishConfigToEtcdWithRetry(ctx, etcdKey, newValue, 3); err != nil { + return fmt.Errorf("config saved to database but failed to publish to etcd: %w", err) + } + + if cfg.Scope == consts.ConfigScopeConsumer { + logrus.Infof("Waiting for consumer config %s response...", opDesc) + resp, err := s.waitForConfigUpdateResponse(ctx, 10*time.Second) + if err != nil { + return fmt.Errorf("config %s but consumer did not respond: %w", opDesc, err) + } + if !resp.Success { + return fmt.Errorf("consumer failed to process config %s: %s", opDesc, resp.Error) + } + logrus.Infof("Config %s successfully processed by consumer", opDesc) + } + + return nil +} + +func (s *Service) publishConfigToEtcdWithRetry(ctx context.Context, key, value string, maxRetries int) error { + var lastErr error + baseDelay := 500 * time.Millisecond + + for attempt := range maxRetries { + if attempt > 0 { + delay := baseDelay * time.Duration(1< 0 { + logrus.Infof("Successfully published config to etcd after %d retries", attempt) + } + return nil + } + + lastErr = err + logrus.Warnf("Failed to publish config to etcd (attempt %d/%d): %v", attempt+1, maxRetries, err) + } + + return fmt.Errorf("failed to publish config to etcd after %d attempts: %w", maxRetries, lastErr) +} + +func (s *Service) waitForConfigUpdateResponse(parent context.Context, timeout time.Duration) (*dto.ConfigUpdateResponse, error) { + ctx, cancel := context.WithTimeout(parent, timeout) + defer cancel() + + pubsub, err := s.redis.Subscribe(ctx, consts.ConfigUpdateResponseChannel) + if err != nil { + return nil, fmt.Errorf("failed to confirm subscription: %w", err) + } + defer func() { _ = pubsub.Close() }() + + msgChan := pubsub.Channel() + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timeout waiting for config update response after %v", timeout) + case msg, ok := <-msgChan: + if !ok { + return nil, fmt.Errorf("subscription channel closed unexpectedly") + } + + var response dto.ConfigUpdateResponse + if err := json.Unmarshal([]byte(msg.Payload), &response); err != nil { + logrus.Warnf("failed to parse response message: %v", err) + continue + } + + logrus.WithFields(logrus.Fields{ + "response_id": response.ID, + "success": response.Success, + }).Info("Received matching config update response") + return &response, nil + } + } +} + +func (s *Service) checkBuildKitHealth(parent context.Context) ServiceInfo { + start := time.Now() + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + + err := s.buildkit.CheckHealth(ctx, 5*time.Second) + responseTime := time.Since(start) + if err != nil { + return ServiceInfo{ + Status: "unhealthy", + LastChecked: time.Now(), + ResponseTime: responseTime.String(), + Error: "BuildKit daemon unreachable", + Details: err.Error(), + } + } + return ServiceInfo{Status: "healthy", LastChecked: time.Now(), ResponseTime: responseTime.String()} +} + +func (s *Service) checkDatabaseHealth(parent context.Context) ServiceInfo { + start := time.Now() + db := s.repo.db + if db == nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: "N/A", Error: "Database connection not available"} + } + + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + var result int + err := db.WithContext(ctx).Raw("SELECT 1").Scan(&result).Error + responseTime := time.Since(start) + if err != nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: responseTime.String(), Error: "Database query failed", Details: err.Error()} + } + return ServiceInfo{Status: "healthy", LastChecked: time.Now(), ResponseTime: responseTime.String()} +} + +func (s *Service) checkJaegerHealth(parent context.Context) ServiceInfo { + start := time.Now() + jaegerURL := fmt.Sprintf("http://%s/v1/traces", config.GetString("jaeger.endpoint")) + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodHead, jaegerURL, nil) + if err != nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: time.Since(start).String(), Error: "Failed to create Jaeger OTLP request", Details: err.Error()} + } + + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Do(req) + responseTime := time.Since(start) + if err != nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: responseTime.String(), Error: "Jaeger OTLP endpoint unreachable", Details: err.Error()} + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusMethodNotAllowed && resp.StatusCode != http.StatusOK { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: responseTime.String(), Error: fmt.Sprintf("Jaeger OTLP returned unexpected status %d", resp.StatusCode)} + } + return ServiceInfo{Status: "healthy", LastChecked: time.Now(), ResponseTime: responseTime.String(), Details: "Jaeger OTLP endpoint responding"} +} + +func (s *Service) checkKubernetesHealth(parent context.Context) ServiceInfo { + start := time.Now() + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + if err := s.k8s.CheckHealth(ctx); err != nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: time.Since(start).String(), Error: "Kubernetes health check failed", Details: err.Error()} + } + return ServiceInfo{Status: "healthy", LastChecked: time.Now(), ResponseTime: time.Since(start).String()} +} + +func (s *Service) checkRedisHealth(parent context.Context) ServiceInfo { + start := time.Now() + if s.redis == nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: "N/A", Error: "Redis connection not available"} + } + + ctx, cancel := context.WithTimeout(parent, 5*time.Second) + defer cancel() + err := s.redis.Ping(ctx) + responseTime := time.Since(start) + if err != nil { + return ServiceInfo{Status: "unhealthy", LastChecked: time.Now(), ResponseTime: responseTime.String(), Error: "Redis ping failed", Details: err.Error()} + } + return ServiceInfo{Status: "healthy", LastChecked: time.Now(), ResponseTime: responseTime.String()} +} diff --git a/src/module/system/service_test.go b/src/module/system/service_test.go new file mode 100644 index 00000000..587c0705 --- /dev/null +++ b/src/module/system/service_test.go @@ -0,0 +1,263 @@ +package systemmodule + +import ( + "testing" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/model" +) + +type fakeConfigHistoryWriter struct { + history *model.ConfigHistory + err error +} + +func (f *fakeConfigHistoryWriter) CreateConfigHistory(history *model.ConfigHistory) error { + f.history = history + return f.err +} + +func TestGetMetricsReturnsExpectedLabels(t *testing.T) { + svc := &Service{} + + resp := svc.GetMetrics() + if resp == nil { + t.Fatal("expected metrics response") + } + if _, ok := resp.Metrics["cpu_usage"]; !ok { + t.Fatal("expected cpu_usage metric") + } + if _, ok := resp.Labels["instance"]; !ok { + t.Fatal("expected instance label") + } +} + +func TestGetSystemInfoReturnsLoadAverage(t *testing.T) { + svc := &Service{} + + resp := svc.GetSystemInfo() + if resp == nil { + t.Fatal("expected system info response") + } + if resp.LoadAverage == "" { + t.Fatal("expected load average") + } +} + +func TestRollbackMetaFieldValueUpdatesDescription(t *testing.T) { + cfg := &model.DynamicConfig{Description: "current"} + + oldValue, newValue, err := rollbackMetaFieldValue(cfg, consts.ChangeFieldDescription, "restored") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if oldValue != "current" { + t.Fatalf("expected old value to be current, got %q", oldValue) + } + if newValue != "restored" { + t.Fatalf("expected new value to be restored, got %q", newValue) + } + if cfg.Description != "restored" { + t.Fatalf("expected config description to be restored, got %q", cfg.Description) + } +} + +func TestRollbackMetaFieldValueClearsMinValue(t *testing.T) { + minValue := 10.5 + cfg := &model.DynamicConfig{MinValue: &minValue} + + oldValue, newValue, err := rollbackMetaFieldValue(cfg, consts.ChangeFieldMinValue, "") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if oldValue == "" { + t.Fatal("expected old value to be populated") + } + if newValue != "" { + t.Fatalf("expected new value to be empty, got %q", newValue) + } + if cfg.MinValue != nil { + t.Fatal("expected min value to be cleared") + } +} + +func TestSetViperIfNeededSetsProducerScopeValue(t *testing.T) { + cfg := &model.DynamicConfig{ + Key: "system.test.int", + Scope: consts.ConfigScopeProducer, + ValueType: consts.ConfigValueTypeInt, + } + + if err := setViperIfNeeded(cfg, "42"); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got := config.GetInt(cfg.Key); got != 42 { + t.Fatalf("expected viper value 42, got %d", got) + } +} + +func TestSetViperIfNeededSkipsConsumerScope(t *testing.T) { + cfg := &model.DynamicConfig{ + Key: "system.test.consumer", + Scope: consts.ConfigScopeConsumer, + ValueType: consts.ConfigValueTypeString, + } + + if err := setViperIfNeeded(cfg, "remote-only"); err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got := config.GetString(cfg.Key); got != "" { + t.Fatalf("expected consumer scope to skip local viper update, got %q", got) + } +} + +func TestCreateConfigHistoryBuildsExpectedEntry(t *testing.T) { + writer := &fakeConfigHistoryWriter{} + svc := &Service{} + rollbackFromID := 9 + + err := svc.createConfigHistory(writer, configHistoryParams{ + ConfigID: 12, + ChangeType: consts.ChangeTypeRollback, + RollbackFromID: &rollbackFromID, + ConfigUpdateContext: configUpdateContext{ + ChangeField: consts.ChangeFieldPattern, + OldValue: "old", + NewValue: "new", + Reason: "test reason", + OperatorID: 3, + IpAddress: "127.0.0.1", + UserAgent: "unit-test", + }, + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if writer.history == nil { + t.Fatal("expected history to be written") + } + if writer.history.ConfigID != 12 { + t.Fatalf("expected config id 12, got %d", writer.history.ConfigID) + } + if writer.history.ChangeType != consts.ChangeTypeRollback { + t.Fatalf("expected rollback change type, got %v", writer.history.ChangeType) + } + if writer.history.ChangeField != consts.ChangeFieldPattern { + t.Fatalf("expected pattern change field, got %v", writer.history.ChangeField) + } + if writer.history.OperatorID == nil || *writer.history.OperatorID != 3 { + t.Fatalf("expected operator id 3, got %+v", writer.history.OperatorID) + } + if writer.history.RolledBackFromID == nil || *writer.history.RolledBackFromID != rollbackFromID { + t.Fatalf("expected rollback from id %d, got %+v", rollbackFromID, writer.history.RolledBackFromID) + } +} + +func TestBuildConfigDetailRespIncludesHistories(t *testing.T) { + operatorID := 7 + cfg := &model.DynamicConfig{ + ID: 1, + Key: "feature.flag", + ValueType: consts.ConfigValueTypeString, + Category: "system", + } + histories := []model.ConfigHistory{ + { + ID: 11, + ConfigID: 1, + ChangeType: consts.ChangeTypeUpdate, + ChangeField: consts.ChangeFieldValue, + OldValue: "off", + NewValue: "on", + OperatorID: &operatorID, + }, + } + + resp := buildConfigDetailResp(cfg, histories) + if resp == nil { + t.Fatal("expected response") + } + if resp.ID != cfg.ID { + t.Fatalf("expected config id %d, got %d", cfg.ID, resp.ID) + } + if len(resp.Histories) != 1 { + t.Fatalf("expected 1 history, got %d", len(resp.Histories)) + } + if resp.Histories[0].ID != 11 { + t.Fatalf("expected history id 11, got %d", resp.Histories[0].ID) + } +} + +func TestBuildAuditLogListRespIncludesPaginationAndItems(t *testing.T) { + state := consts.AuditLogStateSuccess + status := consts.CommonEnabled + req := &ListAuditLogReq{ + PaginationReq: dto.PaginationReq{Page: 2, Size: consts.PageSizeSmall}, + State: &state, + Status: &status, + } + logs := []model.AuditLog{ + {ID: 1, Action: "deploy", IPAddress: "127.0.0.1", State: consts.AuditLogStateSuccess, Status: consts.CommonEnabled, CreatedAt: time.Now()}, + } + + resp := buildAuditLogListResp(logs, req, 21) + if resp == nil { + t.Fatal("expected response") + } + if len(resp.Items) != 1 { + t.Fatalf("expected 1 item, got %d", len(resp.Items)) + } + if resp.Items[0].ID != 1 { + t.Fatalf("expected item id 1, got %d", resp.Items[0].ID) + } + if resp.Pagination == nil || resp.Pagination.Page != 2 { + t.Fatalf("expected page 2, got %+v", resp.Pagination) + } +} + +func TestBuildConfigHistoryListRespIncludesOperatorName(t *testing.T) { + req := &ListConfigHistoryReq{ + PaginationReq: dto.PaginationReq{Page: 1, Size: consts.PageSizeMedium}, + } + operatorID := 5 + histories := []model.ConfigHistory{ + { + ID: 2, + ConfigID: 10, + ChangeType: consts.ChangeTypeUpdate, + ChangeField: consts.ChangeFieldDescription, + OldValue: "old desc", + NewValue: "new desc", + OperatorID: &operatorID, + Operator: &model.User{Username: "tester"}, + }, + } + + resp := buildConfigHistoryListResp(histories, req, 1) + if resp == nil { + t.Fatal("expected response") + } + if len(resp.Items) != 1 { + t.Fatalf("expected 1 item, got %d", len(resp.Items)) + } + if resp.Items[0].OperatorName != "tester" { + t.Fatalf("expected operator name tester, got %q", resp.Items[0].OperatorName) + } +} + +func TestEtcdPrefixForScopeReturnsExpectedPrefix(t *testing.T) { + cases := map[consts.ConfigScope]string{ + consts.ConfigScopeProducer: consts.ConfigEtcdProducerPrefix, + consts.ConfigScopeConsumer: consts.ConfigEtcdConsumerPrefix, + consts.ConfigScopeGlobal: consts.ConfigEtcdGlobalPrefix, + } + + for scope, want := range cases { + if got := etcdPrefixForScope(scope); got != want { + t.Fatalf("expected prefix %q for scope %v, got %q", want, scope, got) + } + } +} diff --git a/src/module/systemmetric/api_types.go b/src/module/systemmetric/api_types.go new file mode 100644 index 00000000..7ed384f7 --- /dev/null +++ b/src/module/systemmetric/api_types.go @@ -0,0 +1,33 @@ +package systemmetricmodule + +import "time" + +type NsMonitorItem struct { + LockedBy string `json:"locked_by"` + EndTime time.Time `json:"end_time"` + Status string `json:"status"` +} + +type ListNamespaceLockResp struct { + Items map[string]NsMonitorItem `json:"items" swaggertype:"object"` +} + +// MetricValue represents a single metric value. +type MetricValue struct { + Value float64 `json:"value"` + Timestamp time.Time `json:"timestamp"` + Unit string `json:"unit,omitempty"` +} + +// SystemMetricsResp represents current system metrics. +type SystemMetricsResp struct { + CPU MetricValue `json:"cpu"` + Memory MetricValue `json:"memory"` + Disk MetricValue `json:"disk"` +} + +// SystemMetricsHistoryResp represents historical system metrics. +type SystemMetricsHistoryResp struct { + CPU []MetricValue `json:"cpu"` + Memory []MetricValue `json:"memory"` +} diff --git a/src/module/systemmetric/collector.go b/src/module/systemmetric/collector.go new file mode 100644 index 00000000..51082120 --- /dev/null +++ b/src/module/systemmetric/collector.go @@ -0,0 +1,44 @@ +package systemmetricmodule + +import ( + "context" + "runtime" + "time" + + "go.uber.org/fx" +) + +func RegisterMetricsCollector(lifecycle fx.Lifecycle, service *Service) { + var cancel context.CancelFunc + + lifecycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + collectorCtx, collectorCancel := context.WithCancel(context.WithoutCancel(ctx)) + cancel = collectorCancel + go func() { + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + case <-collectorCtx.Done(): + return + case <-ticker.C: + } + + if err := service.StoreSystemMetrics(collectorCtx); err != nil { + // Keep the collector alive even if a single write fails. + runtime.Gosched() + } + } + }() + return nil + }, + OnStop: func(context.Context) error { + if cancel != nil { + cancel() + } + return nil + }, + }) +} diff --git a/src/handlers/v2/system.go b/src/module/systemmetric/handler.go similarity index 68% rename from src/handlers/v2/system.go rename to src/module/systemmetric/handler.go index 202d5a3a..fa869e49 100644 --- a/src/handlers/v2/system.go +++ b/src/module/systemmetric/handler.go @@ -1,14 +1,21 @@ -package v2 +package systemmetricmodule import ( "net/http" "aegis/dto" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // GetSystemMetrics retrieves current system metrics // // @Summary Get current system metrics @@ -17,13 +24,13 @@ import ( // @ID get_system_metrics // @Produce json // @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.SystemMetricsResp] "System metrics retrieved successfully" +// @Success 200 {object} dto.GenericResponse[SystemMetricsResp] "System metrics retrieved successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/system/metrics [get] -// @x-api-type {"sdk":"true"} -func GetSystemMetrics(c *gin.Context) { - resp, err := producer.GetSystemMetrics(c.Request.Context()) +// @x-api-type {"admin":"true"} +func (h *Handler) GetSystemMetrics(c *gin.Context) { + resp, err := h.service.GetSystemMetrics(c.Request.Context()) if err != nil { dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get system metrics: "+err.Error()) return @@ -40,13 +47,13 @@ func GetSystemMetrics(c *gin.Context) { // @ID get_system_metrics_history // @Produce json // @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[dto.SystemMetricsHistoryResp] "System metrics history retrieved successfully" +// @Success 200 {object} dto.GenericResponse[SystemMetricsHistoryResp] "System metrics history retrieved successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/system/metrics/history [get] -// @x-api-type {"sdk":"true"} -func GetSystemMetricsHistory(c *gin.Context) { - resp, err := producer.GetSystemMetricsHistory(c.Request.Context()) +// @x-api-type {"admin":"true"} +func (h *Handler) GetSystemMetricsHistory(c *gin.Context) { + resp, err := h.service.GetSystemMetricsHistory(c.Request.Context()) if err != nil { dto.ErrorResponse(c, http.StatusInternalServerError, "Failed to get system metrics history: "+err.Error()) return diff --git a/src/module/systemmetric/module.go b/src/module/systemmetric/module.go new file mode 100644 index 00000000..7aa90f68 --- /dev/null +++ b/src/module/systemmetric/module.go @@ -0,0 +1,10 @@ +package systemmetricmodule + +import "go.uber.org/fx" + +var Module = fx.Module("system_metric", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), + fx.Invoke(RegisterMetricsCollector), +) diff --git a/src/module/systemmetric/repository.go b/src/module/systemmetric/repository.go new file mode 100644 index 00000000..5a2552ac --- /dev/null +++ b/src/module/systemmetric/repository.go @@ -0,0 +1,11 @@ +package systemmetricmodule + +import "gorm.io/gorm" + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} diff --git a/src/module/systemmetric/service.go b/src/module/systemmetric/service.go new file mode 100644 index 00000000..e1da82c1 --- /dev/null +++ b/src/module/systemmetric/service.go @@ -0,0 +1,227 @@ +package systemmetricmodule + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + "aegis/consts" + redisinfra "aegis/infra/redis" + "aegis/model" + taskmodule "aegis/module/task" + + "github.com/redis/go-redis/v9" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/mem" +) + +type Service struct { + repo *Repository + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, redis: redis} +} + +func (s *Service) GetSystemMetrics(ctx context.Context) (*SystemMetricsResp, error) { + now := time.Now() + + cpuPercent, err := cpu.PercentWithContext(ctx, time.Second, false) + if err != nil { + return nil, fmt.Errorf("failed to get CPU usage: %v", err) + } + cpuUsage := 0.0 + if len(cpuPercent) > 0 { + cpuUsage = cpuPercent[0] + } + + memInfo, err := mem.VirtualMemoryWithContext(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get memory usage: %v", err) + } + + diskInfo, err := disk.UsageWithContext(ctx, "/") + if err != nil { + return nil, fmt.Errorf("failed to get disk usage: %v", err) + } + + return &SystemMetricsResp{ + CPU: MetricValue{ + Value: cpuUsage, + Timestamp: now, + Unit: "%", + }, + Memory: MetricValue{ + Value: memInfo.UsedPercent, + Timestamp: now, + Unit: "%", + }, + Disk: MetricValue{ + Value: diskInfo.UsedPercent, + Timestamp: now, + Unit: "%", + }, + }, nil +} + +func (s *Service) GetSystemMetricsHistory(ctx context.Context) (*SystemMetricsHistoryResp, error) { + now := time.Now() + startTime := now.Add(-24 * time.Hour).Unix() + endTime := now.Unix() + + cpuData, err := s.redis.ZRangeByScore(ctx, "system:metrics:cpu", fmt.Sprintf("%d", startTime), fmt.Sprintf("%d", endTime)) + if err != nil && !errors.Is(err, redis.Nil) { + return nil, fmt.Errorf("failed to get CPU history: %v", err) + } + + memData, err := s.redis.ZRangeByScore(ctx, "system:metrics:memory", fmt.Sprintf("%d", startTime), fmt.Sprintf("%d", endTime)) + if err != nil && !errors.Is(err, redis.Nil) { + return nil, fmt.Errorf("failed to get memory history: %v", err) + } + + cpuMetrics := parseMetricValues(cpuData) + memMetrics := parseMetricValues(memData) + + if len(cpuMetrics) == 0 || len(memMetrics) == 0 { + current, err := s.GetSystemMetrics(ctx) + if err != nil { + return nil, err + } + if len(cpuMetrics) == 0 { + cpuMetrics = []MetricValue{current.CPU} + } + if len(memMetrics) == 0 { + memMetrics = []MetricValue{current.Memory} + } + } + + return &SystemMetricsHistoryResp{ + CPU: cpuMetrics, + Memory: memMetrics, + }, nil +} + +func (s *Service) ListNamespaceLocks(ctx context.Context) (*ListNamespaceLockResp, error) { + namespaces, err := s.redis.SetMembers(ctx, consts.NamespacesKey) + if err != nil { + return nil, fmt.Errorf("failed to get namespaces from Redis: %v", err) + } + + items := make(map[string]NsMonitorItem, len(namespaces)) + for _, namespace := range namespaces { + nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) + values, err := s.redis.HashGetAll(ctx, nsKey) + if err != nil { + return nil, fmt.Errorf("failed to get data for namespace %s: %v", namespace, err) + } + + endTimeUnix, err := strconv.ParseInt(values["end_time"], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid end_time format for namespace %s: %v", namespace, err) + } + + status := consts.CommonEnabled + if statusStr, ok := values["status"]; ok { + statusInt, err := strconv.Atoi(statusStr) + if err == nil { + status = consts.StatusType(statusInt) + } + } + + items[namespace] = NsMonitorItem{ + LockedBy: values["trace_id"], + EndTime: time.Unix(endTimeUnix, 0), + Status: consts.GetStatusTypeName(status), + } + } + + return &ListNamespaceLockResp{Items: items}, nil +} + +func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { + readyTaskDatas, err := s.redis.ListReadyTasks(ctx) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, fmt.Errorf("%w: no ready tasks found", consts.ErrNotFound) + } + return nil, err + } + + readyTasks := make([]taskmodule.TaskResp, 0, len(readyTaskDatas)) + for _, taskData := range readyTaskDatas { + var task model.Task + if err := json.Unmarshal([]byte(taskData), &task); err != nil { + return nil, err + } + readyTasks = append(readyTasks, *taskmodule.NewTaskResp(&task)) + } + + delayedTaskDatas, err := s.redis.ListDelayedTasks(ctx, 1000) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, fmt.Errorf("%w: no delayed tasks found", consts.ErrNotFound) + } + return nil, err + } + + delayedTasks := make([]taskmodule.TaskResp, 0, len(delayedTaskDatas)) + for _, taskData := range delayedTaskDatas { + var task model.Task + if err := json.Unmarshal([]byte(taskData), &task); err != nil { + return nil, err + } + delayedTasks = append(delayedTasks, *taskmodule.NewTaskResp(&task)) + } + + return &taskmodule.QueuedTasksResp{ + ReadyTasks: readyTasks, + DelayedTasks: delayedTasks, + }, nil +} + +func parseMetricValues(items []string) []MetricValue { + metrics := make([]MetricValue, 0, len(items)) + for _, item := range items { + var metric MetricValue + if err := json.Unmarshal([]byte(item), &metric); err == nil { + metrics = append(metrics, metric) + } + } + return metrics +} + +func (s *Service) StoreSystemMetrics(ctx context.Context) error { + metrics, err := s.GetSystemMetrics(ctx) + if err != nil { + return err + } + + now := time.Now().Unix() + + cpuData, _ := json.Marshal(metrics.CPU) + if err := s.redis.ZAdd(ctx, "system:metrics:cpu", redis.Z{ + Score: float64(now), + Member: cpuData, + }); err != nil { + return fmt.Errorf("failed to store CPU metric: %v", err) + } + + memData, _ := json.Marshal(metrics.Memory) + if err := s.redis.ZAdd(ctx, "system:metrics:memory", redis.Z{ + Score: float64(now), + Member: memData, + }); err != nil { + return fmt.Errorf("failed to store memory metric: %v", err) + } + + oldTime := time.Now().Add(-24 * time.Hour).Unix() + _ = s.redis.ZRemRangeByScore(ctx, "system:metrics:cpu", "0", fmt.Sprintf("%d", oldTime)) + _ = s.redis.ZRemRangeByScore(ctx, "system:metrics:memory", "0", fmt.Sprintf("%d", oldTime)) + + return nil +} diff --git a/src/module/task/api_types.go b/src/module/task/api_types.go new file mode 100644 index 00000000..fc1a60cc --- /dev/null +++ b/src/module/task/api_types.go @@ -0,0 +1,186 @@ +package taskmodule + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/utils" +) + +// BatchDeleteTaskReq represents the request to batch delete tasks. +type BatchDeleteTaskReq struct { + IDs []string `json:"ids" binding:"required"` +} + +func (req *BatchDeleteTaskReq) Validate() error { + for i, id := range req.IDs { + if strings.TrimSpace(id) == "" { + return fmt.Errorf("empty id at index %d", i) + } + if !utils.IsValidUUID(id) { + return fmt.Errorf("invalid UUID format for id at index %d: %s", i, id) + } + } + return nil +} + +// ListTaskFilters represents the filters for listing tasks. +type ListTaskFilters struct { + TaskType *consts.TaskType + Immediate *bool + TraceID string + GroupID string + ProjectID int + State *consts.TaskState + Status *consts.StatusType +} + +// ListTaskReq represents the request to list tasks. +type ListTaskReq struct { + dto.PaginationReq + TaskType *consts.TaskType `form:"task_type" binding:"omitempty"` + Immediate *bool `form:"immediate" binding:"omitempty"` + TraceID string `form:"trace_id" binding:"omitempty"` + GroupID string `form:"group_id" binding:"omitempty"` + ProjectID int `form:"project_id" binding:"omitempty"` + State *consts.TaskState `form:"state" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListTaskReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if err := validateTaskType(req.TaskType); err != nil { + return err + } + if err := validateUUID(req.TraceID); err != nil { + return err + } + if err := validateUUID(req.GroupID); err != nil { + return err + } + if req.ProjectID < 0 { + return fmt.Errorf("invalid project ID: %d", req.ProjectID) + } + if err := validateState(req.State); err != nil { + return err + } + return validateStatus(req.Status) +} + +func (req *ListTaskReq) ToFilterOptions() *ListTaskFilters { + return &ListTaskFilters{ + Immediate: req.Immediate, + TaskType: req.TaskType, + TraceID: req.TraceID, + GroupID: req.GroupID, + ProjectID: req.ProjectID, + State: req.State, + Status: req.Status, + } +} + +// TaskResp represents the response for a task. +type TaskResp struct { + ID string `json:"id"` + Type string `json:"type"` + Immediate bool `json:"immediate"` + ExecuteTime int64 `json:"execute_time"` + CronExpr string `json:"cron_expr,omitempty"` + TraceID string `json:"trace_id"` + GroupID string `json:"group_id"` + + State string `json:"state"` + Status string `json:"status"` + ProjectID int `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewTaskResp(task *model.Task) *TaskResp { + return &TaskResp{ + ID: task.ID, + Type: consts.GetTaskTypeName(task.Type), + Immediate: task.Immediate, + ExecuteTime: task.ExecuteTime, + CronExpr: task.CronExpr, + TraceID: task.TraceID, + State: consts.GetTaskStateName(task.State), + Status: consts.GetStatusTypeName(task.Status), + CreatedAt: task.CreatedAt, + UpdatedAt: task.UpdatedAt, + } +} + +// TaskDetailResp represents a task with payload and logs. +type TaskDetailResp struct { + TaskResp + + Payload map[string]any `json:"payload,omitempty" swaggertype:"object"` + Logs []string `json:"logs"` +} + +func NewTaskDetailResp(task *model.Task, logs []string) *TaskDetailResp { + resp := &TaskDetailResp{ + TaskResp: *NewTaskResp(task), + Logs: logs, + } + + if task.Payload != "" { + var payload map[string]any + if err := json.Unmarshal([]byte(task.Payload), &payload); err == nil { + resp.Payload = payload + } + } + return resp +} + +// QueuedTasksResp represents ready and delayed queued tasks. +type QueuedTasksResp struct { + ReadyTasks []TaskResp `json:"ready_tasks"` + DelayedTasks []TaskResp `json:"delayed_tasks"` +} + +func validateState(state *consts.TaskState) error { + if state != nil { + if _, exists := consts.ValidTaskStates[*state]; !exists { + return fmt.Errorf("invalid task state: %d", *state) + } + } + return nil +} + +func validateTaskType(taskType *consts.TaskType) error { + if taskType != nil { + if _, exists := consts.ValidTaskTypes[*taskType]; !exists { + return fmt.Errorf("invalid task type: %d", *taskType) + } + } + return nil +} + +func validateUUID(id string) error { + if id == "" { + return nil + } + if !utils.IsValidUUID(id) { + return fmt.Errorf("invalid UUID format: %s", id) + } + return nil +} + +func validateStatus(status *consts.StatusType) error { + if status != nil { + if _, exists := consts.ValidStatuses[*status]; !exists { + return fmt.Errorf("invalid status value: %d", *status) + } + } + return nil +} diff --git a/src/handlers/v2/tasks.go b/src/module/task/handler.go similarity index 79% rename from src/handlers/v2/tasks.go rename to src/module/task/handler.go index 4ff26964..90fd31e7 100644 --- a/src/handlers/v2/tasks.go +++ b/src/module/task/handler.go @@ -1,30 +1,35 @@ -package v2 +package taskmodule import ( + "aegis/httpx" + "errors" "net/http" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/handlers" - "aegis/repository" - producer "aegis/service/producer" "aegis/utils" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" - "gorm.io/gorm" ) var wsUpgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 4096, CheckOrigin: func(r *http.Request) bool { - return true // Allow all origins (JWT already handles auth) + return true }, } +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // BatchDeleteTasks // // @Summary Batch delete tasks @@ -34,15 +39,16 @@ var wsUpgrader = websocket.Upgrader{ // @Accept json // @Produce json // @Security BearerAuth -// @Param batch_delete body dto.BatchDeleteTaskReq true "Batch delete request" +// @Param batch_delete body BatchDeleteTaskReq true "Batch delete request" // @Success 200 {object} dto.GenericResponse[any] "Tasks deleted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks/batch-delete [post] -func BatchDeleteTasks(c *gin.Context) { - var req dto.BatchDeleteTaskReq +// @x-api-type {} +func (h *Handler) BatchDelete(c *gin.Context) { + var req BatchDeleteTaskReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -53,8 +59,8 @@ func BatchDeleteTasks(c *gin.Context) { return } - err := producer.BatchDeleteTasks(req.IDs) - if handlers.HandleServiceError(c, err) { + err := h.service.BatchDelete(c.Request.Context(), req.IDs) + if httpx.HandleServiceError(c, err) { return } @@ -70,23 +76,23 @@ func BatchDeleteTasks(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param task_id path string true "Task ID" -// @Success 200 {object} dto.GenericResponse[dto.TaskDetailResp] "Task retrieved successfully" +// @Success 200 {object} dto.GenericResponse[TaskDetailResp] "Task retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid task ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Task not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks/{task_id} [get] -// @x-api-type {"sdk":"true"} -func GetTask(c *gin.Context) { +// @x-api-type {} +func (h *Handler) Get(c *gin.Context) { taskID := c.Param(consts.URLPathTaskID) if !utils.IsValidUUID(taskID) { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid task ID") return } - resp, err := producer.GetTaskDetail(taskID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetDetail(c.Request.Context(), taskID) + if httpx.HandleServiceError(c, err) { return } @@ -110,14 +116,15 @@ func GetTask(c *gin.Context) { // @Param project_id query int false "Filter by project ID" // @Param state query consts.TaskState false "Filter by state" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.TaskResp]] "Tasks retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[TaskResp]] "Tasks retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks [get] -func ListTasks(c *gin.Context) { - var req dto.ListTaskReq +// @x-api-type {} +func (h *Handler) List(c *gin.Context) { + var req ListTaskReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format : "+err.Error()) return @@ -128,8 +135,8 @@ func ListTasks(c *gin.Context) { return } - resp, err := producer.ListTasks(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.List(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -145,19 +152,19 @@ func ListTasks(c *gin.Context) { // @ID get_task_logs_ws // @Param task_id path string true "Task ID" // @Param token query string true "JWT authentication token" -// @Success 101 {object} dto.WSLogMessage "WebSocket connection established" +// @Success 101 {object} WSLogMessage "WebSocket connection established" // @Failure 400 "Invalid task ID" // @Failure 401 "Authentication failed" // @Failure 404 "Task not found" // @Router /api/v2/tasks/{task_id}/logs/ws [get] -func GetTaskLogsWS(c *gin.Context) { +// @x-api-type {} +func (h *Handler) LogsWS(c *gin.Context) { taskID := c.Param(consts.URLPathTaskID) if taskID == "" { dto.ErrorResponse(c, http.StatusBadRequest, "task_id is required") return } - // Authenticate via query parameter (WebSocket doesn't support custom headers) token := c.Query("token") if token == "" { dto.ErrorResponse(c, http.StatusUnauthorized, "token query parameter is required") @@ -169,10 +176,9 @@ func GetTaskLogsWS(c *gin.Context) { return } - // Verify task exists - task, err := repository.GetTaskByID(database.DB, taskID) + task, err := h.service.GetForLogStream(c.Request.Context(), taskID) if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, consts.ErrNotFound) { dto.ErrorResponse(c, http.StatusNotFound, "task not found") return } @@ -181,7 +187,6 @@ func GetTaskLogsWS(c *gin.Context) { return } - // Upgrade to WebSocket conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { logrus.Errorf("WebSocket upgrade failed for task %s: %v", taskID, err) @@ -189,7 +194,5 @@ func GetTaskLogsWS(c *gin.Context) { } defer func() { _ = conn.Close() }() - // Delegate all streaming logic to the service layer - streamer := producer.NewTaskLogStreamer(conn, taskID) - streamer.StreamLogs(c.Request.Context(), task) + h.service.StreamLogs(c.Request.Context(), conn, task) } diff --git a/src/module/task/log_service.go b/src/module/task/log_service.go new file mode 100644 index 00000000..43013cde --- /dev/null +++ b/src/module/task/log_service.go @@ -0,0 +1,272 @@ +package taskmodule + +import ( + "context" + "encoding/json" + "sync" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + "github.com/gorilla/websocket" + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" +) + +const ( + writeWait = 10 * time.Second + pongWait = 60 * time.Second + pingPeriod = 54 * time.Second + maxMsgSize = 512 + taskPollInterval = 5 * time.Second + completionFlushDelay = 5 * time.Second +) + +type TaskLogService struct { + repository *Repository + queueStore *TaskQueueStore + loki *LokiGateway +} + +func NewTaskLogService(repository *Repository, queueStore *TaskQueueStore, loki *LokiGateway) *TaskLogService { + return &TaskLogService{ + repository: repository, + queueStore: queueStore, + loki: loki, + } +} + +func (s *TaskLogService) StreamLogs(ctx context.Context, conn *websocket.Conn, task *model.Task) { + streamer := &taskLogStreamer{ + ctx: ctx, + conn: conn, + task: task, + taskID: task.ID, + service: s, + log: logrus.WithField("task_id", task.ID), + } + streamer.StreamLogs(ctx) +} + +type taskLogStreamer struct { + ctx context.Context + conn *websocket.Conn + mu sync.Mutex + log *logrus.Entry + taskID string + task *model.Task + service *TaskLogService +} + +func (s *taskLogStreamer) StreamLogs(ctx context.Context) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + s.conn.SetReadLimit(maxMsgSize) + _ = s.conn.SetReadDeadline(time.Now().Add(pongWait)) + s.conn.SetPongHandler(func(string) error { + _ = s.conn.SetReadDeadline(time.Now().Add(pongWait)) + return nil + }) + + go s.runReadLoop(cancel) + go s.runPingLoop(ctx, cancel) + + pubsub, err := s.service.queueStore.SubscribeJobLogs(ctx, s.taskID) + if err != nil { + s.log.Errorf("Failed to subscribe to Redis Pub/Sub for task logs: %v", err) + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeError, + Message: "failed to subscribe to log stream", + }) + return + } + defer func() { _ = pubsub.Close() }() + s.log.Info("Subscribed to Redis Pub/Sub for real-time logs") + + lastHistoricalTime := s.sendHistoricalLogs() + + if isTaskTerminal(s.task.State) { + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeEnd, + Message: "task already completed", + }) + s.closeNormal("task completed") + return + } + + s.streamRealtime(ctx, pubsub.Channel(), lastHistoricalTime) +} + +func (s *taskLogStreamer) runReadLoop(cancel context.CancelFunc) { + defer cancel() + for { + _, _, err := s.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { + s.log.Warnf("WebSocket unexpected close: %v", err) + } + return + } + } +} + +func (s *taskLogStreamer) WriteMessage(msg WSLogMessage) { + s.mu.Lock() + defer s.mu.Unlock() + + _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := s.conn.WriteJSON(msg); err != nil { + s.log.Warnf("WebSocket write error: %v", err) + } +} + +func (s *taskLogStreamer) ForwardRedisLog(payload string, lastHistoricalTime time.Time) { + var entry dto.LogEntry + if err := json.Unmarshal([]byte(payload), &entry); err != nil { + s.log.Warnf("Failed to unmarshal Redis log message: %v", err) + return + } + + if !lastHistoricalTime.IsZero() && !entry.Timestamp.After(lastHistoricalTime) { + return + } + + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeRealtime, + Logs: []dto.LogEntry{entry}, + }) +} + +func (s *taskLogStreamer) runPingLoop(ctx context.Context, cancel context.CancelFunc) { + ticker := time.NewTicker(pingPeriod) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + s.mu.Lock() + _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) + err := s.conn.WriteMessage(websocket.PingMessage, nil) + s.mu.Unlock() + if err != nil { + cancel() + return + } + } + } +} + +func (s *taskLogStreamer) sendHistoricalLogs() time.Time { + lokiCtx, lokiCancel := context.WithTimeout(s.ctx, 15*time.Second) + defer lokiCancel() + + historicalLogs, err := s.service.loki.QueryJobLogs(lokiCtx, s.taskID, s.task.CreatedAt) + if err != nil { + s.log.Warnf("Failed to query Loki for historical logs: %v", err) + return time.Time{} + } + + if len(historicalLogs) > 0 { + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeHistory, + Logs: historicalLogs, + Total: len(historicalLogs), + }) + s.log.Infof("Sent %d historical log entries", len(historicalLogs)) + return historicalLogs[len(historicalLogs)-1].Timestamp + } + + return time.Time{} +} + +func (s *taskLogStreamer) streamRealtime(ctx context.Context, redisCh <-chan *redis.Message, lastHistoricalTime time.Time) { + taskDoneCh := make(chan struct{}) + go s.pollTaskCompletion(ctx, taskDoneCh) + + for { + select { + case <-ctx.Done(): + s.log.Info("Context cancelled, closing WebSocket") + return + + case <-taskDoneCh: + s.flushAndClose(redisCh, lastHistoricalTime) + return + + case msg, ok := <-redisCh: + if !ok { + s.log.Warn("Redis Pub/Sub channel closed") + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeError, + Message: "log stream interrupted", + }) + return + } + s.ForwardRedisLog(msg.Payload, lastHistoricalTime) + } + } +} + +func (s *taskLogStreamer) pollTaskCompletion(ctx context.Context, taskDoneCh chan<- struct{}) { + ticker := time.NewTicker(taskPollInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + task, err := s.service.repository.GetByID(s.taskID) + if err != nil { + s.log.Warnf("Failed to poll task state: %v", err) + continue + } + if isTaskTerminal(task.State) { + s.log.Info("Task detected as terminal, initiating close") + close(taskDoneCh) + return + } + } + } +} + +func (s *taskLogStreamer) flushAndClose(redisCh <-chan *redis.Message, lastHistoricalTime time.Time) { + s.log.Info("Task completed, flushing remaining logs...") + flushTimer := time.NewTimer(completionFlushDelay) + defer flushTimer.Stop() + +flushLoop: + for { + select { + case msg, ok := <-redisCh: + if !ok { + break flushLoop + } + s.ForwardRedisLog(msg.Payload, lastHistoricalTime) + case <-flushTimer.C: + break flushLoop + } + } + + s.WriteMessage(WSLogMessage{ + Type: consts.WSLogTypeEnd, + Message: "task completed", + }) + s.closeNormal("task completed") +} + +func (s *taskLogStreamer) closeNormal(reason string) { + s.mu.Lock() + defer s.mu.Unlock() + + _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) + _ = s.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, reason)) +} + +func isTaskTerminal(state consts.TaskState) bool { + return state == consts.TaskCompleted || state == consts.TaskError || state == consts.TaskCancelled +} diff --git a/src/module/task/log_types.go b/src/module/task/log_types.go new file mode 100644 index 00000000..d4a5606c --- /dev/null +++ b/src/module/task/log_types.go @@ -0,0 +1,14 @@ +package taskmodule + +import ( + "aegis/consts" + "aegis/dto" +) + +// WSLogMessage is the WebSocket payload for task log streaming. +type WSLogMessage struct { + Type consts.WSLogType `json:"type"` + Logs []dto.LogEntry `json:"logs,omitempty"` + Message string `json:"message,omitempty"` + Total int `json:"total,omitempty"` +} diff --git a/src/module/task/loki_gateway.go b/src/module/task/loki_gateway.go new file mode 100644 index 00000000..20f5cea4 --- /dev/null +++ b/src/module/task/loki_gateway.go @@ -0,0 +1,24 @@ +package taskmodule + +import ( + "context" + "time" + + "aegis/dto" + lokiinfra "aegis/infra/loki" +) + +type LokiGateway struct { + client *lokiinfra.Client +} + +func NewLokiGateway(client *lokiinfra.Client) *LokiGateway { + return &LokiGateway{client: client} +} + +func (g *LokiGateway) QueryJobLogs(ctx context.Context, taskID string, start time.Time) ([]dto.LogEntry, error) { + return g.client.QueryJobLogs(ctx, taskID, lokiinfra.QueryOpts{ + Start: start, + Direction: "forward", + }) +} diff --git a/src/module/task/module.go b/src/module/task/module.go new file mode 100644 index 00000000..3de1ac1f --- /dev/null +++ b/src/module/task/module.go @@ -0,0 +1,12 @@ +package taskmodule + +import "go.uber.org/fx" + +var Module = fx.Module("task", + fx.Provide(NewRepository), + fx.Provide(NewTaskQueueStore), + fx.Provide(NewLokiGateway), + fx.Provide(NewTaskLogService), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/task/queue_store.go b/src/module/task/queue_store.go new file mode 100644 index 00000000..f9ecc2b2 --- /dev/null +++ b/src/module/task/queue_store.go @@ -0,0 +1,24 @@ +package taskmodule + +import ( + "context" + "fmt" + + redisinfra "aegis/infra/redis" + "github.com/redis/go-redis/v9" +) + +const jobLogsChannelPrefix = "joblogs" + +type TaskQueueStore struct { + redis *redisinfra.Gateway +} + +func NewTaskQueueStore(redis *redisinfra.Gateway) *TaskQueueStore { + return &TaskQueueStore{redis: redis} +} + +func (s *TaskQueueStore) SubscribeJobLogs(ctx context.Context, taskID string) (*redis.PubSub, error) { + channel := fmt.Sprintf("%s:%s", jobLogsChannelPrefix, taskID) + return s.redis.Subscribe(ctx, channel) +} diff --git a/src/module/task/repository.go b/src/module/task/repository.go new file mode 100644 index 00000000..eb367b99 --- /dev/null +++ b/src/module/task/repository.go @@ -0,0 +1,83 @@ +package taskmodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) BatchDelete(taskIDs []string) error { + if len(taskIDs) == 0 { + return nil + } + + if err := r.db.Model(&model.Task{}). + Where("id IN (?) AND status != ?", taskIDs, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return fmt.Errorf("failed to batch delete tasks: %w", err) + } + return nil +} + +func (r *Repository) GetByID(taskID string) (*model.Task, error) { + var task model.Task + if err := r.db. + Preload("FaultInjection.Benchmark.Container"). + Preload("FaultInjection.Pedestal.Container"). + Preload("Execution.AlgorithmVersion.Container"). + Preload("Execution.Datapack"). + Preload("Execution.DatasetVersion"). + Where("id = ? AND status != ?", taskID, consts.CommonDeleted). + First(&task).Error; err != nil { + return nil, fmt.Errorf("failed to find task with id %s: %w", taskID, err) + } + return &task, nil +} + +func (r *Repository) List(limit, offset int, filters *ListTaskFilters) ([]model.Task, int64, error) { + var ( + tasks []model.Task + total int64 + ) + + query := r.db.Model(&model.Task{}) + if filters.Immediate != nil { + query = query.Where("immediate = ?", *filters.Immediate) + } + if filters.TaskType != nil { + query = query.Where("type = ?", *filters.TaskType) + } + if filters.TraceID != "" { + query = query.Where("trace_id = ?", filters.TraceID) + } + if filters.GroupID != "" { + query = query.Where("group_id = ?", filters.GroupID) + } + if filters.ProjectID > 0 { + query = query.Where("project_id = ?", filters.ProjectID) + } + if filters.State != nil { + query = query.Where("state = ?", *filters.State) + } + if filters.Status != nil { + query = query.Where("status = ?", *filters.Status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count tasks: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&tasks).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list tasks: %w", err) + } + return tasks, total, nil +} diff --git a/src/module/task/service.go b/src/module/task/service.go new file mode 100644 index 00000000..bc591ea9 --- /dev/null +++ b/src/module/task/service.go @@ -0,0 +1,107 @@ +package taskmodule + +import ( + "context" + "errors" + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + "github.com/gorilla/websocket" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type Service struct { + repository *Repository + logService *TaskLogService + loki *LokiGateway +} + +func NewService(repository *Repository, logService *TaskLogService, loki *LokiGateway) *Service { + return &Service{ + repository: repository, + logService: logService, + loki: loki, + } +} + +func (s *Service) BatchDelete(ctx context.Context, taskIDs []string) error { + if len(taskIDs) == 0 { + return nil + } + + return s.repository.BatchDelete(taskIDs) +} + +func (s *Service) GetDetail(ctx context.Context, taskID string) (*TaskDetailResp, error) { + task, err := s.repository.GetByID(taskID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: task id: %s", consts.ErrNotFound, taskID) + } + return nil, fmt.Errorf("failed to get task: %w", err) + } + + logs := s.queryHistoricalLogs(ctx, task) + return NewTaskDetailResp(task, logs), nil +} + +func (s *Service) List(ctx context.Context, req *ListTaskReq) (*dto.ListResp[TaskResp], error) { + if req == nil { + return nil, fmt.Errorf("list tasks request is nil") + } + + limit, offset := req.ToGormParams() + filterOptions := req.ToFilterOptions() + + tasks, total, err := s.repository.List(limit, offset, filterOptions) + if err != nil { + return nil, fmt.Errorf("failed to list tasks: %w", err) + } + + taskResps := make([]TaskResp, 0, len(tasks)) + for _, task := range tasks { + taskResps = append(taskResps, *NewTaskResp(&task)) + } + + return &dto.ListResp[TaskResp]{ + Items: taskResps, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetForLogStream(ctx context.Context, taskID string) (*model.Task, error) { + task, err := s.repository.GetByID(taskID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: task id: %s", consts.ErrNotFound, taskID) + } + return nil, fmt.Errorf("failed to get task: %w", err) + } + return task, nil +} + +func (s *Service) StreamLogs(ctx context.Context, conn *websocket.Conn, task *model.Task) { + s.logService.StreamLogs(ctx, conn, task) +} + +func (s *Service) queryHistoricalLogs(ctx context.Context, task *model.Task) []string { + lokiCtx, lokiCancel := context.WithTimeout(ctx, 10*time.Second) + defer lokiCancel() + + logEntries, err := s.loki.QueryJobLogs(lokiCtx, task.ID, task.CreatedAt) + if err != nil { + logrus.Warnf("Failed to query Loki for task %s logs: %v", task.ID, err) + return []string{} + } + + logs := make([]string, 0, len(logEntries)) + for _, entry := range logEntries { + logs = append(logs, entry.Line) + } + return logs +} diff --git a/src/module/task/service_test.go b/src/module/task/service_test.go new file mode 100644 index 00000000..4db329ea --- /dev/null +++ b/src/module/task/service_test.go @@ -0,0 +1,105 @@ +package taskmodule + +import ( + "context" + "net/http" + "net/http/httptest" + "regexp" + "strings" + "testing" + "time" + + "aegis/consts" + lokiinfra "aegis/infra/loki" + "aegis/model" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newTaskService(t *testing.T, loki *LokiGateway) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + if loki == nil { + loki = NewLokiGateway(&lokiinfra.Client{}) + } + + service := NewService(NewRepository(db), NewTaskLogService(NewRepository(db), nil, loki), loki) + return service, mock, func() { + _ = sqlDB.Close() + } +} + +func TestTaskServiceListSuccess(t *testing.T) { + service, mock, cleanup := newTaskService(t, nil) + defer cleanup() + + now := time.Now() + state := consts.TaskPending + req := &ListTaskReq{State: &state} + + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `tasks` WHERE state = ?")). + WithArgs(state). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `tasks` WHERE state = ? ORDER BY created_at DESC LIMIT ?")). + WithArgs(state, 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "type", "immediate", "execute_time", "cron_expr", "payload", "trace_id", "parent_task_id", + "level", "sequence", "state", "status", "created_at", "updated_at", + }).AddRow("task-1", consts.TaskTypeRunAlgorithm, true, 0, "", "{}", "trace-1", nil, 0, 0, consts.TaskPending, consts.CommonEnabled, now, now)) + + resp, err := service.List(t.Context(), req) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "task-1", resp.Items[0].ID) + require.Equal(t, consts.GetTaskStateName(consts.TaskPending), resp.Items[0].State) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTaskServiceQueryHistoricalLogsSuccess(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/loki/api/v1/query_range", r.URL.Path) + require.True(t, strings.Contains(r.URL.Query().Get("query"), `task_id="task-1"`)) + _, _ = w.Write([]byte(`{ + "status":"success", + "data":{ + "resultType":"streams", + "result":[{ + "stream":{"trace_id":"trace-1","job_id":"job-1"}, + "values":[ + ["1710000000000000000","first log"], + ["1710000001000000000","second log"] + ] + }] + } + }`)) + })) + defer server.Close() + + viper.Set("loki.address", server.URL) + viper.Set("loki.max_entries", 100) + + loki := NewLokiGateway(lokiinfra.NewClient()) + service, _, cleanup := newTaskService(t, loki) + defer cleanup() + + logs := service.queryHistoricalLogs(context.Background(), &model.Task{ + ID: "task-1", + CreatedAt: time.Unix(1710000000, 0), + }) + + require.Equal(t, []string{"first log", "second log"}, logs) +} diff --git a/src/dto/team.go b/src/module/team/api_types.go similarity index 67% rename from src/dto/team.go rename to src/module/team/api_types.go index 6676c68f..d7f46605 100644 --- a/src/dto/team.go +++ b/src/module/team/api_types.go @@ -1,4 +1,4 @@ -package dto +package teammodule import ( "fmt" @@ -6,12 +6,15 @@ import ( "time" "aegis/consts" - "aegis/database" + "aegis/dto" + "aegis/model" + projectmodule "aegis/module/project" ) -// ===================== Team CRUD DTOs ===================== +type TeamProjectListReq = projectmodule.ListProjectReq +type TeamProjectItem = projectmodule.ProjectResp -// CreateTeamReq represents team creation request +// CreateTeamReq represents team creation request. type CreateTeamReq struct { Name string `json:"name" binding:"required"` Description string `json:"description" binding:"omitempty"` @@ -30,8 +33,8 @@ func (req *CreateTeamReq) Validate() error { return nil } -func (req *CreateTeamReq) ConvertToTeam() *database.Team { - return &database.Team{ +func (req *CreateTeamReq) ConvertToTeam() *model.Team { + return &model.Team{ Name: req.Name, Description: req.Description, IsPublic: *req.IsPublic, @@ -39,9 +42,9 @@ func (req *CreateTeamReq) ConvertToTeam() *database.Team { } } -// ListTeamReq represents team list query parameters +// ListTeamReq represents team list query parameters. type ListTeamReq struct { - PaginationReq + dto.PaginationReq IsPublic *bool `form:"is_public" binding:"omitempty"` Status *consts.StatusType `form:"status" binding:"omitempty"` } @@ -50,10 +53,10 @@ func (req *ListTeamReq) Validate() error { if err := req.PaginationReq.Validate(); err != nil { return err } - return validateStatusField(req.Status, false) + return validateStatus(req.Status, false) } -// UpdateTeamReq represents team update request +// UpdateTeamReq represents team update request. type UpdateTeamReq struct { Description *string `json:"description,omitempty"` IsPublic *bool `json:"is_public,omitempty"` @@ -61,10 +64,10 @@ type UpdateTeamReq struct { } func (req *UpdateTeamReq) Validate() error { - return validateStatusField(req.Status, true) + return validateStatus(req.Status, true) } -func (req *UpdateTeamReq) PatchTeamModel(target *database.Team) { +func (req *UpdateTeamReq) PatchTeamModel(target *model.Team) { if req.Description != nil { target.Description = *req.Description } @@ -76,7 +79,7 @@ func (req *UpdateTeamReq) PatchTeamModel(target *database.Team) { } } -// TeamResp represents basic team response +// TeamResp represents basic team response. type TeamResp struct { ID int `json:"id"` Name string `json:"name"` @@ -87,7 +90,7 @@ type TeamResp struct { UpdatedAt time.Time `json:"updated_at"` } -func NewTeamResp(team *database.Team) *TeamResp { +func NewTeamResp(team *model.Team) *TeamResp { return &TeamResp{ ID: team.ID, Name: team.Name, @@ -99,33 +102,31 @@ func NewTeamResp(team *database.Team) *TeamResp { } } -// TeamDetailResp represents detailed team response +// TeamDetailResp represents detailed team response. type TeamDetailResp struct { TeamResp - UserCount int `json:"user_count"` - ProjectCount int `json:"project_count"` - Projects []ProjectResp `json:"projects,omitempty"` + UserCount int `json:"user_count"` + ProjectCount int `json:"project_count"` + Projects []TeamProjectItem `json:"projects,omitempty"` } -func NewTeamDetailResp(team *database.Team) *TeamDetailResp { +func NewTeamDetailResp(team *model.Team) *TeamDetailResp { return &TeamDetailResp{ TeamResp: *NewTeamResp(team), } } -// ===================== Team-User DTOs ===================== - -// ListTeamMemberReq represents team member list query parameters +// ListTeamMemberReq represents team member list query parameters. type ListTeamMemberReq struct { - PaginationReq + dto.PaginationReq } func (req *ListTeamMemberReq) Validate() error { return req.PaginationReq.Validate() } -// AddTeamMemberReq represents request to add a user to team +// AddTeamMemberReq represents request to add a user to team. type AddTeamMemberReq struct { Username string `json:"username" binding:"required"` RoleID int `json:"role_id" binding:"required"` @@ -142,7 +143,7 @@ func (req *AddTeamMemberReq) Validate() error { return nil } -// UpdateTeamMemberRoleReq represents request to update team member's role +// UpdateTeamMemberRoleReq represents request to update team member's role. type UpdateTeamMemberRoleReq struct { RoleID int `json:"role_id" binding:"required"` } @@ -154,7 +155,7 @@ func (req *UpdateTeamMemberRoleReq) Validate() error { return nil } -// TeamMemberResp represents team member information +// TeamMemberResp represents team member information. type TeamMemberResp struct { UserID int `json:"user_id"` Username string `json:"username"` @@ -164,3 +165,18 @@ type TeamMemberResp struct { RoleName string `json:"role_name"` JoinedAt time.Time `json:"joined_at"` } + +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) + } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil +} diff --git a/src/handlers/v2/teams.go b/src/module/team/handler.go similarity index 73% rename from src/handlers/v2/teams.go rename to src/module/team/handler.go index c576143f..61cbed14 100644 --- a/src/handlers/v2/teams.go +++ b/src/module/team/handler.go @@ -1,18 +1,25 @@ -package v2 +package teammodule import ( + "aegis/httpx" "net/http" "strconv" "aegis/consts" "aegis/dto" - "aegis/handlers" "aegis/middleware" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // CreateTeam handles team creation // // @Summary Create a new team @@ -22,38 +29,34 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.CreateTeamReq true "Team creation request" -// @Success 201 {object} dto.GenericResponse[dto.TeamResp] "Team created successfully" +// @Param request body CreateTeamReq true "Team creation request" +// @Success 201 {object} dto.GenericResponse[TeamResp] "Team created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 409 {object} dto.GenericResponse[any] "Team already exists" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams [post] -// @x-api-type {"sdk":"true"} -func CreateTeam(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) CreateTeam(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - - var req dto.CreateTeamReq + var req CreateTeamReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.CreateTeam(&req, userID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateTeam(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusCreated, "Team created successfully", resp) } @@ -73,19 +76,15 @@ func CreateTeam(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Team not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id} [delete] -func DeleteTeam(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) DeleteTeam(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - err = producer.DeleteTeam(teamID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteTeam(c.Request.Context(), teamID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Team deleted successfully", nil) } @@ -98,27 +97,23 @@ func DeleteTeam(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param team_id path int true "Team ID" -// @Success 200 {object} dto.GenericResponse[dto.TeamDetailResp] "Team retrieved successfully" +// @Success 200 {object} dto.GenericResponse[TeamDetailResp] "Team retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Team not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id} [get] -// @x-api-type {"sdk":"true"} -func GetTeamDetail(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) GetTeamDetail(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - resp, err := producer.GetTeamDetail(teamID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetTeamDetail(c.Request.Context(), teamID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -134,38 +129,32 @@ func GetTeamDetail(c *gin.Context) { // @Param size query int false "Page size" default(20) // @Param is_public query bool false "Filter by public status" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.TeamResp]] "Teams retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[TeamResp]] "Teams retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams [get] -// @x-api-type {"sdk":"true"} -func ListTeams(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) ListTeams(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - - isAdmin := middleware.IsCurrentUserAdmin(c) - - var req dto.ListTeamReq + var req ListTeamReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListTeams(&req, userID, isAdmin) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListTeams(c.Request.Context(), &req, userID, middleware.IsCurrentUserAdmin(c)) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -179,43 +168,36 @@ func ListTeams(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param team_id path int true "Team ID" -// @Param request body dto.UpdateTeamReq true "Team update request" -// @Success 202 {object} dto.GenericResponse[dto.TeamResp] "Team updated successfully" +// @Param request body UpdateTeamReq true "Team update request" +// @Success 202 {object} dto.GenericResponse[TeamResp] "Team updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Team not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id} [patch] -func UpdateTeam(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) UpdateTeam(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - var req dto.UpdateTeamReq + var req UpdateTeamReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.UpdateTeam(&req, teamID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateTeam(c.Request.Context(), &req, teamID) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusAccepted, "Team updated successfully", resp) } -// ===================== Team-Project API ===================== - // ListTeamProjects lists all projects belonging to a team // // @Summary List team projects @@ -229,43 +211,35 @@ func UpdateTeam(c *gin.Context) { // @Param size query int false "Page size" default(20) // @Param is_public query bool false "Filter by public status" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.ProjectResp]] "Projects retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[TeamProjectItem]] "Projects retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or request parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Team not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/projects [get] -// @x-api-type {"sdk":"true"} -func ListTeamProjects(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) ListTeamProjects(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - var req dto.ListProjectReq + var req TeamProjectListReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListTeamProjects(&req, teamID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListTeamProjects(c.Request.Context(), &req, teamID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } -// ===================== Team-User API ===================== - // AddTeamMember adds a user to team // // @Summary Add member to team @@ -276,7 +250,7 @@ func ListTeamProjects(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param team_id path int true "Team ID" -// @Param request body dto.AddTeamMemberReq true "Add member request" +// @Param request body AddTeamMemberReq true "Add member request" // @Success 201 {object} dto.GenericResponse[any] "Member added successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -285,30 +259,24 @@ func ListTeamProjects(c *gin.Context) { // @Failure 409 {object} dto.GenericResponse[any] "User already in team" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/members [post] -func AddTeamMember(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) AddTeamMember(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - var req dto.AddTeamMemberReq + var req AddTeamMemberReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - err = producer.AddTeamMember(&req, teamID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AddMember(c.Request.Context(), &req, teamID)) { return } - dto.JSONResponse[any](c, http.StatusCreated, "Member added successfully", nil) } @@ -329,37 +297,28 @@ func AddTeamMember(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Team or user not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/members/{user_id} [delete] -func RemoveTeamMember(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) RemoveTeamMember(c *gin.Context) { currentUserID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") + teamID, ok := parseTeamID(c) + if !ok { return } - - userIDStr := c.Param("user_id") - userID, err := strconv.Atoi(userIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") + userID, ok := parseIntParam(c, "user_id", "Invalid user ID") + if !ok { return } - if currentUserID == userID { dto.ErrorResponse(c, http.StatusBadRequest, "Cannot remove yourself from the team") return } - - err = producer.RemoveTeamMember(teamID, currentUserID, userID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemoveMember(c.Request.Context(), teamID, currentUserID, userID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Member removed successfully", nil) } @@ -374,7 +333,7 @@ func RemoveTeamMember(c *gin.Context) { // @Security BearerAuth // @Param team_id path int true "Team ID" // @Param user_id path int true "User ID" -// @Param request body dto.UpdateTeamMemberRoleReq true "Update role request" +// @Param request body UpdateTeamMemberRoleReq true "Update role request" // @Success 200 {object} dto.GenericResponse[any] "Role updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID, user ID, or request format/parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -382,43 +341,33 @@ func RemoveTeamMember(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Team, user, or role not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/members/{user_id}/role [patch] -func UpdateTeamMemberRole(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) UpdateTeamMemberRole(c *gin.Context) { currentUserID, exists := middleware.GetCurrentUserID(c) if !exists { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") + teamID, ok := parseTeamID(c) + if !ok { return } - - userIDStr := c.Param("user_id") - userID, err := strconv.Atoi(userIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") + userID, ok := parseIntParam(c, "user_id", "Invalid user ID") + if !ok { return } - - var req dto.UpdateTeamMemberRoleReq + var req UpdateTeamMemberRoleReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - err = producer.UpdateTeamMemberRole(&req, teamID, userID, currentUserID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.UpdateMemberRole(c.Request.Context(), &req, teamID, userID, currentUserID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Role updated successfully", nil) } @@ -433,37 +382,45 @@ func UpdateTeamMemberRole(c *gin.Context) { // @Param team_id path int true "Team ID" // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.TeamMemberResp]] "Members retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[TeamMemberResp]] "Members retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or request parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Team not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/members [get] -// @x-api-type {"sdk":"true"} -func ListTeamMembers(c *gin.Context) { - teamIDStr := c.Param(consts.URLPathTeamID) - teamID, err := strconv.Atoi(teamIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid team ID") +// @x-api-type {"portal":"true"} +func (h *Handler) ListTeamMembers(c *gin.Context) { + teamID, ok := parseTeamID(c) + if !ok { return } - - var req dto.ListTeamMemberReq + var req ListTeamMemberReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListTeamMembers(&req, teamID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListMembers(c.Request.Context(), &req, teamID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } + +func parseTeamID(c *gin.Context) (int, bool) { + return parseIntParam(c, consts.URLPathTeamID, "Invalid team ID") +} + +func parseIntParam(c *gin.Context, key, msg string) (int, bool) { + v := c.Param(key) + id, err := strconv.Atoi(v) + if err != nil || id <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, msg) + return 0, false + } + return id, true +} diff --git a/src/module/team/module.go b/src/module/team/module.go new file mode 100644 index 00000000..98a6b978 --- /dev/null +++ b/src/module/team/module.go @@ -0,0 +1,9 @@ +package teammodule + +import "go.uber.org/fx" + +var Module = fx.Module("team", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/team/repository.go b/src/module/team/repository.go new file mode 100644 index 00000000..d32a64a8 --- /dev/null +++ b/src/module/team/repository.go @@ -0,0 +1,371 @@ +package teammodule + +import ( + "aegis/consts" + "aegis/dto" + "aegis/model" + "errors" + "fmt" + "time" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) createTeamWithCreator(team *model.Team, userID int) error { + var superAdminRole model.Role + if err := r.db.Where("name = ? AND status != ?", consts.RoleSuperAdmin.String(), consts.CommonDeleted). + First(&superAdminRole).Error; err != nil { + return fmt.Errorf("failed to get super_admin role: %w", err) + } + + if err := r.db.Omit("ActiveName").Create(team).Error; err != nil { + return fmt.Errorf("failed to create team: %w", err) + } + + if err := r.db.Omit("active_user_team").Create(&model.UserTeam{ + UserID: userID, + TeamID: team.ID, + RoleID: superAdminRole.ID, + Status: consts.CommonEnabled, + }).Error; err != nil { + return fmt.Errorf("failed to create user-team association: %w", err) + } + return nil +} + +func (r *Repository) loadTeamDetail(teamID int) (*model.Team, int, int, error) { + team, err := r.loadTeam(teamID) + if err != nil { + return nil, 0, 0, err + } + + userCount, err := r.countTeamUsers(teamID) + if err != nil { + return nil, 0, 0, err + } + projectCount, err := r.countTeamProjects(teamID) + if err != nil { + return nil, 0, 0, err + } + + return team, userCount, projectCount, nil +} + +func (r *Repository) listVisibleTeams(limit, offset int, req *ListTeamReq, userID int, isAdmin bool) ([]model.Team, int64, error) { + var teamIDs []int + if !isAdmin { + teamIDs, err := r.listVisibleTeamIDsForUser(userID) + if err != nil { + return nil, 0, err + } + if len(teamIDs) == 0 { + return []model.Team{}, 0, nil + } + } + + var teams []model.Team + var total int64 + + query := r.db.Model(&model.Team{}) + if req.IsPublic != nil { + query = query.Where("is_public = ?", *req.IsPublic) + } + if req.Status != nil { + query = query.Where("status = ?", *req.Status) + } + if len(teamIDs) > 0 { + query = query.Where("id IN ?", teamIDs) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count teams: %w", err) + } + if err := query.Limit(limit).Offset(offset).Find(&teams).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list teams: %w", err) + } + return teams, total, nil +} + +func (r *Repository) updateMutableTeam(teamID int, patch func(*model.Team)) (*model.Team, error) { + team, err := r.loadTeam(teamID) + if err != nil { + return nil, err + } + patch(team) + if err := r.db.Omit("ActiveName").Save(team).Error; err != nil { + return nil, fmt.Errorf("failed to update team: %w", err) + } + return team, nil +} + +func (r *Repository) listTeamProjectViews(teamID, limit, offset int, isPublic *bool, status *consts.StatusType) ([]model.Project, map[int]*dto.ProjectStatistics, int64, error) { + var ( + projects []model.Project + total int64 + ) + + query := r.db.Model(&model.Project{}).Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted) + if isPublic != nil { + query = query.Where("is_public = ?", *isPublic) + } + if status != nil { + query = query.Where("status = ?", *status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, nil, 0, fmt.Errorf("failed to count projects for team %d: %w", teamID, err) + } + if err := query.Limit(limit).Offset(offset).Find(&projects).Error; err != nil { + return nil, nil, 0, fmt.Errorf("failed to list projects for team %d: %w", teamID, err) + } + + projectIDs := make([]int, 0, len(projects)) + for _, project := range projects { + projectIDs = append(projectIDs, project.ID) + } + + statsMap, err := listTeamProjectStatistics(r.db, projectIDs) + if err != nil { + return nil, nil, 0, err + } + return projects, statsMap, total, nil +} + +func (r *Repository) AddMember(teamID int, username string, roleID int) error { + if _, err := r.loadTeam(teamID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return consts.ErrNotFound + } + return err + } + + var user model.User + if err := r.db.Where("username = ?", username).First(&user).Error; err != nil { + return fmt.Errorf("failed to find user with username %s: %w", username, err) + } + if err := r.ensureRoleExists(roleID); err != nil { + return err + } + + if err := r.db.Omit("active_user_team").Create(&model.UserTeam{ + UserID: user.ID, + TeamID: teamID, + RoleID: roleID, + Status: consts.CommonEnabled, + }).Error; err != nil { + return fmt.Errorf("failed to create user-team association: %w", err) + } + return nil +} + +func (r *Repository) RemoveMember(teamID, userID int) (int64, error) { + if _, err := r.loadTeam(teamID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, consts.ErrNotFound + } + return 0, err + } + + result := r.db.Model(&model.UserTeam{}). + Where("user_id = ? AND team_id = ? AND status != ?", userID, teamID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete user-team association: %w", result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) UpdateMemberRole(teamID, targetUserID, roleID int) error { + if _, err := r.loadTeam(teamID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return consts.ErrNotFound + } + return err + } + if err := r.ensureRoleExists(roleID); err != nil { + return err + } + + var userTeam model.UserTeam + if err := r.db.Preload("Role"). + Where("user_id = ? AND team_id = ? AND status = ?", targetUserID, teamID, consts.CommonEnabled). + First(&userTeam).Error; err != nil { + return err + } + userTeam.RoleID = roleID + return r.db.Save(&userTeam).Error +} + +func (r *Repository) ListTeamMembers(teamID, limit, offset int) ([]TeamMemberResp, int64, error) { + if _, err := r.loadTeam(teamID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, consts.ErrNotFound + } + return nil, 0, err + } + + var members []TeamMemberResp + var total int64 + + query := r.db.Table("users"). + Joins("JOIN user_teams ON users.id = user_teams.user_id"). + Joins("LEFT JOIN roles ON roles.id = user_teams.role_id"). + Where("user_teams.team_id = ? AND user_teams.status = ? AND users.status != ?", teamID, consts.CommonEnabled, consts.CommonDeleted) + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count team members for team %d: %w", teamID, err) + } + if err := query.Select( + "users.id AS user_id", + "users.username", + "users.full_name", + "users.email", + "user_teams.role_id", + "roles.display_name AS role_name", + "user_teams.created_at AS joined_at", + ).Limit(limit).Offset(offset).Scan(&members).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list team members for team %d: %w", teamID, err) + } + return members, total, nil +} + +func (r *Repository) loadUserTeamMembership(userID, teamID int) (*model.UserTeam, error) { + var userTeam model.UserTeam + if err := r.db. + Preload("Role"). + Where("user_id = ? AND team_id = ? AND status = ?", userID, teamID, consts.CommonEnabled). + First(&userTeam).Error; err != nil { + return nil, err + } + return &userTeam, nil +} + +func (r *Repository) DeleteTeam(teamID int) (int64, error) { + result := r.db.Model(&model.Team{}). + Where("id = ? AND status != ?", teamID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to soft delete team %d: %w", teamID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) isTeamPublic(teamID int) (bool, error) { + team, err := r.loadTeam(teamID) + if err != nil { + return false, err + } + return team.IsPublic, nil +} + +func (r *Repository) ensureRoleExists(roleID int) error { + var role model.Role + if err := r.db.Where("id = ? AND status != ?", roleID, consts.CommonDeleted).First(&role).Error; err != nil { + return fmt.Errorf("failed to find role with id %d: %w", roleID, err) + } + return nil +} + +func (r *Repository) loadTeam(teamID int) (*model.Team, error) { + var team model.Team + if err := r.db.Where("id = ?", teamID).First(&team).Error; err != nil { + return nil, fmt.Errorf("failed to find team with id %d: %w", teamID, err) + } + return &team, nil +} + +func (r *Repository) countTeamUsers(teamID int) (int, error) { + var userCount int64 + if err := r.db.Model(&model.UserTeam{}). + Where("team_id = ? AND status = ?", teamID, consts.CommonEnabled). + Count(&userCount).Error; err != nil { + return 0, fmt.Errorf("failed to get team user count: %w", err) + } + return int(userCount), nil +} + +func (r *Repository) countTeamProjects(teamID int) (int, error) { + var projectCount int64 + if err := r.db.Model(&model.Project{}). + Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted). + Count(&projectCount).Error; err != nil { + return 0, fmt.Errorf("failed to get team project count: %w", err) + } + return int(projectCount), nil +} + +func (r *Repository) listVisibleTeamIDsForUser(userID int) ([]int, error) { + var teamIDs []int + if err := r.db.Model(&model.UserTeam{}). + Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). + Pluck("team_id", &teamIDs).Error; err != nil { + return nil, fmt.Errorf("failed to get user teams: %w", err) + } + return teamIDs, nil +} + +func listTeamProjectStatistics(db *gorm.DB, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { + statsMap := make(map[int]*dto.ProjectStatistics, len(projectIDs)) + for _, projectID := range projectIDs { + statsMap[projectID] = &dto.ProjectStatistics{} + } + if len(projectIDs) == 0 { + return statsMap, nil + } + + var injectionStats []struct { + ProjectID int + Count int64 + LastAt *time.Time + } + if err := db.Table("fault_injections fi"). + Select("tr.project_id, COUNT(*) as count, MAX(fi.updated_at) as last_at"). + Joins("JOIN tasks t ON fi.task_id = t.id"). + Joins("JOIN traces tr ON t.trace_id = tr.id"). + Where("tr.project_id IN (?)", projectIDs). + Group("tr.project_id"). + Scan(&injectionStats).Error; err != nil { + return nil, fmt.Errorf("failed to batch get injection statistics: %w", err) + } + for _, stat := range injectionStats { + statsMap[stat.ProjectID].InjectionCount = int(stat.Count) + statsMap[stat.ProjectID].LastInjectionAt = stat.LastAt + } + + var executionStats []struct { + ProjectID int + Count int64 + LastAt *time.Time + } + if err := db.Table("executions e"). + Select("tr.project_id, COUNT(*) as count, MAX(e.updated_at) as last_at"). + Joins("JOIN tasks t ON e.task_id = t.id"). + Joins("JOIN traces tr ON t.trace_id = tr.id"). + Where("tr.project_id IN (?)", projectIDs). + Group("tr.project_id"). + Scan(&executionStats).Error; err != nil { + return nil, fmt.Errorf("failed to batch get execution statistics: %w", err) + } + for _, stat := range executionStats { + statsMap[stat.ProjectID].ExecutionCount = int(stat.Count) + statsMap[stat.ProjectID].LastExecutionAt = stat.LastAt + } + + return statsMap, nil +} diff --git a/src/module/team/service.go b/src/module/team/service.go new file mode 100644 index 00000000..b661990b --- /dev/null +++ b/src/module/team/service.go @@ -0,0 +1,213 @@ +package teammodule + +import ( + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + projectmodule "aegis/module/project" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) CreateTeam(_ context.Context, req *CreateTeamReq, userID int) (*TeamResp, error) { + team := req.ConvertToTeam() + + err := s.repo.Transaction(func(tx *gorm.DB) error { + if err := s.repo.withDB(tx).createTeamWithCreator(team, userID); err != nil { + if errors.Is(err, consts.ErrAlreadyExists) { + return consts.ErrAlreadyExists + } + return err + } + return nil + }) + if err != nil { + return nil, err + } + + return NewTeamResp(team), nil +} + +func (s *Service) DeleteTeam(_ context.Context, teamID int) error { + rowsAffected, err := s.repo.DeleteTeam(teamID) + if err != nil { + return err + } + if rowsAffected == 0 { + return consts.ErrNotFound + } + return nil +} + +func (s *Service) GetTeamDetail(_ context.Context, teamID int) (*TeamDetailResp, error) { + team, userCount, projectCount, err := s.repo.loadTeamDetail(teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, consts.ErrNotFound + } + return nil, err + } + + resp := NewTeamDetailResp(team) + resp.UserCount = userCount + resp.ProjectCount = projectCount + + return resp, nil +} + +func (s *Service) ListTeams(_ context.Context, req *ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[TeamResp], error) { + limit, offset := req.ToGormParams() + teams, total, err := s.repo.listVisibleTeams(limit, offset, req, userID, isAdmin) + if err != nil { + return nil, err + } + + items := make([]TeamResp, len(teams)) + for i, team := range teams { + items[i] = *NewTeamResp(&team) + } + + return &dto.ListResp[TeamResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateTeam(_ context.Context, req *UpdateTeamReq, teamID int) (*TeamResp, error) { + team, err := s.repo.updateMutableTeam(teamID, func(team *model.Team) { + req.PatchTeamModel(team) + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, consts.ErrNotFound + } + return nil, err + } + + return NewTeamResp(team), nil +} + +func (s *Service) ListTeamProjects(_ context.Context, req *TeamProjectListReq, teamID int) (*dto.ListResp[TeamProjectItem], error) { + limit, offset := req.ToGormParams() + projects, statsMap, total, err := s.repo.listTeamProjectViews(teamID, limit, offset, req.IsPublic, req.Status) + if err != nil { + return nil, err + } + + items := make([]TeamProjectItem, 0, len(projects)) + for i := range projects { + items = append(items, *projectmodule.NewProjectResp(&projects[i], statsMap[projects[i].ID])) + } + + return &dto.ListResp[TeamProjectItem]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) AddMember(_ context.Context, req *AddTeamMemberReq, teamID int) error { + if err := s.repo.AddMember(teamID, req.Username, req.RoleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return consts.ErrNotFound + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("user or role not found") + } + if errors.Is(err, consts.ErrAlreadyExists) { + return consts.ErrAlreadyExists + } + return err + } + return nil +} + +func (s *Service) RemoveMember(_ context.Context, teamID, currentUserID, targetUserID int) error { + if targetUserID == currentUserID { + return fmt.Errorf("cannot remove yourself from the team") + } + + rowsAffected, err := s.repo.RemoveMember(teamID, targetUserID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { + return consts.ErrNotFound + } + return err + } + if rowsAffected == 0 { + return fmt.Errorf("user is not a member of this team") + } + return nil +} + +func (s *Service) UpdateMemberRole(_ context.Context, req *UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { + _ = currentUserID + + if err := s.repo.UpdateMemberRole(teamID, targetUserID, req.RoleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return consts.ErrNotFound + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("role not found") + } + return err + } + return nil +} + +func (s *Service) ListMembers(_ context.Context, req *ListTeamMemberReq, teamID int) (*dto.ListResp[TeamMemberResp], error) { + limit, offset := req.ToGormParams() + members, total, err := s.repo.ListTeamMembers(teamID, limit, offset) + if err != nil { + return nil, err + } + + return &dto.ListResp[TeamMemberResp]{ + Items: members, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) IsUserInTeam(userID, teamID int) (bool, error) { + ut, err := s.repo.loadUserTeamMembership(userID, teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return ut != nil, nil +} + +func (s *Service) IsUserTeamAdmin(userID, teamID int) (bool, error) { + ut, err := s.repo.loadUserTeamMembership(userID, teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return ut != nil && ut.Role != nil && ut.Role.Name == consts.RoleTeamAdmin.String(), nil +} + +func (s *Service) IsTeamPublic(teamID int) (bool, error) { + isPublic, err := s.repo.isTeamPublic(teamID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return isPublic, nil +} diff --git a/src/module/team/service_test.go b/src/module/team/service_test.go new file mode 100644 index 00000000..f7ed54fc --- /dev/null +++ b/src/module/team/service_test.go @@ -0,0 +1,63 @@ +package teammodule + +import ( + "regexp" + "testing" + "time" + + "aegis/consts" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func newTeamService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db)), mock, func() { + _ = sqlDB.Close() + } +} + +func TestTeamServiceListTeamsSuccess(t *testing.T) { + service, mock, cleanup := newTeamService(t) + defer cleanup() + + now := time.Now() + status := consts.CommonEnabled + + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `teams` WHERE status = ?")). + WithArgs(status). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `teams` WHERE status = ? LIMIT ?")). + WithArgs(status, 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "description", "is_public", "status", "created_at", "updated_at", + }).AddRow(1, "platform", "platform team", true, consts.CommonEnabled, now, now)) + + resp, err := service.ListTeams(t.Context(), &ListTeamReq{Status: &status}, 1, true) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "platform", resp.Items[0].Name) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestTeamServiceRemoveMemberSelfRejected(t *testing.T) { + service := NewService(nil) + + err := service.RemoveMember(t.Context(), 1, 7, 7) + + require.Error(t, err) + require.ErrorContains(t, err, "cannot remove yourself from the team") +} diff --git a/src/module/trace/api_types.go b/src/module/trace/api_types.go new file mode 100644 index 00000000..5fae70aa --- /dev/null +++ b/src/module/trace/api_types.go @@ -0,0 +1,161 @@ +package tracemodule + +import ( + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + taskmodule "aegis/module/task" + "aegis/utils" +) + +type GetTraceStreamReq struct { + LastID string `form:"last_id" binding:"omitempty"` +} + +func (req *GetTraceStreamReq) Validate() error { + if req.LastID == "" { + req.LastID = "0" + } + if req.LastID == "0" { + return nil + } + if len(req.LastID) < 3 || req.LastID[0] == '-' || req.LastID[len(req.LastID)-1] == '-' { + return fmt.Errorf("invalid last_id format: must be '0' or a valid stream ID (e.g., 1678886400000-0)") + } + dashCount := 0 + for _, ch := range req.LastID { + if ch == '-' { + dashCount++ + } + } + if dashCount != 1 { + return fmt.Errorf("invalid last_id format: must be '0' or a valid stream ID (e.g., 1678886400000-0)") + } + return nil +} + +type ListTraceFilters struct { + TraceType *consts.TraceType + GroupID string + ProjectID int + State *consts.TraceState + Status *consts.StatusType +} + +type ListTraceReq struct { + dto.PaginationReq + TraceType *consts.TraceType `form:"trace_type" binding:"omitempty"` + GroupID string `form:"group_id" binding:"omitempty"` + ProjectID int `form:"project_id" binding:"omitempty"` + State *consts.TraceState `form:"state" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` +} + +func (req *ListTraceReq) Validate() error { + if err := req.PaginationReq.Validate(); err != nil { + return err + } + if req.TraceType != nil { + if _, exists := consts.ValidTraceTypes[*req.TraceType]; !exists { + return fmt.Errorf("invalid trace type: %d", *req.TraceType) + } + } + if err := validateUUID(req.GroupID); err != nil { + return err + } + if req.ProjectID < 0 { + return fmt.Errorf("invalid project ID: %d", req.ProjectID) + } + if req.State != nil { + if _, exists := consts.ValidTraceStates[*req.State]; !exists { + return fmt.Errorf("invalid trace state: %d", *req.State) + } + } + return validateStatus(req.Status) +} + +func (req *ListTraceReq) ToFilterOptions() *ListTraceFilters { + return &ListTraceFilters{ + TraceType: req.TraceType, + GroupID: req.GroupID, + ProjectID: req.ProjectID, + State: req.State, + Status: req.Status, + } +} + +type TraceResp struct { + ID string `json:"id"` + Type string `json:"type"` + LastEvent string `json:"last_event"` + StartTime time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time,omitempty"` + GroupID string `json:"group_id"` + ProjectID int `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` + LeafNum int `json:"leaf_num"` + State string `json:"state"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func NewTraceResp(trace *model.Trace) *TraceResp { + resp := &TraceResp{ + ID: trace.ID, + Type: consts.GetTraceTypeName(trace.Type), + LastEvent: trace.LastEvent.String(), + StartTime: trace.StartTime, + EndTime: trace.EndTime, + GroupID: trace.GroupID, + ProjectID: trace.ProjectID, + LeafNum: trace.LeafNum, + State: consts.GetTraceStateName(trace.State), + Status: consts.GetStatusTypeName(trace.Status), + CreatedAt: trace.CreatedAt, + UpdatedAt: trace.UpdatedAt, + } + if trace.Project != nil { + resp.ProjectName = trace.Project.Name + } + return resp +} + +type TraceDetailResp struct { + TraceResp + + Tasks []taskmodule.TaskResp `json:"tasks"` +} + +func NewTraceDetailResp(trace *model.Trace) *TraceDetailResp { + resp := &TraceDetailResp{ + TraceResp: *NewTraceResp(trace), + Tasks: make([]taskmodule.TaskResp, 0, len(trace.Tasks)), + } + for i := range trace.Tasks { + resp.Tasks = append(resp.Tasks, *taskmodule.NewTaskResp(&trace.Tasks[i])) + } + return resp +} + +func validateUUID(id string) error { + if id == "" { + return nil + } + if !utils.IsValidUUID(id) { + return fmt.Errorf("invalid UUID format: %s", id) + } + return nil +} + +func validateStatus(status *consts.StatusType) error { + if status != nil { + if _, exists := consts.ValidStatuses[*status]; !exists { + return fmt.Errorf("invalid status value: %d", *status) + } + } + return nil +} diff --git a/src/handlers/v2/traces.go b/src/module/trace/handler.go similarity index 77% rename from src/handlers/v2/traces.go rename to src/module/trace/handler.go index 6aef65c6..679e020d 100644 --- a/src/handlers/v2/traces.go +++ b/src/module/trace/handler.go @@ -1,23 +1,31 @@ -package v2 +package tracemodule import ( - "aegis/consts" - "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" - "aegis/utils" + "aegis/httpx" "context" "errors" "fmt" "net/http" "time" + "aegis/consts" + "aegis/dto" + "aegis/utils" + "github.com/gin-contrib/sse" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // GetTrace handles getting a single trace by ID // // @Summary Get trace by ID @@ -27,23 +35,23 @@ import ( // @Produce json // @Security BearerAuth // @Param trace_id path string true "Trace ID" -// @Success 200 {object} dto.GenericResponse[dto.TraceDetailResp] "Trace retrieved successfully" +// @Success 200 {object} dto.GenericResponse[TraceDetailResp] "Trace retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid trace ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "Trace not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces/{trace_id} [get] -// @x-api-type {"sdk":"true"} -func GetTrace(c *gin.Context) { +// @x-api-type {} +func (h *Handler) GetTrace(c *gin.Context) { traceID := c.Param(consts.URLPathTraceID) if !utils.IsValidUUID(traceID) { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid trace ID") return } - resp, err := producer.GetTraceDetail(traceID) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetTrace(c.Request.Context(), traceID) + if httpx.HandleServiceError(c, err) { return } @@ -65,15 +73,15 @@ func GetTrace(c *gin.Context) { // @Param project_id query int false "Filter by project ID" // @Param state query consts.TraceState false "Filter by state" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.TraceResp]] "Traces retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[TraceResp]] "Traces retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces [get] -// @x-api-type {"sdk":"true"} -func ListTraces(c *gin.Context) { - var req dto.ListTraceReq +// @x-api-type {} +func (h *Handler) ListTraces(c *gin.Context) { + var req ListTraceReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -84,8 +92,8 @@ func ListTraces(c *gin.Context) { return } - resp, err := producer.ListTraces(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListTraces(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -108,16 +116,16 @@ func ListTraces(c *gin.Context) { // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces/{trace_id}/stream [get] -// @x-api-type {"sdk":"true"} +// @x-api-type {} // @x-request-type {"stream":"true"} -func GetTraceStream(c *gin.Context) { +func (h *Handler) GetTraceStream(c *gin.Context) { traceID := c.Param(consts.URLPathTraceID) if !utils.IsValidUUID(traceID) { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid trace ID") return } - var req dto.GetTraceStreamReq + var req GetTraceStreamReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format") return @@ -141,15 +149,14 @@ func GetTraceStream(c *gin.Context) { "stream_key": streamKey, }) - processor, err := producer.GetTraceStreamProcessor(ctx, traceID) + processor, err := h.service.GetTraceStreamProcessor(ctx, traceID) if err != nil { logEntry.Errorf("Failed to initialize stream processor: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize trace stream: %v", err)) return } - logEntry.Infof("Reading historical events from Stream") - historicalMessages, err := producer.ReadTraceStreamMessages(ctx, streamKey, req.LastID, 100, 0) + historicalMessages, err := h.service.ReadTraceStreamMessages(ctx, streamKey, req.LastID, 100, 0) if err != nil { logEntry.Errorf("failed to read historical events from redis: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, "failed to read event history") @@ -157,7 +164,7 @@ func GetTraceStream(c *gin.Context) { } if len(historicalMessages) > 0 { - lastID, completed, err := sendSSEEvents(c, processor, historicalMessages) + lastID, completed, err := sendTraceSSEEvents(c, processor, historicalMessages) if err != nil { logEntry.Errorf("failed to send historical stream events of ID %s: %v", req.LastID, err) dto.ErrorResponse(c, http.StatusInternalServerError, "failed to send stream events") @@ -165,25 +172,20 @@ func GetTraceStream(c *gin.Context) { } if completed { - logEntry.Info("Trace completed during historical events, closing stream connection") return } req.LastID = lastID } - logEntry.Infof("Switching to real-time event monitoring from ID: %s", req.LastID) for { select { case <-c.Done(): - logEntry.Info("Request context done") return - default: - newMessages, err := producer.ReadTraceStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) + newMessages, err := h.service.ReadTraceStreamMessages(ctx, streamKey, req.LastID, 10, time.Second) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - logEntry.Infof("Context done while reading stream: %v", err) return } @@ -193,11 +195,10 @@ func GetTraceStream(c *gin.Context) { } if len(newMessages) == 0 { - logEntry.Debug("No new messages, continuing") continue } - lastID, completed, err := sendSSEEvents(c, processor, newMessages) + lastID, completed, err := sendTraceSSEEvents(c, processor, newMessages) if err != nil { logEntry.Errorf("failed to send stream events of ID %s: %v", lastID, err) return @@ -205,18 +206,14 @@ func GetTraceStream(c *gin.Context) { req.LastID = lastID if completed { - logEntry.Info("Trace completed, closing stream connection") - time.Sleep(1 * time.Second) + time.Sleep(time.Second) return } - - logrus.Info("Sent SSE messages, lastID:", lastID) } } } -// sendSSEEvents processes and sends stream messages as SSE events -func sendSSEEvents(c *gin.Context, processor *producer.StreamProcessor, streams []redis.XStream) (string, bool, error) { +func sendTraceSSEEvents(c *gin.Context, processor *StreamProcessor, streams []redis.XStream) (string, bool, error) { if len(streams) == 0 || len(streams[0].Messages) == 0 { return "", false, fmt.Errorf("no messages to process") } diff --git a/src/module/trace/module.go b/src/module/trace/module.go new file mode 100644 index 00000000..8635116e --- /dev/null +++ b/src/module/trace/module.go @@ -0,0 +1,9 @@ +package tracemodule + +import "go.uber.org/fx" + +var Module = fx.Module("trace", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/trace/repository.go b/src/module/trace/repository.go new file mode 100644 index 00000000..06fb739f --- /dev/null +++ b/src/module/trace/repository.go @@ -0,0 +1,63 @@ +package tracemodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) GetTraceByID(traceID string) (*model.Trace, error) { + var trace model.Trace + if err := r.db.Model(&model.Trace{}). + Preload("Project"). + Preload("Tasks", func(db *gorm.DB) *gorm.DB { + return db.Order("level ASC, sequence ASC") + }). + Where("id = ? AND status != ?", traceID, consts.CommonDeleted). + First(&trace).Error; err != nil { + return nil, err + } + return &trace, nil +} + +func (r *Repository) ListTraces(limit, offset int, filterOptions *ListTraceFilters) ([]model.Trace, int64, error) { + var ( + traces []model.Trace + total int64 + ) + + query := r.db.Model(&model.Trace{}).Preload("Project") + if filterOptions.TraceType != nil { + query = query.Where("type = ?", *filterOptions.TraceType) + } + if filterOptions.GroupID != "" { + query = query.Where("group_id = ?", filterOptions.GroupID) + } + if filterOptions.ProjectID > 0 { + query = query.Where("project_id = ?", filterOptions.ProjectID) + } + if filterOptions.State != nil { + query = query.Where("state = ?", *filterOptions.State) + } + if filterOptions.Status != nil { + query = query.Where("status = ?", *filterOptions.Status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count traces: %w", err) + } + if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&traces).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list traces: %w", err) + } + return traces, total, nil +} diff --git a/src/module/trace/service.go b/src/module/trace/service.go new file mode 100644 index 00000000..9a01ef94 --- /dev/null +++ b/src/module/trace/service.go @@ -0,0 +1,89 @@ +package tracemodule + +import ( + "context" + "fmt" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + + "github.com/redis/go-redis/v9" +) + +type Service struct { + repo *Repository + redis *redisinfra.Gateway +} + +func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { + return &Service{repo: repo, redis: redis} +} + +func (s *Service) GetTrace(_ context.Context, traceID string) (*TraceDetailResp, error) { + trace, err := s.repo.GetTraceByID(traceID) + if err != nil { + return nil, fmt.Errorf("failed to get trace: %w", err) + } + return NewTraceDetailResp(trace), nil +} + +func (s *Service) ListTraces(_ context.Context, req *ListTraceReq) (*dto.ListResp[TraceResp], error) { + if req == nil { + return nil, fmt.Errorf("list traces request is nil") + } + limit, offset := req.ToGormParams() + filterOptions := req.ToFilterOptions() + traces, total, err := s.repo.ListTraces(limit, offset, filterOptions) + if err != nil { + return nil, fmt.Errorf("failed to list traces: %w", err) + } + items := make([]TraceResp, 0, len(traces)) + for i := range traces { + items = append(items, *NewTraceResp(&traces[i])) + } + return &dto.ListResp[TraceResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) GetTraceStreamProcessor(ctx context.Context, traceID string) (*StreamProcessor, error) { + trace, err := s.repo.GetTraceByID(traceID) + if err != nil { + return nil, fmt.Errorf("failed to fetch trace: %w", err) + } + + var algorithms []dto.ContainerVersionItem + if trace.Type == consts.TraceTypeFullPipeline && s.redis.CheckCachedField(ctx, consts.InjectionAlgorithmsKey, trace.GroupID) { + if err := s.redis.GetHashField(ctx, consts.InjectionAlgorithmsKey, trace.GroupID, &algorithms); err != nil { + return nil, fmt.Errorf("failed to get algorithms from Redis: %w", err) + } + } + + if len(algorithms) > 0 { + filtered := algorithms[:0] + for _, algorithm := range algorithms { + if algorithm.ContainerName != config.GetDetectorName() { + filtered = append(filtered, algorithm) + } + } + algorithms = filtered + } + + return NewStreamProcessor(algorithms), nil +} + +func (s *Service) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if lastID == "" { + lastID = "0" + } + + messages, err := s.redis.XRead(ctx, []string{streamKey, lastID}, count, block) + if err != nil { + return nil, fmt.Errorf("failed to read stream messages: %w", err) + } + return messages, nil +} diff --git a/src/module/trace/stream.go b/src/module/trace/stream.go new file mode 100644 index 00000000..b6b23114 --- /dev/null +++ b/src/module/trace/stream.go @@ -0,0 +1,175 @@ +package tracemodule + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + + "aegis/consts" + "aegis/dto" + + "github.com/redis/go-redis/v9" +) + +var payloadTypeRegistry = map[consts.EventType]reflect.Type{ + consts.EventAlgoRunStarted: reflect.TypeFor[dto.ExecutionInfo](), + consts.EventAlgoRunSucceed: reflect.TypeFor[dto.ExecutionResult](), + consts.EventAlgoRunFailed: reflect.TypeFor[dto.ExecutionResult](), + consts.EventDatapackBuildStarted: reflect.TypeFor[dto.DatapackInfo](), + consts.EventDatapackBuildSucceed: reflect.TypeFor[dto.DatapackResult](), + consts.EventDatapackBuildFailed: reflect.TypeFor[dto.DatapackResult](), + consts.EventJobSucceed: reflect.TypeFor[dto.JobMessage](), + consts.EventJobFailed: reflect.TypeFor[dto.JobMessage](), +} + +type StreamProcessor struct { + isCompleted bool + algorithmMap map[string]struct{} + finishedCount int +} + +func NewStreamProcessor(algorithms []dto.ContainerVersionItem) *StreamProcessor { + algorithmMap := make(map[string]struct{}, len(algorithms)) + for _, algorithm := range algorithms { + algorithmMap[algorithm.ContainerName] = struct{}{} + } + + return &StreamProcessor{ + isCompleted: false, + algorithmMap: algorithmMap, + finishedCount: 0, + } +} + +func (sp *StreamProcessor) IsCompleted() bool { + return sp.isCompleted +} + +func (sp *StreamProcessor) ProcessMessageForSSE(msg redis.XMessage) (string, *dto.TraceStreamEvent, error) { + streamEvent, err := parseStreamEvent(msg.ID, msg.Values) + if err != nil { + return "", nil, fmt.Errorf("failed to parse stream message value: %v", err) + } + + switch streamEvent.EventName { + case consts.EventImageBuildSucceed, consts.EventRestartPedestalFailed, consts.EventFaultInjectionFailed, consts.EventDatapackBuildFailed, consts.EventDatapackNoAnomaly, consts.EventDatapackNoDetectorData: + sp.isCompleted = true + case consts.EventDatapackResultCollection: + sp.isCompleted = len(sp.algorithmMap) == 0 + case consts.EventAlgoResultCollection, consts.EventAlgoRunFailed: + payload, ok := streamEvent.Payload.(*dto.ExecutionResult) + if !ok { + return "", nil, fmt.Errorf("invalid payload type for task status update event: %T", streamEvent.Payload) + } + + if len(sp.algorithmMap) == 0 { + sp.isCompleted = true + break + } + if _, exists := sp.algorithmMap[payload.Algorithm]; exists { + sp.finishedCount++ + if sp.finishedCount >= len(sp.algorithmMap) { + sp.isCompleted = true + } + } + } + + return msg.ID, streamEvent, nil +} + +func parseStreamEvent(id string, values map[string]any) (*dto.TraceStreamEvent, error) { + message := "missing or invalid key %s in redis stream message values" + + taskID, ok := values[consts.RdbEventTaskID].(string) + if !ok || taskID == "" { + return nil, fmt.Errorf(message, consts.RdbEventTaskID) + } + + timeStamp, err := strconv.Atoi(strings.Split(id, "-")[0]) + if err != nil { + return nil, err + } + + event := &dto.TraceStreamEvent{ + TimeStamp: timeStamp, + TaskID: taskID, + } + + if _, exists := values[consts.RdbEventTaskType]; exists { + taskTypeStr, ok := values[consts.RdbEventTaskType].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventTaskType) + } + taskTypePtr := consts.GetTaskTypeByName(taskTypeStr) + if taskTypePtr == nil { + return nil, fmt.Errorf("unknown task type name: %s", taskTypeStr) + } + event.TaskType = *taskTypePtr + } + + if _, exists := values[consts.RdbEventFn]; exists { + fnName, ok := values[consts.RdbEventFn].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventFn) + } + event.FnName = fnName + } + + if _, exists := values[consts.RdbEventFileName]; exists { + fileName, ok := values[consts.RdbEventFileName].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventTaskID) + } + event.FileName = fileName + } + + if _, exists := values[consts.RdbEventLine]; exists { + lineInt64, ok := values[consts.RdbEventLine].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventLine) + } + line, err := strconv.Atoi(lineInt64) + if err != nil { + return nil, fmt.Errorf("invalid line number: %w", err) + } + event.Line = line + } + + if _, exists := values[consts.RdbEventName]; exists { + eventName, ok := values[consts.RdbEventName].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventName) + } + event.EventName = consts.EventType(eventName) + } + + if _, exists := values[consts.RdbEventPayload]; exists && values[consts.RdbEventPayload] != nil { + payloadStr, ok := values[consts.RdbEventPayload].(string) + if !ok { + return nil, fmt.Errorf(message, consts.RdbEventPayload) + } + payload, err := parsePayloadByEventType(event.EventName, payloadStr) + if err != nil { + return nil, fmt.Errorf(message, consts.RdbEventPayload) + } + event.Payload = payload + } + + return event, nil +} + +func parsePayloadByEventType(eventType consts.EventType, payloadStr string) (any, error) { + payloadType, exists := payloadTypeRegistry[eventType] + if !exists { + return nil, nil + } + + valuePtr := reflect.New(payloadType) + if err := json.Unmarshal([]byte(payloadStr), valuePtr.Interface()); err != nil { + return nil, fmt.Errorf("failed to unmarshal payload for event %s: %w", eventType, err) + } + + return valuePtr.Interface(), nil +} diff --git a/src/dto/user.go b/src/module/user/api_types.go similarity index 56% rename from src/dto/user.go rename to src/module/user/api_types.go index 71612146..4352014d 100644 --- a/src/dto/user.go +++ b/src/module/user/api_types.go @@ -1,4 +1,4 @@ -package dto +package usermodule import ( "fmt" @@ -6,12 +6,12 @@ import ( "time" "aegis/consts" - "aegis/database" + "aegis/dto" + "aegis/model" + rbacmodule "aegis/module/rbac" ) -// ===================== User CRUD DTOs ===================== - -// CreateUserReq represents user creation request +// CreateUserReq represents user creation request. type CreateUserReq struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required,email"` @@ -37,9 +37,9 @@ func (req *CreateUserReq) Validate() error { return nil } -// ListUserReq represents user list query parameters +// ListUserReq represents user list query parameters. type ListUserReq struct { - PaginationReq + dto.PaginationReq IsActive *bool `form:"is_active"` Status *consts.StatusType `form:"status"` } @@ -48,89 +48,10 @@ func (req *ListUserReq) Validate() error { if err := req.PaginationReq.Validate(); err != nil { return err } - return validateStatusField(req.Status, false) -} - -type UserSearchReq struct { - AdvancedSearchReq[string] - - // User-specific filter shortcuts - UsernamePattern string `json:"username_pattern,omitempty"` // Username fuzzy match - EmailPattern string `json:"email_pattern,omitempty"` // Email fuzzy match - FullNamePattern string `json:"fullname_pattern,omitempty"` // Full name fuzzy match - RoleIDs []int `json:"role_ids,omitempty"` // Role ID filter - ProjectIDs []int `json:"project_ids,omitempty"` // Project ID filter - Departments []string `json:"departments,omitempty"` // Department filter - LastLoginRange *DateRange `json:"last_login_range,omitempty"` // Last login time range + return validateStatus(req.Status, false) } -// ConvertToSearchReq converts UserSearchReq to SearchReq with user-specific filters -func (usr *UserSearchReq) ConvertToSearchReq() *SearchReq[string] { - sr := usr.ConvertAdvancedToSearch() - - // Add user-specific filters - if usr.UsernamePattern != "" { - sr.AddFilter("username", OpLike, usr.UsernamePattern) - } - - if usr.EmailPattern != "" { - sr.AddFilter("email", OpLike, usr.EmailPattern) - } - - if usr.FullNamePattern != "" { - sr.AddFilter("full_name", OpLike, usr.FullNamePattern) - } - - if len(usr.RoleIDs) > 0 { - values := make([]string, len(usr.RoleIDs)) - for i, v := range usr.RoleIDs { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "role_id", - Operator: OpIn, - Values: values, - }) - } - - if len(usr.ProjectIDs) > 0 { - values := make([]string, len(usr.ProjectIDs)) - for i, v := range usr.ProjectIDs { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "project_id", - Operator: OpIn, - Values: values, - }) - } - - if len(usr.Departments) > 0 { - values := make([]string, len(usr.Departments)) - for i, v := range usr.Departments { - values[i] = fmt.Sprintf("%v", v) - } - sr.Filters = append(sr.Filters, SearchFilter{ - Field: "department", - Operator: OpIn, - Values: values, - }) - } - - if usr.LastLoginRange != nil { - if usr.LastLoginRange.From != nil && usr.LastLoginRange.To != nil { - sr.AddFilter("last_login_at", OpDateBetween, []interface{}{usr.LastLoginRange.From, usr.LastLoginRange.To}) - } else if usr.LastLoginRange.From != nil { - sr.AddFilter("last_login_at", OpDateAfter, usr.LastLoginRange.From) - } else if usr.LastLoginRange.To != nil { - sr.AddFilter("last_login_at", OpDateBefore, usr.LastLoginRange.To) - } - } - - return sr -} - -// UpdateUserReq represents user update request +// UpdateUserReq represents user update request. type UpdateUserReq struct { Email *string `json:"email,omitempty" binding:"omitempty,email"` FullName *string `json:"full_name,omitempty" binding:"omitempty"` @@ -141,10 +62,10 @@ type UpdateUserReq struct { } func (req *UpdateUserReq) Validate() error { - return validateStatusField(req.Status, true) + return validateStatus(req.Status, true) } -func (req *UpdateUserReq) PatchUserModel(target *database.User) { +func (req *UpdateUserReq) PatchUserModel(target *model.User) { if req.Email != nil { target.Email = *req.Email } @@ -165,7 +86,7 @@ func (req *UpdateUserReq) PatchUserModel(target *database.User) { } } -// UserResp represents basic user response +// UserResp represents basic user response. type UserResp struct { ID int `json:"id"` Username string `json:"username"` @@ -180,7 +101,7 @@ type UserResp struct { UpdatedAt time.Time `json:"updated_at"` } -func NewUserResp(user *database.User) *UserResp { +func NewUserResp(user *model.User) *UserResp { return &UserResp{ ID: user.ID, Username: user.Username, @@ -196,54 +117,38 @@ func NewUserResp(user *database.User) *UserResp { } } -// UserDetailResp represents detailed user response with roles and projects +// UserDetailResp represents detailed user response with roles and permissions. type UserDetailResp struct { UserResp - GlobalRoles []RoleResp `json:"global_roles,omitempty"` - Permissions []PermissionResp `json:"permissions,omitempty"` - ContainerRoles []UserContainerInfo `json:"container_roles,omitempty"` - DatasetRoles []UserDatasetInfo `json:"dataset_roles,omitempty"` - ProjectRoles []UserProjectInfo `json:"project_roles,omitempty"` + GlobalRoles []rbacmodule.RoleResp `json:"global_roles,omitempty"` + Permissions []rbacmodule.PermissionResp `json:"permissions,omitempty"` + ContainerRoles []UserContainerInfo `json:"container_roles,omitempty"` + DatasetRoles []UserDatasetInfo `json:"dataset_roles,omitempty"` + ProjectRoles []UserProjectInfo `json:"project_roles,omitempty"` } -func NewUserDetailResp(user *database.User) *UserDetailResp { +func NewUserDetailResp(user *model.User) *UserDetailResp { return &UserDetailResp{ UserResp: *NewUserResp(user), } } -type UserProfileResp struct { - ID int `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - FullName string `json:"full_name"` - Avatar string `json:"avatar,omitempty"` - Phone string `json:"phone,omitempty"` - LastLoginAt *time.Time `json:"last_login_at,omitempty"` - CreatedAt time.Time `json:"created_at"` - - ContainerRoles []UserContainerInfo `json:"container_roles,omitempty"` - DatasetRoles []UserDatasetInfo `json:"dataset_roles,omitempty"` - ProjectRoles []UserProjectInfo `json:"project_roles,omitempty"` -} - -func NewUserProfileResp(user *database.User) *UserProfileResp { - return &UserProfileResp{ - ID: user.ID, - Username: user.Username, - Email: user.Email, - FullName: user.FullName, - Avatar: user.Avatar, - Phone: user.Phone, - LastLoginAt: user.LastLoginAt, - CreatedAt: user.CreatedAt, +func validateStatus(statusPtr *consts.StatusType, isMutation bool) error { + if statusPtr == nil { + return nil + } + status := *statusPtr + if _, exists := consts.ValidStatuses[status]; !exists { + return fmt.Errorf("invalid status value: %d", status) } + if isMutation && status == consts.CommonDeleted { + return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) + } + return nil } -// ===================== User-Permission DTOs ===================== - -// AssignUserPermissionItem represents a single user-permission assignment item +// AssignUserPermissionItem represents a single user-permission assignment item. type AssignUserPermissionItem struct { PermissionID int `json:"permission_id" binding:"required,min=1"` GrantType *consts.GrantType `json:"grant_type" binding:"required"` @@ -266,8 +171,8 @@ func (item *AssignUserPermissionItem) Validate() error { return nil } -func (item *AssignUserPermissionItem) ConvertToUserPermission() *database.UserPermission { - return &database.UserPermission{ +func (item *AssignUserPermissionItem) ConvertToUserPermission() *model.UserPermission { + return &model.UserPermission{ PermissionID: item.PermissionID, GrantType: *item.GrantType, ExpiresAt: item.ExpiresAt, @@ -277,7 +182,7 @@ func (item *AssignUserPermissionItem) ConvertToUserPermission() *database.UserPe } } -// AssignUserPermissionReq represents direct user-permission assignment req +// AssignUserPermissionReq represents direct user-permission assignment request. type AssignUserPermissionReq struct { Items []AssignUserPermissionItem `json:"items" binding:"required"` } @@ -294,7 +199,7 @@ func (req *AssignUserPermissionReq) Validate() error { return nil } -// RemoveUserPermissionReq represents direct user-permission removal req +// RemoveUserPermissionReq represents direct user-permission removal request. type RemoveUserPermissionReq struct { PermissionIDs []int `json:"permission_ids" binding:"required"` } @@ -311,9 +216,7 @@ func (req *RemoveUserPermissionReq) Validate() error { return nil } -// ===================== User-Container Relationship DTOs ===================== - -// UserContainerResponse represents user-container relationship +// UserContainerInfo represents a user's role binding on a container. type UserContainerInfo struct { ContainerID int `json:"container_id"` ContainerName string `json:"container_name"` @@ -321,25 +224,21 @@ type UserContainerInfo struct { JoinedAt time.Time `json:"joined_at"` } -func NewUserContainerInfo(userContainer *database.UserContainer) *UserContainerInfo { +func NewUserContainerInfo(userContainer *model.UserContainer) *UserContainerInfo { resp := &UserContainerInfo{ ContainerID: userContainer.ContainerID, JoinedAt: userContainer.CreatedAt, } - if userContainer.Container != nil { resp.ContainerName = userContainer.Container.Name } if userContainer.Role != nil { resp.RoleName = userContainer.Role.Name } - return resp } -// ===================== User-Project Relationship DTOs ===================== - -// UserDatasetInfo represents user-dataset relationship +// UserDatasetInfo represents a user's role binding on a dataset. type UserDatasetInfo struct { DatasetID int `json:"dataset_id"` DatasetName string `json:"dataset_name"` @@ -347,25 +246,21 @@ type UserDatasetInfo struct { JoinedAt time.Time `json:"joined_at"` } -func NewUserDatasetInfo(userDataset *database.UserDataset) *UserDatasetInfo { +func NewUserDatasetInfo(userDataset *model.UserDataset) *UserDatasetInfo { resp := &UserDatasetInfo{ DatasetID: userDataset.DatasetID, JoinedAt: userDataset.CreatedAt, } - if userDataset.Dataset != nil { resp.DatasetName = userDataset.Dataset.Name } if userDataset.Role != nil { resp.RoleName = userDataset.Role.Name } - return resp } -// ===================== User-Project Relationship DTOs ===================== - -// UserProjectResponse represents user-project relationship +// UserProjectInfo represents a user's role binding on a project. type UserProjectInfo struct { ProjectID int `json:"project_id"` ProjectName string `json:"project_name"` @@ -373,18 +268,16 @@ type UserProjectInfo struct { JoinedAt time.Time `json:"joined_at"` } -func NewUserProjectInfo(userProject *database.UserProject) *UserProjectInfo { +func NewUserProjectInfo(userProject *model.UserProject) *UserProjectInfo { resp := &UserProjectInfo{ ProjectID: userProject.ProjectID, JoinedAt: userProject.CreatedAt, } - if userProject.Project != nil { resp.ProjectName = userProject.Project.Name } if userProject.Role != nil { resp.RoleName = userProject.Role.Name } - return resp } diff --git a/src/handlers/v2/users.go b/src/module/user/handler.go similarity index 64% rename from src/handlers/v2/users.go rename to src/module/user/handler.go index f7b18c84..9cee30cc 100644 --- a/src/handlers/v2/users.go +++ b/src/module/user/handler.go @@ -1,17 +1,24 @@ -package v2 +package usermodule import ( - "aegis/consts" + "aegis/httpx" "net/http" "strconv" + "aegis/consts" "aegis/dto" - "aegis/handlers" - producer "aegis/service/producer" "github.com/gin-gonic/gin" ) +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + // CreateUser handles user creation // // @Summary Create a new user @@ -21,15 +28,15 @@ import ( // @Accept json // @Produce json // @Security BearerAuth -// @Param request body dto.CreateUserReq true "User creation request" -// @Success 201 {object} dto.GenericResponse[dto.UserResp] "User created successfully" +// @Param request body CreateUserReq true "User creation request" +// @Success 201 {object} dto.GenericResponse[UserResp] "User created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 409 {object} dto.GenericResponse[any] "User already exists" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users [post] -// @x-api-type {"sdk":"true"} -func CreateUser(c *gin.Context) { - var req dto.CreateUserReq +// @x-api-type {"admin":"true"} +func (h *Handler) CreateUser(c *gin.Context) { + var req CreateUserReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -40,8 +47,8 @@ func CreateUser(c *gin.Context) { return } - resp, err := producer.CreateUser(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.CreateUser(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } @@ -64,20 +71,15 @@ func CreateUser(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{id} [delete] -// @x-api-type {"sdk":"true"} -func DeleteUser(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) DeleteUser(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - err = producer.DeleteUser(id) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.DeleteUser(c.Request.Context(), userID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "User deleted successfully", nil) } @@ -90,27 +92,23 @@ func DeleteUser(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "User ID" -// @Success 200 {object} dto.GenericResponse[dto.UserDetailResp] "User retrieved successfully" +// @Success 200 {object} dto.GenericResponse[UserDetailResp] "User retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "User not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{id}/detail [get] -// @x-api-type {"sdk":"true"} -func GetUserDetailV2(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) GetUserDetail(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - resp, err := producer.GetUserDetail(id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.GetUserDetail(c.Request.Context(), userID) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -128,30 +126,27 @@ func GetUserDetailV2(c *gin.Context) { // @Param email query string false "Filter by email" // @Param is_active query bool false "Filter by active status" // @Param status query consts.StatusType false "Filter by status" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[dto.UserResp]] "Users retrieved successfully" +// @Success 200 {object} dto.GenericResponse[dto.ListResp[UserResp]] "Users retrieved successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users [get] -// @x-api-type {"sdk":"true"} -func ListUsersV2(c *gin.Context) { - var req dto.ListUserReq +// @x-api-type {"admin":"true"} +func (h *Handler) ListUsers(c *gin.Context) { + var req ListUserReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - resp, err := producer.ListUsers(&req) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.ListUsers(c.Request.Context(), &req) + if httpx.HandleServiceError(c, err) { return } - dto.SuccessResponse(c, resp) } @@ -165,39 +160,32 @@ func ListUsersV2(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "User ID" -// @Param request body dto.UpdateUserReq true "User update request" -// @Success 202 {object} dto.GenericResponse[dto.UserResp] "User updated successfully" +// @Param request body UpdateUserReq true "User update request" +// @Success 202 {object} dto.GenericResponse[UserResp] "User updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID/request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 404 {object} dto.GenericResponse[any] "User not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{id} [patch] -// @x-api-type {"sdk":"true"} -func UpdateUser(c *gin.Context) { - idStr := c.Param(consts.URLPathID) - id, err := strconv.Atoi(idStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) UpdateUser(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - var req dto.UpdateUserReq + var req UpdateUserReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - - resp, err := producer.UpdateUser(&req, id) - if handlers.HandleServiceError(c, err) { + resp, err := h.service.UpdateUser(c.Request.Context(), &req, userID) + if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse[any](c, http.StatusAccepted, "User updated successfully", resp) } -// ===================== User-Role API ===================== - // AssignUserRole handles user-role assignment // // @Summary Assign global role to user @@ -215,26 +203,15 @@ func UpdateUser(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Resource not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/role/{role_id} [post] -func AssignUserRole(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) AssignRole(c *gin.Context) { + userID, roleID, ok := parseUserAndRoleIDs(c) + if !ok { return } - - roleIDStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - err = producer.AssignRoleToUser(userID, roleID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AssignRole(c.Request.Context(), userID, roleID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Role assigned successfully", nil) } @@ -255,63 +232,18 @@ func AssignUserRole(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or role not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/roles/{role_id} [delete] -func RemoveGlobalRole(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") - return - } - - roleIDStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") +// @x-api-type {"admin":"true"} +func (h *Handler) RemoveRole(c *gin.Context) { + userID, roleID, ok := parseUserAndRoleIDs(c) + if !ok { return } - - err = producer.RemoveRoleFromUser(userID, roleID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemoveRole(c.Request.Context(), userID, roleID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Role removed successfully", nil) } -// ListUsersFromRole handles listing users assigned to a role -// -// @Summary List users from role -// @Description Get list of users assigned to a specific role -// @Tags Roles -// @ID list_users_by_role -// @Produce json -// @Security BearerAuth -// @Param role_id path int true "Role ID" -// @Success 200 {object} dto.GenericResponse[[]dto.UserResp] "Users retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/roles/{role_id}/users [get] -func ListUsersFromRole(c *gin.Context) { - roleIdStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIdStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") - return - } - - userResps, err := producer.ListUsersFromRole(roleID) - if handlers.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, userResps) -} - -// ===================== User-Permission API ===================== - // AssignUserPermission handles direct user-permission assignment // // @Summary Assign permission to user @@ -322,7 +254,7 @@ func ListUsersFromRole(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param user_id path int true "User ID" -// @Param request body dto.AssignUserPermissionReq true "User permission assignment request" +// @Param request body AssignUserPermissionReq true "User permission assignment request" // @Success 200 {object} dto.GenericResponse[any] "Permission assigned successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -330,30 +262,24 @@ func ListUsersFromRole(c *gin.Context) { // @Failuer 404 {object} dto.GenericResponse[any] "Resource not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/permissions/assign [post] -func AssignUserPermission(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) AssignPermissions(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - var req dto.AssignUserPermissionReq + var req AssignUserPermissionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - err = producer.BatchAssignUserPermissions(&req, userID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AssignPermissions(c.Request.Context(), &req, userID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Permissions assigned successfully", nil) } @@ -367,7 +293,7 @@ func AssignUserPermission(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param user_id path int true "User ID" -// @Param request body dto.RemoveUserPermissionReq true "User permission removal request" +// @Param request body RemoveUserPermissionReq true "User permission removal request" // @Success 200 {object} dto.GenericResponse[any] "Permission removed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user or permission ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -375,35 +301,27 @@ func AssignUserPermission(c *gin.Context) { // @Failuer 404 {object} dto.GenericResponse[any] "Resource not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/permissions/remove [post] -func RemoveUserPermission(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) RemovePermissions(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - var req dto.RemoveUserPermissionReq + var req RemoveUserPermissionReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return } - if err := req.Validate(); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) return } - - err = producer.BatchRemoveUserPermissions(&req, userID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemovePermissions(c.Request.Context(), &req, userID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Permissions assigned successfully", nil) } -// ===================== User-Container API ===================== - // AssignUserContainer handles user-container assignment // // @Summary Assign user to container @@ -422,33 +340,23 @@ func RemoveUserPermission(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or container or role not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/containers/{container_id}/roles/{role_id} [post] -func AssignUserContainer(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) AssignContainer(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil || containerID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + containerID, ok := parsePathID(c, consts.URLPathContainerID, "Invalid container ID") + if !ok { return } - - roleIDStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") + roleID, ok := parsePathID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { return } - - err = producer.AssignContainerToUser(userID, containerID, roleID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AssignContainer(c.Request.Context(), userID, containerID, roleID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "User assigned to container successfully", nil) } @@ -469,31 +377,22 @@ func AssignUserContainer(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/containers/{container_id} [delete] -func RemoveUserContainer(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) RemoveContainer(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - containerIDStr := c.Param(consts.URLPathContainerID) - containerID, err := strconv.Atoi(containerIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid container ID") + containerID, ok := parsePathID(c, consts.URLPathContainerID, "Invalid container ID") + if !ok { return } - - err = producer.RemoveContainerFromUser(userID, containerID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemoveContainer(c.Request.Context(), userID, containerID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "User removed from container successfully", nil) } -// ===================== User-Dataset API ===================== - // AssignUserDataset handles user-dataset assignment // // @Summary Assign user to dataset @@ -512,33 +411,23 @@ func RemoveUserContainer(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or dataset or role not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/datasets/{dataset_id}/roles/{role_id} [post] -func AssignUserDataset(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) AssignDataset(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil || datasetID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") + datasetID, ok := parsePathID(c, consts.URLPathDatasetID, "Invalid dataset ID") + if !ok { return } - - roleIDStr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDStr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") + roleID, ok := parsePathID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { return } - - err = producer.AssignDatasetToUser(userID, datasetID, roleID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AssignDataset(c.Request.Context(), userID, datasetID, roleID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "User assigned to dataset successfully", nil) } @@ -559,31 +448,22 @@ func AssignUserDataset(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/datasets/{dataset_id} [delete] -func RemoveUserDataset(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) RemoveDataset(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - datasetIDStr := c.Param(consts.URLPathDatasetID) - datasetID, err := strconv.Atoi(datasetIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid dataset ID") + datasetID, ok := parsePathID(c, consts.URLPathDatasetID, "Invalid dataset ID") + if !ok { return } - - err = producer.RemoveDatasetFromUser(userID, datasetID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemoveDataset(c.Request.Context(), userID, datasetID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "User removed from dataset successfully", nil) } -// ===================== User-Project API ===================== - // AssignUserToProject handles user-project assignment // // @Summary Assign user to project @@ -602,33 +482,23 @@ func RemoveUserDataset(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or project or role not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/projects/{project_id}/roles/{role_id} [post] -func AssignUserProject(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) AssignProject(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil || projectID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") + projectID, ok := parsePathID(c, consts.URLPathProjectID, "Invalid project ID") + if !ok { return } - - roleIDstr := c.Param(consts.URLPathRoleID) - roleID, err := strconv.Atoi(roleIDstr) - if err != nil || roleID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid role ID") + roleID, ok := parsePathID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { return } - - err = producer.AssignProjectToUser(userID, projectID, roleID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.AssignProject(c.Request.Context(), userID, projectID, roleID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "User assigned to project successfully", nil) } @@ -649,25 +519,44 @@ func AssignUserProject(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "User or project not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{user_id}/projects/{project_id} [delete] -func RemoveUserProject(c *gin.Context) { - userIDStr := c.Param(consts.URLPathUserID) - userID, err := strconv.Atoi(userIDStr) - if err != nil || userID <= 0 { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid user ID") +// @x-api-type {"admin":"true"} +func (h *Handler) RemoveProject(c *gin.Context) { + userID, ok := parseUserID(c) + if !ok { return } - - projectIDStr := c.Param(consts.URLPathProjectID) - projectID, err := strconv.Atoi(projectIDStr) - if err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid project ID") + projectID, ok := parsePathID(c, consts.URLPathProjectID, "Invalid project ID") + if !ok { return } - - err = producer.RemoveProjectFromUser(userID, projectID) - if handlers.HandleServiceError(c, err) { + if httpx.HandleServiceError(c, h.service.RemoveProject(c.Request.Context(), userID, projectID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "User removed from project successfully", nil) } + +func parseUserID(c *gin.Context) (int, bool) { + return parsePathID(c, consts.URLPathUserID, "Invalid user ID") +} + +func parseUserAndRoleIDs(c *gin.Context) (int, int, bool) { + userID, ok := parseUserID(c) + if !ok { + return 0, 0, false + } + roleID, ok := parsePathID(c, consts.URLPathRoleID, "Invalid role ID") + if !ok { + return 0, 0, false + } + return userID, roleID, true +} + +func parsePathID(c *gin.Context, name, message string) (int, bool) { + value := c.Param(name) + id, err := strconv.Atoi(value) + if err != nil || id <= 0 { + dto.ErrorResponse(c, http.StatusBadRequest, message) + return 0, false + } + return id, true +} diff --git a/src/module/user/module.go b/src/module/user/module.go new file mode 100644 index 00000000..30c5f35e --- /dev/null +++ b/src/module/user/module.go @@ -0,0 +1,9 @@ +package usermodule + +import "go.uber.org/fx" + +var Module = fx.Module("user", + fx.Provide(NewRepository), + fx.Provide(NewService), + fx.Provide(NewHandler), +) diff --git a/src/module/user/repository.go b/src/module/user/repository.go new file mode 100644 index 00000000..01a6ddac --- /dev/null +++ b/src/module/user/repository.go @@ -0,0 +1,403 @@ +package usermodule + +import ( + "aegis/consts" + "aegis/model" + "fmt" + + "gorm.io/gorm" +) + +type Repository struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) withDB(db *gorm.DB) *Repository { + return &Repository{db: db} +} + +func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { + return r.db.Transaction(fn) +} + +func (r *Repository) ensureUserUnique(username, email string) error { + var existingByUsername model.User + if err := r.db.Where("username = ?", username).First(&existingByUsername).Error; err == nil { + return fmt.Errorf("%w: username %s already exists", consts.ErrAlreadyExists, username) + } + + var existingByEmail model.User + if err := r.db.Where("email = ?", email).First(&existingByEmail).Error; err == nil { + return fmt.Errorf("%w: email %s already exists", consts.ErrAlreadyExists, email) + } + return nil +} + +func (r *Repository) createUserIfUnique(user *model.User) error { + if err := r.ensureUserUnique(user.Username, user.Email); err != nil { + return err + } + if err := r.db.Omit("active_username").Create(user).Error; err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + return nil +} + +func (r *Repository) getUserDetailBase(userID int) (*model.User, error) { + var user model.User + if err := r.db.Where("id = ?", userID).First(&user).Error; err != nil { + return nil, fmt.Errorf("failed to find user with id %d: %w", userID, err) + } + return &user, nil +} + +func (r *Repository) DeleteUserCascade(userID int) (int64, error) { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return 0, err + } + + if err := r.db.Model(&model.UserContainer{}). + Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove containers from user: %w", err) + } + if err := r.db.Model(&model.UserDataset{}). + Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove datasets from user: %w", err) + } + if err := r.db.Model(&model.UserProject{}). + Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Update("status", consts.CommonDeleted).Error; err != nil { + return 0, fmt.Errorf("failed to remove projects from user: %w", err) + } + if err := r.db.Where("user_id = ?", userID).Delete(&model.UserPermission{}).Error; err != nil { + return 0, fmt.Errorf("failed to remove permissions from user: %w", err) + } + if err := r.db.Where("user_id = ?", userID).Delete(&model.UserRole{}).Error; err != nil { + return 0, fmt.Errorf("failed to remove roles from user: %w", err) + } + + result := r.db.Model(&model.User{}). + Where("id = ? AND status != ?", userID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete user %d: %w", userID, result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) listUserViews(limit, offset int, isActive *bool, status *consts.StatusType) ([]model.User, int64, error) { + var users []model.User + var total int64 + + query := r.db.Model(&model.User{}).Where("status != ?", consts.CommonDeleted) + if status != nil { + query = query.Where("status = ?", *status) + } + if isActive != nil { + query = query.Where("is_active = ?", *isActive) + } + + if err := query.Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count users: %w", err) + } + if err := query.Limit(limit).Offset(offset).Find(&users).Error; err != nil { + return nil, 0, fmt.Errorf("failed to list users: %w", err) + } + return users, total, nil +} + +func (r *Repository) updateMutableUser(userID int, patch func(*model.User)) (*model.User, error) { + var user model.User + if err := r.db.Where("id = ?", userID).First(&user).Error; err != nil { + return nil, fmt.Errorf("failed to find user with id %d: %w", userID, err) + } + patch(&user) + if err := r.db.Omit("active_username").Save(&user).Error; err != nil { + return nil, fmt.Errorf("failed to update user: %w", err) + } + return &user, nil +} + +func (r *Repository) loadUserDetailRelations(userID int) ([]model.Role, []model.Permission, []model.UserContainer, []model.UserDataset, []model.UserProject, error) { + var roles []model.Role + if err := r.db.Table("roles"). + Joins("JOIN user_roles ur ON ur.role_id = roles.id"). + Where("ur.user_id = ? AND roles.status = ?", userID, consts.CommonEnabled). + Find(&roles).Error; err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to list roles by user id: %w", err) + } + + var permissions []model.Permission + if err := r.db.Table("permissions"). + Joins("JOIN user_permissions up ON up.permission_id = permissions.id"). + Where("up.user_id = ? AND permissions.status = ?", userID, consts.CommonEnabled). + Find(&permissions).Error; err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to list permissions by user id: %w", err) + } + + var userContainers []model.UserContainer + if err := r.db.Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Find(&userContainers).Error; err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to list user containers: %w", err) + } + + var userDatasets []model.UserDataset + if err := r.db.Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Find(&userDatasets).Error; err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to list user datasets: %w", err) + } + + var userProjects []model.UserProject + if err := r.db.Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). + Find(&userProjects).Error; err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("failed to list user projects: %w", err) + } + + return roles, permissions, userContainers, userDatasets, userProjects, nil +} + +func (r *Repository) AssignGlobalRole(userID, roleID int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Role{}, roleID, "role"); err != nil { + return err + } + if err := r.db.Create(&model.UserRole{UserID: userID, RoleID: roleID}).Error; err != nil { + return fmt.Errorf("failed to create user-role association: %w", err) + } + return nil +} + +func (r *Repository) RemoveGlobalRole(userID, roleID int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Role{}, roleID, "role"); err != nil { + return err + } + if err := r.db.Where("user_id = ? AND role_id = ?", userID, roleID). + Delete(&model.UserRole{}).Error; err != nil { + return fmt.Errorf("failed to delete user-role association: %w", err) + } + return nil +} + +func (r *Repository) BuildUserPermissions(userID int, items []AssignUserPermissionItem) ([]model.UserPermission, error) { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return nil, err + } + + permissionIDs := make([]int, 0, len(items)) + for _, item := range items { + permissionIDs = append(permissionIDs, item.PermissionID) + } + permissions, err := r.listPermissionsByIDs(permissionIDs) + if err != nil { + return nil, fmt.Errorf("failed to list permissions by ids: %w", err) + } + permissionMap := make(map[int]struct{}, len(permissions)) + for _, permission := range permissions { + permissionMap[permission.ID] = struct{}{} + } + + userPermissions := make([]model.UserPermission, 0, len(items)) + for _, item := range items { + if _, exists := permissionMap[item.PermissionID]; !exists { + return nil, fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, item.PermissionID) + } + if item.ContainerID != nil { + if err := r.ensureActiveRecordExists(&model.Container{}, *item.ContainerID, "container"); err != nil { + return nil, fmt.Errorf("%w: container id %d not found", consts.ErrNotFound, *item.ContainerID) + } + } + if item.DatasetID != nil { + if err := r.ensureActiveRecordExists(&model.Dataset{}, *item.DatasetID, "dataset"); err != nil { + return nil, fmt.Errorf("%w: dataset id %d not found", consts.ErrNotFound, *item.DatasetID) + } + } + if item.ProjectID != nil { + if err := r.ensureActiveRecordExists(&model.Project{}, *item.ProjectID, "project"); err != nil { + return nil, fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, *item.ProjectID) + } + } + + userPermission := item.ConvertToUserPermission() + userPermission.UserID = userID + userPermissions = append(userPermissions, *userPermission) + } + return userPermissions, nil +} + +func (r *Repository) BatchCreateUserPermissions(userPermissions []model.UserPermission) error { + if len(userPermissions) == 0 { + return nil + } + if err := r.db.Create(&userPermissions).Error; err != nil { + return fmt.Errorf("failed to batch create user permissions: %w", err) + } + return nil +} + +func (r *Repository) BatchDeleteUserPermissions(userID int, permissionIDs []int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + + permissions, err := r.listPermissionsByIDs(permissionIDs) + if err != nil { + return fmt.Errorf("failed to list permissions by ids: %w", err) + } + permissionMap := make(map[int]struct{}, len(permissions)) + for _, permission := range permissions { + permissionMap[permission.ID] = struct{}{} + } + for _, permissionID := range permissionIDs { + if _, exists := permissionMap[permissionID]; !exists { + return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, permissionID) + } + } + + if err := r.db.Where("user_id = ? AND permission_id IN (?)", userID, permissionIDs). + Delete(&model.UserPermission{}).Error; err != nil { + return fmt.Errorf("failed to batch delete user permissions: %w", err) + } + return nil +} + +func (r *Repository) AssignContainerRole(userID, containerID, roleID int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Container{}, containerID, "container"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Role{}, roleID, "role"); err != nil { + return err + } + + if err := r.db.Create(&model.UserContainer{ + UserID: userID, + ContainerID: containerID, + RoleID: roleID, + }).Error; err != nil { + return fmt.Errorf("failed to create user-container association: %w", err) + } + return nil +} + +func (r *Repository) RemoveContainerRole(userID, containerID int) (int64, error) { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return 0, err + } + if err := r.ensureActiveRecordExists(&model.Container{}, containerID, "container"); err != nil { + return 0, err + } + result := r.db.Model(&model.UserContainer{}). + Where("user_id = ? AND container_id = ? AND status != ?", userID, containerID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete user-container association: %w", result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) AssignDatasetRole(userID, datasetID, roleID int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Dataset{}, datasetID, "dataset"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Role{}, roleID, "role"); err != nil { + return err + } + + if err := r.db.Create(&model.UserDataset{ + UserID: userID, + DatasetID: datasetID, + RoleID: roleID, + }).Error; err != nil { + return fmt.Errorf("failed to create user-dataset association: %w", err) + } + return nil +} + +func (r *Repository) RemoveDatasetRole(userID, datasetID int) (int64, error) { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return 0, err + } + if err := r.ensureActiveRecordExists(&model.Dataset{}, datasetID, "dataset"); err != nil { + return 0, err + } + result := r.db.Model(&model.UserDataset{}). + Where("user_id = ? AND dataset_id = ? AND status != ?", userID, datasetID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete user-dataset association: %w", result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) AssignProjectRole(userID, projectID, roleID int) error { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Project{}, projectID, "project"); err != nil { + return err + } + if err := r.ensureActiveRecordExists(&model.Role{}, roleID, "role"); err != nil { + return err + } + + if err := r.db.Create(&model.UserProject{ + UserID: userID, + ProjectID: projectID, + RoleID: roleID, + }).Error; err != nil { + return fmt.Errorf("failed to create user-project association: %w", err) + } + return nil +} + +func (r *Repository) RemoveProjectRole(userID, projectID int) (int64, error) { + if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + return 0, err + } + if err := r.ensureActiveRecordExists(&model.Project{}, projectID, "project"); err != nil { + return 0, err + } + result := r.db.Model(&model.UserProject{}). + Where("user_id = ? AND project_id = ? AND status != ?", userID, projectID, consts.CommonDeleted). + Update("status", consts.CommonDeleted) + if result.Error != nil { + return 0, fmt.Errorf("failed to delete user-project association: %w", result.Error) + } + return result.RowsAffected, nil +} + +func (r *Repository) ensureActiveRecordExists(model any, id int, entity string) error { + if err := r.db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(model).Error; err != nil { + return fmt.Errorf("failed to find %s with id %d: %w", entity, id, err) + } + return nil +} + +func (r *Repository) listPermissionsByIDs(permissionIDs []int) ([]model.Permission, error) { + if len(permissionIDs) == 0 { + return []model.Permission{}, nil + } + + var permissions []model.Permission + if err := r.db.Where("id IN (?) AND status = ?", permissionIDs, consts.CommonEnabled). + Find(&permissions).Error; err != nil { + return nil, fmt.Errorf("failed to query permissions: %w", err) + } + return permissions, nil +} diff --git a/src/module/user/service.go b/src/module/user/service.go new file mode 100644 index 00000000..dd881044 --- /dev/null +++ b/src/module/user/service.go @@ -0,0 +1,330 @@ +package usermodule + +import ( + "context" + "errors" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/model" + rbacmodule "aegis/module/rbac" + + "gorm.io/gorm" +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) CreateUser(_ context.Context, req *CreateUserReq) (*UserResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + FullName: req.FullName, + Phone: req.Phone, + Avatar: req.Avatar, + Status: consts.CommonEnabled, + IsActive: true, + } + + if err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + return repo.createUserIfUnique(user) + }); err != nil { + return nil, err + } + + return NewUserResp(user), nil +} + +func (s *Service) DeleteUser(_ context.Context, userID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return fmt.Errorf("failed to get user: %w", err) + } + + rows, err := repo.DeleteUserCascade(userID) + if err != nil { + return err + } + if rows == 0 { + return fmt.Errorf("%w: user id %d not found", consts.ErrNotFound, userID) + } + return nil + }) +} + +func (s *Service) GetUserDetail(_ context.Context, userID int) (*UserDetailResp, error) { + user, err := s.repo.getUserDetailBase(userID) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, fmt.Errorf("%w: user with ID %d not found", consts.ErrNotFound, userID) + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + resp := NewUserDetailResp(user) + + globalRoles, permissions, userContainers, userDatasets, userProjects, err := s.repo.loadUserDetailRelations(user.ID) + if err != nil { + return nil, fmt.Errorf("failed to get user detail relations: %w", err) + } + resp.GlobalRoles = make([]rbacmodule.RoleResp, len(globalRoles)) + for i, role := range globalRoles { + resp.GlobalRoles[i] = *rbacmodule.NewRoleResp(&role) + } + + resp.Permissions = make([]rbacmodule.PermissionResp, len(permissions)) + for i, permission := range permissions { + resp.Permissions[i] = *rbacmodule.NewPermissionResp(&permission) + } + + containerRoles, datasetRoles, projectRoles := buildUserResourceRoles(userContainers, userDatasets, userProjects) + resp.ContainerRoles = containerRoles + resp.DatasetRoles = datasetRoles + resp.ProjectRoles = projectRoles + + return resp, nil +} + +func (s *Service) ListUsers(_ context.Context, req *ListUserReq) (*dto.ListResp[UserResp], error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + limit, offset := req.ToGormParams() + users, total, err := s.repo.listUserViews(limit, offset, req.IsActive, req.Status) + if err != nil { + return nil, fmt.Errorf("failed to list users: %w", err) + } + + items := make([]UserResp, len(users)) + for i, user := range users { + items[i] = *NewUserResp(&user) + } + + return &dto.ListResp[UserResp]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +func (s *Service) UpdateUser(_ context.Context, req *UpdateUserReq, userID int) (*UserResp, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("validation failed: %w", err) + } + + var updatedUser *model.User + err := s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + user, err := repo.updateMutableUser(userID, func(existingUser *model.User) { + req.PatchUserModel(existingUser) + }) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return fmt.Errorf("failed to get user: %w", err) + } + + updatedUser = user + return nil + }) + if err != nil { + return nil, err + } + + return NewUserResp(updatedUser), nil +} + +func (s *Service) AssignRole(_ context.Context, userID, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.AssignGlobalRole(userID, roleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + if userErr := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); userErr != nil { + return fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return fmt.Errorf("%w: role not found", consts.ErrNotFound) + } + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user already has this role", consts.ErrAlreadyExists) + } + return err + } + return nil + }) +} + +func (s *Service) RemoveRole(_ context.Context, userID, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.RemoveGlobalRole(userID, roleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + if userErr := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); userErr != nil { + return fmt.Errorf("%w: user not found", consts.ErrNotFound) + } + return fmt.Errorf("%w: role not found", consts.ErrNotFound) + } + return err + } + return nil + }) +} + +func (s *Service) AssignPermissions(_ context.Context, req *AssignUserPermissionReq, userID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + userPermissions, err := repo.BuildUserPermissions(userID, req.Items) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: failed to resolve permission assignment targets", consts.ErrNotFound) + } + return err + } + + if err := repo.BatchCreateUserPermissions(userPermissions); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user already has one or more of these permissions", consts.ErrAlreadyExists) + } + return fmt.Errorf("failed to assign permissions to user: %w", err) + } + return nil + }) +} + +func (s *Service) RemovePermissions(_ context.Context, req *RemoveUserPermissionReq, userID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.BatchDeleteUserPermissions(userID, req.PermissionIDs); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: failed to resolve user or permissions", consts.ErrNotFound) + } + return fmt.Errorf("failed to remove permissions from user: %w", err) + } + return nil + }) +} + +func (s *Service) AssignContainer(_ context.Context, userID, containerID, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.AssignContainerRole(userID, containerID, roleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: user/container/role not found", consts.ErrNotFound) + } + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user already assigned to this container", consts.ErrAlreadyExists) + } + return err + } + return nil + }) +} + +func (s *Service) RemoveContainer(_ context.Context, userID, containerID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + rows, err := repo.RemoveContainerRole(userID, containerID) + if err != nil { + return fmt.Errorf("failed to remove user from container: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: user is not assigned to this container", consts.ErrNotFound) + } + return nil + }) +} + +func (s *Service) AssignDataset(_ context.Context, userID, datasetID, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.AssignDatasetRole(userID, datasetID, roleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: user/dataset/role not found", consts.ErrNotFound) + } + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user already assigned to this dataset", consts.ErrAlreadyExists) + } + return err + } + return nil + }) +} + +func (s *Service) RemoveDataset(_ context.Context, userID, datasetID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + rows, err := repo.RemoveDatasetRole(userID, datasetID) + if err != nil { + return fmt.Errorf("failed to remove user from dataset: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: user is not assigned to this dataset", consts.ErrNotFound) + } + return nil + }) +} + +func (s *Service) AssignProject(_ context.Context, userID, projectID, roleID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + if err := repo.AssignProjectRole(userID, projectID, roleID); err != nil { + if errors.Is(err, consts.ErrNotFound) { + return fmt.Errorf("%w: user/project/role not found", consts.ErrNotFound) + } + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user already assigned to this project", consts.ErrAlreadyExists) + } + return err + } + return nil + }) +} + +func (s *Service) RemoveProject(_ context.Context, userID, projectID int) error { + return s.repo.Transaction(func(tx *gorm.DB) error { + repo := s.repo.withDB(tx) + rows, err := repo.RemoveProjectRole(userID, projectID) + if err != nil { + return fmt.Errorf("failed to remove user from project: %w", err) + } + if rows == 0 { + return fmt.Errorf("%w: user is not assigned to this project", consts.ErrNotFound) + } + return nil + }) +} + +func buildUserResourceRoles(userContainers []model.UserContainer, userDatasets []model.UserDataset, userProjects []model.UserProject) ([]UserContainerInfo, []UserDatasetInfo, []UserProjectInfo) { + containerRoles := make([]UserContainerInfo, 0, len(userContainers)) + for _, uc := range userContainers { + containerRoles = append(containerRoles, *NewUserContainerInfo(&uc)) + } + + datasetRoles := make([]UserDatasetInfo, 0, len(userDatasets)) + for _, ud := range userDatasets { + datasetRoles = append(datasetRoles, *NewUserDatasetInfo(&ud)) + } + + projectRoles := make([]UserProjectInfo, 0, len(userProjects)) + for _, up := range userProjects { + projectRoles = append(projectRoles, *NewUserProjectInfo(&up)) + } + + return containerRoles, datasetRoles, projectRoles +} diff --git a/src/module/user/service_test.go b/src/module/user/service_test.go new file mode 100644 index 00000000..44f7bde6 --- /dev/null +++ b/src/module/user/service_test.go @@ -0,0 +1,197 @@ +package usermodule + +import ( + "database/sql/driver" + "regexp" + "testing" + "time" + + "aegis/consts" + "aegis/utils" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +type passwordHashMatcher struct { + plain string +} + +func (m passwordHashMatcher) Match(v driver.Value) bool { + hash, ok := v.(string) + if !ok { + return false + } + return utils.VerifyPassword(m.plain, hash) +} + +func newUserTestService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New() + require.NoError(t, err) + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + require.NoError(t, err) + + return NewService(NewRepository(db)), mock, func() { + _ = sqlDB.Close() + } +} + +func TestServiceCreateUserValidationError(t *testing.T) { + service := NewService(nil) + + _, err := service.CreateUser(t.Context(), &CreateUserReq{ + Username: "demo", + Email: "demo@example.com", + Password: "short", + }) + + require.Error(t, err) + require.ErrorContains(t, err, "validation failed") + require.ErrorContains(t, err, "password must be at least 8 characters") +} + +func TestServiceListUsersSuccess(t *testing.T) { + service, mock, cleanup := newUserTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `users` WHERE status != ?")). + WithArgs(consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE status != ? LIMIT ?")). + WithArgs(consts.CommonDeleted, 20). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(1, "demo", "demo@example.com", "hashed", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + + resp, err := service.ListUsers(t.Context(), &ListUserReq{}) + + require.NoError(t, err) + require.Len(t, resp.Items, 1) + require.Equal(t, "demo", resp.Items[0].Username) + require.Equal(t, 1, resp.Pagination.Page) + require.Equal(t, int(consts.PageSizeMedium), resp.Pagination.Size) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceCreateUserSuccess(t *testing.T) { + service, mock, cleanup := newUserTestService(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE username = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs("demo", 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE email = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs("demo@example.com", 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `users` (`username`,`email`,`password`,`full_name`,`avatar`,`phone`,`last_login_at`,`is_active`,`status`,`created_at`,`updated_at`) VALUES (?,?,?,?,?,?,?,?,?,?,?)")). + WithArgs("demo", "demo@example.com", passwordHashMatcher{plain: "password123"}, "Demo User", "", "", nil, true, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(5, 1)) + mock.ExpectCommit() + + resp, err := service.CreateUser(t.Context(), &CreateUserReq{ + Username: "demo", + Email: "demo@example.com", + Password: "password123", + FullName: "Demo User", + }) + + require.NoError(t, err) + require.Equal(t, 5, resp.ID) + require.Equal(t, "demo", resp.Username) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceGetUserDetailSuccess(t *testing.T) { + service, mock, cleanup := newUserTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs(1, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(1, "demo", "demo@example.com", "hashed", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT `roles`.`id`,`roles`.`name`,`roles`.`display_name`,`roles`.`description`,`roles`.`is_system`,`roles`.`status`,`roles`.`created_at`,`roles`.`updated_at`,`roles`.`active_name` FROM `roles` JOIN user_roles ur ON ur.role_id = roles.id WHERE ur.user_id = ? AND roles.status = ?")). + WithArgs(1, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(2, "admin", "Admin", "", true, consts.CommonEnabled, now, now, "admin")) + mock.ExpectQuery(regexp.QuoteMeta("SELECT `permissions`.`id`,`permissions`.`name`,`permissions`.`display_name`,`permissions`.`description`,`permissions`.`action`,`permissions`.`scope`,`permissions`.`resource_id`,`permissions`.`is_system`,`permissions`.`status`,`permissions`.`created_at`,`permissions`.`updated_at`,`permissions`.`active_name` FROM `permissions` JOIN user_permissions up ON up.permission_id = permissions.id WHERE up.user_id = ? AND permissions.status = ?")). + WithArgs(1, consts.CommonEnabled). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "name", "display_name", "description", "action", "scope", "resource_id", "is_system", "status", "created_at", "updated_at", "active_name", + }).AddRow(3, "user.read", "User Read", "", consts.ActionRead, consts.ScopeAll, 1, true, consts.CommonEnabled, now, now, "user.read")) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_containers` WHERE user_id = ? AND status != ?")). + WithArgs(1, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "container_id", "role_id", "status", "created_at", "updated_at"})) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_datasets` WHERE user_id = ? AND status != ?")). + WithArgs(1, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "dataset_id", "role_id", "status", "created_at", "updated_at"})) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_projects` WHERE user_id = ? AND status != ?")). + WithArgs(1, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "project_id", "role_id", "workspace_config", "status", "created_at", "updated_at", "active_user_project"})) + + resp, err := service.GetUserDetail(t.Context(), 1) + + require.NoError(t, err) + require.Equal(t, "demo", resp.Username) + require.Len(t, resp.GlobalRoles, 1) + require.Len(t, resp.Permissions, 1) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestServiceDeleteUserSuccess(t *testing.T) { + service, mock, cleanup := newUserTestService(t) + defer cleanup() + + now := time.Now() + mock.ExpectBegin() + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? AND status != ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs(1, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(1, "demo", "demo@example.com", "hashed", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? AND status != ? ORDER BY `users`.`id` LIMIT ?")). + WithArgs(1, consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "username", "email", "password", "full_name", "avatar", "phone", "last_login_at", + "is_active", "status", "created_at", "updated_at", + }).AddRow(1, "demo", "demo@example.com", "hashed", "Demo User", "", "", nil, true, consts.CommonEnabled, now, now)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_containers` SET `status`=?,`updated_at`=? WHERE user_id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_datasets` SET `status`=?,`updated_at`=? WHERE user_id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_projects` SET `status`=?,`updated_at`=? WHERE user_id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `user_permissions` WHERE user_id = ?")). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `user_roles` WHERE user_id = ?")). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec(regexp.QuoteMeta("UPDATE `users` SET `status`=?,`updated_at`=? WHERE id = ? AND status != ?")). + WithArgs(consts.CommonDeleted, sqlmock.AnyArg(), 1, consts.CommonDeleted). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + err := service.DeleteUser(t.Context(), 1) + + require.NoError(t, err) + require.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/src/repository/audit.go b/src/repository/audit.go deleted file mode 100644 index c703c0f7..00000000 --- a/src/repository/audit.go +++ /dev/null @@ -1,135 +0,0 @@ -package repository - -import ( - "fmt" - "time" - - "aegis/database" - "aegis/dto" - - "gorm.io/gorm" -) - -// CreateAuditLog creates a new audit log entry -func CreateAuditLog(db *gorm.DB, log *database.AuditLog) error { - if err := db.Create(log).Error; err != nil { - return fmt.Errorf("failed to create audit log: %w", err) - } - return nil -} - -// GetAuditLogByID retrieves a single audit log by ID -func GetAuditLogByID(db *gorm.DB, id int) (*database.AuditLog, error) { - var log database.AuditLog - err := db.Where("id = ?", id).First(&log).Error - if err != nil { - return nil, fmt.Errorf("failed to get audit log: %w", err) - } - return &log, nil -} - -// ListAuditLogs retrieves audit logs with pagination and filtering -func ListAuditLogs(db *gorm.DB, limit, offset int, filterOptions *dto.ListAuditLogFilters) ([]database.AuditLog, int64, error) { - var logs []database.AuditLog - var total int64 - - query := db.Model(&database.AuditLog{}).Preload("User").Preload("Resource") - if filterOptions != nil { - if filterOptions.Action != "" { - query = query.Where("action = ?", filterOptions.Action) - } - if filterOptions.IpAddress != "" { - query = query.Where("ip_address = ?", filterOptions.IpAddress) - } - if filterOptions.UserID != 0 { - query = query.Where("user_id = ?", filterOptions.UserID) - } - if filterOptions.ResourceID != 0 { - query = query.Where("resource_id = ?", filterOptions.ResourceID) - } - if filterOptions.State != nil { - query = query.Where("state = ?", *filterOptions.State) - } - if filterOptions.Status != nil { - query = query.Where("status = ?", *filterOptions.Status) - } - if filterOptions.StartTime != nil { - query = query.Where("created_at >= ?", *filterOptions.StartTime) - } - if filterOptions.EndTime != nil { - query = query.Where("created_at <= ?", *filterOptions.EndTime) - } - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count audit logs: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&logs).Error; err != nil { - return nil, 0, fmt.Errorf("failed to get audit logs: %w", err) - } - - return logs, total, nil -} - -// GetAuditLogStatistics returns statistics about audit logs -func GetAuditLogStatistics() (map[string]any, error) { - stats := make(map[string]interface{}) - - // Total logs count - var totalCount int64 - if err := database.DB.Model(&database.AuditLog{}).Count(&totalCount).Error; err != nil { - return nil, fmt.Errorf("failed to count total audit logs: %w", err) - } - stats["total"] = totalCount - - // Count by status - type StatusCount struct { - Status string `json:"status"` - Count int64 `json:"count"` - } - var statusCounts []StatusCount - err := database.DB.Model(&database.AuditLog{}). - Select("status, COUNT(*) as count"). - Group("status"). - Find(&statusCounts).Error - if err != nil { - return nil, fmt.Errorf("failed to get status counts: %w", err) - } - - statusMap := make(map[string]int64) - for _, sc := range statusCounts { - statusMap[sc.Status] = sc.Count - } - stats["by_status"] = statusMap - - // Count by action - type ActionCount struct { - Action string `json:"action"` - Count int64 `json:"count"` - } - var actionCounts []ActionCount - err = database.DB.Model(&database.AuditLog{}). - Select("action, COUNT(*) as count"). - Group("action"). - Find(&actionCounts).Error - if err != nil { - return nil, fmt.Errorf("failed to get action counts: %w", err) - } - - actionMap := make(map[string]int64) - for _, ac := range actionCounts { - actionMap[ac.Action] = ac.Count - } - stats["by_action"] = actionMap - - // Recent activity (last 24 hours) - last24h := time.Now().Add(-24 * time.Hour) - var recentCount int64 - if err := database.DB.Model(&database.AuditLog{}).Where("created_at >= ?", last24h).Count(&recentCount).Error; err != nil { - return nil, fmt.Errorf("failed to count recent audit logs: %w", err) - } - stats["last_24h"] = recentCount - - return stats, nil -} diff --git a/src/repository/common.go b/src/repository/common.go deleted file mode 100644 index 1e473472..00000000 --- a/src/repository/common.go +++ /dev/null @@ -1,5 +0,0 @@ -package repository - -const ( - commonOmitFields = "active_name" -) diff --git a/src/repository/container.go b/src/repository/container.go index 02efec29..2f42d623 100644 --- a/src/repository/container.go +++ b/src/repository/container.go @@ -4,27 +4,28 @@ import ( "fmt" "aegis/consts" - "aegis/database" + "aegis/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( - containerOmitFields = "Versions" - containerVersionOmitFields = "active_version_key,HelmConfig,EnvVars" - helmConfigOmitFields = "Values" + containerActiveNameOmitFields = "active_name" + containerOmitFields = "Versions" + containerVersionOmitFields = "active_version_key,HelmConfig,EnvVars" + helmConfigOmitFields = "Values" ) -type ParameterConfigFetcher func(db *gorm.DB, keys []string, resourceID int) ([]database.ParameterConfig, error) +type ParameterConfigFetcher func(db *gorm.DB, keys []string, resourceID int) ([]model.ParameterConfig, error) // ===================================================================== // Container Repository Functions // ===================================================================== // CreateContainer creates a new container record -func CreateContainer(db *gorm.DB, container *database.Container) error { - if err := db.Omit(commonOmitFields, containerOmitFields).Create(container).Error; err != nil { +func CreateContainer(db *gorm.DB, container *model.Container) error { + if err := db.Omit(containerActiveNameOmitFields, containerOmitFields).Create(container).Error; err != nil { return fmt.Errorf("failed to create container: %w", err) } return nil @@ -32,7 +33,7 @@ func CreateContainer(db *gorm.DB, container *database.Container) error { // DeleteContainer soft deletes a container by setting its status to deleted func DeleteContainer(db *gorm.DB, containerID int) (int64, error) { - result := db.Model(&database.Container{}). + result := db.Model(&model.Container{}). Where("id = ? AND status != ?", containerID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if err := result.Error; err != nil { @@ -42,8 +43,8 @@ func DeleteContainer(db *gorm.DB, containerID int) (int64, error) { } // GetContainerByID retrieves a container by its ID -func GetContainerByID(db *gorm.DB, id int) (*database.Container, error) { - var container database.Container +func GetContainerByID(db *gorm.DB, id int) (*model.Container, error) { + var container model.Container if err := db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&container).Error; err != nil { return nil, fmt.Errorf("failed to find container with id %d: %w", id, err) } @@ -51,33 +52,33 @@ func GetContainerByID(db *gorm.DB, id int) (*database.Container, error) { } // GetContainerStatistics returns statistics about containers -func GetContainerStatistics() (map[string]int64, error) { +func GetContainerStatistics(db *gorm.DB) (map[string]int64, error) { stats := make(map[string]int64) // Total containers var total int64 - if err := database.DB.Model(&database.Container{}).Count(&total).Error; err != nil { + if err := db.Model(&model.Container{}).Count(&total).Error; err != nil { return nil, fmt.Errorf("failed to count total containers: %v", err) } stats["total"] = total // Active containers var active int64 - if err := database.DB.Model(&database.Container{}).Where("status = 1").Count(&active).Error; err != nil { + if err := db.Model(&model.Container{}).Where("status = 1").Count(&active).Error; err != nil { return nil, fmt.Errorf("failed to count active containers: %v", err) } stats["active"] = active // Disabled containers var disabled int64 - if err := database.DB.Model(&database.Container{}).Where("status = 0").Count(&disabled).Error; err != nil { + if err := db.Model(&model.Container{}).Where("status = 0").Count(&disabled).Error; err != nil { return nil, fmt.Errorf("failed to count disabled containers: %v", err) } stats["disabled"] = disabled // Deleted containers var deleted int64 - if err := database.DB.Model(&database.Container{}).Where("status = -1").Count(&deleted).Error; err != nil { + if err := db.Model(&model.Container{}).Where("status = -1").Count(&deleted).Error; err != nil { return nil, fmt.Errorf("failed to count deleted containers: %v", err) } stats["deleted"] = deleted @@ -86,13 +87,13 @@ func GetContainerStatistics() (map[string]int64, error) { } // ListContainers lists containers based on filter options -func ListContainers(db *gorm.DB, limit, offset int, contaierType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]database.Container, int64, error) { - var containers []database.Container +func ListContainers(db *gorm.DB, limit, offset int, containerType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]model.Container, int64, error) { + var containers []model.Container var total int64 - query := db.Model(&database.Container{}) - if contaierType != nil { - query = query.Where("type = ?", *contaierType) + query := db.Model(&model.Container{}) + if containerType != nil { + query = query.Where("type = ?", *containerType) } if isPublic != nil { query = query.Where("is_public = ?", *isPublic) @@ -113,12 +114,12 @@ func ListContainers(db *gorm.DB, limit, offset int, contaierType *consts.Contain } // ListContainersByID retrieves multiple containers by their IDs -func ListContainersByID(tx *gorm.DB, containerIDs []int) ([]database.Container, error) { +func ListContainersByID(tx *gorm.DB, containerIDs []int) ([]model.Container, error) { if len(containerIDs) == 0 { - return []database.Container{}, nil + return []model.Container{}, nil } - var containers []database.Container + var containers []model.Container if err := tx. Where("id IN (?) AND status != ?", containerIDs, consts.CommonDeleted). Find(&containers).Error; err != nil { @@ -128,8 +129,8 @@ func ListContainersByID(tx *gorm.DB, containerIDs []int) ([]database.Container, } // UpdateContainer updates a container -func UpdateContainer(db *gorm.DB, container *database.Container) error { - if err := db.Omit(commonOmitFields).Save(container).Error; err != nil { +func UpdateContainer(db *gorm.DB, container *model.Container) error { + if err := db.Omit(containerActiveNameOmitFields).Save(container).Error; err != nil { return fmt.Errorf("failed to update container: %w", err) } return nil @@ -140,7 +141,7 @@ func UpdateContainer(db *gorm.DB, container *database.Container) error { // ===================================================================== // BatchCreateContainerVersions creates multiple container versions -func BatchCreateContainerVersions(db *gorm.DB, versions []database.ContainerVersion) error { +func BatchCreateContainerVersions(db *gorm.DB, versions []model.ContainerVersion) error { if len(versions) == 0 { return fmt.Errorf("no container versions to create") } @@ -154,7 +155,7 @@ func BatchCreateContainerVersions(db *gorm.DB, versions []database.ContainerVers // BatchDeleteContainerVersions soft deletes all versions of a specific container func BatchDeleteContainerVersions(db *gorm.DB, containerID int) (int64, error) { - result := db.Model(&database.ContainerVersion{}). + result := db.Model(&model.ContainerVersion{}). Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if result.Error != nil { @@ -164,12 +165,12 @@ func BatchDeleteContainerVersions(db *gorm.DB, containerID int) (int64, error) { } // BatchGetContainerVersions retrieves container versions for multiple container names -func BatchGetContainerVersions(db *gorm.DB, containerType consts.ContainerType, containerNames []string, userID int) ([]database.ContainerVersion, error) { +func BatchGetContainerVersions(db *gorm.DB, containerType consts.ContainerType, containerNames []string, userID int) ([]model.ContainerVersion, error) { if len(containerNames) == 0 { - return []database.ContainerVersion{}, nil + return []model.ContainerVersion{}, nil } - var versions []database.ContainerVersion + var versions []model.ContainerVersion query := db.Table("container_versions cv"). Preload("Container"). @@ -198,7 +199,7 @@ func BatchGetContainerVersions(db *gorm.DB, containerType consts.ContainerType, // CheckContainerExistsWithDifferentType checks if a container exists with a different type func CheckContainerExistsWithDifferentType(db *gorm.DB, containerName string, requestedType consts.ContainerType, userID int) (bool, consts.ContainerType, error) { - var container database.Container + var container model.Container query := db.Table("containers"). Where("name = ? AND type != ? AND status = ?", containerName, requestedType, consts.CommonEnabled) @@ -225,7 +226,7 @@ func CheckContainerExistsWithDifferentType(db *gorm.DB, containerName string, re // DeleteContainerVersion soft deletes a container version func DeleteContainerVersion(db *gorm.DB, versionID int) (int64, error) { - result := db.Model(&database.ContainerVersion{}). + result := db.Model(&model.ContainerVersion{}). Where("id = ? AND status != ?", versionID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if result.Error != nil { @@ -235,8 +236,8 @@ func DeleteContainerVersion(db *gorm.DB, versionID int) (int64, error) { } // GetContainerVersionByID retrieves a ContainerVersion by its ID -func GetContainerVersionByID(db *gorm.DB, versionID int) (*database.ContainerVersion, error) { - var version database.ContainerVersion +func GetContainerVersionByID(db *gorm.DB, versionID int) (*model.ContainerVersion, error) { + var version model.ContainerVersion if err := db. Preload("Container"). Preload("HelmConfig"). @@ -247,11 +248,11 @@ func GetContainerVersionByID(db *gorm.DB, versionID int) (*database.ContainerVer } // ListContainerVersions lists container versions with pagination and optional status filtering -func ListContainerVersions(db *gorm.DB, limit, offset int, containerID int, status *consts.StatusType) ([]database.ContainerVersion, int64, error) { - var versions []database.ContainerVersion +func ListContainerVersions(db *gorm.DB, limit, offset int, containerID int, status *consts.StatusType) ([]model.ContainerVersion, int64, error) { + var versions []model.ContainerVersion var total int64 - query := db.Model(&database.ContainerVersion{}).Where("container_id = ?", containerID) + query := db.Model(&model.ContainerVersion{}).Where("container_id = ?", containerID) if status != nil { query = query.Where("status = ?", *status) } @@ -268,8 +269,8 @@ func ListContainerVersions(db *gorm.DB, limit, offset int, containerID int, stat } // ListContainerVersions lists all versions of a specific container -func ListContainerVersionsByContainerID(db *gorm.DB, containerID int) ([]database.ContainerVersion, error) { - var versions []database.ContainerVersion +func ListContainerVersionsByContainerID(db *gorm.DB, containerID int) ([]model.ContainerVersion, error) { + var versions []model.ContainerVersion if err := db. Preload("Container"). Preload("HelmConfig"). @@ -281,7 +282,7 @@ func ListContainerVersionsByContainerID(db *gorm.DB, containerID int) ([]databas } // UpdateContainerVersion updates a container version -func UpdateContainerVersion(db *gorm.DB, version *database.ContainerVersion) error { +func UpdateContainerVersion(db *gorm.DB, version *model.ContainerVersion) error { if err := db.Omit(containerVersionOmitFields).Save(version).Error; err != nil { return fmt.Errorf("failed to update container version: %w", err) } @@ -293,7 +294,7 @@ func UpdateContainerVersion(db *gorm.DB, version *database.ContainerVersion) err // ===================================================================== // BatchCreateHelmConfigs creates multiple helm configs -func BatchCreateHelmConfigs(db *gorm.DB, helmConfigs []*database.HelmConfig) error { +func BatchCreateHelmConfigs(db *gorm.DB, helmConfigs []*model.HelmConfig) error { if len(helmConfigs) == 0 { return fmt.Errorf("no helm configs to create") } @@ -306,8 +307,8 @@ func BatchCreateHelmConfigs(db *gorm.DB, helmConfigs []*database.HelmConfig) err } // GetHelmConfigByContainerVersionID retrieves the HelmConfig associated with a specific ContainerVersion ID -func GetHelmConfigByContainerVersionID(db *gorm.DB, versionID int) (*database.HelmConfig, error) { - var helmConfig database.HelmConfig +func GetHelmConfigByContainerVersionID(db *gorm.DB, versionID int) (*model.HelmConfig, error) { + var helmConfig model.HelmConfig if err := db.Preload("ContainerVersion"). Where("container_version_id = ?", versionID). First(&helmConfig).Error; err != nil { @@ -317,7 +318,7 @@ func GetHelmConfigByContainerVersionID(db *gorm.DB, versionID int) (*database.He } // UpdateHelmConfig updates a helm config -func UpdateHelmConfig(db *gorm.DB, helmConfig *database.HelmConfig) error { +func UpdateHelmConfig(db *gorm.DB, helmConfig *model.HelmConfig) error { if err := db.Save(helmConfig).Error; err != nil { return fmt.Errorf("failed to update helm config: %w", err) } @@ -329,7 +330,7 @@ func UpdateHelmConfig(db *gorm.DB, helmConfig *database.HelmConfig) error { // ===================================================================== // BatchCreateOrFindParameterConfigs creates multiple parameter configs or finds existing ones using upsert -func BatchCreateOrFindParameterConfigs(db *gorm.DB, params []database.ParameterConfig) error { +func BatchCreateOrFindParameterConfigs(db *gorm.DB, params []model.ParameterConfig) error { if len(params) == 0 { return nil } @@ -344,14 +345,14 @@ func BatchCreateOrFindParameterConfigs(db *gorm.DB, params []database.ParameterC } // ListParameterConfigsByKeys retrieves ParameterConfigs by their keys, type and category -func ListParameterConfigsByKeys(db *gorm.DB, configs []database.ParameterConfig) ([]database.ParameterConfig, error) { +func ListParameterConfigsByKeys(db *gorm.DB, configs []model.ParameterConfig) ([]model.ParameterConfig, error) { if len(configs) == 0 { - return []database.ParameterConfig{}, nil + return []model.ParameterConfig{}, nil } // Build query conditions for batch lookup - var results []database.ParameterConfig - query := db.Model(&database.ParameterConfig{}) + var results []model.ParameterConfig + query := db.Model(&model.ParameterConfig{}) // Build OR conditions for each config conditions := db.Where("1 = 0") // Start with false condition @@ -373,7 +374,7 @@ func ListParameterConfigsByKeys(db *gorm.DB, configs []database.ParameterConfig) // ===================================================================== // AddContainerLabels adds multiple container-label associations in a batch -func AddContainerLabels(db *gorm.DB, containerLabels []database.ContainerLabel) error { +func AddContainerLabels(db *gorm.DB, containerLabels []model.ContainerLabel) error { if len(containerLabels) == 0 { return nil } @@ -404,7 +405,7 @@ func ClearContainerLabels(db *gorm.DB, containerIDs []int, labelIDs []int) error // RemoveContainersFromLabel removes all container associations from a specific label func RemoveContainersFromLabel(db *gorm.DB, labelID int) (int64, error) { result := db.Where("label_id = ?", labelID). - Delete(&database.ContainerLabel{}) + Delete(&model.ContainerLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to remove all containers from label %d: %w", labelID, err) } @@ -418,7 +419,7 @@ func RemoveContainersFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } result := db.Where("label_id IN (?)", labelIDs). - Delete(&database.ContainerLabel{}) + Delete(&model.ContainerLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to remove all containers from labels %v: %w", labelIDs, err) } @@ -426,18 +427,18 @@ func RemoveContainersFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } // ListContainerLabels gets labels for multiple containers in batch -func ListContainerLabels(db *gorm.DB, containerIDs []int) (map[int][]database.Label, error) { +func ListContainerLabels(db *gorm.DB, containerIDs []int) (map[int][]model.Label, error) { if len(containerIDs) == 0 { return nil, nil } type containerLabelResult struct { - database.Label + model.Label containerID int `gorm:"column:container_id"` } var flatResults []containerLabelResult - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Joins("JOIN container_labels cl ON cl.label_id = labels.id"). Where("cl.container_id IN (?)", containerIDs). Select("labels.*, cl.container_id"). @@ -445,9 +446,9 @@ func ListContainerLabels(db *gorm.DB, containerIDs []int) (map[int][]database.La return nil, fmt.Errorf("failed to batch query container labels: %w", err) } - labelsMap := make(map[int][]database.Label) + labelsMap := make(map[int][]model.Label) for _, id := range containerIDs { - labelsMap[id] = []database.Label{} + labelsMap[id] = []model.Label{} } for _, res := range flatResults { @@ -470,7 +471,7 @@ func ListContainerLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error } var results []containerLabelResult - if err := db.Model(&database.ContainerLabel{}). + if err := db.Model(&model.ContainerLabel{}). Select("label_id, count(label_id) as count"). Where("label_id IN (?)", labelIDs). Group("label_id"). @@ -487,9 +488,9 @@ func ListContainerLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error } // ListLabelsByContainerID lists all labels associated with a specific container -func ListLabelsByContainerID(db *gorm.DB, containerID int) ([]database.Label, error) { - var labels []database.Label - if err := db.Model(&database.Label{}). +func ListLabelsByContainerID(db *gorm.DB, containerID int) ([]model.Label, error) { + var labels []model.Label + if err := db.Model(&model.Label{}). Joins("JOIN container_labels cl ON cl.label_id = labels.id"). Where("cl.container_id = ?", containerID). Find(&labels).Error; err != nil { @@ -519,7 +520,7 @@ func ListLabelIDsByKeyAndContainerID(db *gorm.DB, containerID int, keys []string // ===================================================================== // AddContainerVersionEnvVars adds multiple environment variable parameters for a specific container version -func AddContainerVersionEnvVars(db *gorm.DB, envVars []database.ContainerVersionEnvVar) error { +func AddContainerVersionEnvVars(db *gorm.DB, envVars []model.ContainerVersionEnvVar) error { if len(envVars) == 0 { return nil } @@ -530,8 +531,8 @@ func AddContainerVersionEnvVars(db *gorm.DB, envVars []database.ContainerVersion } // ListContainerEnvVars lists environment variable parameters for a specific container version -func ListContainerVersionEnvVars(db *gorm.DB, keys []string, containerVersionID int) ([]database.ParameterConfig, error) { - query := db.Model(&database.ParameterConfig{}). +func ListContainerVersionEnvVars(db *gorm.DB, keys []string, containerVersionID int) ([]model.ParameterConfig, error) { + query := db.Model(&model.ParameterConfig{}). Joins("JOIN container_version_env_vars cvev ON cvev.parameter_config_id = parameter_configs.id"). Where("cvev.container_version_id = ?", containerVersionID). Where("parameter_configs.category = ?", consts.ParameterCategoryEnvVars) @@ -540,7 +541,7 @@ func ListContainerVersionEnvVars(db *gorm.DB, keys []string, containerVersionID query = query.Where("parameter_configs.config_key IN (?)", keys) } - var params []database.ParameterConfig + var params []model.ParameterConfig if err := query.Find(¶ms).Error; err != nil { return nil, fmt.Errorf("failed to list container env vars: %w", err) } @@ -552,7 +553,7 @@ func ListContainerVersionEnvVars(db *gorm.DB, keys []string, containerVersionID // ===================================================================== // AddHelmConfigValues adds multiple helm value parameters for a specific helm config -func AddHelmConfigValues(db *gorm.DB, helmValues []database.HelmConfigValue) error { +func AddHelmConfigValues(db *gorm.DB, helmValues []model.HelmConfigValue) error { if len(helmValues) == 0 { return nil } @@ -563,8 +564,8 @@ func AddHelmConfigValues(db *gorm.DB, helmValues []database.HelmConfigValue) err } // ListHelmConfigValues lists helm value parameters for a specific helm config -func ListHelmConfigValues(db *gorm.DB, keys []string, helmConfigID int) ([]database.ParameterConfig, error) { - query := db.Model(&database.ParameterConfig{}). +func ListHelmConfigValues(db *gorm.DB, keys []string, helmConfigID int) ([]model.ParameterConfig, error) { + query := db.Model(&model.ParameterConfig{}). Joins("JOIN helm_config_values hcv ON hcv.parameter_config_id = parameter_configs.id"). Where("hcv.helm_config_id = ?", helmConfigID) @@ -572,7 +573,7 @@ func ListHelmConfigValues(db *gorm.DB, keys []string, helmConfigID int) ([]datab query = query.Where("parameter_configs.config_key IN (?)", keys) } - var params []database.ParameterConfig + var params []model.ParameterConfig if err := query.Find(¶ms).Error; err != nil { return nil, fmt.Errorf("failed to list helm values: %w", err) } diff --git a/src/repository/dataset.go b/src/repository/dataset.go index 9107ba6a..3f1c4a54 100644 --- a/src/repository/dataset.go +++ b/src/repository/dataset.go @@ -4,14 +4,15 @@ import ( "fmt" "aegis/consts" - "aegis/database" + "aegis/model" "gorm.io/gorm" "gorm.io/gorm/clause" ) const ( - datasetVersionOmitFields = "active_version_key" + datasetActiveNameOmitFields = "active_name" + datasetVersionOmitFields = "active_version_key" ) // ===================================================================== @@ -19,8 +20,8 @@ const ( // ===================================================================== // CreateDataset creates a new dataset record -func CreateDataset(db *gorm.DB, dataset *database.Dataset) error { - if err := db.Omit(commonOmitFields).Create(dataset).Error; err != nil { +func CreateDataset(db *gorm.DB, dataset *model.Dataset) error { + if err := db.Omit(datasetActiveNameOmitFields).Create(dataset).Error; err != nil { return fmt.Errorf("failed to create dataset: %v", err) } return nil @@ -28,7 +29,7 @@ func CreateDataset(db *gorm.DB, dataset *database.Dataset) error { // DeleteDataset soft deletes a dataset by setting its status to deleted func DeleteDataset(db *gorm.DB, id int) (int64, error) { - result := db.Model(&database.Dataset{}). + result := db.Model(&model.Dataset{}). Where("id = ? AND status != ?", id, consts.CommonDeleted). Update("status", consts.CommonDeleted) if err := result.Error; err != nil { @@ -38,8 +39,8 @@ func DeleteDataset(db *gorm.DB, id int) (int64, error) { } // GetDatasetByID gets dataset by ID -func GetDatasetByID(db *gorm.DB, id int) (*database.Dataset, error) { - var dataset database.Dataset +func GetDatasetByID(db *gorm.DB, id int) (*model.Dataset, error) { + var dataset model.Dataset if err := db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&dataset).Error; err != nil { return nil, fmt.Errorf("failed to get dataset: %v", err) } @@ -47,11 +48,11 @@ func GetDatasetByID(db *gorm.DB, id int) (*database.Dataset, error) { } // ListDatasets gets dataset list -func ListDatasets(db *gorm.DB, limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]database.Dataset, int64, error) { - var datasets []database.Dataset +func ListDatasets(db *gorm.DB, limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]model.Dataset, int64, error) { + var datasets []model.Dataset var total int64 - query := db.Model(&database.Dataset{}) + query := db.Model(&model.Dataset{}) if datasetType != "" { query = query.Where("type = ?", datasetType) } @@ -75,12 +76,12 @@ func ListDatasets(db *gorm.DB, limit, offset int, datasetType string, isPublic * } // ListDatasetsByID retrieves multiple datasets by their IDs -func ListDatasetsByID(db *gorm.DB, datasetIDs []int) ([]database.Dataset, error) { +func ListDatasetsByID(db *gorm.DB, datasetIDs []int) ([]model.Dataset, error) { if len(datasetIDs) == 0 { - return []database.Dataset{}, nil + return []model.Dataset{}, nil } - var datasets []database.Dataset + var datasets []model.Dataset if err := db. Where("id IN (?) AND status != ?", datasetIDs, consts.CommonDeleted). Find(&datasets).Error; err != nil { @@ -91,41 +92,41 @@ func ListDatasetsByID(db *gorm.DB, datasetIDs []int) ([]database.Dataset, error) } // UpdateDataset updates dataset information -func UpdateDataset(db *gorm.DB, dataset *database.Dataset) error { - if err := db.Omit(commonOmitFields).Save(dataset).Error; err != nil { +func UpdateDataset(db *gorm.DB, dataset *model.Dataset) error { + if err := db.Omit(datasetActiveNameOmitFields).Save(dataset).Error; err != nil { return fmt.Errorf("failed to update dataset: %v", err) } return nil } // GetDatasetStatistics returns statistics about datasets -func GetDatasetStatistics() (map[string]int64, error) { +func GetDatasetStatistics(db *gorm.DB) (map[string]int64, error) { stats := make(map[string]int64) // Total datasets var total int64 - if err := database.DB.Model(&database.Dataset{}).Count(&total).Error; err != nil { + if err := db.Model(&model.Dataset{}).Count(&total).Error; err != nil { return nil, fmt.Errorf("failed to count total datasets: %v", err) } stats["total"] = total // Active datasets var active int64 - if err := database.DB.Model(&database.Dataset{}).Where("status = ?", consts.DatapackInjectSuccess).Count(&active).Error; err != nil { + if err := db.Model(&model.Dataset{}).Where("status = ?", consts.DatapackInjectSuccess).Count(&active).Error; err != nil { return nil, fmt.Errorf("failed to count active datasets: %v", err) } stats["active"] = active // Disabled datasets var disabled int64 - if err := database.DB.Model(&database.Dataset{}).Where("status = ?", consts.DatapackInitial).Count(&disabled).Error; err != nil { + if err := db.Model(&model.Dataset{}).Where("status = ?", consts.DatapackInitial).Count(&disabled).Error; err != nil { return nil, fmt.Errorf("failed to count disabled datasets: %v", err) } stats["disabled"] = disabled // Deleted datasets var deleted int64 - if err := database.DB.Model(&database.Dataset{}).Where("status = ?", consts.CommonDeleted).Count(&deleted).Error; err != nil { + if err := db.Model(&model.Dataset{}).Where("status = ?", consts.CommonDeleted).Count(&deleted).Error; err != nil { return nil, fmt.Errorf("failed to count deleted datasets: %v", err) } stats["deleted"] = deleted @@ -138,7 +139,7 @@ func GetDatasetStatistics() (map[string]int64, error) { // ===================================================================== // BatchCreateDatasetVersions creates multiple dataset versions -func BatchCreateDatasetVersions(db *gorm.DB, versions []database.DatasetVersion) error { +func BatchCreateDatasetVersions(db *gorm.DB, versions []model.DatasetVersion) error { if len(versions) == 0 { return fmt.Errorf("no dataset versions to create") } @@ -152,7 +153,7 @@ func BatchCreateDatasetVersions(db *gorm.DB, versions []database.DatasetVersion) // BatchDeleteDatasetVersions soft deletes all versions of a specific dataset func BatchDeleteDatasetVersions(db *gorm.DB, datasetID int) (int64, error) { - result := db.Model(&database.DatasetVersion{}). + result := db.Model(&model.DatasetVersion{}). Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if result.Error != nil { @@ -162,12 +163,12 @@ func BatchDeleteDatasetVersions(db *gorm.DB, datasetID int) (int64, error) { } // BatchGetDatasetVersions retrieves dataset versions for multiple dataset names -func BatchGetDatasetVersions(db *gorm.DB, datasetNames []string, userID int) ([]database.DatasetVersion, error) { +func BatchGetDatasetVersions(db *gorm.DB, datasetNames []string, userID int) ([]model.DatasetVersion, error) { if len(datasetNames) == 0 { - return []database.DatasetVersion{}, nil + return []model.DatasetVersion{}, nil } - var versions []database.DatasetVersion + var versions []model.DatasetVersion query := db.Table("dataset_versions dv"). Preload("Dataset"). @@ -196,7 +197,7 @@ func BatchGetDatasetVersions(db *gorm.DB, datasetNames []string, userID int) ([] // DeleteDatasetVersion performs a soft delete on the dataset version by setting its status to deleted func DeleteDatasetVersion(db *gorm.DB, versionID int) (int64, error) { - result := db.Model(&database.DatasetVersion{}). + result := db.Model(&model.DatasetVersion{}). Where("id = ? AND status != ?", versionID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if result.Error != nil { @@ -206,8 +207,8 @@ func DeleteDatasetVersion(db *gorm.DB, versionID int) (int64, error) { } // GetDatasetVersionByID retrieves a dataset version by its ID -func GetDatasetVersionByID(db *gorm.DB, id int) (*database.DatasetVersion, error) { - var version database.DatasetVersion +func GetDatasetVersionByID(db *gorm.DB, id int) (*model.DatasetVersion, error) { + var version model.DatasetVersion if err := db.Preload("Datapacks").Where("id = ?", id).First(&version).Error; err != nil { return nil, fmt.Errorf("failed to get dataset version: %v", err) } @@ -215,11 +216,11 @@ func GetDatasetVersionByID(db *gorm.DB, id int) (*database.DatasetVersion, error } // ListDatasetVersions lists dataset versions with pagination and optional status filtering -func ListDatasetVersions(db *gorm.DB, limit, offset int, datasetID int, status *consts.StatusType) ([]database.DatasetVersion, int64, error) { - var versions []database.DatasetVersion +func ListDatasetVersions(db *gorm.DB, limit, offset int, datasetID int, status *consts.StatusType) ([]model.DatasetVersion, int64, error) { + var versions []model.DatasetVersion var total int64 - query := db.Model(&database.DatasetVersion{}).Where("dataset_id = ?", datasetID) + query := db.Model(&model.DatasetVersion{}).Where("dataset_id = ?", datasetID) if status != nil { query = query.Where("status = ?", *status) } @@ -237,8 +238,8 @@ func ListDatasetVersions(db *gorm.DB, limit, offset int, datasetID int, status * } // ListDatasetVersions lists all versions of a specific dataset -func ListDatasetVersionsByDatasetID(db *gorm.DB, datasetID int) ([]database.DatasetVersion, error) { - var versions []database.DatasetVersion +func ListDatasetVersionsByDatasetID(db *gorm.DB, datasetID int) ([]model.DatasetVersion, error) { + var versions []model.DatasetVersion if err := db.Where("dataset_id = ?", datasetID).Find(&versions).Error; err != nil { return nil, fmt.Errorf("failed to list dataset versions for dataset %d: %w", datasetID, err) } @@ -246,7 +247,7 @@ func ListDatasetVersionsByDatasetID(db *gorm.DB, datasetID int) ([]database.Data } // UpdateDatasetVersion updates a dataset version -func UpdateDatasetVersion(db *gorm.DB, version *database.DatasetVersion) error { +func UpdateDatasetVersion(db *gorm.DB, version *model.DatasetVersion) error { if err := db.Omit(datasetVersionOmitFields).Save(version).Error; err != nil { return fmt.Errorf("failed to update dataset version: %w", err) } @@ -258,7 +259,7 @@ func UpdateDatasetVersion(db *gorm.DB, version *database.DatasetVersion) error { // ===================================================================== // AddDatasetLabels adds multiple dataset-label associations in a batch -func AddDatasetLabels(db *gorm.DB, datasetLabels []database.DatasetLabel) error { +func AddDatasetLabels(db *gorm.DB, datasetLabels []model.DatasetLabel) error { if len(datasetLabels) == 0 { return nil } @@ -292,7 +293,7 @@ func ClearDatasetLabels(db *gorm.DB, datasetIDs []int, labelIDs []int) error { // RemoveLabelsFromDataset removes all label associations from a specific dataset func RemoveLabelsFromDataset(db *gorm.DB, datasetID int) error { if err := db.Where("dataset_id = ?", datasetID). - Delete(&database.DatasetLabel{}).Error; err != nil { + Delete(&model.DatasetLabel{}).Error; err != nil { return fmt.Errorf("failed to delete all labels from dataset %d: %w", datasetID, err) } return nil @@ -301,7 +302,7 @@ func RemoveLabelsFromDataset(db *gorm.DB, datasetID int) error { // RemoveDatasetsFromLabel removes all dataset associations from a specific label func RemoveDatasetsFromLabel(db *gorm.DB, labelID int) (int64, error) { result := db.Where("label_id = ?", labelID). - Delete(&database.DatasetLabel{}) + Delete(&model.DatasetLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to delete all datasets from label %d: %w", labelID, err) } @@ -315,7 +316,7 @@ func RemoveDatasetsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } result := db.Where("label_id IN (?)", labelIDs). - Delete(&database.DatasetLabel{}) + Delete(&model.DatasetLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to delete all datasets from labels %v: %w", labelIDs, err) } @@ -334,7 +335,7 @@ func ListDatasetLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) } var results []datasetLabelResult - if err := db.Model(&database.DatasetLabel{}). + if err := db.Model(&model.DatasetLabel{}). Select("label_id, count(label_id) as count"). Where("label_id IN (?)", labelIDs). Group("label_id"). @@ -351,18 +352,18 @@ func ListDatasetLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) } // ListDatasetLabels lists all labels associated with multiple datasets -func ListDatasetLabels(db *gorm.DB, datasetIDs []int) (map[int][]database.Label, error) { +func ListDatasetLabels(db *gorm.DB, datasetIDs []int) (map[int][]model.Label, error) { if len(datasetIDs) == 0 { return nil, nil } type datasetLabelResult struct { - database.Label + model.Label datasetID int `gorm:"column:dataset_id"` } var flatResults []datasetLabelResult - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). Where("dl.dataset_id IN (?)", datasetIDs). Select("labels.*, dl.dataset_id"). @@ -370,9 +371,9 @@ func ListDatasetLabels(db *gorm.DB, datasetIDs []int) (map[int][]database.Label, return nil, fmt.Errorf("failed to batch query dataset labels: %w", err) } - labelsMap := make(map[int][]database.Label) + labelsMap := make(map[int][]model.Label) for _, id := range datasetIDs { - labelsMap[id] = []database.Label{} + labelsMap[id] = []model.Label{} } for _, res := range flatResults { @@ -384,9 +385,9 @@ func ListDatasetLabels(db *gorm.DB, datasetIDs []int) (map[int][]database.Label, } // ListLabelsByDatasetID lists all labels associated with a specific dataset -func ListLabelsByDatasetID(db *gorm.DB, datasetID int) ([]database.Label, error) { - var labels []database.Label - if err := db.Model(&database.Label{}). +func ListLabelsByDatasetID(db *gorm.DB, datasetID int) ([]model.Label, error) { + var labels []model.Label + if err := db.Model(&model.Label{}). Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). Where("dl.dataset_id = ?", datasetID). Find(&labels).Error; err != nil { @@ -416,7 +417,7 @@ func ListLabelIDsByKeyAndDatasetID(db *gorm.DB, datasetID int, keys []string) ([ // ===================================================================== // AddDatasetVersionInjections adds multiple dataset-version-injection associations in a batch -func AddDatasetVersionInjections(db *gorm.DB, datasetVersionInjections []database.DatasetVersionInjection) error { +func AddDatasetVersionInjections(db *gorm.DB, datasetVersionInjections []model.DatasetVersionInjection) error { if len(datasetVersionInjections) == 0 { return nil } @@ -450,7 +451,7 @@ func ClearDatasetVersionInjections(db *gorm.DB, datasetVersionIDs []int, injecti // RemoveInjectionsFromDatasetVersion deletes all injection associations for a given dataset version func RemoveInjectionsFromDatasetVersion(db *gorm.DB, datasetVersionID int) error { if err := db.Where("dataset_version_id = ?", datasetVersionID). - Delete(&database.DatasetVersionInjection{}).Error; err != nil { + Delete(&model.DatasetVersionInjection{}).Error; err != nil { return fmt.Errorf("failed to delete all injections from dataset version %d: %w", datasetVersionID, err) } return nil @@ -459,20 +460,20 @@ func RemoveInjectionsFromDatasetVersion(db *gorm.DB, datasetVersionID int) error // RemoveDatasetVersionsFromInjection deletes all dataset version associations for a given fault injection func RemoveDatasetVersionsFromInjection(db *gorm.DB, faultInjectionID int) error { if err := db.Where("injection_id = ?", faultInjectionID). - Delete(&database.DatasetVersionInjection{}).Error; err != nil { + Delete(&model.DatasetVersionInjection{}).Error; err != nil { return fmt.Errorf("failed to delete all dataset versions from fault injection %d: %w", faultInjectionID, err) } return nil } // ListInjectionsByDatasetVersionID lists all fault injections associated with a specific dataset version -func ListInjectionsByDatasetVersionID(db *gorm.DB, datasetVersionID int, includeLabels bool) ([]database.FaultInjection, error) { - query := db.Model(&database.FaultInjection{}) +func ListInjectionsByDatasetVersionID(db *gorm.DB, datasetVersionID int, includeLabels bool) ([]model.FaultInjection, error) { + query := db.Model(&model.FaultInjection{}) if includeLabels { query = query.Preload("Labels") } - var injections []database.FaultInjection + var injections []model.FaultInjection if err := query. Joins("JOIN dataset_version_injections dvi ON dvi.injection_id = id"). Where("state = ? AND status != ?", consts.DatapackBuildSuccess, consts.CommonDeleted). diff --git a/src/repository/detector.go b/src/repository/detector.go index 75c643fb..290ba786 100644 --- a/src/repository/detector.go +++ b/src/repository/detector.go @@ -3,14 +3,14 @@ package repository import ( "fmt" - "aegis/database" + "aegis/model" "gorm.io/gorm" ) // ListDetectorResultsByExecutionID lists detector results for a specific execution ID -func ListDetectorResultsByExecutionID(db *gorm.DB, executionID int) ([]database.DetectorResult, error) { - var results []database.DetectorResult +func ListDetectorResultsByExecutionID(db *gorm.DB, executionID int) ([]model.DetectorResult, error) { + var results []model.DetectorResult if err := db. Where("execution_id = ?", executionID). Find(&results).Error; err != nil { @@ -20,7 +20,7 @@ func ListDetectorResultsByExecutionID(db *gorm.DB, executionID int) ([]database. } // SaveDetectorResults saves multiple detector results -func SaveDetectorResults(db *gorm.DB, results []database.DetectorResult) error { +func SaveDetectorResults(db *gorm.DB, results []model.DetectorResult) error { if len(results) == 0 { return fmt.Errorf("no detector results to save") } diff --git a/src/repository/dynamic_config.go b/src/repository/dynamic_config.go deleted file mode 100644 index fd04d17e..00000000 --- a/src/repository/dynamic_config.go +++ /dev/null @@ -1,196 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" -) - -// ===================================================================== -// DynamicConfig Repository Functions -// ===================================================================== - -// CreateConfig creates a new configuration item -func CreateConfig(db *gorm.DB, config *database.DynamicConfig) error { - if err := db.Create(config).Error; err != nil { - return fmt.Errorf("failed to create config: %w", err) - } - return nil -} - -// GetConfigByKey retrieves a configuration by its key -func GetConfigByKey(db *gorm.DB, configKey string, includeUser bool) (*database.DynamicConfig, error) { - query := db - if includeUser { - query = query.Preload("UpdatedByUser") - } - - var config database.DynamicConfig - if err := query. - Where("config_key = ?", configKey). - First(&config).Error; err != nil { - return nil, fmt.Errorf("failed to find config with key %s: %w", configKey, err) - } - return &config, nil -} - -// GetConfigByID retrieves a configuration by its ID -func GetConfigByID(db *gorm.DB, configID int, includeUser bool) (*database.DynamicConfig, error) { - query := db - if includeUser { - query = query.Preload("UpdatedByUser") - } - - var config database.DynamicConfig - if err := query. - Where("id = ?", configID). - First(&config).Error; err != nil { - return nil, fmt.Errorf("failed to find config with id %d: %w", configID, err) - } - return &config, nil -} - -// List ExistingConfigs lists all existing configurations -func ListExistingConfigs(db *gorm.DB) ([]database.DynamicConfig, error) { - var configs []database.DynamicConfig - if err := db. - Order("config_key ASC"). - Find(&configs).Error; err != nil { - return nil, fmt.Errorf("failed to list all existing configs: %w", err) - } - return configs, nil -} - -// ListConfigs lists configs based on filter options -func ListConfigs(db *gorm.DB, limit, offset int, valueType *consts.ConfigValueType, category *string, isSecret *bool, updatedBy *int) ([]database.DynamicConfig, int64, error) { - var configs []database.DynamicConfig - var total int64 - - query := db.Model(&database.DynamicConfig{}) - if valueType != nil { - query = query.Where("value_type = ?", *valueType) - } - if category != nil { - query = query.Where("category = ?", *category) - } - if isSecret != nil { - query = query.Where("is_secret = ?", *isSecret) - } - if updatedBy != nil { - query = query.Where("updated_by = ?", *updatedBy) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count configs: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&configs).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list configs: %w", err) - } - - return configs, total, nil -} - -// ListConfigByScope lists configs filtered by scope -func ListConfigByScope(db *gorm.DB, scope consts.ConfigScope) ([]database.DynamicConfig, error) { - var configs []database.DynamicConfig - if err := db. - Where("scope = ?", scope). - Order("config_key ASC"). - Find(&configs).Error; err != nil { - return nil, fmt.Errorf("failed to list configs by scope %s: %w", consts.GetConfigScopeName(scope), err) - } - return configs, nil -} - -// UpdateConfig updates a configuration item -func UpdateConfig(db *gorm.DB, config *database.DynamicConfig) error { - if err := db.Save(config).Error; err != nil { - return fmt.Errorf("failed to update config: %w", err) - } - return nil -} - -// ===================================================================== -// ConfigHistory Repository Functions -// ===================================================================== - -// CreateConfigHistory creates a new history record -func CreateConfigHistory(db *gorm.DB, history *database.ConfigHistory) error { - if err := db.Create(history).Error; err != nil { - return fmt.Errorf("failed to create config history: %w", err) - } - return nil -} - -// GetConfigHistory retrieves a specific history entry by ID -func GetConfigHistory(db *gorm.DB, historyID int) (*database.ConfigHistory, error) { - var history database.ConfigHistory - if err := db. - Preload("Operator"). - Preload("Config"). - First(&history, historyID).Error; err != nil { - return nil, fmt.Errorf("failed to find config history with id %d: %w", historyID, err) - } - return &history, nil -} - -// GetLatestConfigHistory retrieves the most recent configuration change -func GetLatestConfigHistory(db *gorm.DB) (*database.ConfigHistory, error) { - var history database.ConfigHistory - if err := db. - Preload("Operator"). - Preload("Config"). - Order("created_at DESC"). - First(&history).Error; err != nil { - return nil, fmt.Errorf("failed to get latest config history: %w", err) - } - return &history, nil -} - -// ListConfigHistories lists configuration history entries with pagination and optional filters -func ListConfigHistories(db *gorm.DB, limit, offset int, configID int, changeType *consts.ConfigHistoryChangeType, operatorID *int) ([]database.ConfigHistory, int64, error) { - var histories []database.ConfigHistory - var total int64 - - query := db.Model(&database.ConfigHistory{}). - Where("config_id = ?", configID) - - if changeType != nil { - query = query.Where("change_type = ?", *changeType) - } - if operatorID != nil { - query = query.Where("operator_id = ?", *operatorID) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count config histories: %w", err) - } - - if err := query. - Preload("Operator"). - Limit(limit). - Offset(offset). - Order("created_at DESC"). - Find(&histories).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list config histories: %w", err) - } - - return histories, total, nil -} - -// ListConfigHistoriesByConfigID lists all history entries for a specific configuration -func ListConfigHistoriesByConfigID(db *gorm.DB, configID int) ([]database.ConfigHistory, error) { - var histories []database.ConfigHistory - if err := db. - Preload("Operator"). - Where("config_id = ?", configID). - Order("created_at DESC"). - Find(&histories).Error; err != nil { - return nil, fmt.Errorf("failed to list config histories for config %d: %w", configID, err) - } - return histories, nil -} diff --git a/src/repository/execution.go b/src/repository/execution.go index c2433c06..f2299ac9 100644 --- a/src/repository/execution.go +++ b/src/repository/execution.go @@ -8,7 +8,7 @@ import ( "gorm.io/gorm/clause" "aegis/consts" - "aegis/database" + "aegis/model" ) const BATCH_SIZE = 500 @@ -23,7 +23,7 @@ func BatchDeleteExecutions(db *gorm.DB, executions []int) error { return nil } - if err := db.Model(&database.Execution{}). + if err := db.Model(&model.Execution{}). Where("id IN (?) AND status != ?", executions, consts.CommonDeleted). Update("status", consts.CommonDeleted).Error; err != nil { return fmt.Errorf("failed to batch delete executions: %w", err) @@ -33,7 +33,7 @@ func BatchDeleteExecutions(db *gorm.DB, executions []int) error { } // CreateExecution creates a new execution result record -func CreateExecution(db *gorm.DB, execution *database.Execution) error { +func CreateExecution(db *gorm.DB, execution *model.Execution) error { if err := db.Create(execution).Error; err != nil { return fmt.Errorf("failed to create execution result: %w", err) } @@ -41,8 +41,8 @@ func CreateExecution(db *gorm.DB, execution *database.Execution) error { } // GetExecutionByID retrieves an execution result by its ID with preloaded associations -func GetExecutionByID(db *gorm.DB, id int) (*database.Execution, error) { - var result database.Execution +func GetExecutionByID(db *gorm.DB, id int) (*model.Execution, error) { + var result model.Execution if err := db. Preload("AlgorithmVersion.Container"). Preload("Datapack.Benchmark.Container"). @@ -57,11 +57,11 @@ func GetExecutionByID(db *gorm.DB, id int) (*database.Execution, error) { } // ListExecutions lists executions based on filters and pagination -func ListExecutions(db *gorm.DB, limit, offset int, event *consts.ExecutionState, status *consts.StatusType, labelConditions []map[string]string) ([]database.Execution, int64, error) { - var executions []database.Execution +func ListExecutions(db *gorm.DB, limit, offset int, event *consts.ExecutionState, status *consts.StatusType, labelConditions []map[string]string) ([]model.Execution, int64, error) { + var executions []model.Execution var total int64 - query := db.Model(&database.Execution{}). + query := db.Model(&model.Execution{}). Preload("AlgorithmVersion.Container"). Preload("Datapack.Benchmark.Container"). Preload("Datapack.Pedestal.Container"). @@ -96,12 +96,12 @@ func ListExecutions(db *gorm.DB, limit, offset int, event *consts.ExecutionState return executions, total, nil } -func ListExecutionsByDatapackIDs(db *gorm.DB, datapackIDs []int) ([]database.Execution, error) { +func ListExecutionsByDatapackIDs(db *gorm.DB, datapackIDs []int) ([]model.Execution, error) { if len(datapackIDs) == 0 { - return make([]database.Execution, 0), nil + return make([]model.Execution, 0), nil } - var results []database.Execution + var results []model.Execution query := db. Preload("AlgorithmVersion.Container"). @@ -119,7 +119,7 @@ func ListExecutionsByDatapackIDs(db *gorm.DB, datapackIDs []int) ([]database.Exe // UpdateExecution updates fields of an execution record func UpdateExecution(db *gorm.DB, id int, updates map[string]any) error { - result := db.Model(&database.Execution{}). + result := db.Model(&model.Execution{}). Where("id = ? AND status != ?", id, consts.CommonDeleted). Updates(updates) if err := result.Error; err != nil { @@ -142,9 +142,9 @@ func AddExecutionLabels(db *gorm.DB, executionID int, labelIDs []int) error { } // Create ExecutionInjectionLabel associations - executionLabels := make([]database.ExecutionInjectionLabel, 0, len(labelIDs)) + executionLabels := make([]model.ExecutionInjectionLabel, 0, len(labelIDs)) for _, labelID := range labelIDs { - executionLabels = append(executionLabels, database.ExecutionInjectionLabel{ + executionLabels = append(executionLabels, model.ExecutionInjectionLabel{ ExecutionID: executionID, LabelID: labelID, }) @@ -181,7 +181,7 @@ func ClearExecutionLabels(db *gorm.DB, executionIDs []int, labelIDs []int) error // RemoveLabelsFromExecution removes all label associations from a specific execution func RemoveLabelsFromExecution(db *gorm.DB, executionID int) error { if err := db.Where("execution_id = ?", executionID). - Delete(&database.ExecutionInjectionLabel{}).Error; err != nil { + Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { return fmt.Errorf("failed to remove all labels from execution %d: %w", executionID, err) } return nil @@ -194,7 +194,7 @@ func RemoveLabelsFromExecutions(db *gorm.DB, executionIDs []int) error { } if err := db.Where("execution_id IN (?)", executionIDs). - Delete(&database.ExecutionInjectionLabel{}).Error; err != nil { + Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { return fmt.Errorf("failed to remove all labels from executions %v: %w", executionIDs, err) } return nil @@ -203,7 +203,7 @@ func RemoveLabelsFromExecutions(db *gorm.DB, executionIDs []int) error { // RemoveExecutionsFromLabel deletes all execution-label associations for a specific label func RemoveExecutionsFromLabel(db *gorm.DB, labelID int) (int64, error) { result := db.Where("label_id = ?", labelID). - Delete(&database.ExecutionInjectionLabel{}) + Delete(&model.ExecutionInjectionLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to delete execution-label associations for label %d: %w", labelID, err) } @@ -218,7 +218,7 @@ func RemoveExecutionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } result := db.Where("label_id IN (?)", labelIDs). - Delete(&database.ExecutionInjectionLabel{}) + Delete(&model.ExecutionInjectionLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to delete execution-label associations for labels %v: %w", labelIDs, err) } @@ -227,10 +227,10 @@ func RemoveExecutionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } // ListExecutionsByDatapackFilter lists executions for a specific algorithm version and datapack name, with optional label filtering -func ListExecutionsByDatapackFilter(db *gorm.DB, algorithmVersionID int, datapackName string, labelConditions []map[string]string) ([]database.Execution, error) { - var executions []database.Execution +func ListExecutionsByDatapackFilter(db *gorm.DB, algorithmVersionID int, datapackName string, labelConditions []map[string]string) ([]model.Execution, error) { + var executions []model.Execution - query := db.Model(&database.Execution{}). + query := db.Model(&model.Execution{}). Preload("DetectorResults"). Preload("GranularityResults"). Preload("AlgorithmVersion.Container"). @@ -271,10 +271,10 @@ func ListExecutionsByDatapackFilter(db *gorm.DB, algorithmVersionID int, datapac } // ListExecutionsByDatasetFilter lists executions for a specific algorithm version and dataset version, with optional label filtering -func ListExecutionsByDatasetFilter(db *gorm.DB, algorithmVersionID, datasetVersionID int, labelConditions []map[string]string) ([]database.Execution, error) { - var executions []database.Execution +func ListExecutionsByDatasetFilter(db *gorm.DB, algorithmVersionID, datasetVersionID int, labelConditions []map[string]string) ([]model.Execution, error) { + var executions []model.Execution - query := db.Model(&database.Execution{}). + query := db.Model(&model.Execution{}). Preload("DetectorResults"). Preload("GranularityResults"). Preload("AlgorithmVersion.Container"). @@ -318,7 +318,7 @@ func ListExecutionsByDatasetFilter(db *gorm.DB, algorithmVersionID, datasetVersi // ListExecutionIDsByLabels gets execution IDs associated with all specified labels func ListExecutionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) ([]int, error) { var executionIDs []int - query := db.Model(&database.Execution{}). + query := db.Model(&model.Execution{}). Select("DISTINCT executions.id"). Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). Joins("JOIN labels ON labels.id = eil.label_id"). @@ -345,18 +345,18 @@ func ListExecutionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) } // ListExecutionLabels gets labels for multiple executions in batch -func ListExecutionLabels(db *gorm.DB, executionIDs []int) (map[int][]database.Label, error) { +func ListExecutionLabels(db *gorm.DB, executionIDs []int) (map[int][]model.Label, error) { if len(executionIDs) == 0 { return nil, nil } type executionLabelResult struct { - database.Label + model.Label executionID int `gorm:"column:execution_id"` } var flatResults []executionLabelResult - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Joins("JOIN execution_injection_labels eil ON eil.label_id = labels.id"). Where("eil.execution_id IN (?)", executionIDs). Select("labels.*, eil.execution_id"). @@ -364,9 +364,9 @@ func ListExecutionLabels(db *gorm.DB, executionIDs []int) (map[int][]database.La return nil, fmt.Errorf("failed to batch query execution labels: %w", err) } - labelsMap := make(map[int][]database.Label) + labelsMap := make(map[int][]model.Label) for _, id := range executionIDs { - labelsMap[id] = []database.Label{} + labelsMap[id] = []model.Label{} } for _, res := range flatResults { @@ -406,8 +406,8 @@ func ListExecutionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error } // ListLabelsByExecutionID retrieves all labels associated with a specific execution -func ListLabelsByExecutionID(db *gorm.DB, executionID int) ([]database.Label, error) { - var labels []database.Label +func ListLabelsByExecutionID(db *gorm.DB, executionID int) ([]model.Label, error) { + var labels []model.Label if err := db.Table("labels"). Joins("JOIN execution_injection_labels eil ON labels.id = eil.label_id"). Where("eil.execution_id = ?", executionID). @@ -434,12 +434,12 @@ func ListLabelIDsByKeyAndExecutionID(db *gorm.DB, executionID int, keys []string } // GetExecutionStatistics returns statistics about executions -func GetExecutionStatistics() (map[string]int64, error) { +func GetExecutionStatistics(db *gorm.DB) (map[string]int64, error) { stats := make(map[string]int64) // Total executions var total int64 - if err := database.DB.Model(&database.Execution{}).Count(&total).Error; err != nil { + if err := db.Model(&model.Execution{}).Count(&total).Error; err != nil { return nil, fmt.Errorf("failed to count total executions: %w", err) } stats["total"] = total @@ -451,7 +451,7 @@ func GetExecutionStatistics() (map[string]int64, error) { } var statusCounts []StatusCount - err := database.DB.Model(&database.Execution{}). + err := db.Model(&model.Execution{}). Select("status, COUNT(*) as count"). Group("status"). Find(&statusCounts).Error @@ -490,12 +490,12 @@ func GetExecutionStatistics() (map[string]int64, error) { } // ListExecutionsByProjectID retrieves executions for a specific project with pagination -func ListExecutionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]database.Execution, int64, error) { - var executions []database.Execution +func ListExecutionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]model.Execution, int64, error) { + var executions []model.Execution var total int64 // Base query with JOIN and WHERE conditions - baseQuery := db.Model(&database.Execution{}). + baseQuery := db.Model(&model.Execution{}). Joins("JOIN tasks ON tasks.id = executions.task_id"). Joins("JOIN traces on traces.id = tasks.trace_id"). Where("traces.project_id = ? AND executions.status != ?", projectID, consts.CommonDeleted) diff --git a/src/repository/granularity.go b/src/repository/granularity.go index 7a3bf779..63e34d63 100644 --- a/src/repository/granularity.go +++ b/src/repository/granularity.go @@ -5,14 +5,14 @@ import ( "fmt" "aegis/consts" - "aegis/database" + "aegis/model" "gorm.io/gorm" ) // ListGranularityResultsByExecutionID lists granularity results for a specific execution ID -func ListGranularityResultsByExecutionID(db *gorm.DB, executionID int) ([]database.GranularityResult, error) { - var results []database.GranularityResult +func ListGranularityResultsByExecutionID(db *gorm.DB, executionID int) ([]model.GranularityResult, error) { + var results []model.GranularityResult if err := db. Where("execution_id = ?", executionID). Find(&results).Error; err != nil { @@ -22,7 +22,7 @@ func ListGranularityResultsByExecutionID(db *gorm.DB, executionID int) ([]databa } // SaveGranularityResults saves multiple granularity results -func SaveGranularityResults(db *gorm.DB, results []database.GranularityResult) error { +func SaveGranularityResults(db *gorm.DB, results []model.GranularityResult) error { if len(results) == 0 { return fmt.Errorf("no granularity results to create") } diff --git a/src/repository/injection.go b/src/repository/injection.go index b2f9a518..fea20477 100644 --- a/src/repository/injection.go +++ b/src/repository/injection.go @@ -7,8 +7,7 @@ import ( "time" "aegis/consts" - "aegis/database" - "aegis/dto" + "aegis/model" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -18,13 +17,15 @@ import ( // Injection Repository Functions // ===================================================================== -// BatchDelteInjections marks multiple injections as deleted in batch +const injectionActiveNameOmitFields = "active_name" + +// BatchDeleteInjections marks multiple injections as deleted in batch func BatchDeleteInjections(db *gorm.DB, injectionIDs []int) error { if len(injectionIDs) == 0 { return nil } - if err := db.Model(&database.FaultInjection{}). + if err := db.Model(&model.FaultInjection{}). Where("id IN (?) AND status != ?", injectionIDs, consts.CommonDeleted). Update("status", consts.CommonDeleted).Error; err != nil { return fmt.Errorf("failed to batch delete injections: %w", err) @@ -34,16 +35,16 @@ func BatchDeleteInjections(db *gorm.DB, injectionIDs []int) error { } // CreateInjection creates a fault injection record -func CreateInjection(db *gorm.DB, injection *database.FaultInjection) error { - if err := db.Omit(commonOmitFields).Create(injection).Error; err != nil { +func CreateInjection(db *gorm.DB, injection *model.FaultInjection) error { + if err := db.Omit(injectionActiveNameOmitFields).Create(injection).Error; err != nil { return fmt.Errorf("failed to create injection: %w", err) } return nil } // GetInjectionByID gets injection by ID with preloaded associations -func GetInjectionByID(db *gorm.DB, id int) (*database.FaultInjection, error) { - var injection database.FaultInjection +func GetInjectionByID(db *gorm.DB, id int) (*model.FaultInjection, error) { + var injection model.FaultInjection if err := db. Preload("Task"). Preload("Task.Trace"). @@ -56,13 +57,13 @@ func GetInjectionByID(db *gorm.DB, id int) (*database.FaultInjection, error) { } // GetInjectionByName gets injection by name with preloaded associations -func GetInjectionByName(db *gorm.DB, name string, includeLabels bool) (*database.FaultInjection, error) { +func GetInjectionByName(db *gorm.DB, name string, includeLabels bool) (*model.FaultInjection, error) { query := db if includeLabels { query = query.Preload("Labels") } - var injection database.FaultInjection + var injection model.FaultInjection if err := query. Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&injection).Error; err != nil { return nil, fmt.Errorf("failed to find injection with name %s: %w", name, err) @@ -71,12 +72,12 @@ func GetInjectionByName(db *gorm.DB, name string, includeLabels bool) (*database } // ListFaultInjectionsByID retrieves multiple fault injections by their IDs with preloaded associations -func ListFaultInjectionsByID(db *gorm.DB, injectionIDs []int) ([]database.FaultInjection, error) { +func ListFaultInjectionsByID(db *gorm.DB, injectionIDs []int) ([]model.FaultInjection, error) { if len(injectionIDs) == 0 { - return []database.FaultInjection{}, nil + return []model.FaultInjection{}, nil } - var injections []database.FaultInjection + var injections []model.FaultInjection if err := db. Preload("Benchmark.Container"). Preload("Pedestal.Container"). @@ -98,7 +99,7 @@ func ListExistingEngineConfigs(db *gorm.DB, configs []string) ([]string, error) } query := db. - Model(&database.FaultInjection{}). + Model(&model.FaultInjection{}). Select("engine_config"). Where("engine_config in (?) AND state >= ? AND status = ?", configs, consts.DatapackInjectSuccess, consts.CommonEnabled) @@ -124,8 +125,8 @@ func ListEngineConfigByNames(db *gorm.DB, names []string) (map[string]string, er EngineConfig string `gorm:"column:engine_config"` } - if err := database.DB. - Model(&database.FaultInjection{}). + if err := db. + Model(&model.FaultInjection{}). Select("name, engine_config"). Where("name IN (?)", names). Find(&records).Error; err != nil { @@ -140,54 +141,6 @@ func ListEngineConfigByNames(db *gorm.DB, names []string) (map[string]string, er return result, nil } -// ListInjections lists fault injections based on filter options with preloaded associations -func ListInjections(db *gorm.DB, limit, offset int, filterOptions *dto.ListInjectionFilters) ([]database.FaultInjection, int64, error) { - var injections []database.FaultInjection - var total int64 - - query := db.Model(&database.FaultInjection{}). - Preload("Benchmark.Container"). - Preload("Pedestal.Container"). - Preload("Task.Trace.Project"). - Preload("Labels") - if filterOptions.FaultType != nil { - query = query.Where("fault_type = ?", *filterOptions.FaultType) - } - if filterOptions.Category != nil { - query = query.Where("category = ?", *filterOptions.Category) - } - if filterOptions.Benchmark != "" { - query = query.Where("benchmark = ?", filterOptions.Benchmark) - } - if filterOptions.State != nil { - query = query.Where("state = ?", *filterOptions.State) - } - if filterOptions.Status != nil { - query = query.Where("status = ?", *filterOptions.Status) - } - - if len(filterOptions.LabelConditions) > 0 { - for _, condition := range filterOptions.LabelConditions { - subQuery := db.Table("fault_injection_labels fil"). - Select("fil.fault_injection_id"). - Joins("JOIN labels ON labels.id = fil.label_id"). - Where("labels.label_key = ? AND labels.label_value = ?", condition["key"], condition["value"]) - - query = query.Where("fault_injections.id IN (?)", subQuery) - } - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count injections: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&injections).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list injections: %w", err) - } - - return injections, total, nil -} - // ListInjectionIDsByNames retrieves injection IDs by their names func ListInjectionIDsByNames(db *gorm.DB, names []string) (map[string]int, error) { if len(names) == 0 { @@ -199,7 +152,7 @@ func ListInjectionIDsByNames(db *gorm.DB, names []string) (map[string]int, error ID int `gorm:"column:id"` } - if err := db.Model(&database.FaultInjection{}). + if err := db.Model(&model.FaultInjection{}). Select("name, id"). Where("state = ? AND status = ?", consts.DatapackBuildSuccess, consts.CommonEnabled). Where("name IN (?)", names). @@ -216,15 +169,15 @@ func ListInjectionIDsByNames(db *gorm.DB, names []string) (map[string]int, error } // UpdateGroundtruth updates ground truth and its source for an injection -func UpdateGroundtruth(db *gorm.DB, id int, groundtruths []database.Groundtruth, source string) error { +func UpdateGroundtruth(db *gorm.DB, id int, groundtruths []model.Groundtruth, source string) error { gtJSON, err := json.Marshal(groundtruths) if err != nil { return fmt.Errorf("failed to marshal groundtruths: %w", err) } - result := db.Model(&database.FaultInjection{}). + result := db.Model(&model.FaultInjection{}). Where("id = ? AND status != ?", id, consts.CommonDeleted). Updates(map[string]interface{}{ - "groundtruths": string(gtJSON), + "groundtruths": string(gtJSON), "groundtruth_source": source, }) if result.Error != nil { @@ -238,7 +191,7 @@ func UpdateGroundtruth(db *gorm.DB, id int, groundtruths []database.Groundtruth, // UpdateInjection updates fields of a fault injection record func UpdateInjection(db *gorm.DB, id int, updates map[string]any) error { - result := db.Model(&database.FaultInjection{}). + result := db.Model(&model.FaultInjection{}). Where("id = ? AND status != ?", id, consts.CommonDeleted). Updates(updates) if err := result.Error; err != nil { @@ -251,8 +204,8 @@ func UpdateInjection(db *gorm.DB, id int, updates map[string]any) error { } // ListInjectionsNoIssues lists fault injections without issues based on label conditions and time range -func ListInjectionsNoIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]database.FaultInjectionNoIssues, error) { - query := db.Model(&database.FaultInjectionNoIssues{}).Scopes(database.Sort("dataset_id desc")) +func ListInjectionsNoIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionNoIssues, error) { + query := db.Model(&model.FaultInjectionNoIssues{}).Scopes(Sort("dataset_id desc")) if startTime != nil { query = query.Where("created_at >= ?", *startTime) } @@ -284,7 +237,7 @@ func ListInjectionsNoIssues(db *gorm.DB, labelConditions []map[string]string, st Having("COUNT(id) = ?", len(labelConditions)) } - var records []database.FaultInjectionNoIssues + var records []model.FaultInjectionNoIssues if err := query.Find(&records).Error; err != nil { return nil, fmt.Errorf("failed to query fault injections without issues: %v", err) } @@ -293,8 +246,8 @@ func ListInjectionsNoIssues(db *gorm.DB, labelConditions []map[string]string, st } // ListInjectionsWithIssues lists fault injections with issues based on label conditions and time range -func ListInjectionsWithIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]database.FaultInjectionWithIssues, error) { - query := db.Model(&database.FaultInjectionNoIssues{}).Scopes(database.Sort("dataset_id desc")) +func ListInjectionsWithIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionWithIssues, error) { + query := db.Model(&model.FaultInjectionNoIssues{}).Scopes(Sort("dataset_id desc")) if startTime != nil { query = query.Where("created_at >= ?", *startTime) } @@ -326,7 +279,7 @@ func ListInjectionsWithIssues(db *gorm.DB, labelConditions []map[string]string, Having("COUNT(id) = ?", len(labelConditions)) } - var records []database.FaultInjectionWithIssues + var records []model.FaultInjectionWithIssues if err := query.Find(&records).Error; err != nil { return nil, fmt.Errorf("failed to query fault injections without issues: %v", err) } @@ -347,9 +300,9 @@ func AddInjectionLabels(db *gorm.DB, injectionID int, labelIDs []int) error { } // Create FaultInjectionLabel associations - injectionLabels := make([]database.FaultInjectionLabel, 0, len(labelIDs)) + injectionLabels := make([]model.FaultInjectionLabel, 0, len(labelIDs)) for _, labelID := range labelIDs { - injectionLabels = append(injectionLabels, database.FaultInjectionLabel{ + injectionLabels = append(injectionLabels, model.FaultInjectionLabel{ FaultInjectionID: injectionID, LabelID: labelID, }) @@ -377,7 +330,7 @@ func ClearInjectionLabels(db *gorm.DB, injectionIDs []int, labelIDs []int) error query = query.Where("label_id IN (?)", labelIDs) } - if err := query.Delete(&database.FaultInjectionLabel{}).Error; err != nil { + if err := query.Delete(&model.FaultInjectionLabel{}).Error; err != nil { return fmt.Errorf("failed to clear injection labels: %w", err) } return nil @@ -386,7 +339,7 @@ func ClearInjectionLabels(db *gorm.DB, injectionIDs []int, labelIDs []int) error // RemoveInjectionsFromLabel removes all injection-label associations for a specific label func RemoveInjectionsFromLabel(db *gorm.DB, labelID int) (int64, error) { result := db.Where("label_id = ?", labelID). - Delete(&database.FaultInjectionLabel{}) + Delete(&model.FaultInjectionLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to remove injection-label associations for label %d: %w", labelID, err) } @@ -401,7 +354,7 @@ func RemoveInjectionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { } result := db.Where("label_id IN (?)", labelIDs). - Delete(&database.FaultInjectionLabel{}) + Delete(&model.FaultInjectionLabel{}) if err := result.Error; err != nil { return 0, fmt.Errorf("failed to remove injection-label associations for labels %v: %w", labelIDs, err) } @@ -412,7 +365,7 @@ func RemoveInjectionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { // RemoveLabelsFromInjection removes all label associations from a specific injection func RemoveLabelsFromInjection(db *gorm.DB, injectionID int) error { if err := db.Where("fault_injection_id = ?", injectionID). - Delete(&database.FaultInjectionLabel{}).Error; err != nil { + Delete(&model.FaultInjectionLabel{}).Error; err != nil { return fmt.Errorf("failed to remove all labels from injection %d: %w", injectionID, err) } return nil @@ -425,7 +378,7 @@ func RemoveLabelsFromInjections(db *gorm.DB, injectionIDs []int) error { } if err := db.Where("fault_injection_id IN (?)", injectionIDs). - Delete(&database.FaultInjectionLabel{}).Error; err != nil { + Delete(&model.FaultInjectionLabel{}).Error; err != nil { return fmt.Errorf("failed to remove all labels from injections %v: %w", injectionIDs, err) } return nil @@ -434,7 +387,7 @@ func RemoveLabelsFromInjections(db *gorm.DB, injectionIDs []int) error { // ListInjectionIDsByLabels gets injection IDs associated with all specified labels func ListInjectionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) ([]int, error) { var injectionIDs []int - query := db.Model(&database.FaultInjection{}). + query := db.Model(&model.FaultInjection{}). Select("DISTINCT fault_injections.id"). Joins("JOIN fault_injection_labels fil ON fil.fault_injection_id = fault_injections.id"). Joins("JOIN labels ON labels.id = fil.label_id"). @@ -461,18 +414,18 @@ func ListInjectionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) } // ListInjectionLabels gets labels for multiple injections in batch -func ListInjectionLabels(db *gorm.DB, injectionIDs []int) (map[int][]database.Label, error) { +func ListInjectionLabels(db *gorm.DB, injectionIDs []int) (map[int][]model.Label, error) { if len(injectionIDs) == 0 { return nil, nil } type injectionLabelResult struct { - database.Label + model.Label InjectionID int `gorm:"column:injection_id"` } var flatResults []injectionLabelResult - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Joins("JOIN fault_injection_labels fil ON fil.label_id = labels.id"). Where("fil.fault_injection_id IN (?)", injectionIDs). Select("labels.*, fil.fault_injection_id as injection_id"). @@ -480,9 +433,9 @@ func ListInjectionLabels(db *gorm.DB, injectionIDs []int) (map[int][]database.La return nil, fmt.Errorf("failed to batch query fault injection labels: %w", err) } - labelsMap := make(map[int][]database.Label) + labelsMap := make(map[int][]model.Label) for _, id := range injectionIDs { - labelsMap[id] = []database.Label{} + labelsMap[id] = []model.Label{} } for _, res := range flatResults { @@ -522,8 +475,8 @@ func ListInjectionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error } // ListInjectionLabelsByInjectionID gets labels for a specific injection -func ListLabelsByInjectionID(db *gorm.DB, injectionID int) ([]database.Label, error) { - var labels []database.Label +func ListLabelsByInjectionID(db *gorm.DB, injectionID int) ([]model.Label, error) { + var labels []model.Label if err := db.Table("labels"). Joins("JOIN fault_injection_labels fil ON labels.id = fil.label_id"). Where("fil.fault_injection_id = ?", injectionID). @@ -550,12 +503,12 @@ func ListLabelIDsByKeyAndInjectionID(db *gorm.DB, injectionID int, keys []string } // ListInjectionsByProjectID retrieves fault injections for a specific project with pagination -func ListInjectionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]database.FaultInjection, int64, error) { - var injections []database.FaultInjection +func ListInjectionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]model.FaultInjection, int64, error) { + var injections []model.FaultInjection var total int64 // Base query with JOIN and WHERE conditions - baseQuery := db.Model(&database.FaultInjection{}). + baseQuery := db.Model(&model.FaultInjection{}). Joins("JOIN tasks ON tasks.id = fault_injections.task_id"). Joins("JOIN traces on traces.id = tasks.trace_id"). Where("traces.project_id = ? AND fault_injections.status != ?", projectID, consts.CommonDeleted) diff --git a/src/repository/label.go b/src/repository/label.go index 12cea053..7e28b169 100644 --- a/src/repository/label.go +++ b/src/repository/label.go @@ -5,8 +5,8 @@ import ( "fmt" "aegis/consts" - "aegis/database" - "aegis/dto" + "aegis/model" + labelmodule "aegis/module/label" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -21,7 +21,7 @@ const ( // ===================================================================== // BatchCreateLabels inserts multiple labels -func BatchCreateLabels(db *gorm.DB, labels []database.Label) error { +func BatchCreateLabels(db *gorm.DB, labels []model.Label) error { if len(labels) == 0 { return nil } @@ -39,7 +39,7 @@ func BatchDeleteLabels(db *gorm.DB, labelIDs []int) error { return nil } - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). Update("status", consts.CommonDeleted).Error; err != nil { return fmt.Errorf("failed to batch delete labels: %w", err) @@ -54,7 +54,7 @@ func BatchIncreaseLabelUsages(db *gorm.DB, labelIDs []int, increament int) error } expr := gorm.Expr("usage_count + ?", increament) - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Where("id IN (?)", labelIDs). UpdateColumn("usage_count", expr).Error; err != nil { return fmt.Errorf("failed to batch increase label usages: %w", err) @@ -70,7 +70,7 @@ func BatchDecreaseLabelUsages(db *gorm.DB, labelIDs []int, decrement int) error } expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) - if err := db.Model(&database.Label{}). + if err := db.Model(&model.Label{}). Where("id IN (?)", labelIDs). Clauses(clause.Returning{}). UpdateColumn("usage_count", expr).Error; err != nil { @@ -80,7 +80,7 @@ func BatchDecreaseLabelUsages(db *gorm.DB, labelIDs []int, decrement int) error } // BatchUpdateLabels updates multiple labels -func BatchUpdateLabels(db *gorm.DB, labels []database.Label) error { +func BatchUpdateLabels(db *gorm.DB, labels []model.Label) error { if len(labels) == 0 { return fmt.Errorf("no labels to update") } @@ -93,7 +93,7 @@ func BatchUpdateLabels(db *gorm.DB, labels []database.Label) error { } // CreateLabel creates a label -func CreateLabel(db *gorm.DB, label *database.Label) error { +func CreateLabel(db *gorm.DB, label *model.Label) error { if err := db.Omit(labelKeyOmitFields).Create(label).Error; err != nil { return fmt.Errorf("failed to create label: %w", err) } @@ -102,7 +102,7 @@ func CreateLabel(db *gorm.DB, label *database.Label) error { // DeleteLabel soft deletes a label by setting its status to deleted func DeleteLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Model(&database.Label{}). + result := db.Model(&model.Label{}). Where("id = ? AND status != ?", labelID, consts.CommonDeleted). Update("status", consts.CommonDeleted) if result.Error != nil { @@ -112,8 +112,8 @@ func DeleteLabel(db *gorm.DB, labelID int) (int64, error) { } // GetLabelByID gets label by ID -func GetLabelByID(db *gorm.DB, id int) (*database.Label, error) { - var label database.Label +func GetLabelByID(db *gorm.DB, id int) (*model.Label, error) { + var label model.Label if err := db.First(&label, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("label with id %d not found", id) @@ -124,7 +124,7 @@ func GetLabelByID(db *gorm.DB, id int) (*database.Label, error) { } // GetLabelByKeyAndValue gets label by key and value -func GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.StatusType) (*database.Label, error) { +func GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.StatusType) (*model.Label, error) { query := db.Where("label_key = ? AND label_value = ?", key, value) if len(status) == 0 { @@ -135,7 +135,7 @@ func GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.Stat query = query.Where("status IN (?)", status) } - var label database.Label + var label model.Label if err := query.First(&label).Error; err != nil { return nil, fmt.Errorf("failed to get label: %w", err) } @@ -144,11 +144,11 @@ func GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.Stat } // ListLabels gets the label list -func ListLabels(db *gorm.DB, limit, offset int, filterOptions *dto.ListLabelFilters) ([]database.Label, int64, error) { - var labels []database.Label +func ListLabels(db *gorm.DB, limit, offset int, filterOptions *labelmodule.ListLabelFilters) ([]model.Label, int64, error) { + var labels []model.Label var total int64 - query := db.Model(&database.Label{}) + query := db.Model(&model.Label{}) if filterOptions.Key != "" { query = query.Where("label_key = ?", filterOptions.Key) } @@ -177,12 +177,12 @@ func ListLabels(db *gorm.DB, limit, offset int, filterOptions *dto.ListLabelFilt } // ListLabelsByConditions lists labels based on key-value conditions -func ListLabelsByConditions(db *gorm.DB, conditions []map[string]string) ([]database.Label, error) { +func ListLabelsByConditions(db *gorm.DB, conditions []map[string]string) ([]model.Label, error) { if len(conditions) == 0 { - return []database.Label{}, nil + return []model.Label{}, nil } - query := db.Model(&database.Label{}).Where("status != ?", consts.CommonDeleted) + query := db.Model(&model.Label{}).Where("status != ?", consts.CommonDeleted) orBuilder := db.Where("1 = 0") for _, condition := range conditions { @@ -198,7 +198,7 @@ func ListLabelsByConditions(db *gorm.DB, conditions []map[string]string) ([]data orBuilder = orBuilder.Or(andBuilder) } - var labels []database.Label + var labels []model.Label if err := query.Where(orBuilder).Find(&labels).Error; err != nil { return nil, fmt.Errorf("failed to list labels by conditions: %w", err) } @@ -211,7 +211,7 @@ func ListLabelIDsByConditions(db *gorm.DB, conditions []map[string]string, categ return []int{}, nil } - query := db.Model(&database.Label{}). + query := db.Model(&model.Label{}). Where("status != ? AND category = ?", consts.CommonDeleted, category) orBuilder := db.Where("1 = 0") @@ -237,12 +237,12 @@ func ListLabelIDsByConditions(db *gorm.DB, conditions []map[string]string, categ } // ListLabelsByID lists labels by their IDs -func ListLabelsByID(db *gorm.DB, labelIDs []int) ([]database.Label, error) { +func ListLabelsByID(db *gorm.DB, labelIDs []int) ([]model.Label, error) { if len(labelIDs) == 0 { - return []database.Label{}, nil + return []model.Label{}, nil } - var labels []database.Label + var labels []model.Label if err := db. Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). Find(&labels).Error; err != nil { @@ -252,8 +252,8 @@ func ListLabelsByID(db *gorm.DB, labelIDs []int) ([]database.Label, error) { } // ListLabelsGroupByCategory lists labels grouped by their categories -func ListLabelsGroupByCategory(db *gorm.DB) (map[consts.LabelCategory][]database.Label, error) { - var labels []database.Label +func ListLabelsGroupByCategory(db *gorm.DB) (map[consts.LabelCategory][]model.Label, error) { + var labels []model.Label if err := db. Where("status != ?", consts.CommonDeleted). Order("usage_count DESC, created_at DESC"). @@ -261,7 +261,7 @@ func ListLabelsGroupByCategory(db *gorm.DB) (map[consts.LabelCategory][]database return nil, fmt.Errorf("failed to list labels: %w", err) } - groupedLabels := make(map[consts.LabelCategory][]database.Label) + groupedLabels := make(map[consts.LabelCategory][]model.Label) for _, label := range labels { groupedLabels[label.Category] = append(groupedLabels[label.Category], label) } @@ -270,10 +270,10 @@ func ListLabelsGroupByCategory(db *gorm.DB) (map[consts.LabelCategory][]database } // SearchLabels searches for labels -func SearchLabels(keyword string, category string, limit int) ([]database.Label, error) { - var labels []database.Label +func SearchLabels(db *gorm.DB, keyword string, category string, limit int) ([]model.Label, error) { + var labels []model.Label - query := database.DB.Model(&database.Label{}) + query := db.Model(&model.Label{}) if keyword != "" { query = query.Where("key ILIKE ? OR value ILIKE ? OR description ILIKE ?", @@ -295,7 +295,7 @@ func SearchLabels(keyword string, category string, limit int) ([]database.Label, return labels, nil } -func UpdateLabel(db *gorm.DB, label *database.Label) error { +func UpdateLabel(db *gorm.DB, label *model.Label) error { if err := db.Omit(labelKeyOmitFields).Save(label).Error; err != nil { return fmt.Errorf("failed to update label: %w", err) } diff --git a/src/repository/permission.go b/src/repository/permission.go deleted file mode 100644 index ee7fe0ba..00000000 --- a/src/repository/permission.go +++ /dev/null @@ -1,329 +0,0 @@ -package repository - -import ( - "fmt" - "time" - - "aegis/consts" - "aegis/database" - "aegis/dto" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// BatchUpsertPermissions performs batch upsert of permissions -func BatchUpsertPermissions(db *gorm.DB, perimissons []database.Permission) error { - if len(perimissons) == 0 { - return fmt.Errorf("no permissions to upsert") - } - - if err := db.Omit(commonOmitFields).Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "name"}}, - DoUpdates: clause.AssignmentColumns([]string{}), - }).Create(&perimissons).Error; err != nil { - return fmt.Errorf("failed to batch upsert permissions: %v", err) - } - - return nil -} - -// CreatePermission creates a permission -func CreatePermission(db *gorm.DB, permission *database.Permission) error { - if err := db.Omit(commonOmitFields).Create(permission).Error; err != nil { - return fmt.Errorf("failed to create permission: %w", err) - } - return nil -} - -// DeletePermission soft deletes a permission by setting its status to deleted -func DeletePermission(db *gorm.DB, permissionID int) (int64, error) { - result := db.Model(&database.Permission{}). - Where("id = ? AND status != ?", permissionID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete permission %d: %w", permissionID, result.Error) - } - return result.RowsAffected, nil -} - -// GetPermissionByID gets permission by ID -func GetPermissionByID(db *gorm.DB, id int) (*database.Permission, error) { - var permission database.Permission - if err := db.Preload("Resource").Where("id = ? and status != ?", id, consts.CommonDeleted).First(&permission).Error; err != nil { - return nil, fmt.Errorf("failed to find permission with id %d: %w", id, err) - } - return &permission, nil -} - -// GetPermissionByName gets permission by name -func GetPermissionByName(db *gorm.DB, name string) (*database.Permission, error) { - var permission database.Permission - if err := db.Preload("Resource").Where("name = ? and status != ?", name, consts.CommonDeleted).First(&permission).Error; err != nil { - return nil, fmt.Errorf("failed to find permission with name %s: %w", name, err) - } - return &permission, nil -} - -// GetPermissionsByAction gets permissions by action -func GetPermissionsByAction(db *gorm.DB, action string) ([]database.Permission, error) { - var permissions []database.Permission - if err := db.Preload("Resource"). - Where("action = ? AND status = ?", action, consts.CommonEnabled). - Order("name"). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get permissions by action: %v", err) - } - return permissions, nil -} - -// GetPermissionByActionAndResource gets permission by action and resource name -func GetPermissionByActionAndResource(db *gorm.DB, action consts.ActionName, scope consts.ResourceScope, resourceName consts.ResourceName) (*database.Permission, error) { - var permission database.Permission - if err := db. - Select("permissions.*"). - Joins("JOIN resources ON permissions.resource_id = resources.id"). - Where("permissions.action = ? AND permissions.scope= ? AND resources.name = ?", action, scope, resourceName). - Where("permissions.status != ?", consts.CommonDeleted). - First(&permission).Error; err != nil { - return nil, fmt.Errorf("failed to find permission with action %s and resource %s: %w", action, resourceName, err) - } - return &permission, nil -} - -// GetPermissionsByResource gets permissions by resource -func GetPermissionsByResource(db *gorm.DB, resourceID int) ([]database.Permission, error) { - var permissions []database.Permission - if err := db. - Where("resource_id = ? AND status = ?", resourceID, consts.CommonEnabled). - Order("action"). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get permissions by resource: %v", err) - } - return permissions, nil -} - -// GetSystemPermissions gets system permissions -func GetSystemPermissions(db *gorm.DB) ([]database.Permission, error) { - var permissions []database.Permission - if err := db.Preload("Resource"). - Where("is_system = ? AND status = ?", true, consts.CommonEnabled). - Order("resource_id, action"). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get system permissions: %v", err) - } - return permissions, nil -} - -// ListPermissions gets permission list -func ListPermissions(db *gorm.DB, limit, offset int, action consts.ActionName, isSystem *bool, status *consts.StatusType) ([]database.Permission, int64, error) { - var permissions []database.Permission - var total int64 - - query := db.Model(&database.Permission{}) - if action != "" { - query = query.Where("action = ?", action) - } - if isSystem != nil { - query = query.Where("is_system = ?", *isSystem) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count permissions: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&permissions).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list permissions: %v", err) - } - - return permissions, total, nil -} - -// ListPermissionsByID lists permissions by their IDs -func ListPermissionsByID(db *gorm.DB, permissionIDs []int) ([]database.Permission, error) { - if len(permissionIDs) == 0 { - return []database.Permission{}, nil - } - - var permissions []database.Permission - if err := db. - Where("id IN (?) AND status = ?", permissionIDs, consts.CommonEnabled). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to query permissions: %w", err) - } - - return permissions, nil -} - -// ListPermissionsByNames lists permissions by their names -func ListPermissionsByNames(db *gorm.DB, permissionNames []string) ([]database.Permission, error) { - if len(permissionNames) == 0 { - return []database.Permission{}, nil - } - - var permissions []database.Permission - if err := db. - Where("name IN (?) AND status = ?", permissionNames, consts.CommonEnabled). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to query permissions: %w", err) - } - - return permissions, nil -} - -// ListSystemPermissions gets system permissions -func ListSystemPermissions(db *gorm.DB) ([]database.Permission, error) { - var permissions []database.Permission - if err := db.Where("is_system = ? AND status = ?", true, consts.CommonEnabled). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get system permissions: %v", err) - } - return permissions, nil -} - -// UpdatePermission updates permission information -func UpdatePermission(db *gorm.DB, permission *database.Permission) error { - if err := db.Omit(commonOmitFields).Save(permission).Error; err != nil { - return fmt.Errorf("failed to update permission: %w", err) - } - return nil -} - -// GetPermissionRoles retrieves all roles that have a specific permission -func ListRolesByPermissionID(db *gorm.DB, permissionID int) ([]database.Role, error) { - var roles []database.Role - - if err := db.Table("roles"). - Joins("JOIN role_permissions ON roles.id = role_permissions.role_id"). - Where("role_permissions.permission_id = ? AND roles.status != ?", permissionID, consts.CommonDeleted). - Find(&roles).Error; err != nil { - return nil, fmt.Errorf("failed to get roles for permission %d: %v", permissionID, err) - } - - return roles, nil -} - -// CheckUserHasPermission checks if user has specific permission through various sources -func CheckUserHasPermission(db *gorm.DB, params *dto.CheckPermissionParams, permissionID int) (bool, error) { - // Build direct permission query - directQuery := buildDirectPermissionQuery(db, params.UserID, permissionID, params.ProjectID, params.ContainerID, params.DatasetID) - - // Build global role permission query - globalRoleQuery := buildGlobalRolePermissionQuery(db, params.UserID, permissionID) - - // Combine direct and global role permissions - finalQuery := db.Table("(? UNION ALL ?) as base", directQuery, globalRoleQuery) - - // Add team role permissions if teamID is provided - if params.TeamID != nil { - teamRoleQuery := buildTeamRolePermissionQuery(db, params.UserID, permissionID, *params.TeamID) - finalQuery = db.Table("(? UNION ALL ?) as combined", finalQuery, teamRoleQuery) - } - - // Add project role permissions if projectID is provided - if params.ProjectID != nil { - projectRoleQuery := buildProjectRolePermissionQuery(db, params.UserID, permissionID, *params.ProjectID) - finalQuery = db.Table("(? UNION ALL ?) as combined", finalQuery, projectRoleQuery) - } - - // Add container role permissions if containerID is provided - if params.ContainerID != nil { - containerRoleQuery := buildContainerRolePermissionQuery(db, params.UserID, permissionID, *params.ContainerID) - finalQuery = db.Table("(? UNION ALL ?) as combined", finalQuery, containerRoleQuery) - } - - // Add dataset role permissions if datasetID is provided - if params.DatasetID != nil { - datasetRoleQuery := buildDatasetRolePermissionQuery(db, params.UserID, permissionID, *params.DatasetID) - finalQuery = db.Table("(? UNION ALL ?) as combined", finalQuery, datasetRoleQuery) - } - - var count int64 - if err := finalQuery.Limit(1).Count(&count).Error; err != nil { - return false, fmt.Errorf("failed to check user permission: %w", err) - } - - return count > 0, nil -} - -// buildDirectPermissionQuery builds query for direct user permissions -func buildDirectPermissionQuery(db *gorm.DB, userID int, permissionID int, projectID, containerID, datasetID *int) *gorm.DB { - query := db. - Select("up.permission_id"). - Table("user_permissions up"). - Where("up.user_id = ? AND up.permission_id = ?", userID, permissionID). - Where("up.grant_type = ?", consts.GrantTypeGrant). - Where("up.expires_at IS NULL OR up.expires_at > ?", time.Now()) - - if projectID != nil { - query = query.Where("up.project_id IS NULL OR up.project_id = ?", *projectID) - } else { - query = query.Where("up.project_id IS NULL") - } - - if containerID != nil { - query = query.Where("up.container_id IS NULL OR up.container_id = ?", *containerID) - } else { - query = query.Where("up.container_id IS NULL") - } - - if datasetID != nil { - query = query.Where("up.dataset_id IS NULL OR up.dataset_id = ?", *datasetID) - } else { - query = query.Where("up.dataset_id IS NULL") - } - - return query -} - -// buildGlobalRolePermissionQuery builds query for global role permissions -func buildGlobalRolePermissionQuery(db *gorm.DB, userID int, permissionID int) *gorm.DB { - return db. - Select("rp.permission_id"). - Table("role_permissions rp"). - Joins("JOIN user_roles ur ON rp.role_id = ur.role_id"). - Where("ur.user_id = ? AND rp.permission_id = ?", userID, permissionID) -} - -// buildTeamRolePermissionQuery builds query for team-specific role permissions -func buildTeamRolePermissionQuery(db *gorm.DB, userID int, permissionID int, teamID int) *gorm.DB { - return db. - Select("rp.permission_id"). - Table("role_permissions rp"). - Joins("JOIN user_teams ut ON rp.role_id = ut.role_id"). - Where("ut.user_id = ? AND ut.team_id = ? AND rp.permission_id = ?", userID, teamID, permissionID). - Where("ut.status = ?", consts.CommonEnabled) -} - -// buildProjectRolePermissionQuery builds query for project-specific role permissions -func buildProjectRolePermissionQuery(db *gorm.DB, userID int, permissionID int, projectID int) *gorm.DB { - return db. - Select("rp.permission_id"). - Table("role_permissions rp"). - Joins("JOIN user_projects upr ON rp.role_id = upr.role_id"). - Where("upr.user_id = ? AND upr.project_id = ? AND rp.permission_id = ?", userID, projectID, permissionID). - Where("upr.status = ?", consts.CommonEnabled) -} - -// buildContainerRolePermissionQuery builds query for container-specific role permissions -func buildContainerRolePermissionQuery(db *gorm.DB, userID int, permissionID int, containerID int) *gorm.DB { - return db. - Select("rp.permission_id"). - Table("role_permissions rp"). - Joins("JOIN user_containers uc ON rp.role_id = uc.role_id"). - Where("uc.user_id = ? AND uc.container_id = ? AND rp.permission_id = ?", userID, containerID, permissionID). - Where("uc.status = ?", consts.CommonEnabled) -} - -// buildDatasetRolePermissionQuery builds query for dataset-specific role permissions -func buildDatasetRolePermissionQuery(db *gorm.DB, userID int, permissionID int, datasetID int) *gorm.DB { - return db. - Select("rp.permission_id"). - Table("role_permissions rp"). - Joins("JOIN user_datasets ud ON rp.role_id = ud.role_id"). - Where("ud.user_id = ? AND ud.dataset_id = ? AND rp.permission_id = ?", userID, datasetID, permissionID). - Where("ud.status = ?", consts.CommonEnabled) -} diff --git a/src/repository/project.go b/src/repository/project.go deleted file mode 100644 index 7f21c99e..00000000 --- a/src/repository/project.go +++ /dev/null @@ -1,363 +0,0 @@ -package repository - -import ( - "fmt" - "time" - - "aegis/consts" - "aegis/database" - "aegis/dto" - - "gorm.io/gorm" -) - -const ( - projectOmitFields = "ActiveName" -) - -// ===================================================================== -// Project Repository Functions -// ===================================================================== - -// CreateProject creates a new project -func CreateProject(db *gorm.DB, project *database.Project) error { - if err := db.Omit(projectOmitFields).Create(project).Error; err != nil { - return fmt.Errorf("failed to create project: %w", err) - } - return nil -} - -// DeleteProjct soft deletes a project by setting its status to deleted -func DeleteProject(db *gorm.DB, projectID int) (int64, error) { - result := db.Model(&database.Project{}). - Where("id = ? AND status != ?", projectID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to soft delete project %d: %w", projectID, result.Error) - } - return result.RowsAffected, nil -} - -// GetProjectByID retrieves a project by its ID -func GetProjectByID(db *gorm.DB, id int) (*database.Project, error) { - var project database.Project - if err := db.Where("id = ?", id).First(&project).Error; err != nil { - return nil, fmt.Errorf("failed to find project with id %d: %w", id, err) - } - return &project, nil -} - -// GetProjectByName retrieves a project by its name -func GetProjectByName(db *gorm.DB, name string) (*database.Project, error) { - var project database.Project - if err := db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { - return nil, fmt.Errorf("failed to find project with name %s: %w", name, err) - } - return &project, nil -} - -// GetProjectUserCount gets the count of users in a project -func GetProjectUserCount(db *gorm.DB, projectID int) (int, error) { - var count int64 - if err := db.Model(&database.UserProject{}). - Where("project_id = ? AND status = ?", projectID, consts.CommonEnabled). - Count(&count).Error; err != nil { - return 0, fmt.Errorf("failed to count project users: %w", err) - } - return int(count), nil -} - -// GetUserProjectRole retrieves a user's role in a specific project -func GetUserProjectRole(db *gorm.DB, userID, projectID int) (*database.UserProject, error) { - var userProject database.UserProject - if err := db. - Preload("Role"). - Where("user_id = ? AND project_id = ? AND status = ?", userID, projectID, consts.CommonEnabled). - First(&userProject).Error; err != nil { - return nil, err - } - return &userProject, nil -} - -// ListProjects lists projects based on filter options -func ListProjects(db *gorm.DB, limit, offset int, isPublic *bool, status *consts.StatusType) ([]database.Project, int64, error) { - var projects []database.Project - var total int64 - - query := db.Model(&database.Project{}) - if isPublic != nil { - query = query.Where("is_public = ?", *isPublic) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count projects: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Find(&projects).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list projects: %w", err) - } - - return projects, total, nil -} - -// BatchGetProjectsByID retrieves multiple projects by their IDs -func ListProjectsByID(db *gorm.DB, projectIDs []int) ([]database.Project, error) { - if len(projectIDs) == 0 { - return []database.Project{}, nil - } - - var projects []database.Project - if err := db. - Where("id IN (?) AND status != ?", projectIDs, consts.CommonDeleted). - Find(&projects).Error; err != nil { - return nil, fmt.Errorf("failed to query projects: %w", err) - } - return projects, nil -} - -// BatchGetProjectStatistics retrieves statistics for multiple projects in one query -func BatchGetProjectStatistics(db *gorm.DB, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { - if len(projectIDs) == 0 { - return make(map[int]*dto.ProjectStatistics), nil - } - - statsMap := make(map[int]*dto.ProjectStatistics) - - // Initialize map with zero values - for _, id := range projectIDs { - statsMap[id] = &dto.ProjectStatistics{} - } - - // Batch query injection statistics - var injStats []struct { - ProjectID int - Count int64 - LastAt *time.Time - } - - err := db.Table("fault_injections fi"). - Select("tr.project_id, COUNT(*) as count, MAX(fi.updated_at) as last_at"). - Joins("JOIN tasks t ON fi.task_id = t.id"). - Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). - Group("tr.project_id"). - Scan(&injStats).Error - if err != nil { - return nil, fmt.Errorf("failed to batch get injection statistics: %w", err) - } - - for _, stat := range injStats { - if s, exists := statsMap[stat.ProjectID]; exists { - s.InjectionCount = int(stat.Count) - s.LastInjectionAt = stat.LastAt - } - } - - // Batch query execution statistics - var execStats []struct { - ProjectID int - Count int64 - LastAt *time.Time - } - - err = db.Table("executions e"). - Select("tr.project_id, COUNT(*) as count, MAX(e.updated_at) as last_at"). - Joins("JOIN tasks t ON e.task_id = t.id"). - Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). - Group("tr.project_id"). - Scan(&execStats).Error - if err != nil { - return nil, fmt.Errorf("failed to batch get execution statistics: %w", err) - } - - for _, stat := range execStats { - if s, exists := statsMap[stat.ProjectID]; exists { - s.ExecutionCount = int(stat.Count) - s.LastExecutionAt = stat.LastAt - } - } - - return statsMap, nil -} - -// UpdateProject updates a project -func UpdateProject(db *gorm.DB, project *database.Project) error { - if err := db.Omit(projectOmitFields).Save(project).Error; err != nil { - return fmt.Errorf("failed to update project: %w", err) - } - return nil -} - -// ===================================================================== -// ProjectLabel Repository Functions -// ===================================================================== - -// AddProjectLabels adds multiple project-label associations in a batch -func AddProjectLabels(db *gorm.DB, projectLabels []database.ProjectLabel) error { - if len(projectLabels) == 0 { - return nil - } - if err := db.Create(&projectLabels).Error; err != nil { - return fmt.Errorf("failed to add project-label associations: %w", err) - } - return nil -} - -// ClearProjectLabels removes label associations from specified projects -func ClearProjectLabels(db *gorm.DB, projectIDs []int, labelIDs []int) error { - if len(projectIDs) == 0 { - return nil - } - - query := db.Table("project_labels"). - Where("project_id IN (?)", projectIDs) - if len(labelIDs) > 0 { - query = query.Where("label_id IN (?)", labelIDs) - } - - if err := query.Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear project-label associations: %w", err) - } - return nil -} - -// RemoveLabelsFromProject removes all label associations from a specific project -func RemoveLabelsFromProject(db *gorm.DB, projectID int) error { - if err := db.Where("project_id = ?", projectID). - Delete(&database.ProjectLabel{}).Error; err != nil { - return fmt.Errorf("failed to delete all labels from project %d: %w", projectID, err) - } - return nil -} - -// RemoveProjectsFromLabel removes all project associations from a specific label -func RemoveProjectsFromLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Where("label_id = ?", labelID). - Delete(&database.ProjectLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all projects from label %d: %w", labelID, err) - } - return result.RowsAffected, nil -} - -// RemoveProjectsFromLabels removes all project associations from multiple labels -func RemoveProjectsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { - if len(labelIDs) == 0 { - return 0, nil - } - - result := db.Where("label_id IN (?)", labelIDs). - Delete(&database.ProjectLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all projects from labels %v: %w", labelIDs, err) - } - return result.RowsAffected, nil -} - -// ListProjectLabels gets labels for multiple projects in batch -func ListProjectLabels(db *gorm.DB, projectIDs []int) (map[int][]database.Label, error) { - if len(projectIDs) == 0 { - return nil, nil - } - - type projectLabelResult struct { - database.Label - projectID int `gorm:"column:project_id"` - } - - var flatResults []projectLabelResult - if err := db.Model(&database.Label{}). - Joins("JOIN project_labels pl ON pl.label_id = labels.id"). - Where("pl.project_id IN (?)", projectIDs). - Select("labels.*, pl.project_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query project labels: %w", err) - } - - labelsMap := make(map[int][]database.Label) - for _, id := range projectIDs { - labelsMap[id] = []database.Label{} - } - - for _, res := range flatResults { - label := res.Label - labelsMap[res.projectID] = append(labelsMap[res.projectID], label) - } - - return labelsMap, nil -} - -// ListProjectLabelCounts retrieves the count of projects associated with each label ID -func ListProjectLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - if len(labelIDs) == 0 { - return make(map[int]int64), nil - } - - type projectLabelResult struct { - labelID int `gorm:"column:label_id"` - count int64 - } - - var results []projectLabelResult - if err := db.Model(&database.ProjectLabel{}). - Select("label_id, count(label_id) as count"). - Where("label_id IN (?)", labelIDs). - Group("label_id"). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to count project-label associations: %w", err) - } - - countMap := make(map[int]int64, len(results)) - for _, result := range results { - countMap[result.labelID] = result.count - } - - return countMap, nil -} - -// ListLabelsByProjectID lists all labels associated with a specific project -func ListLabelsByProjectID(db *gorm.DB, projectID int) ([]database.Label, error) { - var labels []database.Label - if err := db.Model(&database.Label{}). - Joins("JOIN project_labels pl ON pl.label_id = labels.id"). - Where("pl.project_id = ?", projectID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels for project %d: %w", projectID, err) - } - return labels, nil -} - -// GetProjectTeamID retrieves the team ID for a project -func GetProjectTeamID(db *gorm.DB, projectID int) (int, error) { - var teamID *int - if err := db.Model(&database.Project{}). - Select("team_id"). - Where("id = ? AND status != ?", projectID, consts.CommonDeleted). - Scan(&teamID).Error; err != nil { - return 0, fmt.Errorf("failed to get team ID for project %d: %w", projectID, err) - } - if teamID == nil { - return 0, fmt.Errorf("project %d has no associated team", projectID) - } - return *teamID, nil -} - -// ListLabelIDsByKeyAndProjectID finds label IDs by keys associated with a specific project -func ListLabelIDsByKeyAndProjectID(db *gorm.DB, projectID int, keys []string) ([]int, error) { - var labelIDs []int - - err := db.Table("labels l"). - Select("l.id"). - Joins("JOIN project_labels pl ON pl.label_id = l.id"). - Where("pl.project_id = ? AND l.label_key IN (?)", projectID, keys). - Pluck("l.id", &labelIDs).Error - if err != nil { - return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) - } - - return labelIDs, nil -} diff --git a/src/repository/query_builder.go b/src/repository/query_builder.go index dae103a7..7165465e 100644 --- a/src/repository/query_builder.go +++ b/src/repository/query_builder.go @@ -62,6 +62,10 @@ func (qb *SearchQueryBuilder[F]) applyIncludes(includes []string) { } } +func (qb *SearchQueryBuilder[F]) ApplyIncludes(includes []string) { + qb.applyIncludes(includes) +} + // applyIncludeFields includes specified fields in the query func (qb *SearchQueryBuilder[F]) applyIncludeFields(includeFields []string) { for _, field := range includeFields { @@ -69,6 +73,10 @@ func (qb *SearchQueryBuilder[F]) applyIncludeFields(includeFields []string) { } } +func (qb *SearchQueryBuilder[F]) ApplyIncludeFields(includeFields []string) { + qb.applyIncludeFields(includeFields) +} + // applyExcludeFields excludes specified fields from the query func (qb *SearchQueryBuilder[F]) applyExcludeFields(excludeFields []string, modelType interface{}) { // Get all fields from model type @@ -107,6 +115,10 @@ func (qb *SearchQueryBuilder[F]) applyExcludeFields(excludeFields []string, mode } } +func (qb *SearchQueryBuilder[F]) ApplyExcludeFields(excludeFields []string, modelType interface{}) { + qb.applyExcludeFields(excludeFields, modelType) +} + // applyKeywordSearch applies general keyword search across searchable fields func (qb *SearchQueryBuilder[F]) applyKeywordSearch(keyword string, modelType interface{}) { // Get searchable fields from model type @@ -245,6 +257,14 @@ func (qb *SearchQueryBuilder[F]) getCount() (int64, error) { return count, err } +func (qb *SearchQueryBuilder[F]) GetCount() (int64, error) { + return qb.getCount() +} + +func (qb *SearchQueryBuilder[F]) Query() *gorm.DB { + return qb.query +} + // getSearchableFields returns fields that can be searched with keywords func (qb *SearchQueryBuilder[F]) getSearchableFields(modelType interface{}) []string { // This is a simplified implementation @@ -293,37 +313,6 @@ func (qb *SearchQueryBuilder[F]) sanitizeFieldName(field string) string { return field } -// ExecuteSearch executes a complete search operation -func ExecuteSearch[T any, F ~string](db *gorm.DB, searchReq *dto.SearchReq[F], modelType T, allowedSortFields map[F]string) ([]T, int64, error) { - qb := NewSearchQueryBuilder(db, allowedSortFields) - - qb.applyIncludes(searchReq.Includes) - qb.applyIncludeFields(searchReq.IncludeFields) - qb.applyExcludeFields(searchReq.ExcludeFields, modelType) - - // Pass typed Sort/GroupBy directly — no string conversion needed, whitelist lookup uses typed key - qb.ApplySearchReq(searchReq.Filters, searchReq.Keyword, searchReq.Sort, searchReq.GroupBy, modelType) - - // Get total count - total, err := qb.getCount() - if err != nil { - return nil, 0, fmt.Errorf("failed to get count: %w", err) - } - - if searchReq.Size != 0 && searchReq.Page != 0 { - qb.applyPagination(&searchReq.PaginationReq) - } - - // Apply pagination and execute query - var items []T - err = qb.query.Find(&items).Error - if err != nil { - return nil, 0, fmt.Errorf("failed to execute search query: %w", err) - } - - return items, total, nil -} - // resolveMultiValues returns the effective []string for IN/NOT IN operators. // It prefers filter.Values when populated; otherwise it tries to parse filter.Value // as a JSON array (e.g. "[\"a\",\"b\"]" or "[1,2]"). diff --git a/src/repository/resource.go b/src/repository/resource.go deleted file mode 100644 index 87a179fe..00000000 --- a/src/repository/resource.go +++ /dev/null @@ -1,113 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// BatchUpsertResources upserts multiple resources -func BatchUpsertResources(db *gorm.DB, resources []database.Resource) error { - if len(resources) == 0 { - return fmt.Errorf("no resources to upsert") - } - - if err := db.Omit(commonOmitFields).Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "name"}}, - DoUpdates: clause.AssignmentColumns([]string{}), - }).Create(&resources).Error; err != nil { - return fmt.Errorf("failed to batch upsert resources: %w", err) - } - - return nil -} - -// GetResourceByID gets resource by ID -func GetResourceByID(db *gorm.DB, id int) (*database.Resource, error) { - var resource database.Resource - if err := db.Where("id = ? and status != ?", id, consts.CommonDeleted).First(&resource).Error; err != nil { - return nil, fmt.Errorf("failed to find resource with id %d: %w", id, err) - } - return &resource, nil -} - -// GetResourceByName gets resource by name -func GetResourceByName(db *gorm.DB, name consts.ResourceName) (*database.Resource, error) { - var resource database.Resource - if err := db. - Where("name = ? and status != ?", name, consts.CommonDeleted). - First(&resource).Error; err != nil { - return nil, fmt.Errorf("failed to find resource with name %s: %w", name, err) - } - return &resource, nil -} - -// ListResources gets resource list -func ListResources(db *gorm.DB, limit, offset int, resourceType *consts.ResourceType, category *consts.ResourceCategory) ([]database.Resource, int64, error) { - var resources []database.Resource - var total int64 - - query := database.DB.Model(&database.Resource{}).Preload("Parent") - if resourceType != nil { - query = query.Where("type = ?", resourceType) - } - if category != nil { - query = query.Where("category = ?", category) - } - - // Get total count - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count resources: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Find(&resources).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list resources: %v", err) - } - - return resources, total, nil -} - -// ListResourcesByNames lists resources by names -func ListResourcesByNames(db *gorm.DB, names []consts.ResourceName) ([]database.Resource, error) { - if len(names) == 0 { - return nil, fmt.Errorf("no resource names provided") - } - - var resources []database.Resource - if err := db.Where("name IN (?)", names). - Find(&resources).Error; err != nil { - return nil, fmt.Errorf("failed to list resources by names: %v", err) - } - - return resources, nil -} - -// SearchResources searches resources -func SearchResources(keyword string, resourceType string, category string) ([]database.Resource, error) { - var resources []database.Resource - - query := database.DB.Model(&database.Resource{}).Where("status = 1") - - if keyword != "" { - query = query.Where("name ILIKE ? OR display_name ILIKE ? OR description ILIKE ?", - "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") - } - - if resourceType != "" { - query = query.Where("type = ?", resourceType) - } - - if category != "" { - query = query.Where("category = ?", category) - } - - if err := query.Order("name").Find(&resources).Error; err != nil { - return nil, fmt.Errorf("failed to search resources: %v", err) - } - - return resources, nil -} diff --git a/src/repository/role.go b/src/repository/role.go deleted file mode 100644 index 615c04b2..00000000 --- a/src/repository/role.go +++ /dev/null @@ -1,174 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// BatchUpsertRoles performs batch upsert of roles -func BatchUpsertRoles(db *gorm.DB, roles []database.Role) error { - if len(roles) == 0 { - return fmt.Errorf("no roles to upsert") - } - - if err := db.Omit(commonOmitFields).Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "name"}}, - DoUpdates: clause.AssignmentColumns([]string{}), - }, - ).Create(&roles).Error; err != nil { - return fmt.Errorf("failed to batch upsert roles: %v", err) - } - - return nil -} - -// CreateRole creates a role -func CreateRole(db *gorm.DB, role *database.Role) error { - if err := db.Omit(commonOmitFields).Create(role).Error; err != nil { - return fmt.Errorf("failed to create role: %w", err) - } - return nil -} - -// DeleteRole soft deletes a role by setting its status to deleted -func DeleteRole(db *gorm.DB, roleID int) (int64, error) { - result := db.Model(&database.Role{}). - Where("id = ? AND status != ?", roleID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete role %d: %w", roleID, result.Error) - } - return result.RowsAffected, nil -} - -// GetRoleByID gets role by ID -func GetRoleByID(db *gorm.DB, id int) (*database.Role, error) { - var role database.Role - if err := db.Where("id = ? and status != ?", id, consts.CommonDeleted).First(&role).Error; err != nil { - return nil, fmt.Errorf("failed to find role with id %d: %w", id, err) - } - return &role, nil -} - -// GetRoleByName gets role by name -func GetRoleByName(db *gorm.DB, name string) (*database.Role, error) { - var role database.Role - if err := db. - Where("name = ? and status != ?", name, consts.CommonDeleted). - First(&role).Error; err != nil { - return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) - } - return &role, nil -} - -// GetRolePermissions gets role permissions -func GetRolePermissions(db *gorm.DB, roleID int) ([]database.Permission, error) { - var permissions []database.Permission - if err := db.Table("permissions"). - Joins("JOIN role_permissions ON permissions.id = role_permissions.permission_id"). - Where("role_permissions.role_id = ? AND permissions.status = ?", roleID, consts.CommonEnabled). - Find(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get role permissions: %v", err) - } - return permissions, nil -} - -// ListRoles gets role list -func ListRoles(db *gorm.DB, limit, offset int, isSystem *bool, status *consts.StatusType) ([]database.Role, int64, error) { - var roles []database.Role - var total int64 - - query := db.Model(&database.Role{}) - if isSystem != nil { - query = query.Where("is_system = ?", *isSystem) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count roles: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&roles).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list roles: %v", err) - } - - return roles, total, nil -} - -// ListRolesByIDs gets roles by a list of IDs -func ListRolesByIDs(db *gorm.DB, roleIDs []int) ([]database.Role, error) { - var roles []database.Role - if err := db.Where("id IN (?) AND status = ?", roleIDs, consts.CommonEnabled). - Find(&roles).Error; err != nil { - return nil, fmt.Errorf("failed to list roles by IDs: %v", err) - } - return roles, nil -} - -// ListSystemRoles gets system roles -func ListSystemRoles(db *gorm.DB) ([]database.Role, error) { - var roles []database.Role - if err := db.Where("is_system = ? AND status = ?", true, consts.CommonEnabled). - Order("created_at ASC").Find(&roles).Error; err != nil { - return nil, fmt.Errorf("failed to get system roles: %v", err) - } - return roles, nil -} - -// UpdateRole updates role information -func UpdateRole(db *gorm.DB, role *database.Role) error { - if err := db.Omit(commonOmitFields).Save(role).Error; err != nil { - return fmt.Errorf("failed to update role: %w", err) - } - return nil -} - -// ===================== Role-Permission ===================== - -// BatchCreateRolePermissions creates multiple role-permission associations in a batch -func BatchCreateRolePermissions(db *gorm.DB, rolePermissions []database.RolePermission) error { - if len(rolePermissions) == 0 { - return nil - } - if err := db.Create(&rolePermissions).Error; err != nil { - return fmt.Errorf("failed to batch create role permissions: %w", err) - } - return nil -} - -// BatchDeleteRolePermisssions deletes multiple role-permission associations in a batch -func BatchDeleteRolePermisssions(db *gorm.DB, roleID int, permissionIDs []int) error { - if len(permissionIDs) == 0 { - return nil - } - if err := db.Where("role_id = ? AND permission_id IN (?)", roleID, permissionIDs). - Delete(&database.RolePermission{}).Error; err != nil { - return fmt.Errorf("failed to batch delete role permissions: %w", err) - } - return nil -} - -// RemoveRolesFromPermission deletes all role-permission associations associated with a given permission -func RemoveRolesFromPermission(db *gorm.DB, permissionID int) error { - if err := db.Where("permission_id = ?", permissionID). - Delete(&database.RolePermission{}).Error; err != nil { - return fmt.Errorf("failed to remove all roles from permission: %w", err) - } - return nil -} - -// RemovePermissionsFromRole deletes all role-permission associations associated with a given role -func RemovePermissionsFromRole(db *gorm.DB, roleID int) error { - if err := db.Where("role_id = ?", roleID). - Delete(&database.RolePermission{}).Error; err != nil { - return fmt.Errorf("failed to remove all permissions from role: %w", err) - } - return nil -} diff --git a/src/database/scope.go b/src/repository/scope.go similarity index 82% rename from src/database/scope.go rename to src/repository/scope.go index 506c4eb4..0d57073a 100644 --- a/src/database/scope.go +++ b/src/repository/scope.go @@ -1,4 +1,4 @@ -package database +package repository import ( "fmt" @@ -6,7 +6,7 @@ import ( "gorm.io/gorm" ) -// Fuzzy search Scope +// KeywordSearch applies a fuzzy keyword search across the provided fields. func KeywordSearch(keyword string, fields ...string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if keyword == "" { @@ -32,7 +32,7 @@ func CursorPaginate(lastID uint, size int) func(db *gorm.DB) *gorm.DB { } } -// Pagination Scope +// Paginate applies offset/limit pagination. func Paginate(pageNum, pageSize int) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { offset := (pageNum - 1) * pageSize @@ -40,7 +40,7 @@ func Paginate(pageNum, pageSize int) func(db *gorm.DB) *gorm.DB { } } -// Sort Scope +// Sort applies ordering with a default fallback. func Sort(sort string) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if sort == "" { diff --git a/src/repository/system.go b/src/repository/system.go deleted file mode 100644 index 226a5020..00000000 --- a/src/repository/system.go +++ /dev/null @@ -1,95 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" -) - -func ListSystems(db *gorm.DB, limit, offset int) ([]database.System, int64, error) { - var systems []database.System - var total int64 - - query := db.Model(&database.System{}). - Where("status != ?", consts.CommonDeleted) - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count systems: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&systems).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list systems: %w", err) - } - - return systems, total, nil -} - -func GetSystemByID(db *gorm.DB, id int) (*database.System, error) { - var system database.System - if err := db. - Where("id = ? AND status != ?", id, consts.CommonDeleted). - First(&system).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) - } - return nil, fmt.Errorf("failed to find system with id %d: %w", id, err) - } - return &system, nil -} - -func GetSystemByName(db *gorm.DB, name string) (*database.System, error) { - var system database.System - if err := db. - Where("name = ? AND status != ?", name, consts.CommonDeleted). - First(&system).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("system with name %s: %w", name, consts.ErrNotFound) - } - return nil, fmt.Errorf("failed to find system with name %s: %w", name, err) - } - return &system, nil -} - -func CreateSystem(db *gorm.DB, system *database.System) error { - if err := db.Create(system).Error; err != nil { - return fmt.Errorf("failed to create system: %w", err) - } - return nil -} - -func UpdateSystem(db *gorm.DB, id int, updates map[string]interface{}) error { - result := db.Model(&database.System{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Updates(updates) - if err := result.Error; err != nil { - return fmt.Errorf("failed to update system with id %d: %w", id, err) - } - if result.RowsAffected == 0 { - return fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) - } - return nil -} - -func DeleteSystem(db *gorm.DB, id int) error { - result := db.Model(&database.System{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return fmt.Errorf("failed to delete system with id %d: %w", id, err) - } - if result.RowsAffected == 0 { - return fmt.Errorf("system with id %d: %w", id, consts.ErrNotFound) - } - return nil -} - -func ListEnabledSystems(db *gorm.DB) ([]database.System, error) { - var systems []database.System - if err := db.Where("status = ?", consts.CommonEnabled).Find(&systems).Error; err != nil { - return nil, fmt.Errorf("failed to list enabled systems: %w", err) - } - return systems, nil -} diff --git a/src/repository/system_metadata.go b/src/repository/system_metadata.go deleted file mode 100644 index 9e5124c6..00000000 --- a/src/repository/system_metadata.go +++ /dev/null @@ -1,73 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/database" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -func GetSystemMetadata(db *gorm.DB, systemName, metadataType, serviceName string) (*database.SystemMetadata, error) { - var meta database.SystemMetadata - if err := db. - Where("system_name = ? AND metadata_type = ? AND service_name = ?", systemName, metadataType, serviceName). - First(&meta).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, fmt.Errorf("failed to get system metadata: %w", err) - } - return &meta, nil -} - -func ListSystemMetadata(db *gorm.DB, systemName, metadataType string) ([]database.SystemMetadata, error) { - var metas []database.SystemMetadata - query := db.Where("system_name = ?", systemName) - if metadataType != "" { - query = query.Where("metadata_type = ?", metadataType) - } - if err := query.Find(&metas).Error; err != nil { - return nil, fmt.Errorf("failed to list system metadata: %w", err) - } - return metas, nil -} - -func UpsertSystemMetadata(db *gorm.DB, meta *database.SystemMetadata) error { - if err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "system_name"}, {Name: "metadata_type"}, {Name: "service_name"}}, - DoUpdates: clause.AssignmentColumns([]string{"data", "updated_at"}), - }).Create(meta).Error; err != nil { - // Fallback: try find and update - var existing database.SystemMetadata - if findErr := db.Where("system_name = ? AND metadata_type = ? AND service_name = ?", - meta.SystemName, meta.MetadataType, meta.ServiceName).First(&existing).Error; findErr == nil { - return db.Model(&existing).Updates(map[string]interface{}{ - "data": meta.Data, - }).Error - } - return fmt.Errorf("failed to upsert system metadata: %w", err) - } - return nil -} - -func DeleteSystemMetadata(db *gorm.DB, systemName string) error { - if err := db.Where("system_name = ?", systemName).Delete(&database.SystemMetadata{}).Error; err != nil { - return fmt.Errorf("failed to delete system metadata for %s: %w", systemName, err) - } - return nil -} - -func ListServiceNames(db *gorm.DB, systemName, metadataType string) ([]string, error) { - var names []string - query := db.Model(&database.SystemMetadata{}). - Where("system_name = ?", systemName) - if metadataType != "" { - query = query.Where("metadata_type = ?", metadataType) - } - if err := query.Distinct("service_name").Pluck("service_name", &names).Error; err != nil { - return nil, fmt.Errorf("failed to list service names: %w", err) - } - return names, nil -} diff --git a/src/repository/task.go b/src/repository/task.go deleted file mode 100644 index 00236e01..00000000 --- a/src/repository/task.go +++ /dev/null @@ -1,447 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "aegis/client" - "aegis/consts" - "aegis/database" - "aegis/dto" - - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// ===================== Task Redis ===================== - -// Redis key constants for task queues and indexes -const ( - DelayedQueueKey = "task:delayed" // Sorted set for delayed tasks - ReadyQueueKey = "task:ready" // List for ready-to-execute tasks - DeadLetterKey = "task:dead" // Sorted set for failed tasks - TaskIndexKey = "task:index" // Hash mapping task IDs to their queue - ConcurrencyLockKey = "task:concurrency_lock" // Counter for concurrency control - LastBatchInfoKey = "last_batch_info" // Key for batch processing information - MaxConcurrency = 20 // Maximum concurrent tasks -) - -// ImmediateTask - -// SubmitImmediateTask sends a task to the ready queue for immediate execution -func SubmitImmediateTask(ctx context.Context, taskData []byte, taskID string) error { - redisCli := client.GetRedisClient() - if err := redisCli.LPush(ctx, ReadyQueueKey, taskData).Err(); err != nil { - return err - } - - return redisCli.HSet(ctx, TaskIndexKey, taskID, ReadyQueueKey).Err() -} - -// GetTask retrieves a task from the ready queue with blocking -func GetTask(ctx context.Context, timeout time.Duration) (string, error) { - redisCli := client.GetRedisClient() - result, err := redisCli.BRPop(ctx, timeout, ReadyQueueKey).Result() - if err != nil { - return "", err - } - - return result[1], nil -} - -// HandleFailedTask moves a failed task to the dead letter queue -func HandleFailedTask(ctx context.Context, taskData []byte, backoffSec int) error { - deadLetterTime := time.Now().Add(time.Duration(backoffSec) * time.Second).Unix() - redisCli := client.GetRedisClient() - return redisCli.ZAdd(ctx, DeadLetterKey, redis.Z{ - Score: float64(deadLetterTime), - Member: taskData, - }).Err() -} - -// Delayed Task - -// SubmitDelayedTask sends a task to the delayed queue for future execution -func SubmitDelayedTask(ctx context.Context, taskData []byte, taskID string, executeTime int64) error { - redisCli := client.GetRedisClient() - if err := redisCli.ZAdd(ctx, DelayedQueueKey, redis.Z{ - Score: float64(executeTime), - Member: taskData, - }).Err(); err != nil { - return err - } - - return redisCli.HSet(ctx, TaskIndexKey, taskID, DelayedQueueKey).Err() -} - -// ProcessDelayedTasks moves tasks from delayed queue to ready queue when their time arrives -func ProcessDelayedTasks(ctx context.Context) ([]string, error) { - redisCli := client.GetRedisClient() - now := time.Now().Unix() - - delayedTaskScript := redis.NewScript(` - local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1]) - if #tasks > 0 then - redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) - redis.call('LPUSH', KEYS[2], unpack(tasks)) - -- Update task index - for _, task in ipairs(tasks) do - local t = cjson.decode(task) - redis.call('HSET', KEYS[3], t.task_id, KEYS[2]) - end - end - return tasks - `) - - result, err := delayedTaskScript.Run(ctx, redisCli, - []string{DelayedQueueKey, ReadyQueueKey, TaskIndexKey}, - now, - ).StringSlice() - - if err != nil && err != redis.Nil { - return nil, err - } - - return result, nil -} - -// HandleCronRescheduleFailure moves a failed cron task to the dead letter queue -func HandleCronRescheduleFailure(ctx context.Context, taskData []byte) error { - return client.GetRedisClient().ZAdd(ctx, DeadLetterKey, redis.Z{ - Score: float64(time.Now().Unix()), - Member: taskData, - }).Err() -} - -// AcquireConcurrencyLock attempts to acquire a lock for task execution -func AcquireConcurrencyLock(ctx context.Context) bool { - redisCli := client.GetRedisClient() - currentCount, _ := redisCli.Get(ctx, ConcurrencyLockKey).Int64() - if currentCount >= MaxConcurrency { - return false - } - return redisCli.Incr(ctx, ConcurrencyLockKey).Err() == nil -} - -// InitConcurrencyLock initializes the concurrency lock counter -func InitConcurrencyLock(ctx context.Context) error { - redisCli := client.GetRedisClient() - return redisCli.Set(ctx, ConcurrencyLockKey, 0, 0).Err() -} - -// ReleaseConcurrencyLock releases a lock after task execution -func ReleaseConcurrencyLock(ctx context.Context) { - redisCli := client.GetRedisClient() - if err := redisCli.Decr(ctx, ConcurrencyLockKey).Err(); err != nil { - logrus.Warnf("error releasing concurrency lock: %v", err) - } -} - -// GetTaskQueue retrieves the queue a task is in -func GetTaskQueue(ctx context.Context, taskID string) (string, error) { - return client.GetRedisClient().HGet(ctx, TaskIndexKey, taskID).Result() -} - -// ListDelayedTasks lists all tasks in the delayed queue -func ListDelayedTasks(ctx context.Context, limit int64) ([]string, error) { - delayedTasksWithScore, err := client.GetRedisZRangeByScoreWithScores(ctx, DelayedQueueKey, limit) - if err != nil { - return nil, err - } - - taskDatas := make([]string, 0, len(delayedTasksWithScore)) - for _, z := range delayedTasksWithScore { - taskData, ok := z.Member.(string) - if !ok { - return nil, fmt.Errorf("invalid delayed task data") - } - taskDatas = append(taskDatas, taskData) - } - - return taskDatas, nil -} - -// ListReadyTasks lists all tasks in the ready queue -func ListReadyTasks(ctx context.Context) ([]string, error) { - return client.GetRedisListRange(ctx, ReadyQueueKey) -} - -// RemoveFromList removes a task from a Redis list using Lua script -func RemoveFromList(ctx context.Context, key, taskID string) (bool, error) { - cli := client.GetRedisClient() - // Efficient list removal Lua script - removeFromListScript := redis.NewScript(` - local key = KEYS[1] - local taskID = ARGV[1] - local count = 0 - - for i=0, redis.call('LLEN', key)-1 do - local item = redis.call('LINDEX', key, i) - if item then - local task = cjson.decode(item) - if task.task_id == taskID then - redis.call('LSET', key, i, "__DELETED__") - count = count + 1 - end - end - end - - if count > 0 then - redis.call('LREM', key, count, "__DELETED__") - end - - return count - `) - result, err := removeFromListScript.Run(ctx, cli, []string{key}, taskID).Int() - if err != nil { - return false, fmt.Errorf("failed to remove from list: %w", err) - } - - return result > 0, nil -} - -// RemoveFromZSet removes a task from a Redis sorted set -func RemoveFromZSet(ctx context.Context, key, taskID string) bool { - cli := client.GetRedisClient() - members, err := cli.ZRangeByScore(ctx, key, &redis.ZRangeBy{ - Min: "-inf", - Max: "+inf", - }).Result() - if err != nil { - return false - } - - for _, member := range members { - var t dto.UnifiedTask - if json.Unmarshal([]byte(member), &t) == nil && t.TaskID == taskID { - if err := cli.ZRem(ctx, key, member).Err(); err != nil { - logrus.Warnf("failed to remove from ZSet: %v", err) - return false - } - return true - } - } - - return false -} - -// DeleteTaskIndex removes a task from the task index -func DeleteTaskIndex(ctx context.Context, taskID string) error { - return client.GetRedisClient().HDel(ctx, TaskIndexKey, taskID).Err() -} - -// ===================== Task Database ===================== - -// BatchDeleteTasks marks multiple tasks as deleted in batch -func BatchDeleteTasks(db *gorm.DB, taskIDs []string) error { - if len(taskIDs) == 0 { - return nil - } - - if err := db.Model(&database.Task{}). - Where("id IN (?) AND status != ?", taskIDs, consts.CommonDeleted). - Update("status", consts.CommonDeleted).Error; err != nil { - return fmt.Errorf("failed to batch delete tasks: %w", err) - } - return nil -} - -// GetTaskByID retrieves a task by its ID with preloaded associations -func GetTaskByID(db *gorm.DB, taskID string) (*database.Task, error) { - var result database.Task - if err := db. - Preload("FaultInjection.Benchmark.Container"). - Preload("FaultInjection.Pedestal.Container"). - Preload("Execution.AlgorithmVersion.Container"). - Preload("Execution.Datapack"). - Preload("Execution.DatasetVersion"). - Where("id = ? AND status != ?", taskID, consts.CommonDeleted). - First(&result).Error; err != nil { - return nil, fmt.Errorf("failed to find task with id %s: %w", taskID, err) - } - return &result, nil -} - -// GetTaskWithParentByID retrieves a task along with its parent task by ID -func GetTaskWithParentByID(db *gorm.DB, taskID string) (*database.Task, error) { - var result database.Task - if err := db. - Preload("ParentTask"). - Where("id = ? AND status != ?", taskID, consts.CommonDeleted). - First(&result).Error; err != nil { - return nil, fmt.Errorf("failed to find task with id %s: %w", taskID, err) - } - return &result, nil -} - -// GetParentTaskLevelByID retrieves the level of a parent task by its ID -func GetParentTaskLevelByID(db *gorm.DB, parentTaskID string) (int, error) { - var result database.Task - if err := db. - Select("level"). - Where("id = ? AND status != ?", parentTaskID, consts.CommonDeleted). - First(&result).Error; err != nil { - return 0, fmt.Errorf("failed to find parent task with id %s: %w", parentTaskID, err) - } - return result.Level, nil -} - -// ListTasks lists tasks based on filter and pagination with preloaded associations -func ListTasks(db *gorm.DB, limit, offset int, filterOptions *dto.ListTaskFilters) ([]database.Task, int64, error) { - var tasks []database.Task - var total int64 - - query := db.Model(&database.Task{}) - if filterOptions.Immediate != nil { - query = query.Where("immediate = ?", *filterOptions.Immediate) - } - if filterOptions.TaskType != nil { - query = query.Where("type = ?", *filterOptions.TaskType) - } - if filterOptions.TraceID != "" { - query = query.Where("trace_id = ?", filterOptions.TraceID) - } - if filterOptions.GroupID != "" { - query = query.Where("group_id = ?", filterOptions.GroupID) - } - if filterOptions.ProjectID > 0 { - query = query.Where("project_id = ?", filterOptions.ProjectID) - } - if filterOptions.State != nil { - query = query.Where("state = ?", *filterOptions.State) - } - if filterOptions.Status != nil { - query = query.Where("status = ?", *filterOptions.Status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count tasks: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&tasks).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list tasks: %w", err) - } - - return tasks, total, nil -} - -// ListTasksByTimeRange retrieves tasks created within a specific time range -func ListTasksByTimeRange(db *gorm.DB, startTime, endTime time.Time) ([]database.Task, error) { - var tasks []database.Task - err := database.DB.Model(&database.Task{}). - Where("created_at >= ? AND created_at <= ? AND status != ?", startTime, endTime, consts.CommonDeleted). - Find(&tasks).Error - return tasks, err -} - -// UpdateTaskState updates the task state in the database -func UpdateTaskState(db *gorm.DB, ctx context.Context, taskID string, state consts.TaskState) error { - return db.WithContext(ctx).Model(&database.Task{}). - Where("id = ?", taskID). - Update("state", state).Error -} - -// UpdateTaskStatus updates the task status in the database -func UpdateTaskStatus(db *gorm.DB, ctx context.Context, taskID string, status int) error { - return db.WithContext(ctx).Model(&database.Task{}). - Where("id = ?", taskID). - Update("status", status).Error -} - -// UpsertTask inserts or updates a task in the database -func UpsertTask(db *gorm.DB, task *database.Task) error { - if err := db.Clauses( - clause.OnConflict{ - Columns: []clause.Column{{Name: "id"}}, - DoUpdates: clause.AssignmentColumns([]string{ - "execute_time", - "state", - "updated_at", - }), - }, - ).Create(task).Error; err != nil { - return fmt.Errorf("failed to upsert task: %w", err) - } - return nil -} - -// GetTaskStatistics returns statistics about tasks -func GetTaskStatistics() (map[string]int64, error) { - stats := make(map[string]int64) - - // Total tasks - var total int64 - if err := database.DB.Model(&database.Task{}).Count(&total).Error; err != nil { - return nil, fmt.Errorf("failed to count total tasks: %v", err) - } - stats["total"] = total - - // Tasks by status - type StatusCount struct { - Status string `json:"status"` - Count int64 `json:"count"` - } - - var statusCounts []StatusCount - err := database.DB.Model(&database.Task{}). - Select("status, COUNT(*) as count"). - Group("status"). - Find(&statusCounts).Error - - if err != nil { - return nil, fmt.Errorf("failed to count tasks by status: %v", err) - } - - for _, sc := range statusCounts { - stats[sc.Status] = sc.Count - } - - // Tasks by type - type TypeCount struct { - Type string `json:"type"` - Count int64 `json:"count"` - } - - var typeCounts []TypeCount - err = database.DB.Model(&database.Task{}). - Select("type, COUNT(*) as count"). - Group("type"). - Find(&typeCounts).Error - - if err != nil { - return nil, fmt.Errorf("failed to count tasks by type: %v", err) - } - - for _, tc := range typeCounts { - stats[tc.Type+"_tasks"] = tc.Count - } - - return stats, nil -} - -// GetRecentTaskActivity returns task activity for the last N days -func GetRecentTaskActivity(days int) (map[string]int64, error) { - stats := make(map[string]int64) - - // Last N days activity - startDate := time.Now().AddDate(0, 0, -days) - var recentCount int64 - if err := database.DB.Model(&database.Task{}).Where("created_at >= ?", startDate).Count(&recentCount).Error; err != nil { - return nil, fmt.Errorf("failed to count recent tasks: %v", err) - } - stats[fmt.Sprintf("last_%d_days", days)] = recentCount - - // Today's tasks - today := time.Now().Truncate(24 * time.Hour) - var todayCount int64 - if err := database.DB.Model(&database.Task{}).Where("created_at >= ?", today).Count(&todayCount).Error; err != nil { - return nil, fmt.Errorf("failed to count today's tasks: %v", err) - } - stats["today"] = todayCount - - return stats, nil -} diff --git a/src/repository/team.go b/src/repository/team.go deleted file mode 100644 index f65f507e..00000000 --- a/src/repository/team.go +++ /dev/null @@ -1,257 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" -) - -const ( - teamOmitFields = "ActiveName" -) - -// ===================================================================== -// Team Repository Functions -// ===================================================================== - -// CreateTeam creates a new team -func CreateTeam(db *gorm.DB, team *database.Team) error { - if err := db.Omit(teamOmitFields).Create(team).Error; err != nil { - return fmt.Errorf("failed to create team: %w", err) - } - return nil -} - -// DeleteTeam soft deletes a team by setting its status to deleted -func DeleteTeam(db *gorm.DB, teamID int) (int64, error) { - result := db.Model(&database.Team{}). - Where("id = ? AND status != ?", teamID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to soft delete team %d: %w", teamID, result.Error) - } - return result.RowsAffected, nil -} - -// GetTeamByID retrieves a team by its ID -func GetTeamByID(db *gorm.DB, id int) (*database.Team, error) { - var team database.Team - if err := db.Where("id = ?", id).First(&team).Error; err != nil { - return nil, fmt.Errorf("failed to find team with id %d: %w", id, err) - } - return &team, nil -} - -// GetTeamByName retrieves a team by its name -func GetTeamByName(db *gorm.DB, name string) (*database.Team, error) { - var team database.Team - if err := db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&team).Error; err != nil { - return nil, fmt.Errorf("failed to find team with name %s: %w", name, err) - } - return &team, nil -} - -// GetTeamUserCount gets the count of users in a team -func GetTeamUserCount(db *gorm.DB, teamID int) (int, error) { - var count int64 - if err := db.Model(&database.UserTeam{}). - Where("team_id = ? AND status = ?", teamID, consts.CommonEnabled). - Count(&count).Error; err != nil { - return 0, fmt.Errorf("failed to count team users: %w", err) - } - return int(count), nil -} - -// GetTeamProjectCount gets the count of projects in a team -func GetTeamProjectCount(db *gorm.DB, teamID int) (int, error) { - var count int64 - if err := db.Model(&database.Project{}). - Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted). - Count(&count).Error; err != nil { - return 0, fmt.Errorf("failed to count team projects: %w", err) - } - return int(count), nil -} - -// ListTeams lists teams based on filter options -func ListTeams(db *gorm.DB, limit, offset int, isPublic *bool, status *consts.StatusType, ids []int) ([]database.Team, int64, error) { - var teams []database.Team - var total int64 - - query := db.Model(&database.Team{}) - if isPublic != nil { - query = query.Where("is_public = ?", *isPublic) - } - if status != nil { - query = query.Where("status = ?", *status) - } - if len(ids) > 0 { - query = query.Where("id IN ?", ids) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count teams: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Find(&teams).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list teams: %w", err) - } - - return teams, total, nil -} - -// UpdateTeam updates a team -func UpdateTeam(db *gorm.DB, team *database.Team) error { - if err := db.Omit(teamOmitFields).Save(team).Error; err != nil { - return fmt.Errorf("failed to update team: %w", err) - } - return nil -} - -// ListProjectsByTeamID lists all projects belonging to a team with pagination and filtering -func ListProjectsByTeamID(db *gorm.DB, teamID int, limit, offset int, isPublic *bool, status *consts.StatusType) ([]database.Project, int64, error) { - var projects []database.Project - var total int64 - - query := db.Model(&database.Project{}).Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted) - - if isPublic != nil { - query = query.Where("is_public = ?", *isPublic) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count projects for team %d: %w", teamID, err) - } - - if err := query.Limit(limit).Offset(offset).Find(&projects).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list projects for team %d: %w", teamID, err) - } - return projects, total, nil -} - -// ===================================================================== -// Team Member Relationship Functions -// ===================================================================== - -// CreateUserTeam creates a user-team association -func CreateUserTeam(db *gorm.DB, userTeam *database.UserTeam) error { - if err := db.Omit(userTeamOmitFields).Create(userTeam).Error; err != nil { - return fmt.Errorf("failed to create user-team association: %w", err) - } - return nil -} - -// DeleteUserTeam deletes a user-team association -func DeleteUserTeam(db *gorm.DB, userID, teamID int) (int64, error) { - result := db.Model(&database.UserTeam{}). - Where("user_id = ? AND team_id = ? AND status != ?", userID, teamID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete user-team association: %w", result.Error) - } - return result.RowsAffected, nil -} - -// GetUserTeamRole retrieves a user's role in a specific team -func GetUserTeamRole(db *gorm.DB, userID int, teamID int) (*database.UserTeam, error) { - var userTeam database.UserTeam - if err := db. - Preload("Role"). - Where("user_id = ? AND team_id = ? AND status = ?", userID, teamID, consts.CommonEnabled). - First(&userTeam).Error; err != nil { - return nil, err - } - return &userTeam, nil -} - -// ListTeamsByUserID gets teams the user participates in -func ListTeamsByUserID(db *gorm.DB, userID int) ([]database.Team, error) { - var teams []database.Team - if err := db.Table("teams"). - Joins("JOIN user_teams ON teams.id = user_teams.team_id"). - Where("user_teams.user_id = ? AND user_teams.status = ? AND teams.status != ?", userID, consts.CommonEnabled, consts.CommonDeleted). - Find(&teams).Error; err != nil { - return nil, fmt.Errorf("failed to list teams for user %d: %w", userID, err) - } - return teams, nil -} - -// ListUserTeamsByUserID gets user-team associations for a specific user -func ListUserTeamsByUserID(db *gorm.DB, userID int, status ...consts.StatusType) ([]database.UserTeam, error) { - query := db.Preload("Team").Preload("Role") - if len(status) == 0 { - query = query.Where("user_id = ? AND status != ?", userID, consts.CommonDeleted) - } else if len(status) == 1 { - query = query.Where("user_id = ? AND status = ?", userID, status[0]) - } else { - query = query.Where("user_id = ? AND status IN (?)", userID, status) - } - - var userTeams []database.UserTeam - if err := query. - Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). - Find(&userTeams).Error; err != nil { - return nil, fmt.Errorf("failed to list user-team associations for user %d: %w", userID, err) - } - - return userTeams, nil -} - -// ListUsersByTeamID gets users who are members of a specific team with pagination -func ListUsersByTeamID(db *gorm.DB, teamID int, limit, offset int) ([]database.User, int64, error) { - var users []database.User - var total int64 - - query := db.Model(&database.User{}). - Joins("JOIN user_teams ON users.id = user_teams.user_id"). - Where("user_teams.team_id = ? AND user_teams.status = ? AND users.status != ?", teamID, consts.CommonEnabled, consts.CommonDeleted) - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count users for team %d: %w", teamID, err) - } - - if err := query.Limit(limit).Offset(offset).Find(&users).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list users for team %d: %w", teamID, err) - } - - return users, total, nil -} - -// RemoveUsersFromTeam deletes all user-team associations for a given team -func RemoveUsersFromTeam(db *gorm.DB, teamID int) (int64, error) { - result := db.Model(&database.UserTeam{}). - Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all users from team %d: %w", teamID, result.Error) - } - return result.RowsAffected, nil -} - -// RemoveTeamsFromRole deletes all user-team associations for a given role -func RemoveTeamsFromRole(db *gorm.DB, roleID int) (int64, error) { - result := db.Model(&database.UserTeam{}). - Where("role_id = ? AND status != ?", roleID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all teams with role %d: %w", roleID, result.Error) - } - return result.RowsAffected, nil -} - -// RemoveTeamsFromUser deletes all user-team associations for a given user -func RemoveTeamsFromUser(db *gorm.DB, userID int) (int64, error) { - result := db.Model(&database.UserTeam{}). - Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all teams from user %d: %w", userID, result.Error) - } - return result.RowsAffected, nil -} diff --git a/src/repository/token.go b/src/repository/token.go deleted file mode 100644 index 4cedade3..00000000 --- a/src/repository/token.go +++ /dev/null @@ -1,98 +0,0 @@ -package repository - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "aegis/client" -) - -const ( - tokenBlacklistPrefix = "blacklist:token:%s" - userBlacklistPrefix = "blacklist:user:%d" -) - -// AddTokenToBlacklist adds a token to Redis blacklist with expiry and metadata -func AddTokenToBlacklist(ctx context.Context, tokenID string, expiresAt time.Time, metaData map[string]any) error { - key := fmt.Sprintf(tokenBlacklistPrefix, tokenID) - - ttl := time.Until(expiresAt) - if ttl <= 0 { - return nil - } - - metaDataJSON, err := json.Marshal(metaData) - if err != nil { - return fmt.Errorf("failed to marshal metadata to JSON: %v", err) - } - - if err = client.GetRedisClient().Set(ctx, key, string(metaDataJSON), ttl).Err(); err != nil { - return fmt.Errorf("failed to blacklist token in Redis: %v", err) - } - - return nil -} - -// AddUserTokensToBlacklist blacklists all tokens for a user by setting a key with expiry and metadata -func AddUserTokensToBlacklist(ctx context.Context, userID int, duration time.Duration, metaData map[string]any) error { - key := fmt.Sprintf(userBlacklistPrefix, userID) - - metaDataJSON, err := json.Marshal(metaData) - if err != nil { - return fmt.Errorf("failed to marshal metadata to JSON: %v", err) - } - - if err := client.GetRedisClient().Set(ctx, key, string(metaDataJSON), duration).Err(); err != nil { - return fmt.Errorf("failed to blacklist user tokens in Redis: %v", err) - } - - return nil -} - -// IsTokenBlacklisted checks if a token exists in Redis blacklist -func IsTokenBlacklisted(ctx context.Context, tokenID string) (bool, error) { - key := fmt.Sprintf(tokenBlacklistPrefix, tokenID) - - result, err := client.GetRedisClient().Exists(ctx, key).Result() - if err != nil { - return false, fmt.Errorf("failed to check token blacklist in Redis: %v", err) - } - - return result > 0, nil -} - -// IsUserBlacklisted checks if all user's tokens are blacklisted -func IsUserBlacklisted(ctx context.Context, userID int) (bool, error) { - key := fmt.Sprintf(userBlacklistPrefix, userID) - - result, err := client.GetRedisClient().Exists(ctx, key).Result() - if err != nil { - return false, fmt.Errorf("failed to check user blacklist in Redis: %v", err) - } - - return result > 0, nil -} - -// GetBlacklistedTokensCount retrieves the count of blacklisted tokens in Redis -func GetBlacklistedTokensCount(ctx context.Context) (int64, error) { - var cursor uint64 - var count int64 - - for { - keys, nextCursor, err := client.GetRedisClient().Scan(ctx, cursor, "blacklist:token:*", 100).Result() - if err != nil { - return 0, fmt.Errorf("failed to scan blacklisted tokens: %v", err) - } - - count += int64(len(keys)) - cursor = nextCursor - - if cursor == 0 { - break - } - } - - return count, nil -} diff --git a/src/repository/trace.go b/src/repository/trace.go deleted file mode 100644 index 1894edb8..00000000 --- a/src/repository/trace.go +++ /dev/null @@ -1,126 +0,0 @@ -package repository - -import ( - "fmt" - "time" - - "aegis/consts" - "aegis/database" - "aegis/dto" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// ===================================================================== -// Database Operations -// ===================================================================== - -// GetTraceByID retrieves a trace by its trace ID -func GetTraceByID(db *gorm.DB, traceID string) (*database.Trace, error) { - var trace database.Trace - if err := db.Model(&database.Trace{}). - Preload("Project"). - Preload("Tasks", func(db *gorm.DB) *gorm.DB { - return db.Order("level ASC, sequence ASC") - }). - Where("id = ? AND status != ?", traceID, consts.CommonDeleted). - First(&trace).Error; err != nil { - return nil, err - } - return &trace, nil -} - -// ListTraces lists traces based on filter and pagination with preloaded associations -func ListTraces(db *gorm.DB, limit, offset int, filterOptions *dto.ListTraceFilters) ([]database.Trace, int64, error) { - var traces []database.Trace - var total int64 - - query := db.Model(&database.Trace{}).Preload("Project") - if filterOptions.TraceType != nil { - query = query.Where("type = ?", *filterOptions.TraceType) - } - if filterOptions.GroupID != "" { - query = query.Where("group_id = ?", filterOptions.GroupID) - } - if filterOptions.ProjectID > 0 { - query = query.Where("project_id = ?", filterOptions.ProjectID) - } - if filterOptions.State != nil { - query = query.Where("state = ?", *filterOptions.State) - } - if filterOptions.Status != nil { - query = query.Where("status = ?", *filterOptions.Status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count traces: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&traces).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list traces: %w", err) - } - - return traces, total, nil -} - -// GetTracesByGroupID retrieves all traces belonging to a specific group -func GetTracesByGroupID(db *gorm.DB, groupID string) ([]database.Trace, error) { - var traces []database.Trace - if err := db.Model(&database.Trace{}). - Preload("Tasks"). - Where("group_id = ? AND status != ?", groupID, consts.CommonDeleted). - Order("start_time DESC"). - Find(&traces).Error; err != nil { - return nil, err - } - return traces, nil -} - -// CountTracesByGroupID counts the total number of non-deleted traces in a group -func CountTracesByGroupID(db *gorm.DB, groupID string) (int64, error) { - var count int64 - if err := db.Model(&database.Trace{}). - Where("group_id = ? AND status != ?", groupID, consts.CommonDeleted). - Count(&count).Error; err != nil { - return 0, err - } - return count, nil -} - -// ListTraceIDs retrieves distinct trace IDs from tasks within the specified time range -func ListTraceIDs(db *gorm.DB, startTime, endTime *time.Time) ([]string, error) { - var traceIDs []string - - query := db.Model(&database.Task{}).Select("DISTINCT trace_id") - if startTime != nil { - query = query.Where("created_at >= ?", *startTime) - } - if endTime != nil { - query = query.Where("created_at <= ?", *endTime) - } - - if err := query.Find(&traceIDs).Error; err != nil { - return nil, err - } - - return traceIDs, nil -} - -// UpsertTrace inserts or updates a trace in the database -func UpsertTrace(db *gorm.DB, trace *database.Trace) error { - if err := db.Clauses( - clause.OnConflict{ - Columns: []clause.Column{{Name: "id"}}, - DoUpdates: clause.AssignmentColumns([]string{ - "last_event", - "end_time", - "state", - "updated_at", - }), - }, - ).Create(trace).Error; err != nil { - return fmt.Errorf("failed to upsert task: %w", err) - } - return nil -} diff --git a/src/repository/user.go b/src/repository/user.go deleted file mode 100644 index 53844de0..00000000 --- a/src/repository/user.go +++ /dev/null @@ -1,501 +0,0 @@ -package repository - -import ( - "fmt" - "time" - - "aegis/consts" - "aegis/database" - - "gorm.io/gorm" -) - -const ( - userOmitFields = "active_username" - userContainerOmitFields = "active_user_container" - userDatasetOmitFields = "active_user_dataset" - userProjectOmitFields = "active_user_project" - userTeamOmitFields = "active_user_team" -) - -// CreateUser creates a user -func CreateUser(db *gorm.DB, user *database.User) error { - if err := db.Omit(userOmitFields).Create(user).Error; err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - return nil -} - -// DeleteUser soft deletes a user by setting its status to deleted -func DeleteUser(db *gorm.DB, userID int) (int64, error) { - result := db.Model(&database.User{}). - Where("id = ? AND status != ?", userID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete user %d: %w", userID, result.Error) - } - return result.RowsAffected, nil -} - -// GetUserByID gets a user by ID -func GetUserByID(db *gorm.DB, id int) (*database.User, error) { - var user database.User - if err := db.Where("id = ?", id).First(&user).Error; err != nil { - return nil, fmt.Errorf("failed to find user with id %d: %w", id, err) - } - return &user, nil -} - -// GetUserByUsername gets a user by username -func GetUserByUsername(db *gorm.DB, username string) (*database.User, error) { - var user database.User - if err := db.Where("username = ?", username).First(&user).Error; err != nil { - return nil, fmt.Errorf("failed to find user with username %s: %w", username, err) - } - return &user, nil -} - -// GetUserByEmail gets a user by email -func GetUserByEmail(db *gorm.DB, email string) (*database.User, error) { - var user database.User - if err := db.Where("email = ?", email).First(&user).Error; err != nil { - return nil, fmt.Errorf("failed to find user with email %s: %w", email, err) - } - return &user, nil -} - -// ListUsers lists users with filters and pagination -func ListUsers(db *gorm.DB, limit, offset int, isActive *bool, status *consts.StatusType) ([]database.User, int64, error) { - var users []database.User - var total int64 - - query := db.Model(&database.User{}).Where("status != ?", consts.CommonDeleted) - if status != nil { - query = query.Where("status = ?", *status) - } - if isActive != nil { - query = query.Where("is_active = ?", *isActive) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count users: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Find(&users).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list users: %w", err) - } - - return users, total, nil -} - -// UpdateUser updates user information -func UpdateUser(db *gorm.DB, user *database.User) error { - if err := db.Omit(userOmitFields).Save(user).Error; err != nil { - return fmt.Errorf("failed to update user: %w", err) - } - return nil -} - -// UpdateUserLoginTime updates user's last login time -func UpdateUserLoginTime(db *gorm.DB, userID int) error { - now := db.NowFunc() - if err := db.Model(&database.User{}). - Where("id = ? AND status != ?", userID, consts.CommonDeleted). - Update("last_login_at", now).Error; err != nil { - return fmt.Errorf("failed to update user login time: %w", err) - } - return nil -} - -// ===================== User-Role ===================== - -// CreateUserRole creates a user-role association -func CreateUserRole(db *gorm.DB, userRole *database.UserRole) error { - if err := db.Create(userRole).Error; err != nil { - return fmt.Errorf("failed to create user-role association: %w", err) - } - return nil -} - -// DeleteUserRole deletes a user-role association -func DeleteUserRole(db *gorm.DB, userID, roleID int) error { - if err := db.Where("user_id = ? AND role_id = ?", userID, roleID). - Delete(&database.UserRole{}).Error; err != nil { - return fmt.Errorf("failed to delete user-role association: %w", err) - } - return nil -} - -// IsSystemAdmin checks if a user has system admin role -func IsSystemAdmin(db *gorm.DB, userID int) (bool, error) { - var count int64 - if err := db.Table("user_roles"). - Joins("JOIN roles ON user_roles.role_id = roles.id"). - Where("user_roles.user_id = ? AND roles.name IN (?, ?)", - userID, consts.RoleSuperAdmin, consts.RoleAdmin). - Count(&count).Error; err != nil { - return false, fmt.Errorf("failed to check system admin status: %w", err) - } - return count > 0, nil -} - -// RemoveUsersFromRole deletes all user-role associations associated with a given role -func RemoveUsersFromRole(db *gorm.DB, roleID int) error { - if err := db.Where("role_id = ?", roleID). - Delete(&database.UserRole{}).Error; err != nil { - return fmt.Errorf("failed to delete all users from role: %w", err) - } - return nil -} - -// RemoveRolesFromUser deletes all user-role associations associated with a given user -func RemoveRolesFromUser(db *gorm.DB, userID int) error { - if err := db.Where("user_id = ?", userID). - Delete(&database.UserRole{}).Error; err != nil { - return fmt.Errorf("failed to delete all roles from user: %w", err) - } - return nil -} - -// GetRoleUserCount gets count of users who have this role -func GetRoleUserCount(db *gorm.DB, roleID int) (int64, error) { - var count int64 - if err := db.Table("users"). - Joins("JOIN user_roles ON users.id = user_roles.user_id"). - Where("user_roles.role_id = ? AND users.status = ?", roleID, consts.CommonEnabled). - Count(&count).Error; err != nil { - return 0, fmt.Errorf("failed to get role users: %v", err) - } - return count, nil -} - -// ListUsersByRoleID gets users who have a specific role -func ListUsersByRoleID(db *gorm.DB, roleID int) ([]database.User, error) { - var users []database.User - if err := db.Table("users"). - Joins("JOIN user_roles ON users.id = user_roles.user_id"). - Where("user_roles.role_id = ? AND users.status = ?", roleID, consts.CommonEnabled). - Find(&users).Error; err != nil { - return nil, fmt.Errorf("failed to get role users: %v", err) - } - return users, nil -} - -// ListRolesByUserID gets roles the user has -func ListRolesByUserID(db *gorm.DB, userID int) ([]database.Role, error) { - var roles []database.Role - if err := db.Table("roles"). - Joins("JOIN user_roles ur ON ur.role_id = roles.id"). - Where("ur.user_id = ? AND roles.status = ?", userID, consts.CommonEnabled). - Find(&roles).Error; err != nil { - return nil, fmt.Errorf("failed to get global roles of the specific user: %w", err) - } - return roles, nil -} - -// ===================== User-Permission ===================== - -// BatchCreateUserPermissions creates multiple user-permission associations in a batch -func BatchCreateUserPermissions(db *gorm.DB, userPermissions []database.UserPermission) error { - if len(userPermissions) == 0 { - return nil - } - if err := db.Create(&userPermissions).Error; err != nil { - return fmt.Errorf("failed to batch create user permissions: %w", err) - } - return nil -} - -// BatchDeleteUserPermisssions deletes multiple user-permission associations in a batch -func BatchDeleteUserPermisssions(db *gorm.DB, userID int, permissionIDs []int) error { - if len(permissionIDs) == 0 { - return nil - } - if err := db.Where("user_id = ? AND permission_id IN (?)", userID, permissionIDs). - Delete(&database.UserPermission{}).Error; err != nil { - return fmt.Errorf("failed to batch delete user permissions: %w", err) - } - return nil -} - -// RemoveUsersFromPermission deletes all user-permission associations associated with a given permission -func RemoveUsersFromPermission(db *gorm.DB, permissionID int) error { - if err := db.Where("permission_id = ?", permissionID). - Delete(&database.UserPermission{}).Error; err != nil { - return fmt.Errorf("failed to delete all users from permission: %w", err) - } - return nil -} - -// RemovePermissionsFromUser deletes all user-permission associations associated with a given user -func RemovePermissionsFromUser(db *gorm.DB, userID int) error { - if err := db.Where("user_id = ?", userID). - Delete(&database.UserPermission{}).Error; err != nil { - return fmt.Errorf("failed to delete all permissions from user: %w", err) - } - return nil -} - -// ListPermissionsByUserID lists all permissions a user has, including direct and role-based permissions -func ListPermissionsByUserID(db *gorm.DB, userID int) ([]database.Permission, error) { - var permissions []database.Permission - - // Subquery 1: Get permissions from user's global roles - rolePermissionsQuery := db. - Table("permissions p"). - Select("p.*"). - Joins("JOIN role_permissions rp ON p.id = rp.permission_id"). - Joins("JOIN user_roles ur ON rp.role_id = ur.role_id"). - Where("ur.user_id = ? AND p.status = ?", userID, consts.CommonEnabled) - - // Subquery 2: Get direct permissions assigned to user - directPermissionsQuery := db. - Table("permissions p"). - Select("p.*"). - Joins("JOIN user_permissions up ON p.id = up.permission_id"). - Where("up.user_id = ? AND p.status = ?", userID, consts.CommonEnabled). - Where("up.grant_type = ?", consts.GrantTypeGrant). - Where("up.expires_at IS NULL OR up.expires_at > ?", time.Now()) - - // Union both queries and get distinct permissions - if err := db.Table("(?) UNION (?)", rolePermissionsQuery, directPermissionsQuery). - Scan(&permissions).Error; err != nil { - return nil, fmt.Errorf("failed to get user permissions: %w", err) - } - - return permissions, nil -} - -// ===================== User-Container ===================== - -// CreateUserContainer creates a user-container association -func CreateUserContainer(db *gorm.DB, userContainer *database.UserContainer) error { - if err := db.Omit(userContainerOmitFields).Create(userContainer).Error; err != nil { - return fmt.Errorf("failed to create user-container association: %w", err) - } - return nil -} - -// DeleteUserContainer deletes a user-container association -func DeleteUserContainer(db *gorm.DB, userID, containerID int) (int64, error) { - result := db.Model(&database.UserContainer{}). - Where("user_id = ? AND container_id = ? AND status != ?", userID, containerID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete user-container association: %w", result.Error) - } - return result.RowsAffected, nil -} - -// ListContainersByUserID gets containers the user participates -func ListContainersByUserID(db *gorm.DB, userID int) ([]database.Container, error) { - var containers []database.Container - if err := db.Table("containers"). - Joins("JOIN user_containers uc ON uc.container_id = containers.id"). - Where("uc.user_id = ? AND containers.status = ?", userID, consts.CommonEnabled). - Find(&containers).Error; err != nil { - return nil, fmt.Errorf("failed to get containers of the specific user: %w", err) - } - return containers, nil -} - -// ListUserContainersByUserID gets user-container associations for a specific user -func ListUserContainersByUserID(db *gorm.DB, userID int) ([]database.UserContainer, error) { - var userContainers []database.UserContainer - if err := db.Preload("Container"). - Preload("Role"). - Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). - Find(&userContainers).Error; err != nil { - return nil, fmt.Errorf("failed to get user-container associations of the specific user: %w", err) - } - return userContainers, nil -} - -// RemoveUsersFromContainer deletes all user-container associations for a given container -func RemoveUsersFromContainer(db *gorm.DB, containerID int) (int64, error) { - result := db.Model(&database.UserContainer{}). - Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all users from container: %v", result.Error) - } - return result.RowsAffected, nil -} - -// RemoveContainersFromRole deletes all user-container associations for a given role -func RemoveContainersFromRole(db *gorm.DB, roleID int) (int64, error) { - result := db.Model(&database.UserContainer{}). - Where("role_id = ? AND status != ?", roleID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all containers from role: %v", err) - } - return result.RowsAffected, nil -} - -// RemoveContainersFromUser deletes all user-container associations for a given user -func RemoveContainersFromUser(db *gorm.DB, userID int) (int64, error) { - result := db.Model(&database.UserContainer{}). - Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all containers from user: %v", err) - } - return result.RowsAffected, nil -} - -// ===================== User-Dataset ===================== - -// CreateUserDataset creates a user-dataset association -func CreateUserDataset(db *gorm.DB, userDataset *database.UserDataset) error { - if err := db.Omit(userDatasetOmitFields).Create(userDataset).Error; err != nil { - return fmt.Errorf("failed to create user-dataset association: %w", err) - } - return nil -} - -// DeleteUserDataset deletes a user-dataset association -func DeleteUserDataset(db *gorm.DB, userID, datasetID int) (int64, error) { - result := db.Model(&database.UserDataset{}). - Where("user_id = ? AND dataset_id = ? AND status != ?", userID, datasetID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete user-dataset association: %w", result.Error) - } - return result.RowsAffected, nil -} - -// ListDatasetsByUserID gets datasets the user participates -func ListDatasetsByUserID(db *gorm.DB, userID int) ([]database.Dataset, error) { - var datasets []database.Dataset - if err := db.Table("datasets"). - Joins("JOIN user_datasets ud ON ud.dataset_id = datasets.id"). - Where("ud.user_id = ? AND datasets.status = ?", userID, consts.CommonEnabled). - Find(&datasets).Error; err != nil { - return nil, fmt.Errorf("failed to get datasets of the specific user: %w", err) - } - return datasets, nil -} - -// ListUserDatasetsByUserID gets user-dataset associations for a specific user -func ListUserDatasetsByUserID(db *gorm.DB, userID int) ([]database.UserDataset, error) { - var userDatasets []database.UserDataset - if err := db.Preload("Dataset"). - Preload("Role"). - Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). - Find(&userDatasets).Error; err != nil { - return nil, fmt.Errorf("failed to get user-dataset associations of the specific user: %w", err) - } - return userDatasets, nil -} - -// RemoveUsersFromDataset deletes all user-dataset associations for a given dataset -func RemoveUsersFromDataset(db *gorm.DB, datasetID int) (int64, error) { - result := db.Model(&database.UserDataset{}). - Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all users from dataset: %v", result.Error) - } - return result.RowsAffected, nil -} - -// RemoveDatasetsFromRole deletes all user-dataset associations for a given role -func RemoveDatasetsFromRole(db *gorm.DB, roleID int) (int64, error) { - result := db.Model(&database.UserDataset{}). - Where("role_id = ? AND status != ?", roleID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all datasets from role: %v", err) - } - return result.RowsAffected, nil -} - -// RemoveDatasetsFromUser deletes all user-dataset associations for a given user -func RemoveDatasetsFromUser(db *gorm.DB, userID int) (int64, error) { - result := db.Model(&database.UserDataset{}). - Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all datasets from user: %v", result.Error) - } - return result.RowsAffected, nil -} - -// ===================== User-Project ===================== - -// CreateUserProject creates a user-project association -func CreateUserProject(db *gorm.DB, userProject *database.UserProject) error { - if err := db.Omit(userProjectOmitFields).Create(userProject).Error; err != nil { - return fmt.Errorf("failed to create user-project association: %w", err) - } - return nil -} - -// DeleteUserProject deletes a user-project association -func DeleteUserProject(db *gorm.DB, userID, projectID int) (int64, error) { - result := db.Model(&database.UserProject{}). - Where("user_id = ? AND project_id = ? AND status != ?", userID, projectID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete user-project association: %w", result.Error) - } - return result.RowsAffected, nil -} - -// ListProjectsByUserID gets projects the user participates -func ListProjectsByUserID(db *gorm.DB, userID int) ([]database.Project, error) { - var projects []database.Project - if err := db.Table("projects"). - Joins("JOIN user_projects up ON up.project_id = projects.id"). - Where("up.user_id = ? AND projects.status = ?", userID, consts.CommonEnabled). - Find(&projects).Error; err != nil { - return nil, fmt.Errorf("failed to get projects of the specific user: %w", err) - } - return projects, nil -} - -// ListUserProjectsByUserID gets user-project associations for a specific user -func ListUserProjectsByUserID(db *gorm.DB, userID int) ([]database.UserProject, error) { - var userProjects []database.UserProject - if err := db.Preload("Project"). - Preload("Role"). - Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). - Find(&userProjects).Error; err != nil { - return nil, fmt.Errorf("failed to get user-project associations of the specific user: %w", err) - } - return userProjects, nil -} - -// RemoveUsersFromProject deletes all user-project associations for a given project -func RemoveUsersFromProject(db *gorm.DB, projectID int) (int64, error) { - result := db.Model(&database.UserProject{}). - Where("project_id = ? AND status != ?", projectID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all users from project: %v", result.Error) - } - return result.RowsAffected, nil -} - -// RemoveProjectsFromRole deletes all user-project associations for a given role -func RemoveProjectsFromRole(db *gorm.DB, roleID int) (int64, error) { - result := db.Model(&database.UserProject{}). - Where("role_id = ? AND status != ?", roleID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all projects from role: %v", err) - } - return result.RowsAffected, nil -} - -// RemoveProjectsFromUser deletes all user-project associations for a given user -func RemoveProjectsFromUser(db *gorm.DB, userID int) (int64, error) { - result := db.Model(&database.UserProject{}). - Where("user_id = ? AND status != ?", userID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to delete all projects from user: %v", result.Error) - } - return result.RowsAffected, nil -} diff --git a/src/router/admin.go b/src/router/admin.go new file mode 100644 index 00000000..3b13fccd --- /dev/null +++ b/src/router/admin.go @@ -0,0 +1,112 @@ +package router + +import ( + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupAdminV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + users := v2.Group("/users", middleware.JWTAuth()) + { + roles := users.Group("/:user_id/roles") + { + roles.POST("/:role_id", middleware.RequireUserAssign, handlers.User.AssignRole) + roles.DELETE("/:role_id", middleware.RequireUserAssign, handlers.User.RemoveRole) + } + + projects := users.Group("/:user_id/projects") + { + projects.POST("/:project_id/roles/:role_id", middleware.RequireUserAssign, handlers.User.AssignProject) + projects.DELETE("/:project_id", middleware.RequireUserAssign, handlers.User.RemoveProject) + } + + permissions := users.Group("/:user_id/permissions") + { + permissions.POST("/assign", middleware.RequireUserAssign, handlers.User.AssignPermissions) + permissions.POST("/remove", middleware.RequireUserAssign, handlers.User.RemovePermissions) + } + + containers := users.Group("/:user_id/containers") + { + containers.POST("/:container_id/roles/:role_id", middleware.RequireUserAssign, handlers.User.AssignContainer) + containers.DELETE("/:container_id", middleware.RequireUserAssign, handlers.User.RemoveContainer) + } + + datasets := users.Group("/:user_id/datasets") + { + datasets.POST("/:dataset_id/roles/:role_id", middleware.RequireUserAssign, handlers.User.AssignDataset) + datasets.DELETE("/:dataset_id", middleware.RequireUserAssign, handlers.User.RemoveDataset) + } + + userRead := users.Group("", middleware.RequireUserRead) + { + userRead.GET("", handlers.User.ListUsers) + userRead.GET("/:user_id/detail", middleware.RequireAdminOrUserOwnership, handlers.User.GetUserDetail) + } + + users.POST("", middleware.RequireUserCreate, handlers.User.CreateUser) + users.PATCH("/:user_id", middleware.RequireUserUpdate, handlers.User.UpdateUser) + users.DELETE("/:user_id", middleware.RequireUserDelete, handlers.User.DeleteUser) + } + + roles := v2.Group("/roles", middleware.JWTAuth()) + { + permissions := roles.Group("/:role_id/permissions") + { + permissions.POST("/assign", middleware.RequireRoleGrant, handlers.RBAC.AssignRolePermissions) + permissions.POST("/remove", middleware.RequireRoleRevoke, handlers.RBAC.RemoveRolePermissions) + } + + users := roles.Group("/:role_id/users") + { + users.GET("", middleware.RequireRoleRead, handlers.RBAC.ListUsersFromRole) + } + + roleRead := roles.Group("", middleware.RequireRoleRead) + { + roleRead.GET("/:role_id", handlers.RBAC.GetRole) + roleRead.GET("", handlers.RBAC.ListRoles) + } + + roles.POST("", middleware.RequireRoleCreate, handlers.RBAC.CreateRole) + roles.PATCH("/:role_id", middleware.RequireRoleUpdate, handlers.RBAC.UpdateRole) + roles.DELETE("/:role_id", middleware.RequireRoleDelete, handlers.RBAC.DeleteRole) + } + + permissions := v2.Group("/permissions", middleware.JWTAuth()) + { + roles := permissions.Group("/:permission_id/roles") + { + roles.GET("", middleware.RequirePermissionRead, handlers.RBAC.ListRolesFromPermission) + } + + permRead := permissions.Group("", middleware.RequirePermissionRead) + { + permRead.GET("", handlers.RBAC.ListPermissions) + permRead.GET("/:permission_id", handlers.RBAC.GetPermission) + } + } + + resources := v2.Group("/resources", middleware.JWTAuth()) + { + permissions := resources.Group("/:resource_id/permissions") + { + permissions.GET("", handlers.RBAC.ListResourcePermissions) + } + + resources.GET("/:resource_id", handlers.RBAC.GetResource) + resources.GET("", handlers.RBAC.ListResources) + } + + systems := v2.Group("/systems", middleware.JWTAuth()) + { + systems.GET("", handlers.ChaosSystem.ListSystems) + systems.POST("", handlers.ChaosSystem.CreateSystem) + systems.GET("/:id", handlers.ChaosSystem.GetSystem) + systems.PUT("/:id", handlers.ChaosSystem.UpdateSystem) + systems.DELETE("/:id", handlers.ChaosSystem.DeleteSystem) + systems.POST("/:id/metadata", handlers.ChaosSystem.UpsertMetadata) + systems.GET("/:id/metadata", handlers.ChaosSystem.ListMetadata) + } +} diff --git a/src/router/handlers.go b/src/router/handlers.go new file mode 100644 index 00000000..15ae5320 --- /dev/null +++ b/src/router/handlers.go @@ -0,0 +1,93 @@ +package router + +import ( + authmodule "aegis/module/auth" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + labelmodule "aegis/module/label" + metricmodule "aegis/module/metric" + notificationmodule "aegis/module/notification" + projectmodule "aegis/module/project" + rbacmodule "aegis/module/rbac" + sdkmodule "aegis/module/sdk" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" + teammodule "aegis/module/team" + tracemodule "aegis/module/trace" + usermodule "aegis/module/user" +) + +type Handlers struct { + Auth *authmodule.Handler + Project *projectmodule.Handler + Task *taskmodule.Handler + Injection *injectionmodule.Handler + Execution *executionmodule.Handler + Container *containermodule.Handler + Dataset *datasetmodule.Handler + Evaluation *evaluationmodule.Handler + Trace *tracemodule.Handler + Group *groupmodule.Handler + Metric *metricmodule.Handler + User *usermodule.Handler + RBAC *rbacmodule.Handler + SDK *sdkmodule.Handler + System *systemmodule.Handler + Notification *notificationmodule.Handler + ChaosSystem *chaossystemmodule.Handler + Team *teammodule.Handler + Label *labelmodule.Handler + SystemMetric *systemmetricmodule.Handler +} + +func NewHandlers( + auth *authmodule.Handler, + project *projectmodule.Handler, + task *taskmodule.Handler, + injection *injectionmodule.Handler, + execution *executionmodule.Handler, + container *containermodule.Handler, + dataset *datasetmodule.Handler, + evaluation *evaluationmodule.Handler, + trace *tracemodule.Handler, + group *groupmodule.Handler, + metric *metricmodule.Handler, + user *usermodule.Handler, + rbac *rbacmodule.Handler, + sdk *sdkmodule.Handler, + system *systemmodule.Handler, + notification *notificationmodule.Handler, + chaosSystem *chaossystemmodule.Handler, + team *teammodule.Handler, + label *labelmodule.Handler, + systemMetric *systemmetricmodule.Handler, +) *Handlers { + return &Handlers{ + Auth: auth, + Project: project, + Task: task, + Injection: injection, + Execution: execution, + Container: container, + Dataset: dataset, + Evaluation: evaluation, + Trace: trace, + Group: group, + Metric: metric, + User: user, + RBAC: rbac, + SDK: sdk, + System: system, + Notification: notification, + ChaosSystem: chaosSystem, + Team: team, + Label: label, + SystemMetric: systemMetric, + } +} diff --git a/src/router/module.go b/src/router/module.go new file mode 100644 index 00000000..41a89cae --- /dev/null +++ b/src/router/module.go @@ -0,0 +1,7 @@ +package router + +import "go.uber.org/fx" + +var Module = fx.Module("router", + fx.Provide(NewHandlers), +) diff --git a/src/router/portal.go b/src/router/portal.go new file mode 100644 index 00000000..5e080c80 --- /dev/null +++ b/src/router/portal.go @@ -0,0 +1,106 @@ +package router + +import ( + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupPortalV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + projects := v2.Group("/projects", middleware.JWTAuth()) + { + injections := projects.Group("/:project_id/injections") + { + injectionRead := injections.Group("", middleware.RequireProjectRead) + { + analysis := injectionRead.Group("/analysis") + { + analysis.GET("/no-issues", handlers.Injection.ListProjectFaultInjectionNoIssues) + analysis.GET("/with-issues", handlers.Injection.ListProjectFaultInjectionWithIssues) + } + + injectionRead.GET("", handlers.Injection.ListProjectInjections) + injectionRead.POST("/search", handlers.Injection.SearchProjectInjections) + } + + injectionExecute := injections.Group("", middleware.RequireProjectInjectionExecute) + { + injectionExecute.POST("/inject", handlers.Injection.SubmitProjectFaultInjection) + injectionExecute.POST("/build", handlers.Injection.SubmitProjectDatapackBuilding) + } + } + + executions := projects.Group("/:project_id/executions") + { + executionRead := executions.Group("", middleware.RequireProjectRead) + { + executionRead.GET("", handlers.Execution.ListProjectExecutions) + } + + executionExecute := executions.Group("", middleware.RequireProjectExecutionExecute) + { + executionExecute.POST("/execute", handlers.Execution.SubmitAlgorithmExecution) + } + } + + projectRead := projects.Group("", middleware.RequireProjectRead) + { + projectRead.GET("/:project_id", handlers.Project.GetProjectDetail) + projectRead.GET("", handlers.Project.ListProjects) + } + + projects.POST("", middleware.RequireProjectCreate, handlers.Project.CreateProject) + projects.PATCH("/:project_id", middleware.RequireProjectUpdate, handlers.Project.UpdateProject) + projects.PATCH("/:project_id/labels", middleware.RequireProjectUpdate, handlers.Project.ManageProjectCustomLabels) + projects.DELETE("/:project_id", middleware.RequireProjectDelete, handlers.Project.DeleteProject) + } + + teams := v2.Group("/teams", middleware.JWTAuth()) + { + teams.POST("", handlers.Team.CreateTeam) + teams.GET("", handlers.Team.ListTeams) + + teamAdmin := teams.Group("/:team_id", middleware.RequireTeamAdminAccess) + { + teamAdmin.PATCH("", handlers.Team.UpdateTeam) + teamAdmin.DELETE("", handlers.Team.DeleteTeam) + + teamManagement := teamAdmin.Group("/members") + teamManagement.POST("", handlers.Team.AddTeamMember) + teamManagement.DELETE("/:user_id", handlers.Team.RemoveTeamMember) + teamManagement.PATCH("/:user_id/role", handlers.Team.UpdateTeamMemberRole) + } + + teamMember := teams.Group("", middleware.RequireTeamMemberAccess) + { + teamMember.GET("/:team_id", handlers.Team.GetTeamDetail) + teamMember.GET("/:team_id/members", handlers.Team.ListTeamMembers) + teamMember.GET("/:team_id/projects", handlers.Team.ListTeamProjects) + } + } + + labels := v2.Group("/labels", middleware.JWTAuth()) + { + labelRead := labels.Group("", middleware.RequireLabelRead) + { + labelRead.GET("/:label_id", handlers.Label.GetLabelDetail) + labelRead.GET("", handlers.Label.ListLabels) + } + + labels.POST("", middleware.RequireLabelCreate, handlers.Label.CreateLabel) + labels.PATCH("/:label_id", middleware.RequireLabelUpdate, handlers.Label.UpdateLabel) + labels.DELETE("/:label_id", middleware.RequireLabelDelete, handlers.Label.DeleteLabel) + labels.POST("/batch-delete", middleware.RequireLabelDelete, handlers.Label.BatchDeleteLabels) + } + + accessKeys := v2.Group("/access-keys", middleware.JWTAuth()) + { + accessKeys.GET("", handlers.Auth.ListAccessKeys) + accessKeys.POST("", handlers.Auth.CreateAccessKey) + accessKeys.GET("/:access_key_id", handlers.Auth.GetAccessKey) + accessKeys.DELETE("/:access_key_id", handlers.Auth.DeleteAccessKey) + accessKeys.POST("/:access_key_id/rotate", handlers.Auth.RotateAccessKey) + accessKeys.POST("/:access_key_id/disable", handlers.Auth.DisableAccessKey) + accessKeys.POST("/:access_key_id/enable", handlers.Auth.EnableAccessKey) + } +} diff --git a/src/router/public.go b/src/router/public.go new file mode 100644 index 00000000..99fdf165 --- /dev/null +++ b/src/router/public.go @@ -0,0 +1,25 @@ +package router + +import ( + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupPublicV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + auth := v2.Group("/auth") + { + auth.POST("/login", handlers.Auth.Login) // User login + auth.POST("/register", handlers.Auth.Register) // User registration + auth.POST("/refresh", handlers.Auth.RefreshToken) // Token refresh + auth.POST("/access-key/token", handlers.Auth.ExchangeAccessKeyToken) + + // These require authentication + authProtected := auth.Group("", middleware.JWTAuth()) + { + authProtected.POST("/logout", handlers.Auth.Logout) // User logout + authProtected.POST("/change-password", handlers.Auth.ChangePassword) // Change password + authProtected.GET("/profile", handlers.Auth.GetProfile) // Get current user profile + } + } +} diff --git a/src/router/router.go b/src/router/router.go index 5f21e701..ebb3645a 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -1,6 +1,7 @@ package router import ( + _ "aegis/docs/openapi2" "aegis/middleware" "github.com/gin-contrib/cors" @@ -9,8 +10,12 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -func New() *gin.Engine { +func New(handlers *Handlers, services ...middleware.Service) *gin.Engine { router := gin.Default() + var middlewareService middleware.Service + if len(services) > 0 { + middlewareService = services[0] + } // CORS configuration config := cors.DefaultConfig() @@ -22,6 +27,7 @@ func New() *gin.Engine { // Middleware setup router.Use( + middleware.InjectService(middlewareService), middleware.GroupID(), middleware.SSEPath(), cors.New(config), @@ -29,10 +35,10 @@ func New() *gin.Engine { ) // Set up system routes - SetupSystemRoutes(router) + SetupSystemRoutes(router, handlers) // Set up API routes - SetupV2Routes(router) + SetupV2Routes(router, handlers) // Swagger documentation router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/src/router/router_test.go b/src/router/router_test.go new file mode 100644 index 00000000..7c1270c8 --- /dev/null +++ b/src/router/router_test.go @@ -0,0 +1,57 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestRouterSeparatesRouteGroups(t *testing.T) { + engine := New(&Handlers{}) + routes := engine.Routes() + + requiredPrefixes := []string{ + "/api/v2/auth", + "/api/v2/projects", + "/api/v2/users", + "/api/v2/sdk", + "/system/audit", + "/system/configs", + "/system/monitor", + "/system/health", + "/docs/", + } + + for _, prefix := range requiredPrefixes { + if !hasRoutePrefix(routes, prefix) { + t.Fatalf("expected route prefix %q to be registered", prefix) + } + } +} + +func hasRoutePrefix(routes []gin.RouteInfo, prefix string) bool { + for _, route := range routes { + if len(route.Path) >= len(prefix) && route.Path[:len(prefix)] == prefix { + return true + } + } + return false +} + +func TestSwaggerDocEndpointServesRegisteredSpec(t *testing.T) { + engine := New(&Handlers{}) + + req := httptest.NewRequest(http.MethodGet, "/docs/doc.json", nil) + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected swagger doc endpoint status 200, got %d", w.Code) + } + if !strings.Contains(w.Body.String(), "/api/v2/auth/login") { + t.Fatalf("expected swagger doc to include auth login path") + } +} diff --git a/src/router/sdk.go b/src/router/sdk.go new file mode 100644 index 00000000..1cf42c00 --- /dev/null +++ b/src/router/sdk.go @@ -0,0 +1,21 @@ +package router + +import ( + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupSDKV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + sdkEval := v2.Group("/sdk/evaluations", middleware.JWTAuth()) + { + sdkEval.GET("", handlers.SDK.ListEvaluations) + sdkEval.GET("/experiments", handlers.SDK.ListExperiments) + sdkEval.GET("/:id", handlers.SDK.GetEvaluation) + } + + sdkData := v2.Group("/sdk/datasets", middleware.JWTAuth()) + { + sdkData.GET("", handlers.SDK.ListDatasetSamples) + } +} diff --git a/src/router/system.go b/src/router/system.go index 9db9644d..1ec88af6 100644 --- a/src/router/system.go +++ b/src/router/system.go @@ -1,18 +1,17 @@ package router import ( - "aegis/handlers/system" "aegis/middleware" "github.com/gin-gonic/gin" ) // SetupSystemRoutes sets up system routes -func SetupSystemRoutes(router *gin.Engine) { +func SetupSystemRoutes(router *gin.Engine, handlers *Handlers) { audit := router.Group("/system/audit", middleware.JWTAuth(), middleware.RequireAuditRead) { - audit.GET("", system.ListAuditLogs) - audit.GET("/:id", system.GetAuditLog) + audit.GET("", handlers.System.ListAuditLogs) + audit.GET("/:id", handlers.System.GetAuditLog) } // Dynamic Configuration Management @@ -20,30 +19,38 @@ func SetupSystemRoutes(router *gin.Engine) { { configsRead := configs.Group("", middleware.RequireConfigurationRead) { - configsRead.GET("", system.ListConfigs) // Search configurations with filters - configsRead.GET("/:config_id", system.GetConfig) // Get configuration by ID - configsRead.GET("/:config_id/histories", system.ListConfigHistories) // Get configuration change history + configsRead.GET("", handlers.System.ListConfigs) // Search configurations with filters + configsRead.GET("/:config_id", handlers.System.GetConfig) // Get configuration by ID + configsRead.GET("/:config_id/histories", handlers.System.ListConfigHistories) // Get configuration change history } // Configuration Update operations - configs.PATCH("/:config_id", middleware.RequireConfigurationUpdate, system.UpdateConfigValue) // Update configuration value - configs.POST("/:config_id/value/rollback", middleware.RequireConfigurationUpdate, system.RollbackConfigValue) // Rollback configuration value + configs.PATCH("/:config_id", middleware.RequireConfigurationUpdate, handlers.System.UpdateConfigValue) // Update configuration value + configs.POST("/:config_id/value/rollback", middleware.RequireConfigurationUpdate, handlers.System.RollbackConfigValue) // Rollback configuration value // Configuration Configure operations (metadata management, higher privilege) - configs.PUT("/:config_id/metadata", middleware.RequireConfigurationConfigure, system.UpdateConfigMetadata) // Update configuration metadata (schema) - configs.POST("/:config_id/metadata/rollback", middleware.RequireConfigurationConfigure, system.RollbackConfigMetadata) // Rollback configuration metadata + configs.PUT("/:config_id/metadata", middleware.RequireConfigurationConfigure, handlers.System.UpdateConfigMetadata) // Update configuration metadata (schema) + configs.POST("/:config_id/metadata/rollback", middleware.RequireConfigurationConfigure, handlers.System.RollbackConfigMetadata) // Rollback configuration metadata } health := router.Group("/system/health") { - health.GET("", system.GetHealth) + health.GET("", handlers.System.GetHealth) } monitor := router.Group("/system/monitor", middleware.JWTAuth(), middleware.RequireSystemRead) { - monitor.POST("/metrics", system.GetMetrics) - monitor.GET("/info", system.GetSystemInfo) - monitor.GET("/namespaces/locks", system.ListNamespaceLocks) - monitor.GET("/tasks/queue", system.ListQueuedTasks) + monitor.POST("/metrics", handlers.System.GetMetrics) + monitor.GET("/info", handlers.System.GetSystemInfo) + monitor.GET("/namespaces/locks", handlers.System.ListNamespaceLocks) + monitor.GET("/tasks/queue", handlers.System.ListQueuedTasks) + } +} + +func SetupSystemV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + system := v2.Group("/system", middleware.JWTAuth()) + { + system.GET("/metrics", handlers.SystemMetric.GetSystemMetrics) // Get current system metrics + system.GET("/metrics/history", handlers.SystemMetric.GetSystemMetricsHistory) // Get historical system metrics } } diff --git a/src/router/v2.go b/src/router/v2.go index adbd6ff9..24328b72 100644 --- a/src/router/v2.go +++ b/src/router/v2.go @@ -1,7 +1,6 @@ package router import ( - v2handlers "aegis/handlers/v2" "aegis/middleware" "github.com/gin-gonic/gin" @@ -109,25 +108,15 @@ Note: v1 API design is chaotic and does not follow a unified standard. It will g */ // SetupV2Routes sets up API v2 routes - stable version of the API -func SetupV2Routes(router *gin.Engine) { +func SetupV2Routes(router *gin.Engine, handlers *Handlers) { middleware.StartCleanupRoutine() v2 := router.Group("/api/v2") - // Authentication routes (with auth rate limiting) - auth := v2.Group("/auth") - { - auth.POST("/login", v2handlers.Login) // User login - auth.POST("/register", v2handlers.Register) // User registration - auth.POST("/refresh", v2handlers.RefreshToken) // Token refresh - - // These require authentication - authProtected := auth.Group("", middleware.JWTAuth()) - { - authProtected.POST("/logout", v2handlers.Logout) // User logout - authProtected.POST("/change-password", v2handlers.ChangePassword) // Change password - authProtected.GET("/profile", v2handlers.GetProfile) // Get current user profile - } - } + SetupPublicV2Routes(v2, handlers) + SetupSDKV2Routes(v2, handlers) + SetupAdminV2Routes(v2, handlers) + SetupPortalV2Routes(v2, handlers) + SetupSystemV2Routes(v2, handlers) // ===================================================================== // Admin Entity API Group @@ -143,43 +132,43 @@ func SetupV2Routes(router *gin.Engine) { // Container Version Read operations versionRead := versions.Group("", middleware.RequireContainerVersionRead) { - versionRead.GET("/:version_id", v2handlers.GetContainerVersion) // Get container version by ID - versionRead.GET("", v2handlers.ListContainerVersions) // List container versions + versionRead.GET("/:version_id", handlers.Container.GetContainerVersion) // Get container version by ID + versionRead.GET("", handlers.Container.ListContainerVersions) // List container versions } // Container Version Create operations - versions.POST("", middleware.RequireContainerVersionCreate, v2handlers.CreateContainerVersion) // Create container version + versions.POST("", middleware.RequireContainerVersionCreate, handlers.Container.CreateContainerVersion) // Create container version // Container Version Upload operations - versions.POST("/:version_id/helm-chart", middleware.RequireContainerVersionUpload, v2handlers.UploadHelmChart) // Upload Helm chart tgz file - versions.POST("/:version_id/helm-values", middleware.RequireContainerVersionUpload, v2handlers.UploadHelmValueFile) // Upload Helm values file + versions.POST("/:version_id/helm-chart", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmChart) // Upload Helm chart tgz file + versions.POST("/:version_id/helm-values", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmValueFile) // Upload Helm values file // Container Version Update operations - versions.PATCH("/:version_id", middleware.RequireContainerVersionUpdate, v2handlers.UpdateContainerVersion) // Update container version + versions.PATCH("/:version_id", middleware.RequireContainerVersionUpdate, handlers.Container.UpdateContainerVersion) // Update container version // Container Version Delete operations - versions.DELETE("/:version_id", middleware.RequireContainerVersionDelete, v2handlers.DeleteContainerVersion) + versions.DELETE("/:version_id", middleware.RequireContainerVersionDelete, handlers.Container.DeleteContainerVersion) } // Container Read operations containerRead := containers.Group("", middleware.RequireContainerRead) { - containerRead.GET("/:container_id", v2handlers.GetContainer) // Get container by ID - containerRead.GET("", v2handlers.ListContainers) // List containers + containerRead.GET("/:container_id", handlers.Container.GetContainer) // Get container by ID + containerRead.GET("", handlers.Container.ListContainers) // List containers } // Container Create operations - containers.POST("", middleware.RequireContainerCreate, v2handlers.CreateContainer) // Create container + containers.POST("", middleware.RequireContainerCreate, handlers.Container.CreateContainer) // Create container // Container Execute operations (build requires execute permission) - containers.POST("/build", middleware.RequireContainerExecute, v2handlers.SubmitContainerBuilding) // Build container + containers.POST("/build", middleware.RequireContainerExecute, handlers.Container.SubmitContainerBuilding) // Build container // Container Update operations - containers.PATCH("/:container_id", middleware.RequireContainerUpdate, v2handlers.UpdateContainer) // Update container - containers.PATCH("/:container_id/labels", middleware.RequireContainerUpdate, v2handlers.ManageContainerCustomLabels) // Manage container labels + containers.PATCH("/:container_id", middleware.RequireContainerUpdate, handlers.Container.UpdateContainer) // Update container + containers.PATCH("/:container_id/labels", middleware.RequireContainerUpdate, handlers.Container.ManageContainerCustomLabels) // Manage container labels // Container Delete operations - containers.DELETE("/:container_id", middleware.RequireContainerDelete, v2handlers.DeleteContainer) // Delete container + containers.DELETE("/:container_id", middleware.RequireContainerDelete, handlers.Container.DeleteContainer) // Delete container } // Dataset Management - Dataset Entity @@ -190,267 +179,37 @@ func SetupV2Routes(router *gin.Engine) { { versionRead := versions.Group("", middleware.RequireDatasetVersionRead) { - versionRead.GET("", v2handlers.ListDatasetVersions) // List dataset versions - versionRead.GET("/:version_id", v2handlers.GetDatasetVersion) // Get dataset version by ID - versionRead.GET("/:version_id/download", v2handlers.DownloadDatasetVersion) // Download dataset version + versionRead.GET("", handlers.Dataset.ListDatasetVersions) // List dataset versions + versionRead.GET("/:version_id", handlers.Dataset.GetDatasetVersion) // Get dataset version by ID + versionRead.GET("/:version_id/download", handlers.Dataset.DownloadDatasetVersion) // Download dataset version } // Dataset Version Create operations - versions.POST("", middleware.RequireDatasetVersionCreate, v2handlers.CreateDatasetVersion) // Create dataset version + versions.POST("", middleware.RequireDatasetVersionCreate, handlers.Dataset.CreateDatasetVersion) // Create dataset version // Dataset Version Update operations - versions.PATCH("/:version_id", middleware.RequireDatasetVersionUpdate, v2handlers.UpdateDatasetVersion) // Update dataset version - versions.PATCH("/:version_id/injections", middleware.RequireDatasetVersionUpdate, v2handlers.ManageDatasetVersionInjections) // Manage dataset version injections + versions.PATCH("/:version_id", middleware.RequireDatasetVersionUpdate, handlers.Dataset.UpdateDatasetVersion) // Update dataset version + versions.PATCH("/:version_id/injections", middleware.RequireDatasetVersionUpdate, handlers.Dataset.ManageDatasetVersionInjections) // Manage dataset version injections - versions.DELETE("/:version_id", middleware.RequireDatasetVersionDelete, v2handlers.DeleteDatasetVersion) // Delete dataset version + versions.DELETE("/:version_id", middleware.RequireDatasetVersionDelete, handlers.Dataset.DeleteDatasetVersion) // Delete dataset version } // Dataset Read operations datasetRead := datasets.Group("", middleware.RequireDatasetRead) { - datasetRead.GET("/:dataset_id", v2handlers.GetDataset) // Get dataset by ID - datasetRead.GET("", v2handlers.ListDatasets) // List datasets + datasetRead.GET("/:dataset_id", handlers.Dataset.GetDataset) // Get dataset by ID + datasetRead.GET("", handlers.Dataset.ListDatasets) // List datasets } // Dataset Create operations - datasets.POST("", middleware.RequireDatasetCreate, v2handlers.CreateDataset) // Create dataset + datasets.POST("", middleware.RequireDatasetCreate, handlers.Dataset.CreateDataset) // Create dataset // Dataset Update operations - datasets.PATCH("/:dataset_id", middleware.RequireDatasetUpdate, v2handlers.UpdateDataset) // Update dataset - datasets.PATCH("/:dataset_id/labels", middleware.RequireDatasetUpdate, v2handlers.ManageDatasetCustomLabels) // Manage dataset labels + datasets.PATCH("/:dataset_id", middleware.RequireDatasetUpdate, handlers.Dataset.UpdateDataset) // Update dataset + datasets.PATCH("/:dataset_id/labels", middleware.RequireDatasetUpdate, handlers.Dataset.ManageDatasetCustomLabels) // Manage dataset labels // Dataset Delete operations - datasets.DELETE("/:dataset_id", middleware.RequireDatasetDelete, v2handlers.DeleteDataset) // Delete dataset - } - - // Project Management - Project Entity - projects := v2.Group("/projects", middleware.JWTAuth()) - { - injections := projects.Group("/:project_id/injections") - { - injectionRead := injections.Group("", middleware.RequireProjectRead) - { - analysis := injectionRead.Group("/analysis") - { - analysis.GET("/no-issues", v2handlers.ListFaultInjectionNoIssues) - analysis.GET("/with-issues", v2handlers.ListFaultInjectionWithIssues) - } - - injectionRead.GET("", v2handlers.ListProjectInjections) - injectionRead.POST("/search", v2handlers.SearchInjections) - } - - injectionExecute := injections.Group("", middleware.RequireProjectInjectionExecute) - { - injectionExecute.POST("/inject", v2handlers.SubmitProjectFaultInjection) - injectionExecute.POST("/build", v2handlers.SubmitProjectDatapackBuilding) - } - } - - executions := projects.Group("/:project_id/executions") - { - executionRead := executions.Group("", middleware.RequireProjectRead) - { - executionRead.GET("", v2handlers.ListProjectExecutions) - } - - executionExecute := executions.Group("", middleware.RequireProjectExecutionExecute) - { - executionExecute.POST("/execute", v2handlers.SubmitAlgorithmExecution) - } - } - - // Project Read operations - projectRead := projects.Group("", middleware.RequireProjectRead) - { - projectRead.GET("/:project_id", v2handlers.GetProjectDetail) // Get project by ID - projectRead.GET("", v2handlers.ListProjects) // List projects - } - - // Project Create operations - projects.POST("", middleware.RequireProjectCreate, v2handlers.CreateProject) // Create project - - // Project Update operations - projects.PATCH("/:project_id", middleware.RequireProjectUpdate, v2handlers.UpdateProject) // Update project - projects.PATCH("/:project_id/labels", middleware.RequireProjectUpdate, v2handlers.ManageProjectCustomLabels) // Manage project labels - - // Project Delete operations - projects.DELETE("/:project_id", middleware.RequireProjectDelete, v2handlers.DeleteProject) // Delete project - } - - // Team Management - Team Entity - teams := v2.Group("/teams", middleware.JWTAuth()) - { - // Anyone can create a team (will become team admin automatically) - teams.POST("", v2handlers.CreateTeam) - - // List teams - returns public teams + user's teams (no special permission needed) - teams.GET("", v2handlers.ListTeams) - - // Team Write/Delete/Manage operations - requires team admin OR system admin - TeamAdmin := teams.Group("/:team_id", middleware.RequireTeamAdminAccess) - { - TeamAdmin.PATCH("", v2handlers.UpdateTeam) // Update team - TeamAdmin.DELETE("", v2handlers.DeleteTeam) // Delete team - - // Team Member Management - only team admins can manage members - TeamManagement := TeamAdmin.Group("/members") - TeamManagement.POST("", v2handlers.AddTeamMember) // Add team member - TeamManagement.DELETE("/:user_id", v2handlers.RemoveTeamMember) // Remove team member - TeamManagement.PATCH("/:user_id/role", v2handlers.UpdateTeamMemberRole) // Update team member role - } - - // Team Read operations - requires being a member OR team is public OR system admin - TeamMember := teams.Group("", middleware.RequireTeamMemberAccess) - { - TeamMember.GET("/:team_id", v2handlers.GetTeamDetail) // Get team by ID - TeamMember.GET("/:team_id/members", v2handlers.ListTeamMembers) // List team members - TeamMember.GET("/:team_id/projects", v2handlers.ListTeamProjects) // List team projects - } - } - - // Label Management - Label Entity - labels := v2.Group("/labels", middleware.JWTAuth()) - { - // Label Read operations - labelRead := labels.Group("", middleware.RequireLabelRead) - { - labelRead.GET("/:label_id", v2handlers.GetLabelDetail) // Get label by ID - labelRead.GET("", v2handlers.ListLabels) // List labels - } - - // Label Create operations - labels.POST("", middleware.RequireLabelCreate, v2handlers.CreateLabel) // Create label - - // Label Update operations - labels.PATCH("/:label_id", middleware.RequireLabelUpdate, v2handlers.UpdateLabel) // Update label - - // Label Delete operations - labels.DELETE("/:label_id", middleware.RequireLabelDelete, v2handlers.DeleteLabel) // Delete label - labels.POST("/batch-delete", middleware.RequireLabelDelete, v2handlers.BatchDeleteLabels) // Batch delete labels - } - - // User Management - User Entity - users := v2.Group("/users", middleware.JWTAuth()) - { - // User-Role relationship routes (assign roles requires assign permission) - roles := users.Group("/:user_id/roles") - { - roles.POST("/:role_id", middleware.RequireUserAssign, v2handlers.AssignUserRole) // Assign role to user - roles.DELETE("/:role_id", middleware.RequireUserAssign, v2handlers.RemoveGlobalRole) // Remove role from user - } - - // User-Project relationship routes (assign requires assign permission) - projects := users.Group("/:user_id/projects") - { - projects.POST("/:project_id/roles/:role_id", middleware.RequireUserAssign, v2handlers.AssignUserProject) // Assign user to project - projects.DELETE("/:project_id", middleware.RequireUserAssign, v2handlers.RemoveUserProject) // Remove user from project - } - - // User-Permission relationship routes (assign requires assign permission) - permissions := users.Group("/:user_id/permissions") - { - permissions.POST("/assign", middleware.RequireUserAssign, v2handlers.AssignUserPermission) // Assign permission to user - permissions.POST("/remove", middleware.RequireUserAssign, v2handlers.RemoveUserPermission) // Remove permission from user - } - - // User-Container relationship routes (assign requires assign permission) - containers := users.Group("/:user_id/containers") - { - containers.POST("/:container_id/roles/:role_id", middleware.RequireUserAssign, v2handlers.AssignUserContainer) // Assign container to user - containers.DELETE("/:container_id", middleware.RequireUserAssign, v2handlers.RemoveUserContainer) // Remove container from user - } - - // User-Dataset relationship routes (assign requires assign permission) - datasets := users.Group("/:user_id/datasets") - { - datasets.POST("/:dataset_id/roles/:role_id", middleware.RequireUserAssign, v2handlers.AssignUserDataset) // Assign dataset to user - datasets.DELETE("/:dataset_id", middleware.RequireUserAssign, v2handlers.RemoveUserDataset) // Remove dataset from user - } - - // User Read operations - userRead := users.Group("", middleware.RequireUserRead) - { - userRead.GET("", v2handlers.ListUsersV2) // List users - userRead.GET("/:user_id/detail", middleware.RequireAdminOrUserOwnership, v2handlers.GetUserDetailV2) // Get user by ID - } - - // User Create operations - users.POST("", middleware.RequireUserCreate, v2handlers.CreateUser) // Create user - - // User Update operations - users.PATCH("/:user_id", middleware.RequireUserUpdate, v2handlers.UpdateUser) // Update user - - // User Delete operations - users.DELETE("/:user_id", middleware.RequireUserDelete, v2handlers.DeleteUser) // Delete user - } - - // ===================================================================== - // Authentication and Authorization API Group - // ===================================================================== - - // Role Management - Role Entity - roles := v2.Group("/roles", middleware.JWTAuth()) - { - // Role-Permission relationship routes (grant/revoke) - permissions := roles.Group("/:role_id/permissions") - { - permissions.POST("/assign", middleware.RequireRoleGrant, v2handlers.AssignRolePermission) // Assign permissions to role - permissions.POST("/remove", middleware.RequireRoleRevoke, v2handlers.RemovePermissionsFromRole) // Remove permissions from role - } - - // Role-User relationship routes - users := roles.Group("/:role_id/users") - { - users.GET("", middleware.RequireRoleRead, v2handlers.ListUsersFromRole) // List users with this role - } - - // Role Read operations - roleRead := roles.Group("", middleware.RequireRoleRead) - { - roleRead.GET("/:role_id", v2handlers.GetRole) // Get role by ID - roleRead.GET("", v2handlers.ListRoles) // List roles - } - - // Role Create operations - roles.POST("", middleware.RequireRoleCreate, v2handlers.CreateRole) // Create role - - // Role Update operations - roles.PATCH("/:role_id", middleware.RequireRoleUpdate, v2handlers.UpdateRole) // Update role - - // Role Delete operations - roles.DELETE("/:role_id", middleware.RequireRoleDelete, v2handlers.DeleteRole) // Delete role - } - - // Permission Management - Permission Entity - permissions := v2.Group("/permissions", middleware.JWTAuth()) - { - // Permission-Role relationship routes - roles := permissions.Group("/:permission_id/roles") - { - roles.GET("", middleware.RequirePermissionRead, v2handlers.ListRolesFromPermission) // List roles assigned to permission - } - - // Permission Read operations - permRead := permissions.Group("", middleware.RequirePermissionRead) - { - permRead.GET("", v2handlers.ListPermissions) // List permissions - permRead.GET("/:permission_id", v2handlers.GetPermission) // Get permission by ID - } - } - - // Resource Management - Resource Entity - resources := v2.Group("/resources", middleware.JWTAuth()) - { - // Resource-Permission relationship routes - permissions := resources.Group("/:resource_id/permissions") - { - permissions.GET("", v2handlers.ListResourcePermissions) // List permissions assigned to resource - } - - // Resource Read operations - resources.GET("/:resource_id", v2handlers.GetResourceDetail) // Get resource by ID - resources.GET("", v2handlers.ListResources) // List resources + datasets.DELETE("/:dataset_id", middleware.RequireDatasetDelete, handlers.Dataset.DeleteDataset) // Delete dataset } // ===================================================================== @@ -466,16 +225,16 @@ func SetupV2Routes(router *gin.Engine) { // Task Read operations taskRead := taskWithAuth.Group("", middleware.RequireTaskRead) { - taskRead.GET("", v2handlers.ListTasks) // List tasks - taskRead.GET("/:task_id", v2handlers.GetTask) // Get task by ID + taskRead.GET("", handlers.Task.List) // List tasks + taskRead.GET("/:task_id", handlers.Task.Get) // Get task by ID } // Task Delete operations - taskWithAuth.POST("/batch-delete", middleware.RequireTaskDelete, v2handlers.BatchDeleteTasks) // Batch delete tasks + taskWithAuth.POST("/batch-delete", middleware.RequireTaskDelete, handlers.Task.BatchDelete) // Batch delete tasks } // Task Log streaming (WebSocket) - auth via query param, not middleware - tasks.GET("/:task_id/logs/ws", v2handlers.GetTaskLogsWS) // Stream task logs via WebSocket + tasks.GET("/:task_id/logs/ws", handlers.Task.LogsWS) // Stream task logs via WebSocket } // Fault Injection Management - FaultInjectionSchedule Entity @@ -484,32 +243,32 @@ func SetupV2Routes(router *gin.Engine) { { injectionSystemAdmin := injections.Group("", middleware.RequireSystemAdmin()) { - injectionSystemAdmin.GET("", v2handlers.ListInjections) // List injections - injectionSystemAdmin.POST("/search", v2handlers.SearchInjections) // Advanced search + injectionSystemAdmin.GET("", handlers.Injection.ListInjections) // List injections + injectionSystemAdmin.POST("/search", handlers.Injection.SearchInjections) // Advanced search } // Manual upload (must be before /:id routes) - injections.POST("/upload", v2handlers.UploadDatapack) // Upload manual datapack + injections.POST("/upload", handlers.Injection.UploadDatapack) // Upload manual datapack // Injection Read operations - injections.GET("/:id", v2handlers.GetInjection) // Get injection by ID - injections.GET("/:id/download", v2handlers.DownloadDatapack) // Download injection datapack - injections.GET("/:id/logs", v2handlers.GetInjectionLogs) // Get injection execution logs - injections.GET("/:id/files", v2handlers.ListDatapackFiles) // Get injection file structure - injections.GET("/:id/files/download", v2handlers.DownloadDatapackFile) // Download specific injection file - injections.GET("/:id/files/query", v2handlers.QueryDatapackFile) // Query parquet file content - injections.GET("/metadata", v2handlers.GetInjectionMetadata) // Get injection metadata + injections.GET("/:id", handlers.Injection.GetInjection) // Get injection by ID + injections.GET("/:id/download", handlers.Injection.DownloadDatapack) // Download injection datapack + injections.GET("/:id/logs", handlers.Injection.GetInjectionLogs) // Get injection execution logs + injections.GET("/:id/files", handlers.Injection.ListDatapackFiles) // Get injection file structure + injections.GET("/:id/files/download", handlers.Injection.DownloadDatapackFile) // Download specific injection file + injections.GET("/:id/files/query", handlers.Injection.QueryDatapackFile) // Query parquet file content + injections.GET("/metadata", handlers.Injection.GetInjectionMetadata) // Get injection metadata // Injection Clone operations - injections.POST("/:id/clone", v2handlers.CloneInjection) // Clone injection + injections.POST("/:id/clone", handlers.Injection.CloneInjection) // Clone injection // Injection Update operations (label management, ground truth) - injections.PUT("/:id/groundtruth", v2handlers.UpdateGroundtruth) // Update ground truth - injections.PATCH("/:id/labels", v2handlers.ManageInjectionCustomLabels) // Manage injection custom labels - injections.PATCH("/labels/batch", v2handlers.BatchManageInjectionLabels) // Batch manage injection labels + injections.PUT("/:id/groundtruth", handlers.Injection.UpdateGroundtruth) // Update ground truth + injections.PATCH("/:id/labels", handlers.Injection.ManageInjectionCustomLabels) // Manage injection custom labels + injections.PATCH("/labels/batch", handlers.Injection.BatchManageInjectionLabels) // Batch manage injection labels // Injection Delete operations - injections.POST("/batch-delete", v2handlers.BatchDeleteInjections) // Batch delete injections + injections.POST("/batch-delete", handlers.Injection.BatchDeleteInjections) // Batch delete injections } // Execution Result Management - ExecutionResult Entity @@ -518,35 +277,35 @@ func SetupV2Routes(router *gin.Engine) { { executionSystemAdmin := executions.Group("", middleware.RequireSystemAdmin()) { - executionSystemAdmin.GET("", v2handlers.ListExecutions) // List executions - executionSystemAdmin.GET("/labels", v2handlers.ListAvaliableExecutionLabels) // List available execution labels + executionSystemAdmin.GET("", handlers.Execution.ListExecutions) // List executions + executionSystemAdmin.GET("/labels", handlers.Execution.ListAvailableExecutionLabels) // List available execution labels } // Execution Read operations - executions.GET("/:execution_id", v2handlers.GetExecution) // Get execution by ID + executions.GET("/:execution_id", handlers.Execution.GetExecution) // Get execution by ID // Execution Update operations (upload results and manage labels) - executions.POST("/:execution_id/detector_results", v2handlers.UploadDetectorResults) // Upload detector results - executions.POST("/:execution_id/granularity_results", v2handlers.UploadGranularityResults) // Upload granularity results - executions.PATCH("/:execution_id/labels", v2handlers.ManageExecutionCustomLabels) // Manage execution custom labels + executions.POST("/:execution_id/detector_results", handlers.Execution.UploadDetectorResults) // Upload detector results + executions.POST("/:execution_id/granularity_results", handlers.Execution.UploadGranularityResults) // Upload granularity results + executions.PATCH("/:execution_id/labels", handlers.Execution.ManageExecutionCustomLabels) // Manage execution custom labels // Execution Delete operations - executions.POST("/batch-delete", v2handlers.BatchDeleteExecutions) // Batch delete executions + executions.POST("/batch-delete", handlers.Execution.BatchDeleteExecutions) // Batch delete executions } // Trace Management - Trace Entity traces := v2.Group("/traces", middleware.JWTAuth()) { - traces.GET("", v2handlers.ListTraces) // List traces - traces.GET("/:trace_id", v2handlers.GetTrace) // Get trace by ID - traces.GET("/:trace_id/stream", v2handlers.GetTraceStream) // Get trace stream (SSE) + traces.GET("", handlers.Trace.ListTraces) // List traces + traces.GET("/:trace_id", handlers.Trace.GetTrace) // Get trace by ID + traces.GET("/:trace_id/stream", handlers.Trace.GetTraceStream) // Get trace stream (SSE) } // Group Management - Group stream for real-time batch progress groups := v2.Group("/groups", middleware.JWTAuth()) { - groups.GET("/:group_id/stats", v2handlers.GetAlgorithmMetrics) // Get group stats (can be used for progress tracking) - groups.GET("/:group_id/stream", v2handlers.GetGroupStream) // Stream group trace events (SSE) + groups.GET("/:group_id/stats", handlers.Group.GetGroupStats) // Get group stats (can be used for progress tracking) + groups.GET("/:group_id/stream", handlers.Group.GetGroupStream) // Stream group trace events (SSE) } // ===================================================================== @@ -556,7 +315,7 @@ func SetupV2Routes(router *gin.Engine) { // Notification Management - Global workflow notifications notifications := v2.Group("/notifications", middleware.JWTAuth()) { - notifications.GET("/stream", v2handlers.GetNotificationStream) // Stream global notifications (SSE) + notifications.GET("/stream", handlers.Notification.GetStream) // Stream global notifications (SSE) } // ===================================================================== @@ -575,35 +334,19 @@ func SetupV2Routes(router *gin.Engine) { evaluations := v2.Group("/evaluations", middleware.JWTAuth()) { // GET /api/v2/evaluations - List persisted evaluations with pagination - evaluations.GET("", v2handlers.ListEvaluations) + evaluations.GET("", handlers.Evaluation.ListEvaluations) // GET /api/v2/evaluations/:id - Get a single evaluation by ID - evaluations.GET("/:id", v2handlers.GetEvaluation) + evaluations.GET("/:id", handlers.Evaluation.GetEvaluation) // DELETE /api/v2/evaluations/:id - Delete an evaluation by ID - evaluations.DELETE("/:id", v2handlers.DeleteEvaluation) + evaluations.DELETE("/:id", handlers.Evaluation.DeleteEvaluation) // POST /api/v2/evaluations/datasets - Get algorithm evaluations on multiple datasets (requires dataset read permission) - evaluations.POST("/datasets", middleware.RequireDatasetRead, v2handlers.ListDatasetEvaluationResults) + evaluations.POST("/datasets", middleware.RequireDatasetRead, handlers.Evaluation.ListDatasetEvaluationResults) // POST /api/v2/evaluations/datapacks - Get algorithm evaluations on multiple datapacks (requires dataset read permission) - evaluations.POST("/datapacks", middleware.RequireDatasetRead, v2handlers.ListDatapackEvaluationResults) - } - - // ===================================================================== - // SDK Evaluation API Group (read-only access to Python SDK tables) - // ===================================================================== - - sdkEval := v2.Group("/sdk/evaluations", middleware.JWTAuth()) - { - sdkEval.GET("", v2handlers.ListSDKEvaluations) - sdkEval.GET("/experiments", v2handlers.ListSDKExperiments) - sdkEval.GET("/:id", v2handlers.GetSDKEvaluation) - } - - sdkData := v2.Group("/sdk/datasets", middleware.JWTAuth()) - { - sdkData.GET("", v2handlers.ListSDKDatasetSamples) + evaluations.POST("/datapacks", middleware.RequireDatasetRead, handlers.Evaluation.ListDatapackEvaluationResults) } // ===================================================================== @@ -613,35 +356,9 @@ func SetupV2Routes(router *gin.Engine) { // Metrics routes metrics := v2.Group("/metrics", middleware.JWTAuth()) { - metrics.GET("/injections", v2handlers.GetInjectionMetrics) // Get injection metrics - metrics.GET("/executions", v2handlers.GetExecutionMetrics) // Get execution metrics - metrics.GET("/algorithms", v2handlers.GetAlgorithmMetrics) // Get algorithm comparison metrics + metrics.GET("/injections", handlers.Metric.GetInjectionMetrics) // Get injection metrics + metrics.GET("/executions", handlers.Metric.GetExecutionMetrics) // Get execution metrics + metrics.GET("/algorithms", handlers.Metric.GetAlgorithmMetrics) // Get algorithm comparison metrics } - // ===================================================================== - // Chaos Systems API Group - // ===================================================================== - - // Chaos System Management - System Entity - systems := v2.Group("/systems", middleware.JWTAuth()) - { - systems.GET("", v2handlers.ListChaosSystemsHandler) - systems.POST("", v2handlers.CreateChaosSystemHandler) - systems.GET("/:id", v2handlers.GetChaosSystemHandler) - systems.PUT("/:id", v2handlers.UpdateChaosSystemHandler) - systems.DELETE("/:id", v2handlers.DeleteChaosSystemHandler) - systems.POST("/:id/metadata", v2handlers.UpsertChaosSystemMetadataHandler) - systems.GET("/:id/metadata", v2handlers.ListChaosSystemMetadataHandler) - } - - // ===================================================================== - // System Metrics API Group - // ===================================================================== - - // System metrics routes - system := v2.Group("/system", middleware.JWTAuth()) - { - system.GET("/metrics", v2handlers.GetSystemMetrics) // Get current system metrics - system.GET("/metrics/history", v2handlers.GetSystemMetricsHistory) // Get historical system metrics - } } diff --git a/src/service/common/config_listener.go b/src/service/common/config_listener.go index 45d6d8ae..d4fd0819 100644 --- a/src/service/common/config_listener.go +++ b/src/service/common/config_listener.go @@ -6,14 +6,13 @@ import ( "sync" "time" - "aegis/client" "aegis/config" "aegis/consts" - "aegis/database" - "aegis/repository" + etcdinfra "aegis/infra/etcd" "github.com/sirupsen/logrus" clientv3 "go.etcd.io/etcd/client/v3" + "gorm.io/gorm" ) // scopePrefix maps configuration scopes to their etcd key prefix. @@ -28,42 +27,39 @@ var scopePrefix = map[consts.ConfigScope]string{ // It supports incremental scope activation via EnsureScope — each scope is // loaded and watched independently, making it safe for both, producer-only // and consumer-only modes. -type configUpdateListener struct { - ctx context.Context - cancel context.CancelFunc - mu sync.Mutex - active map[consts.ConfigScope]bool // scopes already loaded + watched +type ConfigUpdateListener struct { + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex + active map[consts.ConfigScope]bool // scopes already loaded + watched + db *gorm.DB + gateway *etcdinfra.Gateway } -var ( - configListenerInstance *configUpdateListener - configListenerOnce sync.Once -) +func NewConfigUpdateListener(ctx context.Context, db *gorm.DB, gateway *etcdinfra.Gateway) *ConfigUpdateListener { + listenerCtx, cancel := context.WithCancel(ctx) + listener := &ConfigUpdateListener{ + ctx: listenerCtx, + cancel: cancel, + active: make(map[consts.ConfigScope]bool), + db: db, + gateway: gateway, + } -// GetConfigUpdateListener returns the singleton instance of configUpdateListener -func GetConfigUpdateListener(ctx context.Context) *configUpdateListener { - configListenerOnce.Do(func() { - listenerCtx, cancel := context.WithCancel(ctx) - configListenerInstance = &configUpdateListener{ - ctx: listenerCtx, - cancel: cancel, - active: make(map[consts.ConfigScope]bool), - } + go func() { + <-ctx.Done() + logrus.Info("Parent context cancelled, stopping config update listener...") + listener.Stop() + }() - go func() { - <-ctx.Done() - logrus.Info("Parent context cancelled, stopping config update listener...") - configListenerInstance.Stop() - }() - }) - return configListenerInstance + return listener } // EnsureScope loads initial config values from etcd and starts a watcher for // the given scope. The call is idempotent — invoking it multiple times for the // same scope is a safe no-op. Scopes without an etcd prefix (e.g. producer) // are silently skipped. -func (l *configUpdateListener) EnsureScope(scope consts.ConfigScope) error { +func (l *ConfigUpdateListener) EnsureScope(scope consts.ConfigScope) error { prefix, ok := scopePrefix[scope] if !ok { logrus.Debugf("Scope %s has no etcd prefix, skipping listener setup", @@ -94,15 +90,15 @@ func (l *configUpdateListener) EnsureScope(scope consts.ConfigScope) error { } // Stop cancels the listener context, stopping all watcher goroutines. -func (l *configUpdateListener) Stop() { +func (l *ConfigUpdateListener) Stop() { l.cancel() logrus.Info("Config update listener stopped") } // loadScopeFromEtcd loads all configs for a given scope from etcd into viper. // Falls back to MySQL defaults only if config doesn't exist in etcd. -func (l *configUpdateListener) loadScopeFromEtcd(scope consts.ConfigScope, prefix, scopeName string) error { - configMetadata, err := repository.ListConfigByScope(database.DB, scope) +func (l *ConfigUpdateListener) loadScopeFromEtcd(scope consts.ConfigScope, prefix, scopeName string) error { + configMetadata, err := newConfigStore(l.db).listConfigsByScope(scope) if err != nil { return fmt.Errorf("failed to list %s config metadata from database: %w", scopeName, err) } @@ -114,7 +110,7 @@ func (l *configUpdateListener) loadScopeFromEtcd(scope consts.ConfigScope, prefi etcdKey := fmt.Sprintf("%s%s", prefix, meta.Key) // Try to get current value from etcd first - etcdValue, err := client.EtcdGet(l.ctx, etcdKey) + etcdValue, err := l.gateway.Get(l.ctx, etcdKey) if err != nil { logrus.Errorf("Failed to get config %s from etcd: %v", meta.Key, err) continue @@ -123,7 +119,7 @@ func (l *configUpdateListener) loadScopeFromEtcd(scope consts.ConfigScope, prefi var valueToLoad string if etcdValue == "" { // Config doesn't exist in etcd, initialize it with MySQL default value - if err := client.EtcdPut(l.ctx, etcdKey, meta.DefaultValue, 0); err != nil { + if err := l.gateway.Put(l.ctx, etcdKey, meta.DefaultValue, 0); err != nil { logrus.Errorf("Failed to initialize config %s in etcd: %v", meta.Key, err) continue } @@ -151,8 +147,8 @@ func (l *configUpdateListener) loadScopeFromEtcd(scope consts.ConfigScope, prefi // watchPrefix watches a single etcd prefix for configuration changes. // Each scope gets its own goroutine calling this method. -func (l *configUpdateListener) watchPrefix(prefix, scopeName string) { - watchChan := client.EtcdWatch(l.ctx, prefix, true) +func (l *ConfigUpdateListener) watchPrefix(prefix, scopeName string) { + watchChan := l.gateway.Watch(l.ctx, prefix, true) logrus.Infof("Started watching etcd prefix %s for %s config changes", prefix, scopeName) for { @@ -165,19 +161,19 @@ func (l *configUpdateListener) watchPrefix(prefix, scopeName string) { if !ok { logrus.Warnf("etcd %s watch channel closed, restarting...", scopeName) time.Sleep(1 * time.Second) - watchChan = client.EtcdWatch(l.ctx, prefix, true) + watchChan = l.gateway.Watch(l.ctx, prefix, true) continue } if watchResp.Canceled { logrus.Warnf("etcd %s watch was canceled, restarting...", scopeName) time.Sleep(1 * time.Second) - watchChan = client.EtcdWatch(l.ctx, prefix, true) + watchChan = l.gateway.Watch(l.ctx, prefix, true) continue } if err := watchResp.Err(); err != nil { logrus.Errorf("etcd %s watch error: %v", scopeName, err) time.Sleep(1 * time.Second) - watchChan = client.EtcdWatch(l.ctx, prefix, true) + watchChan = l.gateway.Watch(l.ctx, prefix, true) continue } for _, event := range watchResp.Events { @@ -188,7 +184,7 @@ func (l *configUpdateListener) watchPrefix(prefix, scopeName string) { } // handleEtcdEvent handles a single etcd event from a given prefix -func (l *configUpdateListener) handleEtcdEvent(event *clientv3.Event, prefix string) { +func (l *ConfigUpdateListener) handleEtcdEvent(event *clientv3.Event, prefix string) { key := string(event.Kv.Key) newValue := string(event.Kv.Value) @@ -212,7 +208,7 @@ func (l *configUpdateListener) handleEtcdEvent(event *clientv3.Event, prefix str }).Info("received config change from etcd") // Apply config change via registry - if err := handleConfigChange(l.ctx, configKey, oldValue, newValue); err != nil { + if err := handleConfigChange(l.ctx, l.db, configKey, oldValue, newValue); err != nil { logrus.Errorf("failed to apply config update for %s: %v", configKey, err) return } diff --git a/src/service/common/config_registry.go b/src/service/common/config_registry.go index 4ca4a5b6..4e2641e2 100644 --- a/src/service/common/config_registry.go +++ b/src/service/common/config_registry.go @@ -5,14 +5,12 @@ import ( "fmt" "sync" - "aegis/client" "aegis/config" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) // ConfigHandler defines the interface for handling configuration changes. @@ -34,10 +32,11 @@ type configRegistry struct { handlers map[consts.ConfigScope]map[string]ConfigHandler } -var ( - registryInstance *configRegistry - registryOnce sync.Once -) +type ConfigPublisher interface { + Publish(ctx context.Context, channel string, message any) error +} + +var registryInstance = newConfigRegistry() // RegisterHandler registers a configuration handler. // External packages (e.g. consumer) call this to plug in their own handlers. @@ -45,16 +44,14 @@ func RegisterHandler(handler ConfigHandler) { getConfigRegistry().register(handler) } -var globalHandlersOnce sync.Once - // RegisterGlobalHandlers registers handlers for global-scope configurations. -// Safe to call multiple times — subsequent calls are a no-op. -func RegisterGlobalHandlers() { - globalHandlersOnce.Do(func() { - RegisterHandler(&algoConfigHandler{}) +// Safe to call multiple times — duplicate registrations are skipped. +func RegisterGlobalHandlers(publisher ConfigPublisher) { + registry := getConfigRegistry() + if registry.ensureRegistered(&algoConfigHandler{publisher: publisher}) { scope := consts.ConfigScopeGlobal logrus.Infof("Registered %d global config handler(s)", len(ListRegisteredConfigKeys(&scope))) - }) + } } // ListRegisteredConfigKeys returns all registered configuration keys for informational purposes (e.g. logging) @@ -77,11 +74,14 @@ func ListRegisteredConfigKeys(scope *consts.ConfigScope) []string { // PublishWrapper wraps a config update function and publishes the result to Redis. // Exported so consumer and producer can reuse it in their own handlers. -func PublishWrapper(ctx context.Context, function func() error) error { +func PublishWrapper(ctx context.Context, publisher ConfigPublisher, function func() error) error { updateResponse := dto.NewConfigUpdateResponse() defer func() { - if err := client.RedisPublish(ctx, consts.ConfigUpdateResponseChannel, updateResponse); err != nil { + if publisher == nil { + return + } + if err := publisher.Publish(ctx, consts.ConfigUpdateResponseChannel, updateResponse); err != nil { logrus.Errorf("failed to publish config update response to Redis: %v", err) } }() @@ -97,17 +97,22 @@ func PublishWrapper(ctx context.Context, function func() error) error { // getConfigRegistry returns the singleton config registry instance func getConfigRegistry() *configRegistry { - registryOnce.Do(func() { - registryInstance = &configRegistry{ - handlers: make(map[consts.ConfigScope]map[string]ConfigHandler), - } - }) return registryInstance } +func newConfigRegistry() *configRegistry { + return &configRegistry{ + handlers: make(map[consts.ConfigScope]map[string]ConfigHandler), + } +} + +func resetConfigRegistryForTest() { + registryInstance = newConfigRegistry() +} + // handleConfigChange routes a configuration change to the appropriate handler -func handleConfigChange(ctx context.Context, key, oldValue, newValue string) error { - existingConfig, err := repository.GetConfigByKey(database.DB, key, false) +func handleConfigChange(ctx context.Context, db *gorm.DB, key, oldValue, newValue string) error { + existingConfig, err := newConfigStore(db).getConfigByKey(key) if err != nil { return fmt.Errorf("failed to retrieve existing config %s from database: %w", key, err) } @@ -160,20 +165,40 @@ func (r *configRegistry) register(handler ConfigHandler) { consts.GetConfigScopeName(scope), category) } +func (r *configRegistry) ensureRegistered(handler ConfigHandler) bool { + r.mu.Lock() + defer r.mu.Unlock() + + scope := handler.Scope() + category := handler.Category() + + if _, ok := r.handlers[scope]; !ok { + r.handlers[scope] = make(map[string]ConfigHandler) + } + if _, exists := r.handlers[scope][category]; exists { + return false + } + + r.handlers[scope][category] = handler + logrus.Debugf("Registered config handler for scope=%s category=%s", + consts.GetConfigScopeName(scope), category) + return true +} + // ===================================================================== // AlgoConfigHandler - handles algo configuration (e.g. algo.detector) // ===================================================================== -// algoConfigHandler handles algo configuration changes and keeps the global -// config.DetectorName variable in sync whenever algo.detector is updated. -type algoConfigHandler struct{} +type algoConfigHandler struct { + publisher ConfigPublisher +} func (h *algoConfigHandler) Category() string { return "algo" } func (h *algoConfigHandler) Scope() consts.ConfigScope { return consts.ConfigScopeGlobal } func (h *algoConfigHandler) Handle(ctx context.Context, key, oldValue, newValue string) error { - return PublishWrapper(ctx, func() error { + return PublishWrapper(ctx, h.publisher, func() error { switch key { case consts.DetectorKey: config.SetDetectorName(newValue) diff --git a/src/service/common/config_registry_test.go b/src/service/common/config_registry_test.go new file mode 100644 index 00000000..203d078e --- /dev/null +++ b/src/service/common/config_registry_test.go @@ -0,0 +1,24 @@ +package common + +import ( + "testing" + + "aegis/consts" +) + +func TestRegisterGlobalHandlersIsIdempotent(t *testing.T) { + resetConfigRegistryForTest() + t.Cleanup(resetConfigRegistryForTest) + + RegisterGlobalHandlers(nil) + RegisterGlobalHandlers(nil) + + scope := consts.ConfigScopeGlobal + keys := ListRegisteredConfigKeys(&scope) + if len(keys) != 1 { + t.Fatalf("expected 1 global handler, got %d: %v", len(keys), keys) + } + if keys[0] != "algo" { + t.Fatalf("expected algo handler to be registered, got %v", keys) + } +} diff --git a/src/service/common/config_store.go b/src/service/common/config_store.go new file mode 100644 index 00000000..f712bbc7 --- /dev/null +++ b/src/service/common/config_store.go @@ -0,0 +1,34 @@ +package common + +import ( + "fmt" + + "aegis/consts" + "aegis/model" + + "gorm.io/gorm" +) + +type configStore struct { + db *gorm.DB +} + +func newConfigStore(db *gorm.DB) *configStore { + return &configStore{db: db} +} + +func (s *configStore) getConfigByKey(key string) (*model.DynamicConfig, error) { + var cfg model.DynamicConfig + if err := s.db.Where("config_key = ?", key).First(&cfg).Error; err != nil { + return nil, fmt.Errorf("failed to find config with key %s: %w", key, err) + } + return &cfg, nil +} + +func (s *configStore) listConfigsByScope(scope consts.ConfigScope) ([]model.DynamicConfig, error) { + var configs []model.DynamicConfig + if err := s.db.Where("scope = ?", scope).Order("config_key ASC").Find(&configs).Error; err != nil { + return nil, fmt.Errorf("failed to list configs by scope %s: %w", consts.GetConfigScopeName(scope), err) + } + return configs, nil +} diff --git a/src/service/common/container.go b/src/service/common/container.go index b089ca0a..c1981ccd 100644 --- a/src/service/common/container.go +++ b/src/service/common/container.go @@ -2,32 +2,31 @@ package common import ( "aegis/consts" - "aegis/database" "aegis/dto" + "aegis/model" "aegis/repository" "aegis/utils" "fmt" + + "gorm.io/gorm" ) -// ListContainerVersionEnvVars retrieves and validates environment variables for a container version based on provided specs -func ListContainerVersionEnvVars(specs []dto.ParameterSpec, version *database.ContainerVersion) ([]dto.ParameterItem, error) { - return listParameterItems(specs, repository.ListContainerVersionEnvVars, version.ID, version) +func ListContainerVersionEnvVarsWithDB(db *gorm.DB, specs []dto.ParameterSpec, version *model.ContainerVersion) ([]dto.ParameterItem, error) { + return listParameterItemsWithDB(db, specs, repository.ListContainerVersionEnvVars, version.ID, version) } -// ListHelmConfigValues retrieves and validates Helm values based on provided specs and Helm configuration -func ListHelmConfigValues(specs []dto.ParameterSpec, cfg *database.HelmConfig) ([]dto.ParameterItem, error) { - return listParameterItems(specs, repository.ListHelmConfigValues, cfg.ID, cfg.ContainerVersion) +func ListHelmConfigValuesWithDB(db *gorm.DB, specs []dto.ParameterSpec, cfg *model.HelmConfig) ([]dto.ParameterItem, error) { + return listParameterItemsWithDB(db, specs, repository.ListHelmConfigValues, cfg.ID, cfg.ContainerVersion) } -// MapRefsToContainerVersions maps container refs to their corresponding container versions -func MapRefsToContainerVersions(refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) (map[*dto.ContainerRef]database.ContainerVersion, error) { - versions, err := getUniqueVersionsForContainerRefs(refs, containerType, userID) +func MapRefsToContainerVersionsWithDB(db *gorm.DB, refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) (map[*dto.ContainerRef]model.ContainerVersion, error) { + versions, err := getUniqueVersionsForContainerRefsWithDB(db, refs, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to batch get container versions: %w", err) } - flatMap := make(map[string][]database.ContainerVersion) - hierarchicalMap := make(map[string]map[string]database.ContainerVersion) + flatMap := make(map[string][]model.ContainerVersion) + hierarchicalMap := make(map[string]map[string]model.ContainerVersion) for _, version := range versions { containerName := version.Container.Name @@ -36,21 +35,21 @@ func MapRefsToContainerVersions(refs []*dto.ContainerRef, containerType consts.C flatMap[containerName] = append(flatMap[containerName], version) if _, exists := hierarchicalMap[containerName]; !exists { - hierarchicalMap[containerName] = make(map[string]database.ContainerVersion) + hierarchicalMap[containerName] = make(map[string]model.ContainerVersion) } hierarchicalMap[containerName][versionName] = version } - results := make(map[*dto.ContainerRef]database.ContainerVersion, len(refs)) + results := make(map[*dto.ContainerRef]model.ContainerVersion, len(refs)) for _, ref := range refs { - var result database.ContainerVersion + var result model.ContainerVersion containerTypeName := consts.GetContainerTypeName(containerType) if ref.Version != "" { if _, exists := hierarchicalMap[ref.Name]; !exists { availableContainers := getAvailableContainerNames(hierarchicalMap) if len(availableContainers) == 0 { // Check if container exists with different type - exists, actualType, err := repository.CheckContainerExistsWithDifferentType(database.DB, ref.Name, containerType, userID) + exists, actualType, err := repository.CheckContainerExistsWithDifferentType(db, ref.Name, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to check container type: %w", err) } @@ -74,7 +73,7 @@ func MapRefsToContainerVersions(refs []*dto.ContainerRef, containerType consts.C availableContainers := getAvailableContainerNames(hierarchicalMap) if len(availableContainers) == 0 { // Check if container exists with different type - exists, actualType, err := repository.CheckContainerExistsWithDifferentType(database.DB, ref.Name, containerType, userID) + exists, actualType, err := repository.CheckContainerExistsWithDifferentType(db, ref.Name, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to check container type: %w", err) } @@ -96,8 +95,7 @@ func MapRefsToContainerVersions(refs []*dto.ContainerRef, containerType consts.C return results, nil } -// getUniqueVersionsForContainerRefs retrieves unique container versions for the given container refs -func getUniqueVersionsForContainerRefs(refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) ([]database.ContainerVersion, error) { +func getUniqueVersionsForContainerRefsWithDB(db *gorm.DB, refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) ([]model.ContainerVersion, error) { containerNamesSet := make(map[string]struct{}, len(refs)) for _, ref := range refs { if ref.Name != "" { @@ -106,7 +104,7 @@ func getUniqueVersionsForContainerRefs(refs []*dto.ContainerRef, containerType c } if len(containerNamesSet) == 0 { - return []database.ContainerVersion{}, nil + return []model.ContainerVersion{}, nil } requiredNames := make([]string, 0, len(containerNamesSet)) @@ -114,7 +112,7 @@ func getUniqueVersionsForContainerRefs(refs []*dto.ContainerRef, containerType c requiredNames = append(requiredNames, name) } - versions, err := repository.BatchGetContainerVersions(database.DB, containerType, requiredNames, userID) + versions, err := repository.BatchGetContainerVersions(db, containerType, requiredNames, userID) if err != nil { return nil, fmt.Errorf("failed to batch get container versions: %w", err) } @@ -122,14 +120,13 @@ func getUniqueVersionsForContainerRefs(refs []*dto.ContainerRef, containerType c return versions, nil } -// listParameterItems retrieves and validates parameter items based on provided specs and a parameter config fetcher -func listParameterItems(specs []dto.ParameterSpec, fetcher repository.ParameterConfigFetcher, resourceID int, contextCfg any) ([]dto.ParameterItem, error) { +func listParameterItemsWithDB(db *gorm.DB, specs []dto.ParameterSpec, fetcher repository.ParameterConfigFetcher, resourceID int, contextCfg any) ([]dto.ParameterItem, error) { keys := make([]string, 0, len(specs)) for _, item := range specs { keys = append(keys, item.Key) } - paramConfigs, err := fetcher(database.DB, keys, resourceID) + paramConfigs, err := fetcher(db, keys, resourceID) if err != nil { return nil, fmt.Errorf("failed to list configurations: %w", err) } @@ -138,7 +135,7 @@ func listParameterItems(specs []dto.ParameterSpec, fetcher repository.ParameterC return nil, fmt.Errorf("no configurations found for the provided specs") } - paramConfigMap := make(map[string]database.ParameterConfig, len(paramConfigs)) + paramConfigMap := make(map[string]model.ParameterConfig, len(paramConfigs)) for _, config := range paramConfigs { paramConfigMap[config.Key] = config } @@ -179,7 +176,7 @@ func listParameterItems(specs []dto.ParameterSpec, fetcher repository.ParameterC } // processParameterConfig processes a single parameter configuration and returns the corresponding parameter item -func processParameterConfig(config database.ParameterConfig, userValue any, contextCfg any) (*dto.ParameterItem, error) { +func processParameterConfig(config model.ParameterConfig, userValue any, contextCfg any) (*dto.ParameterItem, error) { switch config.Type { case consts.ParameterTypeFixed: finalValue := userValue @@ -234,7 +231,7 @@ func processParameterConfig(config database.ParameterConfig, userValue any, cont } // getAvailableContainerNames returns a list of available container names from the hierarchical map -func getAvailableContainerNames(hierarchicalMap map[string]map[string]database.ContainerVersion) []string { +func getAvailableContainerNames(hierarchicalMap map[string]map[string]model.ContainerVersion) []string { names := make([]string, 0, len(hierarchicalMap)) for name := range hierarchicalMap { names = append(names, name) @@ -243,7 +240,7 @@ func getAvailableContainerNames(hierarchicalMap map[string]map[string]database.C } // getAvailableVersions returns a list of available versions for a specific container -func getAvailableVersions(hierarchicalMap map[string]map[string]database.ContainerVersion, containerName string) []string { +func getAvailableVersions(hierarchicalMap map[string]map[string]model.ContainerVersion, containerName string) []string { if versions, exists := hierarchicalMap[containerName]; exists { versionNames := make([]string, 0, len(versions)) for versionName := range versions { diff --git a/src/service/producer/common.go b/src/service/common/datapack_resolver.go similarity index 59% rename from src/service/producer/common.go rename to src/service/common/datapack_resolver.go index e9fafb7f..d614e3f8 100644 --- a/src/service/producer/common.go +++ b/src/service/common/datapack_resolver.go @@ -1,11 +1,10 @@ -package producer +package common import ( "aegis/consts" - "aegis/database" "aegis/dto" + "aegis/model" "aegis/repository" - "aegis/service/common" "fmt" "gorm.io/gorm" @@ -24,8 +23,7 @@ var taskTypeDatapackStates = map[consts.TaskType][]consts.DatapackState{ }, } -// checkLabelKeyValue checks if a label with the specified key and value exists in the provided label slice -func checkLabelKeyValue(labels []database.Label, key, value string) bool { +func hasLabelKeyValue(labels []model.Label, key, value string) bool { for _, label := range labels { if label.Key == key && label.Value == value { return true @@ -34,30 +32,25 @@ func checkLabelKeyValue(labels []database.Label, key, value string) bool { return false } -// extractDatapacks extracts datapacks based on the provided datapack name or dataset ref -func extractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.DatasetRef, userID int, taskType consts.TaskType) ([]database.FaultInjection, *int, error) { +func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.DatasetRef, userID int, taskType consts.TaskType) ([]model.FaultInjection, *int, error) { states, exists := taskTypeDatapackStates[taskType] if !exists { return nil, nil, fmt.Errorf("unsupported task type: %s", consts.GetTaskTypeName(taskType)) } - validStates := map[consts.DatapackState]struct{}{} + validStates := make(map[consts.DatapackState]struct{}, len(states)) for _, state := range states { validStates[state] = struct{}{} } - // validateDatapack validates a single datapack's state and labels - validateDatapack := func(datapack *database.FaultInjection) error { - if _, exists := validStates[datapack.State]; !exists { + validateDatapack := func(datapack *model.FaultInjection) error { + if _, ok := validStates[datapack.State]; !ok { return fmt.Errorf("datapack %s is not in a valid state for execution", datapack.Name) } - - if len(datapack.Labels) > 0 && taskType == consts.TaskTypeRunAlgorithm { - if exists := checkLabelKeyValue(datapack.Labels, consts.LabelKeyTag, consts.DetectorNoAnomaly); exists { - return fmt.Errorf("cannot execute detector algorithm on no_anomaly datapack: %s", datapack.Name) - } + if len(datapack.Labels) > 0 && taskType == consts.TaskTypeRunAlgorithm && + hasLabelKeyValue(datapack.Labels, consts.LabelKeyTag, consts.DetectorNoAnomaly) { + return fmt.Errorf("cannot execute detector algorithm on no_anomaly datapack: %s", datapack.Name) } - return nil } @@ -66,22 +59,20 @@ func extractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset if err != nil { return nil, nil, fmt.Errorf("failed to get datapack: %w", err) } - if err := validateDatapack(datapack); err != nil { return nil, nil, err } - - return []database.FaultInjection{*datapack}, nil, nil + return []model.FaultInjection{*datapack}, nil, nil } if datasetRef != nil { - datasetVersionResults, err := common.MapRefsToDatasetVersions([]*dto.DatasetRef{datasetRef}, userID) + datasetVersionResults, err := MapRefsToDatasetVersionsWithDB(db, []*dto.DatasetRef{datasetRef}, userID) if err != nil { return nil, nil, fmt.Errorf("failed to get dataset versions: %w", err) } - version, exists := datasetVersionResults[datasetRef] - if !exists { + version, ok := datasetVersionResults[datasetRef] + if !ok { return nil, nil, fmt.Errorf("dataset version not found for %v", datasetRef) } @@ -89,17 +80,15 @@ func extractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset if err != nil { return nil, nil, fmt.Errorf("failed to get dataset datapacks: %s", err.Error()) } - if len(datapacks) == 0 { return nil, nil, fmt.Errorf("dataset contains no datapacks") } - for _, datapack := range datapacks { - if err := validateDatapack(&datapack); err != nil { + for i := range datapacks { + if err := validateDatapack(&datapacks[i]); err != nil { return nil, nil, err } } - return datapacks, &version.ID, nil } diff --git a/src/service/common/dataset.go b/src/service/common/dataset.go index 3700f55e..73925757 100644 --- a/src/service/common/dataset.go +++ b/src/service/common/dataset.go @@ -1,21 +1,23 @@ package common import ( - "aegis/database" "aegis/dto" + "aegis/model" "aegis/repository" "fmt" + + "gorm.io/gorm" ) // mapRefsToDatasetVersions maps dataset refs to their corresponding dataset versions -func MapRefsToDatasetVersions(refs []*dto.DatasetRef, userID int) (map[*dto.DatasetRef]database.DatasetVersion, error) { - versions, err := getUniqueVersionsForDatasetRefs(refs, userID) +func MapRefsToDatasetVersionsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID int) (map[*dto.DatasetRef]model.DatasetVersion, error) { + versions, err := getUniqueVersionsForDatasetRefsWithDB(db, refs, userID) if err != nil { return nil, fmt.Errorf("failed to batch get dataset versions: %w", err) } - flatMap := make(map[string][]database.DatasetVersion) - hierarchicalMap := make(map[string]map[string]database.DatasetVersion) + flatMap := make(map[string][]model.DatasetVersion) + hierarchicalMap := make(map[string]map[string]model.DatasetVersion) for _, version := range versions { datasetName := version.Dataset.Name @@ -24,14 +26,14 @@ func MapRefsToDatasetVersions(refs []*dto.DatasetRef, userID int) (map[*dto.Data flatMap[datasetName] = append(flatMap[datasetName], version) if _, exists := hierarchicalMap[datasetName]; !exists { - hierarchicalMap[datasetName] = make(map[string]database.DatasetVersion) + hierarchicalMap[datasetName] = make(map[string]model.DatasetVersion) } hierarchicalMap[datasetName][versionName] = version } - results := make(map[*dto.DatasetRef]database.DatasetVersion, len(refs)) + results := make(map[*dto.DatasetRef]model.DatasetVersion, len(refs)) for _, ref := range refs { - var result database.DatasetVersion + var result model.DatasetVersion if ref.Version != "" { if _, exists := hierarchicalMap[ref.Name]; !exists { return nil, fmt.Errorf("dataset not found: %s", ref.Name) @@ -55,8 +57,7 @@ func MapRefsToDatasetVersions(refs []*dto.DatasetRef, userID int) (map[*dto.Data return results, nil } -// getUniqueVersionsForDatasetrefs retrieves unique dataset versions for the given dataset refs -func getUniqueVersionsForDatasetRefs(refs []*dto.DatasetRef, userID int) ([]database.DatasetVersion, error) { +func getUniqueVersionsForDatasetRefsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID int) ([]model.DatasetVersion, error) { datasetNamesSet := make(map[string]struct{}, len(refs)) for _, ref := range refs { if ref.Name != "" { @@ -65,7 +66,7 @@ func getUniqueVersionsForDatasetRefs(refs []*dto.DatasetRef, userID int) ([]data } if len(datasetNamesSet) == 0 { - return []database.DatasetVersion{}, nil + return []model.DatasetVersion{}, nil } requiredNames := make([]string, 0, len(datasetNamesSet)) @@ -73,7 +74,7 @@ func getUniqueVersionsForDatasetRefs(refs []*dto.DatasetRef, userID int) ([]data requiredNames = append(requiredNames, name) } - versions, err := repository.BatchGetDatasetVersions(database.DB, requiredNames, userID) + versions, err := repository.BatchGetDatasetVersions(db, requiredNames, userID) if err != nil { return nil, fmt.Errorf("failed to batch get dataset versions: %w", err) } diff --git a/src/service/common/dynamic_config.go b/src/service/common/dynamic_config.go index fb9700cf..deccfb85 100644 --- a/src/service/common/dynamic_config.go +++ b/src/service/common/dynamic_config.go @@ -2,8 +2,7 @@ package common import ( "aegis/consts" - "aegis/database" - "aegis/repository" + "aegis/model" "encoding/json" "errors" "fmt" @@ -51,8 +50,8 @@ var configTypeRules = map[consts.ConfigValueType]configTypeConstraints{ } // CreateConfig creates a new configuration with history tracking -func CreateConfig(db *gorm.DB, config *database.DynamicConfig) error { - if err := repository.CreateConfig(db, config); err != nil { +func CreateConfig(db *gorm.DB, config *model.DynamicConfig) error { + if err := db.Create(config).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: configuration with key '%s' already exists", consts.ErrAlreadyExists, config.Key) } @@ -62,7 +61,7 @@ func CreateConfig(db *gorm.DB, config *database.DynamicConfig) error { } // ValidateConfig validates a configuration against its type and constraints -func ValidateConfig(cfg *database.DynamicConfig, value string) error { +func ValidateConfig(cfg *model.DynamicConfig, value string) error { // Validate metadata constraints if err := ValidateConfigMetadataConstraints(cfg); err != nil { return err @@ -128,7 +127,7 @@ func ValidateConfig(cfg *database.DynamicConfig, value string) error { } // ValidateConfigMetadataConstraints validates that metadata fields are appropriate for the value type -func ValidateConfigMetadataConstraints(cfg *database.DynamicConfig) error { +func ValidateConfigMetadataConstraints(cfg *model.DynamicConfig) error { rules, exists := configTypeRules[cfg.ValueType] if !exists { return fmt.Errorf("unknown value type: %d", cfg.ValueType) @@ -158,7 +157,7 @@ func ValidateConfigMetadataConstraints(cfg *database.DynamicConfig) error { } // validateConfigOptions validates the config value against allowed options based on value type -func validateConfigOptions(cfg *database.DynamicConfig, value string) error { +func validateConfigOptions(cfg *model.DynamicConfig, value string) error { switch cfg.ValueType { case consts.ConfigValueTypeString: var allowedOptions []string diff --git a/src/service/common/injection.go b/src/service/common/injection.go index 94963943..4e097507 100644 --- a/src/service/common/injection.go +++ b/src/service/common/injection.go @@ -3,14 +3,16 @@ package common import ( "aegis/consts" "aegis/dto" + redisinfra "aegis/infra/redis" "aegis/utils" "context" "fmt" "time" + + "gorm.io/gorm" ) -// ProduceFaultInjectionTasks produces fault injection tasks into Redis based on the request specifications -func ProduceFaultInjectionTasks(ctx context.Context, task *dto.UnifiedTask, injectTime time.Time, payload map[string]any) error { +func ProduceFaultInjectionTasksWithDB(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, injectTime time.Time, payload map[string]any) error { newTask := &dto.UnifiedTask{ Type: consts.TaskTypeFaultInjection, Immediate: false, @@ -25,7 +27,7 @@ func ProduceFaultInjectionTasks(ctx context.Context, task *dto.UnifiedTask, inje TraceCarrier: task.TraceCarrier, GroupCarrier: task.GroupCarrier, } - err := SubmitTask(ctx, newTask) + err := SubmitTaskWithDB(ctx, db, redisGateway, newTask) if err != nil { return fmt.Errorf("failed to submit fault injection task: %w", err) } diff --git a/src/service/common/label.go b/src/service/common/label.go index 53d8a34e..2c396819 100644 --- a/src/service/common/label.go +++ b/src/service/common/label.go @@ -2,8 +2,8 @@ package common import ( "aegis/consts" - "aegis/database" "aegis/dto" + "aegis/model" "aegis/repository" "aegis/utils" "fmt" @@ -31,9 +31,9 @@ func ConvertLabelFiltersToConditions(labelItems []dto.LabelItem) []map[string]st // CreateOrUpdateLabelsFromItems creates or updates labels based on the provided label items // Returns labels with correct IDs and updates usage_count for existing labels -func CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, category consts.LabelCategory) ([]database.Label, error) { +func CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, category consts.LabelCategory) ([]model.Label, error) { if len(labelItems) == 0 { - return []database.Label{}, nil + return []model.Label{}, nil } // Build key -> value map for quick lookup @@ -50,7 +50,7 @@ func CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, cate } // Separate existing and new labels - result := make([]database.Label, 0, len(labelItems)) + result := make([]model.Label, 0, len(labelItems)) existingIDs := make([]int, 0, len(existingLabels)) for _, existing := range existingLabels { if item, ok := kvMap[existing.Key]; ok && item.Value == existing.Value { @@ -69,10 +69,10 @@ func CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, cate // Create new labels (only those not found in existing) if len(kvMap) > 0 { - newLabels := make([]database.Label, 0, len(kvMap)) + newLabels := make([]model.Label, 0, len(kvMap)) for key, item := range kvMap { - newLabels = append(newLabels, database.Label{ + newLabels = append(newLabels, model.Label{ Key: key, Value: item.Value, Category: category, diff --git a/src/service/common/metadata_store.go b/src/service/common/metadata_store.go index c38a88ee..a15da4e0 100644 --- a/src/service/common/metadata_store.go +++ b/src/service/common/metadata_store.go @@ -5,20 +5,21 @@ import ( "fmt" "sync" - "aegis/database" - "aegis/repository" + "aegis/model" chaos "github.com/OperationsPAI/chaos-experiment/handler" + "gorm.io/gorm" ) // DBMetadataStore implements chaos.MetadataStore by reading from MySQL with in-memory caching. type DBMetadataStore struct { + db *gorm.DB cache sync.Map // key: "system:type:service" -> cached data } // NewDBMetadataStore creates a new DBMetadataStore instance. -func NewDBMetadataStore() *DBMetadataStore { - return &DBMetadataStore{} +func NewDBMetadataStore(db *gorm.DB) *DBMetadataStore { + return &DBMetadataStore{db: db} } func (s *DBMetadataStore) cacheKey(system, metaType, service string) string { @@ -31,7 +32,7 @@ func (s *DBMetadataStore) GetServiceEndpoints(system, serviceName string) ([]cha return cached.([]chaos.ServiceEndpointData), nil } - meta, err := repository.GetSystemMetadata(database.DB, system, "service_endpoint", serviceName) + meta, err := s.getSystemMetadata(system, "service_endpoint", serviceName) if err != nil { return nil, fmt.Errorf("failed to get service endpoints: %w", err) } @@ -54,7 +55,7 @@ func (s *DBMetadataStore) GetAllServiceNames(system string) ([]string, error) { return cached.([]string), nil } - names, err := repository.ListServiceNames(database.DB, system, "service_endpoint") + names, err := s.listServiceNames(system, "service_endpoint") if err != nil { return nil, fmt.Errorf("failed to get service names: %w", err) } @@ -69,7 +70,7 @@ func (s *DBMetadataStore) GetJavaClassMethods(system, serviceName string) ([]cha return cached.([]chaos.JavaClassMethodData), nil } - meta, err := repository.GetSystemMetadata(database.DB, system, "java_class_method", serviceName) + meta, err := s.getSystemMetadata(system, "java_class_method", serviceName) if err != nil { return nil, fmt.Errorf("failed to get java class methods: %w", err) } @@ -92,7 +93,7 @@ func (s *DBMetadataStore) GetDatabaseOperations(system, serviceName string) ([]c return cached.([]chaos.DatabaseOperationData), nil } - meta, err := repository.GetSystemMetadata(database.DB, system, "database_operation", serviceName) + meta, err := s.getSystemMetadata(system, "database_operation", serviceName) if err != nil { return nil, fmt.Errorf("failed to get database operations: %w", err) } @@ -115,7 +116,7 @@ func (s *DBMetadataStore) GetGRPCOperations(system, serviceName string) ([]chaos return cached.([]chaos.GRPCOperationData), nil } - meta, err := repository.GetSystemMetadata(database.DB, system, "grpc_operation", serviceName) + meta, err := s.getSystemMetadata(system, "grpc_operation", serviceName) if err != nil { return nil, fmt.Errorf("failed to get gRPC operations: %w", err) } @@ -138,7 +139,7 @@ func (s *DBMetadataStore) GetNetworkPairs(system string) ([]chaos.NetworkPairDat return cached.([]chaos.NetworkPairData), nil } - metas, err := repository.ListSystemMetadata(database.DB, system, "network_dependency") + metas, err := s.listSystemMetadata(system, "network_dependency") if err != nil { return nil, fmt.Errorf("failed to get network pairs: %w", err) } @@ -163,3 +164,39 @@ func (s *DBMetadataStore) InvalidateCache() { return true }) } + +func (s *DBMetadataStore) getSystemMetadata(systemName, metadataType, serviceName string) (*model.SystemMetadata, error) { + var meta model.SystemMetadata + if err := s.db.Where("system_name = ? AND metadata_type = ? AND service_name = ?", systemName, metadataType, serviceName). + First(&meta).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to get system metadata: %w", err) + } + return &meta, nil +} + +func (s *DBMetadataStore) listSystemMetadata(systemName, metadataType string) ([]model.SystemMetadata, error) { + var metas []model.SystemMetadata + query := s.db.Where("system_name = ?", systemName) + if metadataType != "" { + query = query.Where("metadata_type = ?", metadataType) + } + if err := query.Find(&metas).Error; err != nil { + return nil, fmt.Errorf("failed to list system metadata: %w", err) + } + return metas, nil +} + +func (s *DBMetadataStore) listServiceNames(systemName, metadataType string) ([]string, error) { + var names []string + query := s.db.Model(&model.SystemMetadata{}).Where("system_name = ?", systemName) + if metadataType != "" { + query = query.Where("metadata_type = ?", metadataType) + } + if err := query.Distinct("service_name").Pluck("service_name", &names).Error; err != nil { + return nil, fmt.Errorf("failed to list service names: %w", err) + } + return names, nil +} diff --git a/src/service/common/task.go b/src/service/common/task.go index 9d9dc4a8..0ea923e1 100644 --- a/src/service/common/task.go +++ b/src/service/common/task.go @@ -2,9 +2,9 @@ package common import ( "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" + redisinfra "aegis/infra/redis" + "aegis/model" "context" "encoding/json" "fmt" @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/robfig/cron/v3" "gorm.io/gorm" + "gorm.io/gorm/clause" ) // cronNextTime calculates the next execution time from a cron expression @@ -45,7 +46,14 @@ func CronNextTime(expr string) (time.Time, error) { // -> Task 3 // -> Task 4 // -> Task 5 -func SubmitTask(ctx context.Context, t *dto.UnifiedTask) error { +func SubmitTaskWithDB(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, t *dto.UnifiedTask) error { + if db == nil { + return fmt.Errorf("task db is nil") + } + if redisGateway == nil { + return fmt.Errorf("task redis gateway is nil") + } + if t.TraceID == "" { t.TraceID = uuid.NewString() } @@ -55,7 +63,7 @@ func SubmitTask(ctx context.Context, t *dto.UnifiedTask) error { } if t.ParentTaskID != nil && t.State != consts.TaskRescheduled { - parentLevel, err := repository.GetParentTaskLevelByID(database.DB, *t.ParentTaskID) + parentLevel, err := getParentTaskLevelByID(db, *t.ParentTaskID) if err != nil { return fmt.Errorf("failed to get parent task level: %w", err) } @@ -68,7 +76,7 @@ func SubmitTask(ctx context.Context, t *dto.UnifiedTask) error { } } - var trace *database.Trace + var trace *model.Trace var err error if t.ParentTaskID == nil && t.State != consts.TaskRescheduled { withAlgorithms := false @@ -93,14 +101,14 @@ func SubmitTask(ctx context.Context, t *dto.UnifiedTask) error { return fmt.Errorf("failed to convert to task: %w", err) } - err = database.DB.Transaction(func(tx *gorm.DB) error { + err = db.Transaction(func(tx *gorm.DB) error { if trace != nil { - if err := repository.UpsertTrace(tx, trace); err != nil { + if err := upsertTrace(tx, trace); err != nil { return fmt.Errorf("failed to upsert trace to database: %w", err) } } - if err := repository.UpsertTask(tx, task); err != nil { + if err := upsertTask(tx, task); err != nil { return fmt.Errorf("failed to upsert task to database: %w", err) } @@ -116,9 +124,9 @@ func SubmitTask(ctx context.Context, t *dto.UnifiedTask) error { } if t.Immediate { - err = repository.SubmitImmediateTask(ctx, taskData, t.TaskID) + err = redisGateway.SubmitImmediateTask(ctx, taskData, t.TaskID) } else { - err = repository.SubmitDelayedTask(ctx, taskData, t.TaskID, t.ExecuteTime) + err = redisGateway.SubmitDelayedTask(ctx, taskData, t.TaskID, t.ExecuteTime) } if err != nil { @@ -140,3 +148,46 @@ func calculateExecuteTime(task *dto.UnifiedTask) error { } return nil } + +func getParentTaskLevelByID(db *gorm.DB, parentTaskID string) (int, error) { + var result model.Task + if err := db.Select("level"). + Where("id = ? AND status != ?", parentTaskID, consts.CommonDeleted). + First(&result).Error; err != nil { + return 0, fmt.Errorf("failed to find parent task with id %s: %w", parentTaskID, err) + } + return result.Level, nil +} + +func upsertTask(db *gorm.DB, task *model.Task) error { + if err := db.Clauses( + clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "execute_time", + "state", + "updated_at", + }), + }, + ).Create(task).Error; err != nil { + return fmt.Errorf("failed to upsert task: %w", err) + } + return nil +} + +func upsertTrace(db *gorm.DB, trace *model.Trace) error { + if err := db.Clauses( + clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "last_event", + "end_time", + "state", + "updated_at", + }), + }, + ).Create(trace).Error; err != nil { + return fmt.Errorf("failed to upsert trace: %w", err) + } + return nil +} diff --git a/src/service/consumer/algo_execution.go b/src/service/consumer/algo_execution.go index c0e2bafd..ef548b51 100644 --- a/src/service/consumer/algo_execution.go +++ b/src/service/consumer/algo_execution.go @@ -11,11 +11,12 @@ import ( "strings" "time" - "aegis/client/k8s" "aegis/config" "aegis/consts" - "aegis/database" "aegis/dto" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + "aegis/model" "aegis/repository" "aegis/service/common" "aegis/tracing" @@ -44,8 +45,8 @@ type algoJobCreationParams struct { payload *executionPayload } -func (p *algoJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, initContainers []corev1.Container, volumeMountconfigs []k8s.VolumeMountConfig) *k8s.JobConfig { - return &k8s.JobConfig{ +func (p *algoJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, initContainers []corev1.Container, volumeMountconfigs []k8sinfra.VolumeMountConfig) *k8sinfra.JobConfig { + return &k8sinfra.JobConfig{ JobName: p.jobName, Image: p.image, Command: strings.Split(p.payload.algorithm.Command, " "), @@ -59,7 +60,7 @@ func (p *algoJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, initCont } // executeAlgorithm handles the execution of an algorithm task -func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { +func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) span.AddEvent(fmt.Sprintf("Starting algorithm execution attempt %d", task.ReStartNum+1)) @@ -67,8 +68,19 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { "task_id": task.TaskID, "trace_id": task.TraceID, }) + k8sGateway := deps.K8sGateway + if k8sGateway == nil { + return handleExecutionError(span, logEntry, "k8s gateway not initialized", fmt.Errorf("k8s gateway not initialized")) + } + redisGateway := deps.RedisGateway + if redisGateway == nil { + return handleExecutionError(span, logEntry, "redis gateway not initialized", fmt.Errorf("redis gateway not initialized")) + } - rateLimiter := GetAlgoExecutionRateLimiter() + rateLimiter := deps.AlgorithmRateLimiter + if rateLimiter == nil { + return handleExecutionError(span, logEntry, "algorithm execution rate limiter not initialized", fmt.Errorf("algorithm execution rate limiter not initialized")) + } acquired, err := rateLimiter.AcquireToken(childCtx, task.TaskID, task.TraceID) if err != nil { return handleExecutionError(span, logEntry, "failed to acquire rate limit token", err) @@ -84,7 +96,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { } if !acquired { - if err := rescheduleAlgoExecutionTask(childCtx, task, "failed to acquire algorithm execution token within timeout, retrying later"); err != nil { + if err := rescheduleAlgoExecutionTask(childCtx, deps.DB, redisGateway, task, "failed to acquire algorithm execution token within timeout, retrying later"); err != nil { return err } return nil @@ -96,7 +108,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { return handleExecutionError(span, logEntry, "failed to parse execution payload", err) } - executionID, err := createExecution(task.TaskID, payload.algorithm.ID, payload.datapack.ID, payload.datasetVersionID, payload.labels) + executionID, err := createExecution(deps.DB, task.TaskID, payload.algorithm.ID, payload.datapack.ID, payload.datasetVersionID, payload.labels) if err != nil { return handleExecutionError(span, logEntry, "failed to create execution result", err) } @@ -136,7 +148,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { executionID: executionID, payload: payload, } - if err := createAlgoJob(childCtx, params); err != nil { + if err := createAlgoJob(childCtx, k8sGateway, params); err != nil { return err } @@ -145,7 +157,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask) error { } // rescheduleAlgoExecutionTask reschedules a algorithm execution task with a random delay between 1 to 5 minutes -func rescheduleAlgoExecutionTask(ctx context.Context, task *dto.UnifiedTask, reason string) error { +func rescheduleAlgoExecutionTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) @@ -169,11 +181,11 @@ func rescheduleAlgoExecutionTask(ctx context.Context, task *dto.UnifiedTask, rea consts.TaskTypeRunAlgorithm, consts.TaskRescheduled, reason, - ).withEvent(consts.EventNoTokenAvailable, executeTime.String()), + ).withEvent(consts.EventNoTokenAvailable, executeTime.String()).withDB(db).withRedis(redisGateway), ) task.Reschedule(executeTime) - if err := common.SubmitTask(childCtx, task); err != nil { + if err := common.SubmitTaskWithDB(childCtx, db, redisGateway, task); err != nil { span.RecordError(err) span.AddEvent("failed to submit rescheduled task") return fmt.Errorf("failed to submit rescheduled algorithm execution task: %w", err) @@ -216,7 +228,7 @@ func parseExecutionPayload(payload map[string]any) (*executionPayload, error) { } // createAlgoJob creates and submits a Kubernetes job for algorithm execution -func createAlgoJob(ctx context.Context, params *algoJobCreationParams) error { +func createAlgoJob(ctx context.Context, gateway *k8sinfra.Gateway, params *algoJobCreationParams) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ @@ -224,7 +236,7 @@ func createAlgoJob(ctx context.Context, params *algoJobCreationParams) error { "execution_id": params.executionID, }) - volumeMountConfigs, err := getRequiredVolumeMountConfigs([]consts.VolumeMountName{ + volumeMountConfigs, err := getRequiredVolumeMountConfigs(gateway, []consts.VolumeMountName{ consts.VolumeMountDataset, consts.VolumeMountExperimentStorage, }) @@ -262,7 +274,7 @@ func createAlgoJob(ctx context.Context, params *algoJobCreationParams) error { }, } - return k8s.CreateJob(childCtx, params.toK8sJobConfig(jobEnvVars, initContainers, volumeMountConfigs)) + return gateway.CreateJob(childCtx, params.toK8sJobConfig(jobEnvVars, initContainers, volumeMountConfigs)) }) } @@ -331,11 +343,14 @@ func getAlgoJobEnvVars(taskID string, executionID int, datapackPathPrefix, expPa } // createExecution creates a new execution record with associated labels -func createExecution(taskID string, algorithmVersionID, datapackID int, datasetVersionID *int, labelItems []dto.LabelItem) (int, error) { +func createExecution(db *gorm.DB, taskID string, algorithmVersionID, datapackID int, datasetVersionID *int, labelItems []dto.LabelItem) (int, error) { var createdExecutionID int + if db == nil { + return 0, fmt.Errorf("consumer runtime db is nil") + } - err := database.DB.Transaction(func(tx *gorm.DB) error { - execution := &database.Execution{ + err := db.Transaction(func(tx *gorm.DB) error { + execution := &model.Execution{ TaskID: &taskID, AlgorithmVersionID: algorithmVersionID, DatapackID: datapackID, diff --git a/src/service/consumer/build_container.go b/src/service/consumer/build_container.go index 11d7cccf..1839b80a 100644 --- a/src/service/consumer/build_container.go +++ b/src/service/consumer/build_container.go @@ -8,9 +8,10 @@ import ( "path/filepath" "time" - "aegis/config" "aegis/consts" "aegis/dto" + buildkitinfra "aegis/infra/buildkit" + redisinfra "aegis/infra/redis" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -29,6 +30,7 @@ import ( "github.com/tonistiigi/fsutil" "go.opentelemetry.io/otel/trace" "golang.org/x/sync/errgroup" + "gorm.io/gorm" ) type containerPayload struct { @@ -38,7 +40,7 @@ type containerPayload struct { } // executeBuildContainer handles the execution of a build container task -func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { +func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) span.AddEvent(fmt.Sprintf("Starting build attempt %d", task.ReStartNum+1)) @@ -46,8 +48,19 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { "task_id": task.TaskID, "trace_id": task.TraceID, }) + buildKitGateway := deps.BuildKitGateway + if buildKitGateway == nil { + return handleExecutionError(span, logEntry, "buildkit gateway not initialized", fmt.Errorf("buildkit gateway not initialized")) + } + redisGateway := deps.RedisGateway + if redisGateway == nil { + return handleExecutionError(span, logEntry, "redis gateway not initialized", fmt.Errorf("redis gateway not initialized")) + } - rateLimiter := GetBuildContainerRateLimiter() + rateLimiter := deps.BuildRateLimiter + if rateLimiter == nil { + return handleExecutionError(span, logEntry, "build container rate limiter not initialized", fmt.Errorf("build container rate limiter not initialized")) + } acquired, err := rateLimiter.AcquireToken(childCtx, task.TaskID, task.TraceID) if err != nil { return handleExecutionError(span, logEntry, "failed to acquire rate limit token", err) @@ -63,7 +76,7 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { } if !acquired { - if err := rescheduleContainerBuildingTask(childCtx, task, "failed to acquire build token within timeout, retrying later"); err != nil { + if err := rescheduleContainerBuildingTask(childCtx, deps.DB, redisGateway, task, "failed to acquire build token within timeout, retrying later"); err != nil { return err } return nil @@ -83,7 +96,7 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { return handleExecutionError(span, logEntry, "failed to parse build payload", err) } - if err := buildImageAndPush(childCtx, payload, logEntry); err != nil { + if err := buildImageAndPush(childCtx, buildKitGateway, payload, logEntry); err != nil { return err } @@ -94,7 +107,7 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { task.Type, consts.TaskCompleted, fmt.Sprintf("Container image %s built and pushed successfully", payload.imageRef), - ).withEvent(consts.EventImageBuildSucceed, payload.imageRef), + ).withEvent(consts.EventImageBuildSucceed, payload.imageRef).withDB(deps.DB).withRedis(redisGateway), ) if err := os.RemoveAll(payload.sourcePath); err != nil { @@ -107,7 +120,7 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask) error { } // rescheduleContainerBuildingTask reschedules a container building task with a random delay between 1 to 5 minutes -func rescheduleContainerBuildingTask(ctx context.Context, task *dto.UnifiedTask, reason string) error { +func rescheduleContainerBuildingTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) @@ -131,11 +144,11 @@ func rescheduleContainerBuildingTask(ctx context.Context, task *dto.UnifiedTask, task.Type, consts.TaskRescheduled, reason, - ).withEvent(consts.EventNoTokenAvailable, executeTime.String()), + ).withEvent(consts.EventNoTokenAvailable, executeTime.String()).withDB(db).withRedis(redisGateway), ) task.Reschedule(executeTime) - if err := common.SubmitTask(childCtx, task); err != nil { + if err := common.SubmitTaskWithDB(childCtx, db, redisGateway, task); err != nil { span.RecordError(err) span.AddEvent("failed to submit rescheduled task") return fmt.Errorf("failed to submit rescheduled container building task: %v", err) @@ -172,17 +185,11 @@ func parseContainerPayload(payload map[string]any) (*containerPayload, error) { } // buildImageAndPush builds the container image using BuildKit and pushes it to the registry -func buildImageAndPush(ctx context.Context, payload *containerPayload, logEntry *logrus.Entry) error { +func buildImageAndPush(ctx context.Context, buildKitGateway *buildkitinfra.Gateway, payload *containerPayload, logEntry *logrus.Entry) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) - address := fmt.Sprintf("tcp://%s", config.GetString("buildkit.address")) - if address == "" { - err := fmt.Errorf("buildkit address is not configured") - return handleExecutionError(span, logEntry, "buildkit address is not configured", err) - } - - c, err := buildkitclient.New(childCtx, address) + c, err := buildKitGateway.NewClient(childCtx) if err != nil { return handleExecutionError(span, logEntry, "failed to create buildkit client", err) } diff --git a/src/service/consumer/build_datapack.go b/src/service/consumer/build_datapack.go index f8dcae51..db8fa38f 100644 --- a/src/service/consumer/build_datapack.go +++ b/src/service/consumer/build_datapack.go @@ -13,11 +13,11 @@ import ( "go.opentelemetry.io/otel/trace" corev1 "k8s.io/api/core/v1" - "aegis/client/k8s" "aegis/config" "aegis/consts" - "aegis/database" "aegis/dto" + dbinfra "aegis/infra/db" + k8sinfra "aegis/infra/k8s" "aegis/tracing" "aegis/utils" ) @@ -35,11 +35,11 @@ type datapackJobCreationParams struct { annotations map[string]string labels map[string]string payload *datapackPayload - dbConfig *database.DatabaseConfig + dbConfig *dbinfra.DatabaseConfig } -func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volumeMountConfigs []k8s.VolumeMountConfig) *k8s.JobConfig { - return &k8s.JobConfig{ +func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volumeMountConfigs []k8sinfra.VolumeMountConfig) *k8sinfra.JobConfig { + return &k8sinfra.JobConfig{ JobName: p.jobName, Image: p.image, Command: strings.Split(p.payload.benchmark.Command, " "), @@ -53,9 +53,17 @@ func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volu // executeBuildDatapack handles the execution of a datapack building task func executeBuildDatapack(ctx context.Context, task *dto.UnifiedTask) error { + return executeBuildDatapackWithDeps(ctx, task, RuntimeDeps{}) +} + +func executeBuildDatapackWithDeps(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{"task_id": task.TaskID, "trace_id": task.TraceID}) + k8sGateway := deps.K8sGateway + if k8sGateway == nil { + return handleExecutionError(span, logEntry, "k8s gateway not initialized", fmt.Errorf("k8s gateway not initialized")) + } payload, err := parseDatapackPayload(task.Payload) if err != nil { @@ -88,9 +96,9 @@ func executeBuildDatapack(ctx context.Context, task *dto.UnifiedTask) error { annotations: annotations, labels: jobLabels, payload: payload, - dbConfig: database.NewDatabaseConfig("clickhouse"), + dbConfig: dbinfra.NewDatabaseConfig("clickhouse"), } - return createDatapackJob(childCtx, params) + return createDatapackJob(childCtx, k8sGateway, params) }) } @@ -124,7 +132,7 @@ func parseDatapackPayload(payload map[string]any) (*datapackPayload, error) { }, nil } -func createDatapackJob(ctx context.Context, params *datapackJobCreationParams) error { +func createDatapackJob(ctx context.Context, gateway *k8sinfra.Gateway, params *datapackJobCreationParams) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ @@ -132,7 +140,7 @@ func createDatapackJob(ctx context.Context, params *datapackJobCreationParams) e "datapack_id": params.payload.datapack.ID, }) - volumeMountConfigs, err := getRequiredVolumeMountConfigs([]consts.VolumeMountName{ + volumeMountConfigs, err := getRequiredVolumeMountConfigs(gateway, []consts.VolumeMountName{ consts.VolumeMountDataset, }) if err != nil { @@ -146,11 +154,11 @@ func createDatapackJob(ctx context.Context, params *datapackJobCreationParams) e return handleExecutionError(span, logEntry, "failed to get job environment variables", err) } - return k8s.CreateJob(childCtx, params.toK8sJobConfig(jobEnvVars, volumeMountConfigs)) + return gateway.CreateJob(childCtx, params.toK8sJobConfig(jobEnvVars, volumeMountConfigs)) }) } -func getDatapackJobEnvVars(taskID string, datapackPathPrefix string, payload *datapackPayload, dbConfig *database.DatabaseConfig) ([]corev1.EnvVar, error) { +func getDatapackJobEnvVars(taskID string, datapackPathPrefix string, payload *datapackPayload, dbConfig *dbinfra.DatabaseConfig) ([]corev1.EnvVar, error) { tz := config.GetString("system.timezone") if tz == "" { tz = time.Local.String() diff --git a/src/service/consumer/collect_result.go b/src/service/consumer/collect_result.go index fcc73793..cc0ae82a 100644 --- a/src/service/consumer/collect_result.go +++ b/src/service/consumer/collect_result.go @@ -4,11 +4,10 @@ import ( "context" "fmt" - "aegis/client" "aegis/config" "aegis/consts" - "aegis/database" "aegis/dto" + redisinfra "aegis/infra/redis" "aegis/repository" "aegis/service/common" "aegis/tracing" @@ -16,6 +15,7 @@ import ( "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" ) type collectionPayload struct { @@ -24,8 +24,17 @@ type collectionPayload struct { executionID int } -func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { +func executeCollectResult(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { + db := deps.DB + if db == nil { + return fmt.Errorf("consumer runtime db is nil") + } + redisGateway := deps.RedisGateway + if redisGateway == nil { + return fmt.Errorf("consumer redis gateway is nil") + } + logEntry := logrus.WithField("task_id", task.TaskID) span := trace.SpanFromContext(childCtx) @@ -38,7 +47,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { } if collectPayload.algorithm.ContainerName == config.GetDetectorName() { - results, err := repository.ListDetectorResultsByExecutionID(database.DB, collectPayload.executionID) + results, err := repository.ListDetectorResultsByExecutionID(db, collectPayload.executionID) if err != nil { logEntry.Errorf("failed to get detector results by execution ID: %v", err) span.AddEvent("failed to get detector results by execution ID") @@ -72,18 +81,20 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { span.AddEvent(message) } - updateTaskState(childCtx, taskCompletedWithEvent(task, eventName, results)) + updateTaskState(childCtx, taskCompletedWithEvent(task, eventName, results).withDB(db).withRedis(redisGateway)) logEntry.Info("Collect detector result task completed successfully") - if hasIssues && client.CheckCachedField(childCtx, consts.InjectionAlgorithmsKey, task.GroupID) { - var algorithms []dto.ContainerVersionItem - err := client.GetHashField(childCtx, consts.InjectionAlgorithmsKey, task.GroupID, &algorithms) + if hasIssues { + algorithms, cached, err := loadCachedInjectionAlgorithms(redisGateway, childCtx, task.GroupID) if err != nil { span.AddEvent("failed to get algorithms from redis") span.RecordError(err) return fmt.Errorf("failed to get algorithms from redis: %w", err) } + if !cached { + return nil + } for idx, algorithm := range algorithms { payload := map[string]any{ @@ -91,7 +102,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { consts.ExecuteDatapack: collectPayload.datapack, } - if err := produceAlgorithmExeuctionTask(childCtx, task, payload, idx); err != nil { + if err := produceAlgorithmExeuctionTask(childCtx, db, deps.RedisGateway, task, payload, idx); err != nil { span.AddEvent("failed to submit algorithm execution task") span.RecordError(err) return fmt.Errorf("failed to submit algorithm execution task: %w", err) @@ -104,7 +115,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { return nil } - results, err := repository.ListGranularityResultsByExecutionID(database.DB, collectPayload.executionID) + results, err := repository.ListGranularityResultsByExecutionID(db, collectPayload.executionID) if err != nil { span.AddEvent("failed to get detector results by execution ID") span.RecordError(err) @@ -119,7 +130,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask) error { span.AddEvent(message) } - updateTaskState(childCtx, taskCompletedWithEvent(task, eventName, results)) + updateTaskState(childCtx, taskCompletedWithEvent(task, eventName, results).withDB(db).withRedis(redisGateway)) logEntry.Info("Collect algorithm result task completed successfully") return nil @@ -152,7 +163,7 @@ func parseCollectPayload(payload map[string]any) (*collectionPayload, error) { } // produceAlgorithmExeuctionTask produces an algorithm execution task into Redis -func produceAlgorithmExeuctionTask(ctx context.Context, task *dto.UnifiedTask, payload map[string]any, index int) error { +func produceAlgorithmExeuctionTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, payload map[string]any, index int) error { newTask := &dto.UnifiedTask{ Type: consts.TaskTypeRunAlgorithm, Immediate: true, @@ -166,7 +177,7 @@ func produceAlgorithmExeuctionTask(ctx context.Context, task *dto.UnifiedTask, p State: consts.TaskPending, TraceCarrier: task.TraceCarrier, } - err := common.SubmitTask(ctx, newTask) + err := common.SubmitTaskWithDB(ctx, db, redisGateway, newTask) if err != nil { return fmt.Errorf("failed to submit algorithm exectuion task: %w", err) } diff --git a/src/service/consumer/common.go b/src/service/consumer/common.go index 39002190..53f2b430 100644 --- a/src/service/consumer/common.go +++ b/src/service/consumer/common.go @@ -1,8 +1,8 @@ package consumer import ( - "aegis/client/k8s" "aegis/consts" + k8sinfra "aegis/infra/k8s" "fmt" "github.com/sirupsen/logrus" @@ -16,13 +16,17 @@ const ( ) // getRequiredVolumeMountConfigs retrieves the volume mount configurations for the specified required keys -func getRequiredVolumeMountConfigs(requiredKeys []consts.VolumeMountName) ([]k8s.VolumeMountConfig, error) { - volumeMountConfigMap, err := k8s.GetVolumeMountConfigMap() +func getRequiredVolumeMountConfigs(gateway *k8sinfra.Gateway, requiredKeys []consts.VolumeMountName) ([]k8sinfra.VolumeMountConfig, error) { + if gateway == nil { + return nil, fmt.Errorf("k8s gateway is nil") + } + + volumeMountConfigMap, err := gateway.GetVolumeMountConfigMap() if err != nil { return nil, fmt.Errorf("failed to get volume mount configuration map: %w", err) } - volumeMountConfigs := make([]k8s.VolumeMountConfig, 0, len(requiredKeys)) + volumeMountConfigs := make([]k8sinfra.VolumeMountConfig, 0, len(requiredKeys)) for _, vmName := range requiredKeys { cfg, exists := volumeMountConfigMap[vmName] diff --git a/src/service/consumer/config_handlers.go b/src/service/consumer/config_handlers.go index 4d129539..4ffbc558 100644 --- a/src/service/consumer/config_handlers.go +++ b/src/service/consumer/config_handlers.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "aegis/client/k8s" "aegis/config" "aegis/consts" + k8sinfra "aegis/infra/k8s" "aegis/service/common" "github.com/sirupsen/logrus" @@ -15,19 +15,27 @@ import ( // RegisterConsumerHandlers registers all consumer-scoped configuration handlers. // Should be called during consumer initialization, after RegisterGlobalHandlers. -func RegisterConsumerHandlers() { +func RegisterConsumerHandlers( + controller *k8sinfra.Controller, + monitor NamespaceMonitor, + publisher common.ConfigPublisher, + restartLimiter *TokenBucketRateLimiter, + buildLimiter *TokenBucketRateLimiter, + algoLimiter *TokenBucketRateLimiter, +) { scope := consts.ConfigScopeConsumer - common.RegisterHandler(newChaosSystemCountHandler(GetMonitor(), k8s.GetK8sController())) + common.RegisterHandler(newChaosSystemCountHandler(monitor, controller, publisher)) common.RegisterHandler(newRateLimitingConfigHandler( - GetRestartPedestalRateLimiter(), - GetBuildContainerRateLimiter(), - GetAlgoExecutionRateLimiter(), + publisher, + restartLimiter, + buildLimiter, + algoLimiter, )) logrus.Infof("Registered consumer config handlers: %v", common.ListRegisteredConfigKeys(&scope)) } // UpdateK8sController updates K8s controller informers based on namespace changes. -func UpdateK8sController(controller *k8s.Controller, toAdd, toRemove []string) error { +func UpdateK8sController(controller *k8sinfra.Controller, toAdd, toRemove []string) error { if controller == nil { logrus.Warn("Controller not initialized, skipping informer update") return nil @@ -53,19 +61,20 @@ func UpdateK8sController(controller *k8s.Controller, toAdd, toRemove []string) e // ===================================================================== type chaosSystemCountHandler struct { - monitor *monitor - controller *k8s.Controller + monitor NamespaceMonitor + controller *k8sinfra.Controller + publisher common.ConfigPublisher } -func newChaosSystemCountHandler(m *monitor, c *k8s.Controller) *chaosSystemCountHandler { - return &chaosSystemCountHandler{monitor: m, controller: c} +func newChaosSystemCountHandler(m NamespaceMonitor, c *k8sinfra.Controller, publisher common.ConfigPublisher) *chaosSystemCountHandler { + return &chaosSystemCountHandler{monitor: m, controller: c, publisher: publisher} } func (h *chaosSystemCountHandler) Category() string { return "injection.system.count" } func (h *chaosSystemCountHandler) Scope() consts.ConfigScope { return consts.ConfigScopeConsumer } func (h *chaosSystemCountHandler) Handle(ctx context.Context, key, oldValue, newValue string) error { - return common.PublishWrapper(ctx, func() error { + return common.PublishWrapper(ctx, h.publisher, func() error { return config.GetChaosSystemConfigManager().Reload(h.onUpdate) }) } @@ -115,17 +124,20 @@ func (h *chaosSystemCountHandler) onUpdate() error { // ===================================================================== type rateLimitingConfigHandler struct { + publisher common.ConfigPublisher restartLimiter *TokenBucketRateLimiter buildLimiter *TokenBucketRateLimiter algoLimiter *TokenBucketRateLimiter } func newRateLimitingConfigHandler( + publisher common.ConfigPublisher, restartLimiter *TokenBucketRateLimiter, buildLimiter *TokenBucketRateLimiter, algoLimiter *TokenBucketRateLimiter, ) *rateLimitingConfigHandler { return &rateLimitingConfigHandler{ + publisher: publisher, restartLimiter: restartLimiter, buildLimiter: buildLimiter, algoLimiter: algoLimiter, @@ -136,7 +148,7 @@ func (h *rateLimitingConfigHandler) Category() string { return "rate_li func (h *rateLimitingConfigHandler) Scope() consts.ConfigScope { return consts.ConfigScopeConsumer } func (h *rateLimitingConfigHandler) Handle(ctx context.Context, key, oldValue, newValue string) error { - return common.PublishWrapper(ctx, func() error { + return common.PublishWrapper(ctx, h.publisher, func() error { logrus.WithFields(logrus.Fields{ "key": key, "old_value": oldValue, diff --git a/src/service/consumer/distribute_tasks.go b/src/service/consumer/distribute_tasks.go index fc35b4ea..10be995c 100644 --- a/src/service/consumer/distribute_tasks.go +++ b/src/service/consumer/distribute_tasks.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -func dispatchTask(ctx context.Context, task *dto.UnifiedTask) error { +func dispatchTask(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { defer func() { if r := recover(); r != nil { logrus.Errorf("Task panic: %v\n%s", r, debug.Stack()) @@ -23,7 +23,7 @@ func dispatchTask(ctx context.Context, task *dto.UnifiedTask) error { tracing.SetSpanAttribute(ctx, consts.TaskTypeKey, consts.GetTaskTypeName(task.Type)) tracing.SetSpanAttribute(ctx, consts.TaskStateKey, consts.GetTaskStateName(consts.TaskPending)) - publishEvent(ctx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ + publishEvent(deps.RedisGateway, ctx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ TaskID: task.TaskID, TaskType: task.Type, EventName: consts.EventTaskStarted, @@ -33,17 +33,17 @@ func dispatchTask(ctx context.Context, task *dto.UnifiedTask) error { var err error switch task.Type { case consts.TaskTypeBuildContainer: - err = executeBuildContainer(ctx, task) + err = executeBuildContainer(ctx, task, deps) case consts.TaskTypeRestartPedestal: - err = executeRestartPedestal(ctx, task) + err = executeRestartPedestal(ctx, task, deps) case consts.TaskTypeFaultInjection: - err = executeFaultInjection(ctx, task) + err = executeFaultInjection(ctx, task, deps) case consts.TaskTypeBuildDatapack: - err = executeBuildDatapack(ctx, task) + err = executeBuildDatapackWithDeps(ctx, task, deps) case consts.TaskTypeRunAlgorithm: - err = executeAlgorithm(ctx, task) + err = executeAlgorithm(ctx, task, deps) case consts.TaskTypeCollectResult: - err = executeCollectResult(ctx, task) + err = executeCollectResult(ctx, task, deps) default: err = fmt.Errorf("unknown task type: %d", task.Type) } diff --git a/src/service/consumer/fault_injection.go b/src/service/consumer/fault_injection.go index c82416bc..588d1677 100644 --- a/src/service/consumer/fault_injection.go +++ b/src/service/consumer/fault_injection.go @@ -9,8 +9,8 @@ import ( "time" "aegis/consts" - "aegis/database" "aegis/dto" + "aegis/model" "aegis/repository" "aegis/service/common" "aegis/tracing" @@ -34,41 +34,33 @@ type injectionPayload struct { system chaos.SystemType } -type batchManager struct { +type FaultBatchManager struct { mu sync.RWMutex batchCounts map[string]int batchInjections map[string][]string } -var ( - batchManagerInstance *batchManager - batchManagerOnce sync.Once -) - -func getBatchManager() *batchManager { - batchManagerOnce.Do(func() { - batchManagerInstance = &batchManager{ - batchCounts: make(map[string]int), - batchInjections: make(map[string][]string), - } - }) - return batchManagerInstance +func NewFaultBatchManager() *FaultBatchManager { + return &FaultBatchManager{ + batchCounts: make(map[string]int), + batchInjections: make(map[string][]string), + } } -func (bm *batchManager) deleteBatch(batchID string) { +func (bm *FaultBatchManager) deleteBatch(batchID string) { bm.mu.Lock() defer bm.mu.Unlock() delete(bm.batchCounts, batchID) delete(bm.batchInjections, batchID) } -func (bm *batchManager) incrementBatchCount(batchID string) { +func (bm *FaultBatchManager) incrementBatchCount(batchID string) { bm.mu.Lock() defer bm.mu.Unlock() bm.batchCounts[batchID]++ } -func (bm *batchManager) isFinished(batchID string) bool { +func (bm *FaultBatchManager) isFinished(batchID string) bool { bm.mu.RLock() defer bm.mu.RUnlock() @@ -84,7 +76,7 @@ func (bm *batchManager) isFinished(batchID string) bool { return count >= len(injectionNames) } -func (bm *batchManager) setBatchInjections(batchID string, injectionNames []string) { +func (bm *FaultBatchManager) setBatchInjections(batchID string, injectionNames []string) { bm.mu.Lock() defer bm.mu.Unlock() bm.batchCounts[batchID] = 0 @@ -103,8 +95,17 @@ func (bm *batchManager) setBatchInjections(batchID string, injectionNames []stri // Storage format: // - engine_config: JSON array of all chaos.Node objects // - display_config: JSON array of display maps for each fault -func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { +func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { + db := deps.DB + if db == nil { + return fmt.Errorf("consumer runtime db is nil") + } + batchManager := deps.FaultBatchManager + if batchManager == nil { + return fmt.Errorf("fault batch manager is nil") + } + span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ "task_id": task.TaskID, @@ -116,7 +117,10 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { return handleExecutionError(span, logEntry, "failed to parse injection payload", err) } - monitor := GetMonitor() + monitor := deps.Monitor + if monitor == nil { + return handleExecutionError(span, logEntry, "monitor not initialized", fmt.Errorf("monitor not initialized")) + } toReleased := false if err := monitor.CheckNamespaceToInject(payload.namespace, time.Now(), task.TraceID); err != nil { toReleased = true @@ -137,7 +141,7 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { // Process all fault nodes in the batch injectionConfs := make([]chaos.InjectionConf, 0, len(payload.nodes)) displayMaps := make([]map[string]any, 0, len(payload.nodes)) - groundtruths := make([]database.Groundtruth, 0, len(payload.nodes)) + groundtruths := make([]model.Groundtruth, 0, len(payload.nodes)) for i, node := range payload.nodes { injectionConf, err := chaos.NodeToStruct[chaos.InjectionConf](&node) @@ -157,7 +161,7 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { injectionConfs = append(injectionConfs, *injectionConf) displayMaps = append(displayMaps, displayMap) - groundtruths = append(groundtruths, *database.NewDBGroundtruth(&chaosGroundtruth)) + groundtruths = append(groundtruths, *model.NewDBGroundtruth(&chaosGroundtruth)) } // Marshal display config as array @@ -205,14 +209,14 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { if len(names) > 1 { name = batchID faultType = consts.Hybrid - getBatchManager().setBatchInjections(batchID, names) + batchManager.setBatchInjections(batchID, names) } else { name = names[0] faultType = chaos.ChaosType(payload.nodes[0].Value) } - return database.DB.Transaction(func(tx *gorm.DB) error { - injection := &database.FaultInjection{ + return db.Transaction(func(tx *gorm.DB) error { + injection := &model.FaultInjection{ Name: name, FaultType: faultType, Category: payload.pedestal, @@ -229,7 +233,7 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask) error { PedestalID: utils.IntPtr(payload.pedestalID), } - if err = repository.CreateInjection(database.DB, injection); err != nil { + if err = repository.CreateInjection(tx, injection); err != nil { return handleExecutionError(span, logEntry, "failed to write fault injection schedule to database", err) } diff --git a/src/service/consumer/jvm_runtime_mutator.go b/src/service/consumer/jvm_runtime_mutator.go index 4d33ee56..087f25fc 100644 --- a/src/service/consumer/jvm_runtime_mutator.go +++ b/src/service/consumer/jvm_runtime_mutator.go @@ -10,7 +10,7 @@ import ( "encoding/json" "fmt" - "aegis/database" + "aegis/model" "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" ) @@ -38,7 +38,7 @@ type JVMRuntimeMutatorConfig struct { } // ExecuteJVMRuntimeMutatorChaos executes a JVM runtime mutator chaos injection -func (c *Consumer) ExecuteJVMRuntimeMutatorChaos(task *database.Task) error { +func (c *Consumer) ExecuteJVMRuntimeMutatorChaos(task *model.Task) error { logrus.Infof("Executing JVM runtime mutator chaos for task %s", task.ID) // Parse task parameters @@ -81,7 +81,7 @@ func (c *Consumer) ExecuteJVMRuntimeMutatorChaos(task *database.Task) error { } // Execute chaos injection - ctx := context.Background() + ctx := consumerDetachedContext() chaosName, err := spec.Create(c.k8sClient, handler.WithNamespace(mutatorTask.Target.Namespace), handler.WithContext(ctx), diff --git a/src/service/consumer/k8s_handler.go b/src/service/consumer/k8s_handler.go index e0c8d208..3e12bd13 100644 --- a/src/service/consumer/k8s_handler.go +++ b/src/service/consumer/k8s_handler.go @@ -3,17 +3,15 @@ package consumer import ( "context" "encoding/json" - "errors" "fmt" "strconv" "time" - "aegis/client/k8s" "aegis/config" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" "aegis/service/common" "aegis/utils" @@ -33,14 +31,16 @@ const ( // errorContext holds common context for error handling type errorContext struct { - ctx context.Context - span trace.Span - logEntry *logrus.Entry - labels *taskIdentifiers + ctx context.Context + span trace.Span + logEntry *logrus.Entry + labels *taskIdentifiers + db *gorm.DB + redisGateway *redisinfra.Gateway } // NewErrorContext creates an ErrorContext from parsed labels -func NewErrorContext(ctx context.Context, span trace.Span, labels *taskIdentifiers) *errorContext { +func NewErrorContext(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, span trace.Span, labels *taskIdentifiers) *errorContext { return &errorContext{ ctx: ctx, span: span, @@ -48,7 +48,9 @@ func NewErrorContext(ctx context.Context, span trace.Span, labels *taskIdentifie "task_id": labels.taskID, "trace_id": labels.traceID, }), - labels: labels, + labels: labels, + db: db, + redisGateway: redisGateway, } } @@ -74,7 +76,7 @@ func (e *errorContext) Fatal(logEntry *logrus.Entry, message string, err error) e.labels.taskType, consts.TaskError, message, - ), + ).withDB(e.db).withRedis(e.redisGateway), ) } @@ -124,10 +126,25 @@ type jobLabels struct { } type k8sHandler struct { + db *gorm.DB + store *stateStore + monitor NamespaceMonitor + algoLimiter *TokenBucketRateLimiter + k8sGateway *k8sinfra.Gateway + redisGateway *redisinfra.Gateway + batchManager *FaultBatchManager } -func NewHandler() *k8sHandler { - return &k8sHandler{} +func NewHandler(db *gorm.DB, monitor NamespaceMonitor, algoLimiter *TokenBucketRateLimiter, k8sGateway *k8sinfra.Gateway, redisGateway *redisinfra.Gateway, batchManager *FaultBatchManager) *k8sHandler { + return &k8sHandler{ + db: db, + store: newStateStore(db), + monitor: monitor, + algoLimiter: algoLimiter, + k8sGateway: k8sGateway, + redisGateway: redisGateway, + batchManager: batchManager, + } } func (h *k8sHandler) HandleCRDAdd(name string, annotations map[string]string, labels map[string]string) { @@ -143,7 +160,7 @@ func (h *k8sHandler) HandleCRDAdd(name string, annotations map[string]string, la return } - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) updateTaskState(taskCtx, newTaskStateUpdate( parsedLabels.traceID, @@ -151,7 +168,7 @@ func (h *k8sHandler) HandleCRDAdd(name string, annotations map[string]string, la parsedLabels.taskType, consts.TaskRunning, fmt.Sprintf("injecting fault for task %s", parsedLabels.taskID), - ).withEvent(consts.EventFaultInjectionStarted, name), + ).withEvent(consts.EventFaultInjectionStarted, name).withDB(h.db).withRedis(h.redisGateway), ) } @@ -168,8 +185,12 @@ func (h *k8sHandler) HandleCRDDelete(namespace string, annotations map[string]st return } - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) - if err := GetMonitor().ReleaseLock(taskCtx, namespace, parsedLabels.traceID); err != nil { + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) + if h.monitor == nil { + logrus.Warn("namespace monitor not initialized, skipping lock release") + return + } + if err := h.monitor.ReleaseLock(taskCtx, namespace, parsedLabels.traceID); err != nil { logrus.Errorf("failed to release lock for namespace %s: %v", namespace, err) } } @@ -187,7 +208,7 @@ func (h *k8sHandler) HandleCRDFailed(name string, annotations map[string]string, return } - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) taskSpan := trace.SpanFromContext(taskCtx) updateTaskState(taskCtx, @@ -202,13 +223,13 @@ func (h *k8sHandler) HandleCRDFailed(name string, annotations map[string]string, State: consts.GetTaskStateName(consts.TaskError), Msg: errMsg, }, - ), + ).withDB(h.db).withRedis(h.redisGateway), ) - errCtx := NewErrorContext(taskCtx, taskSpan, &parsedLabels.taskIdentifiers) + errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) postprocess := func(injectionName string) { - if err := updateInjectionState(injectionName, consts.DatapackInjectFailed); err != nil { + if err := h.store.updateInjectionState(injectionName, consts.DatapackInjectFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } } @@ -216,7 +237,11 @@ func (h *k8sHandler) HandleCRDFailed(name string, annotations map[string]string, if !parsedLabels.IsHybrid { postprocess(name) } else { - bm := getBatchManager() + bm := h.batchManager + if bm == nil { + errCtx.Warn(nil, "fault batch manager not initialized", fmt.Errorf("fault batch manager not initialized")) + return + } bm.incrementBatchCount(parsedLabels.batchID) // Check if batch is finished and delete if done @@ -240,8 +265,8 @@ func (h *k8sHandler) HandleCRDSucceeded(namespace, pod, name string, startTime, return } - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) - traceCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.traceCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) + traceCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.traceCarrier) logEntry := logrus.WithFields(logrus.Fields{ "task_id": parsedLabels.taskID, @@ -259,17 +284,17 @@ func (h *k8sHandler) HandleCRDSucceeded(namespace, pod, name string, startTime, parsedLabels.taskType, consts.TaskCompleted, fmt.Sprintf(consts.TaskMsgCompleted, parsedLabels.taskID), - ).withEvent(consts.EventFaultInjectionCompleted, name), + ).withEvent(consts.EventFaultInjectionCompleted, name).withDB(h.db).withRedis(h.redisGateway), ) - errCtx := NewErrorContext(taskCtx, taskSpan, &parsedLabels.taskIdentifiers) + errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) postProcess := func(injectionName string) { - if err := updateInjectionState(injectionName, consts.DatapackInjectSuccess); err != nil { + if err := h.store.updateInjectionState(injectionName, consts.DatapackInjectSuccess); err != nil { errCtx.Warn(nil, "update injection state failed", err) } - datapack, err := updateInjectionTimestamp(injectionName, startTime, endTime) + datapack, err := h.store.updateInjectionTimestamp(injectionName, startTime, endTime) if err != nil { errCtx.Warn(nil, "update injection timestamps failed", err) return @@ -298,7 +323,7 @@ func (h *k8sHandler) HandleCRDSucceeded(namespace, pod, name string, startTime, } task.SetTraceCtx(traceCtx) - if err = common.SubmitTask(taskCtx, task); err != nil { + if err = common.SubmitTaskWithDB(taskCtx, h.db, h.redisGateway, task); err != nil { errCtx.Fatal(nil, "failed to submit datapack build task", err) } } @@ -306,7 +331,11 @@ func (h *k8sHandler) HandleCRDSucceeded(namespace, pod, name string, startTime, if !parsedLabels.IsHybrid { postProcess(name) } else { - bm := getBatchManager() + bm := h.batchManager + if bm == nil { + errCtx.Warn(nil, "fault batch manager not initialized", fmt.Errorf("fault batch manager not initialized")) + return + } bm.incrementBatchCount(parsedLabels.batchID) if bm.isFinished(parsedLabels.batchID) { @@ -351,7 +380,7 @@ func (h *k8sHandler) HandleJobAdd(name string, annotations map[string]string, la } } - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) updateTaskState(taskCtx, newTaskStateUpdate( parsedLabels.traceID, @@ -359,7 +388,7 @@ func (h *k8sHandler) HandleJobAdd(name string, annotations map[string]string, la parsedLabels.taskType, consts.TaskRunning, message, - ).withEvent(eventType, payload), + ).withEvent(eventType, payload).withDB(h.db).withRedis(h.redisGateway), ) } @@ -380,17 +409,21 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st "task_id": parsedLabels.taskID, "trace_id": parsedLabels.traceID, }) - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) taskSpan := trace.SpanFromContext(taskCtx) - errCtx := NewErrorContext(taskCtx, taskSpan, &parsedLabels.taskIdentifiers) + errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) if parsedAnnotations.datapack == nil { errCtx.Fatal(nil, "missing datapack information in annotations", nil) return } - logMap, err := k8s.GetJobPodLogs(taskCtx, job.Namespace, job.Name) + if h.k8sGateway == nil { + errCtx.Warn(nil, "k8s gateway not initialized", fmt.Errorf("k8s gateway not initialized")) + return + } + logMap, err := h.k8sGateway.GetJobPodLogs(taskCtx, job.Namespace, job.Name) if err != nil { errCtx.Warn(logrus.WithField("job_name", job.Name), "failed to get job logs", err) } @@ -406,7 +439,7 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st taskSpan.AddEvent("job failed", spanAttrs...) } - publishEvent(taskCtx, fmt.Sprintf(consts.StreamTraceLogKey, parsedLabels.traceID), dto.TraceStreamEvent{ + publishEvent(h.redisGateway, taskCtx, fmt.Sprintf(consts.StreamTraceLogKey, parsedLabels.traceID), dto.TraceStreamEvent{ TaskID: parsedLabels.taskID, TaskType: parsedLabels.taskType, EventName: consts.EventJobFailed, @@ -430,12 +463,16 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st JobName: job.Name, } - if err := updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildFailed); err != nil { + if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } case consts.TaskTypeRunAlgorithm: - rateLimiter := GetAlgoExecutionRateLimiter() + rateLimiter := h.algoLimiter + if rateLimiter == nil { + errCtx.Warn(nil, "algorithm execution rate limiter not initialized on job failure", fmt.Errorf("algorithm execution rate limiter not initialized")) + return + } if releaseErr := rateLimiter.ReleaseToken(taskCtx, parsedLabels.taskID, parsedLabels.traceID); releaseErr != nil { errCtx.Warn(nil, "failed to release algorithm execution token on job failure", releaseErr) } else { @@ -462,12 +499,12 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st } if parsedAnnotations.algorithm.ContainerName == config.GetDetectorName() { - if err := updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorFailed); err != nil { + if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } } - if err := updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionFailed); err != nil { + if err := h.store.updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionFailed); err != nil { errCtx.Fatal(nil, "update execution state failed", err) return } @@ -480,7 +517,7 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st parsedLabels.taskType, consts.TaskError, fmt.Sprintf(consts.TaskMsgFailed, parsedLabels.taskID), - ).withEvent(eventName, payload), + ).withEvent(eventName, payload).withDB(h.db).withRedis(h.redisGateway), ) } @@ -499,8 +536,8 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string stream := fmt.Sprintf(consts.StreamTraceLogKey, parsedLabels.traceID) - taskCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.taskCarrier) - traceCtx := otel.GetTextMapPropagator().Extract(context.Background(), parsedAnnotations.traceCarrier) + taskCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.taskCarrier) + traceCtx := otel.GetTextMapPropagator().Extract(consumerDetachedContext(), parsedAnnotations.traceCarrier) logEntry := logrus.WithFields(logrus.Fields{ "task_id": parsedLabels.taskID, @@ -508,14 +545,14 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string }) taskSpan := trace.SpanFromContext(taskCtx) - errCtx := NewErrorContext(taskCtx, taskSpan, &parsedLabels.taskIdentifiers) + errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) if parsedAnnotations.datapack == nil { errCtx.Fatal(nil, "missing datapack information in annotations", nil) return } - publishEvent(taskCtx, stream, dto.TraceStreamEvent{ + publishEvent(h.redisGateway, taskCtx, stream, dto.TraceStreamEvent{ TaskID: parsedLabels.taskID, TaskType: parsedLabels.taskType, EventName: consts.EventJobSucceed, @@ -530,7 +567,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string logEntry.Info("datapack build successfully") taskSpan.AddEvent("datapack build successfully") - if err := updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildSuccess); err != nil { + if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildSuccess); err != nil { errCtx.Fatal(nil, "update injection state failed", err) return } @@ -548,14 +585,14 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string Datapack: parsedAnnotations.datapack.Name, JobName: job.Name, }, - ), + ).withDB(h.db).withRedis(h.redisGateway), ) ref := &dto.ContainerRef{ Name: config.GetDetectorName(), } - algorithmVersionResults, err := common.MapRefsToContainerVersions([]*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) + algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(h.db, []*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) if err != nil { errCtx.Fatal(nil, "failed to map container refs to versions", err) return @@ -589,12 +626,16 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string } task.SetTraceCtx(traceCtx) - if err := common.SubmitTask(taskCtx, task); err != nil { + if err := common.SubmitTaskWithDB(taskCtx, h.db, h.redisGateway, task); err != nil { errCtx.Warn(nil, "submit algorithm execution task failed", err) } case consts.TaskTypeRunAlgorithm: - rateLimiter := GetAlgoExecutionRateLimiter() + rateLimiter := h.algoLimiter + if rateLimiter == nil { + errCtx.Warn(nil, "algorithm execution rate limiter not initialized on job success", fmt.Errorf("algorithm execution rate limiter not initialized")) + return + } if releaseErr := rateLimiter.ReleaseToken(taskCtx, parsedLabels.taskID, parsedLabels.traceID); releaseErr != nil { errCtx.Warn(nil, "failed to release algorithm execution token on job success", releaseErr) } else { @@ -616,13 +657,13 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string taskSpan.AddEvent("algorithm execute successfully") if parsedAnnotations.algorithm.ContainerName == config.GetDetectorName() { - if err := updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorSuccess); err != nil { + if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorSuccess); err != nil { errCtx.Fatal(nil, "update injection state failed", err) return } } - if err := updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionSuccess); err != nil { + if err := h.store.updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionSuccess); err != nil { errCtx.Fatal(nil, "update execution state failed", err) return } @@ -640,7 +681,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string Algorithm: parsedAnnotations.algorithm.ContainerName, JobName: job.Name, }, - ), + ).withDB(h.db).withRedis(h.redisGateway), ) payload := map[string]any{ @@ -661,7 +702,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string } task.SetTraceCtx(traceCtx) - if err := common.SubmitTask(taskCtx, task); err != nil { + if err := common.SubmitTaskWithDB(taskCtx, h.db, h.redisGateway, task); err != nil { errCtx.Warn(nil, "submit result collection task failed", err) } } @@ -822,81 +863,3 @@ func parseJobLabels(labels map[string]string) (*jobLabels, error) { return data, nil } - -// updateExecutionState updates the state of an execution -func updateExecutionState(executionID int, newState consts.ExecutionState) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - execution, err := repository.GetExecutionByID(tx, executionID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) - } - return fmt.Errorf("execution %d not found: %w", executionID, err) - } - - if execution.State != consts.ExecutionInitial { - return fmt.Errorf("cannot change state of execution %d from %s to %s", executionID, consts.GetExecutionStateName(execution.State), consts.GetExecutionStateName(newState)) - } - - if err := repository.UpdateExecution(tx, executionID, map[string]any{ - "state": newState, - }); err != nil { - return fmt.Errorf("failed to update execution %d duration: %w", executionID, err) - } - - return nil - }) -} - -// updateInjectionState updates the state of a fault injection -func updateInjectionState(injectionName string, newState consts.DatapackState) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - injection, err := repository.GetInjectionByName(tx, injectionName, false) - if err != nil { - return fmt.Errorf("failed to get injection %s: %w", injectionName, err) - } - - if err := repository.UpdateInjection(tx, injection.ID, map[string]any{ - "state": newState, - }); err != nil { - return fmt.Errorf("failed to update injection %s state: %w", injectionName, err) - } - - return nil - }) -} - -// updateInjectionTimestamp updates the start and end timestamps of a fault injection -func updateInjectionTimestamp(injectionName string, startTime time.Time, endTime time.Time) (*dto.InjectionItem, error) { - var updatedInjection *database.FaultInjection - err := database.DB.Transaction(func(tx *gorm.DB) error { - injection, err := repository.GetInjectionByName(tx, injectionName, false) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("injection %s not found", injectionName) - } - return fmt.Errorf("failed to get injection %s: %w", injectionName, err) - } - - if err = repository.UpdateInjection(tx, injection.ID, map[string]any{ - "start_time": startTime, - "end_time": endTime, - }); err != nil { - return fmt.Errorf("update injection timestamps failed: %w", err) - } - - reloadedInjection, err := repository.GetInjectionByID(tx, injection.ID) - if err != nil { - return fmt.Errorf("failed to reload injection %d after update: %w", injection.ID, err) - } - - updatedInjection = reloadedInjection - return nil - }) - if err != nil { - return nil, err - } - - injectionItem := dto.NewInjectionItem(updatedInjection) - return &injectionItem, err -} diff --git a/src/service/consumer/monitor.go b/src/service/consumer/monitor.go index f674604d..bedf8258 100644 --- a/src/service/consumer/monitor.go +++ b/src/service/consumer/monitor.go @@ -5,14 +5,13 @@ import ( "fmt" "regexp" "slices" - "strconv" "sync" "time" - "aegis/client" "aegis/config" "aegis/consts" "aegis/dto" + redisinfra "aegis/infra/redis" "aegis/utils" "github.com/redis/go-redis/v9" @@ -45,38 +44,70 @@ type NamespaceInitResult struct { Initialized []string // Namespaces that were re-initialized (all enabled namespaces) } +type NamespaceMonitor interface { + SetContext(ctx context.Context) + InitializeNamespaces() ([]string, error) + RefreshNamespaces() (*NamespaceRefreshResult, error) + ReleaseLock(ctx context.Context, namespace string, traceID string) error + CheckNamespaceToInject(namespace string, executeTime time.Time, traceID string) error + GetNamespaceToRestart(endTime time.Time, nsPattern, traceID string) string +} + // monitor manages namespace locks and status using Redis type monitor struct { - redisClient *redis.Client - ctx context.Context - mu sync.RWMutex // Protects namespace operations + ctx context.Context + redisGateway *redisinfra.Gateway + namespaces namespaceCatalogStore + locks namespaceLockStore + status namespaceStatusStore + mu sync.RWMutex // Protects namespace operations } -// Singleton instance and initialization control -var ( - monitorInstance *monitor - monitorOnce sync.Once -) +func NewMonitor(gateway *redisinfra.Gateway) NamespaceMonitor { + return &monitor{ + ctx: context.TODO(), + redisGateway: gateway, + namespaces: newNamespaceCatalogStore(gateway), + locks: newNamespaceLockStore(gateway), + status: newNamespaceStatusStore(gateway), + } +} -// GetMonitor returns the singleton Monitor instance, -// ensuring initialization is only performed once across all processes -func GetMonitor() *monitor { - // Local process singleton pattern - monitorOnce.Do(func() { - monitorInstance = &monitor{ - redisClient: client.GetRedisClient(), - ctx: context.Background(), - } - }) +func (m *monitor) SetContext(ctx context.Context) { + if ctx == nil { + return + } + m.mu.Lock() + defer m.mu.Unlock() + m.ctx = ctx +} + +func (m *monitor) currentContext() context.Context { + m.mu.RLock() + defer m.mu.RUnlock() + if m.ctx != nil { + return m.ctx + } + return context.TODO() +} - return monitorInstance +func (m *monitor) listNamespaces() ([]string, error) { + return m.namespaces.list(m.currentContext()) +} + +func (m *monitor) namespaceExists(namespace string) (bool, error) { + return m.namespaces.exists(m.currentContext(), namespace) +} + +func (m *monitor) seedNamespace(namespace string, endTime time.Time) error { + return m.namespaces.seed(m.currentContext(), namespace, endTime) } // AcquireLock attempts to acquire a lock on a namespace // Returns nil on success, error if the lock cannot be acquired func (m *monitor) AcquireLock(namespace string, endTime time.Time, traceID string, taskType consts.TaskType) (err error) { defer func() { - publishEvent(context.Background(), fmt.Sprintf(consts.StreamTraceLogKey, namespace), dto.TraceStreamEvent{ + publishEvent(m.redisGateway, m.currentContext(), fmt.Sprintf(consts.StreamTraceLogKey, namespace), dto.TraceStreamEvent{ TaskType: taskType, EventName: consts.EventAcquireLock, Payload: LockMessage{ @@ -87,16 +118,15 @@ func (m *monitor) AcquireLock(namespace string, endTime time.Time, traceID strin }) }() - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) nowTime := time.Now().Unix() // Check if namespace exists - exists, err := m.redisClient.Exists(m.ctx, nsKey).Result() + exists, err := m.namespaceExists(namespace) if err != nil { return fmt.Errorf("failed to check namespace existence: %v", err) } - if exists == 0 { + if !exists { // Lazy loading: verify namespace is valid in current configuration latestNamespaces, err := config.GetAllNamespaces() if err != nil { @@ -128,37 +158,7 @@ func (m *monitor) AcquireLock(namespace string, endTime time.Time, traceID strin } // All lock checking and acquisition happens in a single atomic transaction - err = m.redisClient.Watch(m.ctx, func(tx *redis.Tx) error { - // Check if the lock is still available - currentEndTimeStr, e := tx.HGet(m.ctx, nsKey, "end_time").Result() - if e != nil && e != redis.Nil { - return e - } - - currentEndTime, e := strconv.ParseInt(currentEndTimeStr, 10, 64) - if e != nil { - return e - } - - currentTraceID, e := tx.HGet(m.ctx, nsKey, "trace_id").Result() - if e != nil && e != redis.Nil { - return e - } - - // If lock is held by someone else and not expired - if currentTraceID != "" && currentTraceID != traceID && nowTime < currentEndTime { - return fmt.Errorf("namespace %s is locked by %s until %v", - namespace, currentTraceID, time.Unix(currentEndTime, 0).Format(time.RFC3339)) - } - - // Try to acquire the lock - _, e = tx.TxPipelined(m.ctx, func(pipe redis.Pipeliner) error { - pipe.HSet(m.ctx, nsKey, "end_time", endTime.Unix()) - pipe.HSet(m.ctx, nsKey, "trace_id", traceID) - return nil - }) - return e - }, nsKey) + err = m.locks.acquire(m.currentContext(), namespace, endTime, traceID, time.Unix(nowTime, 0)) logEntry := logrus.WithFields( logrus.Fields{ @@ -180,7 +180,7 @@ func (m *monitor) AcquireLock(namespace string, endTime time.Time, traceID strin // ReleaseLock releases a lock on a namespace if it's owned by the specified traceID func (m *monitor) ReleaseLock(ctx context.Context, namespace string, traceID string) (err error) { defer func() { - publishEvent(ctx, fmt.Sprintf(consts.StreamTraceLogKey, namespace), dto.TraceStreamEvent{ + publishEvent(m.redisGateway, ctx, fmt.Sprintf(consts.StreamTraceLogKey, namespace), dto.TraceStreamEvent{ TaskType: consts.TaskTypeRestartPedestal, EventName: consts.EventReleaseLock, Payload: LockMessage{ @@ -205,41 +205,21 @@ func (m *monitor) ReleaseLock(ctx context.Context, namespace string, traceID str return fmt.Errorf("namespace or trace_id is empty") } - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) - // Check if namespace exists - var exists int64 - exists, err = m.redisClient.Exists(m.ctx, nsKey).Result() + exists, existsErr := m.namespaceExists(namespace) + err = existsErr if err != nil { err = fmt.Errorf("failed to check namespace existence: %v", err) return } - if exists == 0 { + if !exists { err = fmt.Errorf("namespace %s not found", namespace) return } // Check if the lock is actually held by this traceID - currentTraceID, err := m.redisClient.HGet(m.ctx, nsKey, "trace_id").Result() - if err != nil && err != redis.Nil { - err = fmt.Errorf("failed to get current trace_id: %v", err) - return - } - - // If the lock is held by someone else or is already released - if currentTraceID != traceID && currentTraceID != "" { - err = fmt.Errorf("cannot release lock: namespace %s is not owned by trace_id %s (current owner: %s)", - namespace, traceID, currentTraceID) - return - } - - // Update namespace lock info - release by setting current time and empty trace ID - _, err = m.redisClient.Pipelined(m.ctx, func(pipe redis.Pipeliner) error { - pipe.HSet(m.ctx, nsKey, "end_time", time.Now().Unix()) - pipe.HSet(m.ctx, nsKey, "trace_id", "") - return nil - }) + err = m.locks.release(m.currentContext(), namespace, traceID, time.Now()) return } @@ -263,7 +243,7 @@ func (m *monitor) CheckNamespaceToInject(namespace string, executeTime time.Time // GetNamespaceToRestart finds an available namespace for restart and acquires it func (m *monitor) GetNamespaceToRestart(endTime time.Time, nsPattern, traceID string) string { - namespaces, err := m.redisClient.SMembers(m.ctx, consts.NamespacesKey).Result() + namespaces, err := m.listNamespaces() if err != nil { logrus.Errorf("failed to get namespaces from Redis: %v", err) return "" @@ -312,7 +292,7 @@ func (m *monitor) InitializeNamespaces() ([]string, error) { } // Get all enabled namespaces from Redis - allNamespaces, err := m.redisClient.SMembers(m.ctx, consts.NamespacesKey).Result() + allNamespaces, err := m.listNamespaces() if err != nil { return nil, fmt.Errorf("failed to get namespaces from Redis: %w", err) } @@ -358,7 +338,7 @@ func (m *monitor) RefreshNamespaces() (*NamespaceRefreshResult, error) { } // Get existing namespaces from Redis - existingNamespaces, err := m.redisClient.SMembers(m.ctx, consts.NamespacesKey).Result() + existingNamespaces, err := m.listNamespaces() if err != nil { return nil, fmt.Errorf("failed to get existing namespaces: %w", err) } @@ -449,70 +429,20 @@ func (m *monitor) RefreshNamespaces() (*NamespaceRefreshResult, error) { // addNamespace adds a new namespace to Redis with initial state (idempotent) func (m *monitor) addNamespace(namespace string, endTime time.Time) error { - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) - - _, err := m.redisClient.Pipelined(m.ctx, func(pipe redis.Pipeliner) error { - pipe.SAdd(m.ctx, consts.NamespacesKey, namespace) - pipe.HSetNX(m.ctx, nsKey, "end_time", endTime.Unix()) - pipe.HSetNX(m.ctx, nsKey, "trace_id", "") - pipe.HSetNX(m.ctx, nsKey, "status", int(consts.CommonEnabled)) - return nil - }) - - return err + return m.seedNamespace(namespace, endTime) } // isNamespaceLocked checks if a namespace currently has an active lock func (m *monitor) isNamespaceLocked(namespace string) (bool, error) { - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) - - traceID, err := m.redisClient.HGet(m.ctx, nsKey, "trace_id").Result() - if err == redis.Nil { - return false, nil - } - if err != nil { - return false, err - } - if traceID == "" { - return false, nil - } - - // Check if lock has expired - endTimeStr, err := m.redisClient.HGet(m.ctx, nsKey, "end_time").Result() - if err != nil { - return false, err - } - - endTime, err := strconv.ParseInt(endTimeStr, 10, 64) - if err != nil { - return false, err - } - - return time.Now().Unix() < endTime, nil + return m.locks.isActive(m.currentContext(), namespace, time.Now()) } // getNamespaceStatus gets the status of a namespace func (m *monitor) getNamespaceStatus(namespace string) (consts.StatusType, error) { - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) - statusStr, err := m.redisClient.HGet(m.ctx, nsKey, "status").Result() - if err == redis.Nil { - // For backward compatibility, assume enabled if status field doesn't exist - return consts.CommonEnabled, nil - } - if err != nil { - return 0, err - } - - status, err := strconv.Atoi(statusStr) - if err != nil { - return 0, fmt.Errorf("invalid status value: %w", err) - } - - return consts.StatusType(status), nil + return m.status.get(m.currentContext(), namespace) } // setNamespaceStatus sets the status of a namespace func (m *monitor) setNamespaceStatus(namespace string, status consts.StatusType) error { - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) - return m.redisClient.HSet(m.ctx, nsKey, "status", int(status)).Err() + return m.status.set(m.currentContext(), namespace, status) } diff --git a/src/service/consumer/namespace_catalog_store.go b/src/service/consumer/namespace_catalog_store.go new file mode 100644 index 00000000..49b61017 --- /dev/null +++ b/src/service/consumer/namespace_catalog_store.go @@ -0,0 +1,34 @@ +package consumer + +import ( + "context" + "fmt" + "time" + + "aegis/consts" + redisinfra "aegis/infra/redis" +) + +type namespaceCatalogStore struct { + client *redisinfra.Gateway +} + +func newNamespaceCatalogStore(client *redisinfra.Gateway) namespaceCatalogStore { + return namespaceCatalogStore{client: client} +} + +func (s namespaceCatalogStore) key(namespace string) string { + return fmt.Sprintf(consts.NamespaceKeyPattern, namespace) +} + +func (s namespaceCatalogStore) list(ctx context.Context) ([]string, error) { + return s.client.SetMembers(ctx, consts.NamespacesKey) +} + +func (s namespaceCatalogStore) exists(ctx context.Context, namespace string) (bool, error) { + return s.client.Exists(ctx, s.key(namespace)) +} + +func (s namespaceCatalogStore) seed(ctx context.Context, namespace string, endTime time.Time) error { + return s.client.SeedNamespaceState(ctx, s.key(namespace), namespace, endTime.Unix(), int(consts.CommonEnabled)) +} diff --git a/src/service/consumer/namespace_lock_store.go b/src/service/consumer/namespace_lock_store.go new file mode 100644 index 00000000..f938db12 --- /dev/null +++ b/src/service/consumer/namespace_lock_store.go @@ -0,0 +1,128 @@ +package consumer + +import ( + "context" + "fmt" + "strconv" + "time" + + "aegis/consts" + redisinfra "aegis/infra/redis" + + "github.com/redis/go-redis/v9" +) + +type namespaceLockState struct { + EndTime int64 + TraceID string +} + +type namespaceLockStore struct { + client *redisinfra.Gateway +} + +func newNamespaceLockStore(client *redisinfra.Gateway) namespaceLockStore { + return namespaceLockStore{client: client} +} + +func (s namespaceLockStore) key(namespace string) string { + return fmt.Sprintf(consts.NamespaceKeyPattern, namespace) +} + +func (s namespaceLockStore) read(ctx context.Context, namespace string) (*namespaceLockState, error) { + endTimeStr, err := s.client.HashGet(ctx, s.key(namespace), "end_time") + if err != nil && err != redis.Nil { + return nil, err + } + + traceID, err := s.client.HashGet(ctx, s.key(namespace), "trace_id") + if err != nil && err != redis.Nil { + return nil, err + } + + if endTimeStr == "" { + return &namespaceLockState{TraceID: traceID}, nil + } + + endTime, err := strconv.ParseInt(endTimeStr, 10, 64) + if err != nil { + return nil, err + } + + return &namespaceLockState{EndTime: endTime, TraceID: traceID}, nil +} + +func (s namespaceLockStore) readFromHash(reader redis.HashCmdable, ctx context.Context, namespace string) (*namespaceLockState, error) { + endTimeStr, err := reader.HGet(ctx, s.key(namespace), "end_time").Result() + if err != nil && err != redis.Nil { + return nil, err + } + + traceID, err := reader.HGet(ctx, s.key(namespace), "trace_id").Result() + if err != nil && err != redis.Nil { + return nil, err + } + + if endTimeStr == "" { + return &namespaceLockState{TraceID: traceID}, nil + } + + endTime, err := strconv.ParseInt(endTimeStr, 10, 64) + if err != nil { + return nil, err + } + + return &namespaceLockState{EndTime: endTime, TraceID: traceID}, nil +} + +func (s namespaceLockStore) write(ctx context.Context, namespace string, endTime int64, traceID string) error { + return s.client.HashSet(ctx, s.key(namespace), map[string]any{ + "end_time": endTime, + "trace_id": traceID, + }) +} + +func (s namespaceLockStore) acquire(ctx context.Context, namespace string, endTime time.Time, traceID string, now time.Time) error { + return s.client.Watch(ctx, func(tx *redis.Tx) error { + state, err := s.readFromHash(tx, ctx, namespace) + if err != nil { + return err + } + if state.TraceID != "" && state.TraceID != traceID && now.Unix() < state.EndTime { + return fmt.Errorf("namespace %s is locked by %s until %v", + namespace, state.TraceID, time.Unix(state.EndTime, 0).Format(time.RFC3339)) + } + _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + pipe.HSet(ctx, s.key(namespace), "end_time", endTime.Unix()) + pipe.HSet(ctx, s.key(namespace), "trace_id", traceID) + return nil + }) + return err + }, s.key(namespace)) +} + +func (s namespaceLockStore) release(ctx context.Context, namespace, traceID string, releasedAt time.Time) error { + state, err := s.read(ctx, namespace) + if err != nil && err != redis.Nil { + return fmt.Errorf("failed to get current trace_id: %v", err) + } + if state != nil && state.TraceID != traceID && state.TraceID != "" { + return fmt.Errorf("cannot release lock: namespace %s is not owned by trace_id %s (current owner: %s)", + namespace, traceID, state.TraceID) + } + return s.write(ctx, namespace, releasedAt.Unix(), "") +} + +func (s namespaceLockStore) isActive(ctx context.Context, namespace string, now time.Time) (bool, error) { + state, err := s.read(ctx, namespace) + if err == redis.Nil { + return false, nil + } + if err != nil { + return false, err + } + if state.TraceID == "" { + return false, nil + } + return now.Unix() < state.EndTime, nil +} diff --git a/src/service/consumer/namespace_status_store.go b/src/service/consumer/namespace_status_store.go new file mode 100644 index 00000000..6005c392 --- /dev/null +++ b/src/service/consumer/namespace_status_store.go @@ -0,0 +1,43 @@ +package consumer + +import ( + "context" + "fmt" + "strconv" + + "aegis/consts" + redisinfra "aegis/infra/redis" + "github.com/redis/go-redis/v9" +) + +type namespaceStatusStore struct { + client *redisinfra.Gateway +} + +func newNamespaceStatusStore(client *redisinfra.Gateway) namespaceStatusStore { + return namespaceStatusStore{client: client} +} + +func (s namespaceStatusStore) key(namespace string) string { + return fmt.Sprintf(consts.NamespaceKeyPattern, namespace) +} + +func (s namespaceStatusStore) get(ctx context.Context, namespace string) (consts.StatusType, error) { + statusStr, err := s.client.HashGet(ctx, s.key(namespace), "status") + if err == redis.Nil { + return consts.CommonEnabled, nil + } + if err != nil { + return 0, err + } + + status, err := strconv.Atoi(statusStr) + if err != nil { + return 0, fmt.Errorf("invalid status value: %w", err) + } + return consts.StatusType(status), nil +} + +func (s namespaceStatusStore) set(ctx context.Context, namespace string, status consts.StatusType) error { + return s.client.HashSet(ctx, s.key(namespace), map[string]any{"status": int(status)}) +} diff --git a/src/service/consumer/rate_limiter.go b/src/service/consumer/rate_limiter.go index 7f06ad5f..ff620b71 100644 --- a/src/service/consumer/rate_limiter.go +++ b/src/service/consumer/rate_limiter.go @@ -2,15 +2,13 @@ package consumer import ( "context" - "fmt" "sync" "time" - "aegis/client" "aegis/config" "aegis/consts" + redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" ) @@ -26,8 +24,8 @@ type RateLimiterConfig struct { // TokenBucketRateLimiter token bucket rate limiter type TokenBucketRateLimiter struct { - redisClient *redis.Client bucketKey string + store tokenBucketStore mu sync.RWMutex maxTokens int waitTimeout time.Duration @@ -75,34 +73,11 @@ func (r *TokenBucketRateLimiter) AcquireToken(ctx context.Context, taskID, trace maxTokens := r.maxTokens r.mu.RUnlock() - script := redis.NewScript(` - local bucket_key = KEYS[1] - local max_tokens = tonumber(ARGV[1]) - local task_id = ARGV[2] - local trace_id = ARGV[3] - local expire_time = tonumber(ARGV[4]) - - local current_tokens = redis.call('SCARD', bucket_key) - - if current_tokens < max_tokens then - redis.call('SADD', bucket_key, task_id) - redis.call('EXPIRE', bucket_key, expire_time) - return 1 - else - return 0 - end - `) - - expireTime := 10 * 60 - - result, err := script.Run(ctx, r.redisClient, []string{r.bucketKey}, - maxTokens, taskID, traceID, expireTime).Result() + acquired, err := r.store.acquire(ctx, maxTokens, taskID, traceID) if err != nil { span.RecordError(err) - return false, fmt.Errorf("failed to acquire token: %v", err) + return false, err } - - acquired := result.(int64) == 1 if acquired { span.AddEvent("token acquired successfully") logrus.WithFields(logrus.Fields{ @@ -120,10 +95,10 @@ func (r *TokenBucketRateLimiter) AcquireToken(ctx context.Context, taskID, trace func (r *TokenBucketRateLimiter) ReleaseToken(ctx context.Context, taskID, traceID string) error { span := trace.SpanFromContext(ctx) - result, err := r.redisClient.SRem(ctx, r.bucketKey, taskID).Result() + result, err := r.store.release(ctx, taskID) if err != nil { span.RecordError(err) - return fmt.Errorf("failed to release token: %v", err) + return err } if result > 0 { @@ -178,50 +153,28 @@ func (r *TokenBucketRateLimiter) WaitForToken(ctx context.Context, taskID, trace } } -var ( - restartPedestalRateLimiter *TokenBucketRateLimiter - buildContainerRateLimiter *TokenBucketRateLimiter - algoExecutionRateLimiter *TokenBucketRateLimiter - rateLimiterOnce sync.Once -) - -// GetRestartPedestalRateLimiter returns the singleton restart pedestal rate limiter -func GetRestartPedestalRateLimiter() *TokenBucketRateLimiter { - rateLimiterOnce.Do(initRateLimiters) - return restartPedestalRateLimiter -} - -// GetBuildContainerRateLimiter returns the singleton build container rate limiter -func GetBuildContainerRateLimiter() *TokenBucketRateLimiter { - rateLimiterOnce.Do(initRateLimiters) - return buildContainerRateLimiter -} - -// GetAlgoExecutionRateLimiter returns the singleton algorithm execution rate limiter -func GetAlgoExecutionRateLimiter() *TokenBucketRateLimiter { - rateLimiterOnce.Do(initRateLimiters) - return algoExecutionRateLimiter -} - -// initRateLimiters initializes all rate limiters -func initRateLimiters() { - restartPedestalRateLimiter = newTokenBucketRateLimiter(RateLimiterConfig{ +func NewRestartPedestalRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { + return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.RestartPedestalTokenBucket, MaxTokensKey: consts.MaxTokensKeyRestartPedestal, DefaultMaxTokens: consts.MaxConcurrentRestartPedestal, DefaultTimeout: consts.TokenWaitTimeout, ServiceName: consts.RestartPedestalServiceName, }) +} - buildContainerRateLimiter = newTokenBucketRateLimiter(RateLimiterConfig{ +func NewBuildContainerRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { + return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.BuildContainerTokenBucket, MaxTokensKey: consts.MaxTokensKeyBuildContainer, DefaultMaxTokens: consts.MaxConcurrentBuildContainer, DefaultTimeout: consts.TokenWaitTimeout, ServiceName: consts.BuildContainerServiceName, }) +} - algoExecutionRateLimiter = newTokenBucketRateLimiter(RateLimiterConfig{ +func NewAlgoExecutionRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { + return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.AlgoExecutionTokenBucket, MaxTokensKey: consts.MaxTokensKeyAlgoExecution, DefaultMaxTokens: consts.MaxConcurrentAlgoExecution, @@ -231,7 +184,7 @@ func initRateLimiters() { } // newTokenBucketRateLimiter creates a new token bucket rate limiter -func newTokenBucketRateLimiter(cfg RateLimiterConfig) *TokenBucketRateLimiter { +func newTokenBucketRateLimiter(gateway *redisinfra.Gateway, cfg RateLimiterConfig) *TokenBucketRateLimiter { maxTokens := config.GetInt(cfg.MaxTokensKey) if maxTokens <= 0 { maxTokens = cfg.DefaultMaxTokens @@ -243,8 +196,8 @@ func newTokenBucketRateLimiter(cfg RateLimiterConfig) *TokenBucketRateLimiter { } return &TokenBucketRateLimiter{ - redisClient: client.GetRedisClient(), bucketKey: cfg.TokenBucketKey, + store: newTokenBucketStore(gateway, cfg.TokenBucketKey), maxTokens: maxTokens, waitTimeout: time.Duration(waitTimeout) * time.Second, serviceName: cfg.ServiceName, diff --git a/src/service/consumer/rate_limiter_store.go b/src/service/consumer/rate_limiter_store.go new file mode 100644 index 00000000..a1ec5e85 --- /dev/null +++ b/src/service/consumer/rate_limiter_store.go @@ -0,0 +1,54 @@ +package consumer + +import ( + "context" + "fmt" + + redisinfra "aegis/infra/redis" + "github.com/redis/go-redis/v9" +) + +type tokenBucketStore struct { + bucketKey string + client *redisinfra.Gateway +} + +func newTokenBucketStore(client *redisinfra.Gateway, bucketKey string) tokenBucketStore { + return tokenBucketStore{bucketKey: bucketKey, client: client} +} + +func (s tokenBucketStore) acquire(ctx context.Context, maxTokens int, taskID, traceID string) (bool, error) { + script := redis.NewScript(` + local bucket_key = KEYS[1] + local max_tokens = tonumber(ARGV[1]) + local task_id = ARGV[2] + local trace_id = ARGV[3] + local expire_time = tonumber(ARGV[4]) + + local current_tokens = redis.call('SCARD', bucket_key) + + if current_tokens < max_tokens then + redis.call('SADD', bucket_key, task_id) + redis.call('EXPIRE', bucket_key, expire_time) + return 1 + else + return 0 + end + `) + + const expireTime = 10 * 60 + result, err := s.client.RunScript(ctx, script, []string{s.bucketKey}, + maxTokens, taskID, traceID, expireTime) + if err != nil { + return false, fmt.Errorf("failed to acquire token: %v", err) + } + return result.(int64) == 1, nil +} + +func (s tokenBucketStore) release(ctx context.Context, taskID string) (int64, error) { + result, err := s.client.SetRemove(ctx, s.bucketKey, taskID) + if err != nil { + return 0, fmt.Errorf("failed to release token: %v", err) + } + return result, nil +} diff --git a/src/service/consumer/redis.go b/src/service/consumer/redis.go new file mode 100644 index 00000000..0d8a9369 --- /dev/null +++ b/src/service/consumer/redis.go @@ -0,0 +1,50 @@ +package consumer + +import ( + "context" + "fmt" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" +) + +func consumerDetachedContext() context.Context { + return context.TODO() +} + +type redisStreamEvent interface { + ToRedisStream() map[string]any +} + +func publishRedisStreamEvent(gateway *redisinfra.Gateway, ctx context.Context, stream string, event redisStreamEvent) error { + if gateway == nil { + return fmt.Errorf("redis gateway is nil") + } + if err := gateway.XAdd(ctx, stream, event.ToRedisStream()); err != nil { + return fmt.Errorf("failed to publish redis stream event: %w", err) + } + return nil +} + +func publishTraceStreamEvent(gateway *redisinfra.Gateway, ctx context.Context, stream string, event *dto.TraceStreamEvent) error { + if event == nil { + return nil + } + return publishRedisStreamEvent(gateway, ctx, stream, event) +} + +func loadCachedInjectionAlgorithms(gateway *redisinfra.Gateway, ctx context.Context, groupID string) ([]dto.ContainerVersionItem, bool, error) { + if gateway == nil { + return nil, false, fmt.Errorf("redis gateway is nil") + } + if !gateway.CheckCachedField(ctx, consts.InjectionAlgorithmsKey, groupID) { + return nil, false, nil + } + + var algorithms []dto.ContainerVersionItem + if err := gateway.GetHashField(ctx, consts.InjectionAlgorithmsKey, groupID, &algorithms); err != nil { + return nil, false, err + } + return algorithms, true, nil +} diff --git a/src/service/consumer/restart_pedestal.go b/src/service/consumer/restart_pedestal.go index 7da2c49c..c466b9f0 100644 --- a/src/service/consumer/restart_pedestal.go +++ b/src/service/consumer/restart_pedestal.go @@ -1,10 +1,11 @@ package consumer import ( - "aegis/client" "aegis/config" "aegis/consts" "aegis/dto" + helminfra "aegis/infra/helm" + redisinfra "aegis/infra/redis" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -18,6 +19,7 @@ import ( chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" ) type restartPayload struct { @@ -28,7 +30,7 @@ type restartPayload struct { } // executeRestartPedestal handles the execution of a restart pedestal task -func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { +func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) span.AddEvent(fmt.Sprintf("Starting restarting pedestal attempt %d", task.ReStartNum+1)) @@ -36,8 +38,19 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { "task_id": task.TaskID, "trace_id": task.TraceID, }) + helmGateway := deps.HelmGateway + if helmGateway == nil { + return handleExecutionError(span, logEntry, "helm gateway not initialized", fmt.Errorf("helm gateway not initialized")) + } + redisGateway := deps.RedisGateway + if redisGateway == nil { + return handleExecutionError(span, logEntry, "redis gateway not initialized", fmt.Errorf("redis gateway not initialized")) + } - rateLimiter := GetRestartPedestalRateLimiter() + rateLimiter := deps.RestartRateLimiter + if rateLimiter == nil { + return handleExecutionError(span, logEntry, "restart pedestal rate limiter not initialized", errors.New("restart pedestal rate limiter not initialized")) + } acquired, err := rateLimiter.AcquireToken(childCtx, task.TaskID, task.TraceID) if err != nil { return handleExecutionError(span, logEntry, "failed to acquire rate limit token", err) @@ -53,7 +66,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { } if !acquired { - if err := rescheduleRestartPedestalTask(childCtx, task, "rate limited, retrying later"); err != nil { + if err := rescheduleRestartPedestalTask(childCtx, deps.DB, redisGateway, task, "rate limited, retrying later"); err != nil { return err } return nil @@ -75,7 +88,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { return handleExecutionError(span, logEntry, fmt.Sprintf("no configuration found for system type: %s", system), fmt.Errorf("no configuration found for system type: %s", system)) } - monitor := GetMonitor() + monitor := deps.Monitor if monitor == nil { return handleExecutionError(span, logEntry, "monitor not initialized", errors.New("monitor not initialized")) } @@ -109,7 +122,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { } acquired = false - if err := rescheduleRestartPedestalTask(childCtx, task, "failed to acquire lock for namespace, retrying"); err != nil { + if err := rescheduleRestartPedestalTask(childCtx, deps.DB, redisGateway, task, "failed to acquire lock for namespace, retrying"); err != nil { return err } @@ -132,12 +145,12 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { consts.TaskTypeRestartPedestal, consts.TaskRunning, fmt.Sprintf("Restarting pedestal in namespace %s", namespace), - ).withSimpleEvent(consts.EventRestartPedestalStarted), + ).withSimpleEvent(consts.EventRestartPedestalStarted).withDB(deps.DB).withRedis(redisGateway), ) if payload.pedestal.Extra == nil { toReleased = true - publishEvent(childCtx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ + publishEvent(redisGateway, childCtx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ TaskID: task.TaskID, TaskType: consts.TaskTypeRestartPedestal, EventName: consts.EventRestartPedestalFailed, @@ -147,9 +160,9 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { return handleExecutionError(span, logEntry, "missing extra info in pedestal item", fmt.Errorf("missing extra info in pedestal item")) } - if err := installPedestal(childCtx, namespace, index, payload.pedestal.Extra); err != nil { + if err := installPedestal(childCtx, helmGateway, namespace, index, payload.pedestal.Extra); err != nil { toReleased = true - publishEvent(childCtx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ + publishEvent(redisGateway, childCtx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ TaskID: task.TaskID, TaskType: consts.TaskTypeRestartPedestal, EventName: consts.EventRestartPedestalFailed, @@ -167,7 +180,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { consts.TaskTypeRestartPedestal, consts.TaskCompleted, message, - ).withEvent(consts.EventRestartPedestalCompleted, message), + ).withEvent(consts.EventRestartPedestalCompleted, message).withDB(deps.DB).withRedis(redisGateway), ) tracing.SetSpanAttribute(childCtx, consts.TaskStateKey, consts.GetTaskStateName(consts.TaskCompleted)) @@ -176,7 +189,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { payload.injectPayload[consts.InjectPedestal] = system payload.injectPayload[consts.InjectPedestalID] = payload.pedestal.ID - if err := common.ProduceFaultInjectionTasks(childCtx, task, injectTime, payload.injectPayload); err != nil { + if err := common.ProduceFaultInjectionTasksWithDB(childCtx, deps.DB, deps.RedisGateway, task, injectTime, payload.injectPayload); err != nil { toReleased = true return handleExecutionError(span, logEntry, "failed to submit inject task", err) } @@ -186,7 +199,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask) error { } // rescheduleRestartPedestalTask reschedules a pedestal restart task with exponential backoff and jitter -func rescheduleRestartPedestalTask(ctx context.Context, task *dto.UnifiedTask, reason string) error { +func rescheduleRestartPedestalTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(ctx) @@ -211,11 +224,11 @@ func rescheduleRestartPedestalTask(ctx context.Context, task *dto.UnifiedTask, r consts.TaskTypeRestartPedestal, consts.TaskRescheduled, reason, - ).withEvent(consts.EventNoNamespaceAvailable, executeTime.String()), + ).withEvent(consts.EventNoNamespaceAvailable, executeTime.String()).withDB(db).withRedis(redisGateway), ) task.Reschedule(executeTime) - if err := common.SubmitTask(ctx, task); err != nil { + if err := common.SubmitTaskWithDB(ctx, db, redisGateway, task); err != nil { span.RecordError(err) span.AddEvent("failed to submit rescheduled task") return fmt.Errorf("failed to submit rescheduled restart task: %w", err) @@ -261,7 +274,7 @@ func parseRestartPayload(payload map[string]any) (*restartPayload, error) { // installPedestal installs or upgrades the pedestal using Helm // Priority: Remote (if configured) -> Local fallback (if remote fails and LocalPath is set) -func installPedestal(ctx context.Context, releaseName string, namespaceIdx int, item *dto.HelmConfigItem) error { +func installPedestal(ctx context.Context, gateway *helminfra.Gateway, releaseName string, namespaceIdx int, item *dto.HelmConfigItem) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ @@ -273,11 +286,6 @@ func installPedestal(ctx context.Context, releaseName string, namespaceIdx int, return handleExecutionError(span, logEntry, "missing helm config in container extra info", fmt.Errorf("missing helm config in container extra info")) } - helmClient, err := client.NewHelmClient(releaseName) - if err != nil { - return handleExecutionError(span, logEntry, "failed to create Helm client", err) - } - paramItems := item.DynamicValues for i := range paramItems { if paramItems[i].TemplateString != "" { @@ -296,10 +304,10 @@ func installPedestal(ctx context.Context, releaseName string, namespaceIdx int, if hasRemote { logEntry.Infof("Attempting to install chart from remote repository: %s/%s", item.RepoName, item.ChartName) - if err := helmClient.AddRepo(item.RepoName, item.RepoURL); err != nil { + if err := gateway.AddRepo(releaseName, item.RepoName, item.RepoURL); err != nil { logEntry.Warnf("Failed to add repository: %v", err) installErr = err - } else if err := helmClient.UpdateRepo(item.RepoName); err != nil { + } else if err := gateway.UpdateRepo(releaseName, item.RepoName); err != nil { logEntry.Warnf("Failed to update repository: %v", err) installErr = err } else { @@ -312,7 +320,8 @@ func installPedestal(ctx context.Context, releaseName string, namespaceIdx int, "namespace": releaseName, }).Infof("Installing Helm chart from remote with parameters: %+v", helmValues) - if err := helmClient.Install(ctx, + if err := gateway.Install(ctx, + releaseName, releaseName, fullChart, item.Version, @@ -343,7 +352,8 @@ func installPedestal(ctx context.Context, releaseName string, namespaceIdx int, "namespace": releaseName, }).Infof("Installing Helm chart from local path with parameters: %+v", helmValues) - if err := helmClient.Install(ctx, + if err := gateway.Install(ctx, + releaseName, releaseName, item.LocalPath, item.Version, diff --git a/src/service/consumer/runtime_deps.go b/src/service/consumer/runtime_deps.go new file mode 100644 index 00000000..1694d052 --- /dev/null +++ b/src/service/consumer/runtime_deps.go @@ -0,0 +1,23 @@ +package consumer + +import ( + buildkitinfra "aegis/infra/buildkit" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + + "gorm.io/gorm" +) + +type RuntimeDeps struct { + DB *gorm.DB + Monitor NamespaceMonitor + RestartRateLimiter *TokenBucketRateLimiter + BuildRateLimiter *TokenBucketRateLimiter + AlgorithmRateLimiter *TokenBucketRateLimiter + RedisGateway *redisinfra.Gateway + K8sGateway *k8sinfra.Gateway + BuildKitGateway *buildkitinfra.Gateway + HelmGateway *helminfra.Gateway + FaultBatchManager *FaultBatchManager +} diff --git a/src/service/consumer/state_store.go b/src/service/consumer/state_store.go new file mode 100644 index 00000000..fc8cfd1f --- /dev/null +++ b/src/service/consumer/state_store.go @@ -0,0 +1,100 @@ +package consumer + +import ( + "aegis/consts" + "aegis/dto" + "aegis/model" + "aegis/repository" + "errors" + "fmt" + "time" + + "gorm.io/gorm" +) + +type stateStore struct { + db *gorm.DB +} + +func newStateStore(db *gorm.DB) *stateStore { + return &stateStore{db: db} +} + +func (s *stateStore) updateExecutionState(executionID int, newState consts.ExecutionState) error { + return s.db.Transaction(func(tx *gorm.DB) error { + execution, err := repository.GetExecutionByID(tx, executionID) + if err != nil { + if errorsIsRecordNotFound(err) { + return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) + } + return fmt.Errorf("execution %d not found: %w", executionID, err) + } + + if execution.State != consts.ExecutionInitial { + return fmt.Errorf("cannot change state of execution %d from %s to %s", executionID, consts.GetExecutionStateName(execution.State), consts.GetExecutionStateName(newState)) + } + + if err := repository.UpdateExecution(tx, executionID, map[string]any{ + "state": newState, + }); err != nil { + return fmt.Errorf("failed to update execution %d duration: %w", executionID, err) + } + + return nil + }) +} + +func (s *stateStore) updateInjectionState(injectionName string, newState consts.DatapackState) error { + return s.db.Transaction(func(tx *gorm.DB) error { + injection, err := repository.GetInjectionByName(tx, injectionName, false) + if err != nil { + return fmt.Errorf("failed to get injection %s: %w", injectionName, err) + } + + if err := repository.UpdateInjection(tx, injection.ID, map[string]any{ + "state": newState, + }); err != nil { + return fmt.Errorf("failed to update injection %s state: %w", injectionName, err) + } + + return nil + }) +} + +func (s *stateStore) updateInjectionTimestamp(injectionName string, startTime time.Time, endTime time.Time) (*dto.InjectionItem, error) { + var updatedInjection *model.FaultInjection + err := s.db.Transaction(func(tx *gorm.DB) error { + injection, err := repository.GetInjectionByName(tx, injectionName, false) + if err != nil { + if errorsIsRecordNotFound(err) { + return fmt.Errorf("injection %s not found", injectionName) + } + return fmt.Errorf("failed to get injection %s: %w", injectionName, err) + } + + if err = repository.UpdateInjection(tx, injection.ID, map[string]any{ + "start_time": startTime, + "end_time": endTime, + }); err != nil { + return fmt.Errorf("update injection timestamps failed: %w", err) + } + + reloadedInjection, err := repository.GetInjectionByID(tx, injection.ID) + if err != nil { + return fmt.Errorf("failed to reload injection %d after update: %w", injection.ID, err) + } + + updatedInjection = reloadedInjection + return nil + }) + if err != nil { + return nil, err + } + + injectionItem := dto.NewInjectionItem(updatedInjection) + return &injectionItem, nil +} + +func errorsIsRecordNotFound(err error) bool { + return errors.Is(err, gorm.ErrRecordNotFound) +} diff --git a/src/service/consumer/task.go b/src/service/consumer/task.go index 5d5a3d5c..4179d57c 100644 --- a/src/service/consumer/task.go +++ b/src/service/consumer/task.go @@ -9,11 +9,10 @@ import ( "sync" "time" - "aegis/client" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" + redisinfra "aegis/infra/redis" + "aegis/model" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -26,6 +25,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" ) // ----------------------------------------------------------------------------- @@ -87,12 +87,14 @@ func withCallerLevel(level int) eventPublishOption { // taskStateUpdate encapsulates all information needed to update and notify task state changes type taskStateUpdate struct { - traceID string - taskID string - taskType consts.TaskType - taskState consts.TaskState - message string - event *dto.TraceStreamEvent // Optional: custom event to publish + traceID string + taskID string + taskType consts.TaskType + taskState consts.TaskState + message string + event *dto.TraceStreamEvent // Optional: custom event to publish + db *gorm.DB + redisGateway *redisinfra.Gateway } // newTaskStateUpdate creates a basic TaskStateUpdate with required fields @@ -138,15 +140,25 @@ func (u *taskStateUpdate) withSimpleEvent(eventType consts.EventType) *taskState return u.withEvent(eventType, nil) } +func (u *taskStateUpdate) withDB(db *gorm.DB) *taskStateUpdate { + u.db = db + return u +} + +func (u *taskStateUpdate) withRedis(gateway *redisinfra.Gateway) *taskStateUpdate { + u.redisGateway = gateway + return u +} + // StartScheduler starts the scheduler that moves tasks from delayed to ready queue -func StartScheduler(ctx context.Context) { +func StartScheduler(ctx context.Context, redisGateway *redisinfra.Gateway) { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: - processDelayedTasks(ctx) + processDelayedTasks(ctx, redisGateway) case <-ctx.Done(): return } @@ -154,8 +166,8 @@ func StartScheduler(ctx context.Context) { } // processDelayedTasks moves tasks from delayed queue to ready queue when their time arrives -func processDelayedTasks(ctx context.Context) { - result, err := repository.ProcessDelayedTasks(ctx) +func processDelayedTasks(ctx context.Context, redisGateway *redisinfra.Gateway) { + result, err := redisGateway.ProcessDelayedTasks(ctx) if err != nil && err != redis.Nil { logrus.Errorf("scheduler error: %v", err) @@ -173,7 +185,7 @@ func processDelayedTasks(ctx context.Context) { nextTime, err := common.CronNextTime(task.CronExpr) if err != nil { logrus.Warnf("invalid cron expr: %v", err) - if err := repository.HandleCronRescheduleFailure(ctx, []byte(taskData)); err != nil { + if err := redisGateway.HandleCronRescheduleFailure(ctx, []byte(taskData)); err != nil { logrus.Errorf("failed to handle cron reschedule failure: %v", err) } continue @@ -186,9 +198,9 @@ func processDelayedTasks(ctx context.Context) { return } - if err := repository.SubmitDelayedTask(ctx, taskData, task.TaskID, task.ExecuteTime); err != nil { + if err := redisGateway.SubmitDelayedTask(ctx, taskData, task.TaskID, task.ExecuteTime); err != nil { logrus.Errorf("failed to reschedule cron task %s: %v", task.TaskID, err) - err := repository.HandleCronRescheduleFailure(ctx, []byte(taskData)) + err := redisGateway.HandleCronRescheduleFailure(ctx, []byte(taskData)) if err != nil { logrus.Errorf("failed to handle cron reschedule failure: %v", err) } @@ -203,7 +215,7 @@ func processDelayedTasks(ctx context.Context) { // ----------------------------------------------------------------------------- // ConsumeTasks starts a consumer that processes tasks from the ready queue -func ConsumeTasks(ctx context.Context) { +func ConsumeTasks(ctx context.Context, deps RuntimeDeps) { defer func() { if r := recover(); r != nil { logrus.Errorf("consumer panic: %v", r) @@ -212,14 +224,14 @@ func ConsumeTasks(ctx context.Context) { logrus.Info("Starting consume tasks") for { - if !repository.AcquireConcurrencyLock(ctx) { + if !deps.RedisGateway.AcquireConcurrencyLock(ctx) { time.Sleep(100 * time.Millisecond) continue } - taskData, err := repository.GetTask(ctx, 30*time.Second) + taskData, err := deps.RedisGateway.GetTask(ctx, 30*time.Second) if err != nil { - repository.ReleaseConcurrencyLock(ctx) + deps.RedisGateway.ReleaseConcurrencyLock(ctx) if err == redis.Nil { continue } @@ -228,13 +240,13 @@ func ConsumeTasks(ctx context.Context) { continue } - go processTask(ctx, taskData) + go processTask(ctx, taskData, deps) } } // processTask handles a task from the queue -func processTask(ctx context.Context, taskData string) { - defer repository.ReleaseConcurrencyLock(ctx) +func processTask(ctx context.Context, taskData string, deps RuntimeDeps) { + defer deps.RedisGateway.ReleaseConcurrencyLock(ctx) defer func() { if r := recover(); r != nil { logrus.Errorf("task panic: %v\n%s", r, debug.Stack()) @@ -260,7 +272,7 @@ func processTask(ctx context.Context, taskData string) { tasksProcessed.WithLabelValues(consts.GetTaskTypeName(task.Type), "started").Inc() - executeTaskWithRetry(taskCtx, &task) + executeTaskWithRetry(taskCtx, &task, deps) taskDuration.WithLabelValues(consts.GetTaskTypeName(task.Type)).Observe(time.Since(startTime).Seconds()) } @@ -307,7 +319,7 @@ func extractContext(task *dto.UnifiedTask) (context.Context, context.Context) { } // executeTaskWithRetry attempts to execute a task with retry logic -func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask) { +func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) { retryCtx, retryCancel := context.WithCancel(ctx) registerCancelFunc(task.TaskID, retryCancel) defer retryCancel() @@ -330,7 +342,7 @@ func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask) { ctxWithCancel, cancel := context.WithCancel(ctx) _ = cancel - err := dispatchTask(ctxWithCancel, task) + err := dispatchTask(ctxWithCancel, task, deps) if err == nil { tasksProcessed.WithLabelValues(consts.GetTaskTypeName(task.Type), "success").Inc() span.SetStatus(codes.Ok, fmt.Sprintf("Task %s of type %s completed successfully after %d attempts", @@ -349,7 +361,7 @@ func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask) { message := fmt.Sprintf("Attempt %d failed: %v", attempt+1, err) span.AddEvent(message) logrus.WithField("task_id", task.TaskID).Warn(message) - publishEvent(ctx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ + publishEvent(deps.RedisGateway, ctx, fmt.Sprintf(consts.StreamTraceLogKey, task.TraceID), dto.TraceStreamEvent{ TaskID: task.TaskID, TaskType: task.Type, EventName: consts.EventTaskRetryStatus, @@ -363,7 +375,7 @@ func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask) { tasksProcessed.WithLabelValues(consts.GetTaskTypeName(task.Type), "failed").Inc() message := fmt.Sprintf("Task failed after %d attempts, errors: [%v]", task.RetryPolicy.MaxAttempts, errs) - handleFinalFailure(ctx, task, message) + handleFinalFailure(ctx, deps.RedisGateway, task, message) // Simple usage: no custom event needed updateTaskState(ctx, newTaskStateUpdate( @@ -372,7 +384,7 @@ func executeTaskWithRetry(ctx context.Context, task *dto.UnifiedTask) { task.Type, consts.TaskError, message, - )) + ).withDB(deps.DB).withRedis(deps.RedisGateway)) } // ----------------------------------------------------------------------------- @@ -394,14 +406,14 @@ func unregisterCancelFunc(taskID string) { } // handleFinalFailure moves a failed task to the dead letter queue -func handleFinalFailure(ctx context.Context, task *dto.UnifiedTask, errMsg string) { +func handleFinalFailure(ctx context.Context, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, errMsg string) { taskData, err := json.Marshal(task) if err != nil { logrus.Errorf("failed to marshal failed task %s: %v", task.TaskID, err) return } - if err := repository.HandleFailedTask(ctx, taskData, task.RetryPolicy.BackoffSec); err != nil { + if err := redisGateway.HandleFailedTask(ctx, taskData, task.RetryPolicy.BackoffSec); err != nil { logrus.Errorf("failed to handle failed task %s: %v", task.TaskID, err) } @@ -414,7 +426,7 @@ func handleFinalFailure(ctx context.Context, task *dto.UnifiedTask, errMsg strin } // CancelTask cancels a task and removes it from the queues -func CancelTask(taskID string) error { +func CancelTask(redisGateway *redisinfra.Gateway, taskID string) error { // Cancel execution context taskCancelFuncsMutex.RLock() cancelFunc, exists := taskCancelFuncs[taskID] @@ -425,29 +437,29 @@ func CancelTask(taskID string) error { } // Remove task from Redis - ctx := context.Background() + ctx := consumerDetachedContext() // Locate queue using index - queueType, err := repository.GetTaskQueue(ctx, taskID) + queueType, err := redisGateway.GetTaskQueue(ctx, taskID) if err == nil { switch queueType { - case repository.ReadyQueueKey: - if _, err := repository.RemoveFromList(ctx, repository.ReadyQueueKey, taskID); err != nil { + case redisinfra.ReadyQueueKey: + if _, err := redisGateway.RemoveFromList(ctx, redisinfra.ReadyQueueKey, taskID); err != nil { logrus.Warnf("failed to remove from list: %v", err) } - case repository.DelayedQueueKey: - if s := repository.RemoveFromZSet(ctx, repository.DelayedQueueKey, taskID); !s { + case redisinfra.DelayedQueueKey: + if s := redisGateway.RemoveFromZSet(ctx, redisinfra.DelayedQueueKey, taskID); !s { logrus.Warnf("failed to remove from delayed queue: %v", err) } - case repository.DeadLetterKey: - if s := repository.RemoveFromZSet(ctx, repository.DeadLetterKey, taskID); !s { + case redisinfra.DeadLetterKey: + if s := redisGateway.RemoveFromZSet(ctx, redisinfra.DeadLetterKey, taskID); !s { logrus.Warnf("failed to remove from dead letter queue: %v", err) } } } // Clean up index - if err := repository.DeleteTaskIndex(ctx, taskID); err != nil { + if err := redisGateway.DeleteTaskIndex(ctx, taskID); err != nil { logrus.Warnf("failed to delete task index: %v", err) } @@ -462,7 +474,7 @@ func CancelTask(taskID string) error { // publishEvent publishes a StreamEvent to the specified Redis stream // This adds caller information and handles error logging -func publishEvent(ctx context.Context, stream string, event dto.TraceStreamEvent, opts ...eventPublishOption) { +func publishEvent(gateway *redisinfra.Gateway, ctx context.Context, stream string, event dto.TraceStreamEvent, opts ...eventPublishOption) { options := &eventPublishOptions{ callerLevel: 2, } @@ -477,7 +489,7 @@ func publishEvent(ctx context.Context, stream string, event dto.TraceStreamEvent event.FnName = fn // Call repository layer for data access - if err := client.RedisXAdd(ctx, stream, event.ToRedisStream()); err != nil { + if err := publishTraceStreamEvent(gateway, ctx, stream, &event); err != nil { if err == redis.Nil { logrus.Warnf("No new messages to publish to Redis stream %s", stream) return @@ -489,6 +501,14 @@ func publishEvent(ctx context.Context, stream string, event dto.TraceStreamEvent // updateTaskState updates the task states and publishes the update func updateTaskState(ctx context.Context, update *taskStateUpdate) { err := tracing.WithSpan(ctx, func(childCtx context.Context) error { + db := update.db + if db == nil { + return fmt.Errorf("task state update db is nil") + } + if update.redisGateway == nil { + return fmt.Errorf("task state update redis gateway is nil") + } + span := trace.SpanFromContext(childCtx) logEntry := logrus.WithField("trace_id", update.traceID).WithField("task_id", update.taskID) span.AddEvent(update.message) @@ -506,15 +526,15 @@ func updateTaskState(ctx context.Context, update *taskStateUpdate) { // Publish custom event or default state update event if update.event != nil { - publishEvent(childCtx, stream, *update.event, withCallerLevel(5)) + publishEvent(update.redisGateway, childCtx, stream, *update.event, withCallerLevel(5)) } - if err := repository.UpdateTaskState(database.DB, childCtx, update.taskID, update.taskState); err != nil { + if err := updateTaskStateRecord(db, childCtx, update.taskID, update.taskState); err != nil { logEntry.Errorf("failed to update database: %v", err) return err } - if err := updateTraceState(update.traceID, update.taskID, update.taskState, update.event); err != nil { + if err := updateTraceState(update.redisGateway, db, update.traceID, update.taskID, update.taskState, update.event); err != nil { logEntry.Errorf("failed to update trace state: %v", err) return err } @@ -526,3 +546,9 @@ func updateTaskState(ctx context.Context, update *taskStateUpdate) { logrus.WithField("task_id", update.taskID).Errorf("failed to update task state: %v", err) } } + +func updateTaskStateRecord(db *gorm.DB, ctx context.Context, taskID string, state consts.TaskState) error { + return db.WithContext(ctx).Model(&model.Task{}). + Where("id = ?", taskID). + Update("state", state).Error +} diff --git a/src/service/consumer/trace.go b/src/service/consumer/trace.go index dc288708..591e853f 100644 --- a/src/service/consumer/trace.go +++ b/src/service/consumer/trace.go @@ -1,16 +1,17 @@ package consumer import ( - "aegis/client" "aegis/consts" - "aegis/database" "aegis/dto" - "aegis/repository" + redisinfra "aegis/infra/redis" + "aegis/model" + groupmodule "aegis/module/group" "context" "fmt" "time" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) // levelStatistics holds statistics for a specific level in the task tree @@ -81,15 +82,14 @@ func getEventTypeByTask(taskType consts.TaskType, taskState consts.TaskState) co // updateTraceState updates trace state based on task state change // This function is called after task state is persisted to ensure real-time sync -func updateTraceState(traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { +func updateTraceState(redisGateway *redisinfra.Gateway, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { logEntry := logrus.WithField("trace_id", traceID).WithField("task_id", taskID) // Update trace state asynchronously to avoid blocking task processing go func() { - // Use background context since this is async - ctx := context.Background() + ctx := consumerDetachedContext() - if err := performTraceStateUpdate(ctx, traceID, taskID, newState, event); err != nil { + if err := performTraceStateUpdate(redisGateway, ctx, db, traceID, taskID, newState, event); err != nil { logEntry.Errorf("failed to update trace state: %v", err) } }() @@ -98,12 +98,12 @@ func updateTraceState(traceID, taskID string, newState consts.TaskState, event * } // performTraceStateUpdate performs the actual trace state update with retry logic -func performTraceStateUpdate(ctx context.Context, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { +func performTraceStateUpdate(redisGateway *redisinfra.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { const maxRetries = 3 logEntry := logrus.WithField("trace_id", traceID) for attempt := range maxRetries { - err := tryUpdateTraceStateCore(ctx, traceID, taskID, newState, event) + err := tryUpdateTraceStateCore(redisGateway, ctx, db, traceID, taskID, newState, event) if err == nil { return nil } @@ -122,11 +122,15 @@ func performTraceStateUpdate(ctx context.Context, traceID, taskID string, newSta } // tryUpdateTraceStateCore attempts to update trace state once -func tryUpdateTraceStateCore(ctx context.Context, traceID, taskID string, newState consts.TaskState, streamEvent *dto.TraceStreamEvent) error { +func tryUpdateTraceStateCore(redisGateway *redisinfra.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, streamEvent *dto.TraceStreamEvent) error { + if db == nil { + return fmt.Errorf("trace state update db is nil") + } + logEntry := logrus.WithField("trace_id", traceID) // 1. Fetch trace with all tasks (including the just-updated task) - trace, err := repository.GetTraceByID(database.DB, traceID) + trace, err := getTraceByID(db, traceID) if err != nil { return fmt.Errorf("failed to get trace: %w", err) } @@ -135,7 +139,7 @@ func tryUpdateTraceStateCore(ctx context.Context, traceID, taskID string, newSta originalUpdatedAt := trace.UpdatedAt // 2. Find the task that was just updated - var updatedTask *database.Task + var updatedTask *model.Task for i := range trace.Tasks { if trace.Tasks[i].ID == taskID { updatedTask = &trace.Tasks[i] @@ -194,12 +198,12 @@ func tryUpdateTraceStateCore(ctx context.Context, traceID, taskID string, newSta // Publish to group-level stream for real-time group progress SSE if trace.GroupID != "" { - publishGroupStreamEvent(ctx, trace.GroupID, traceID, inferredState, inferredEventType) + publishGroupStreamEvent(redisGateway, ctx, trace.GroupID, traceID, inferredState, inferredEventType) } } // 6. Execute optimistic locking update - result := database.DB.Model(&database.Trace{}). + result := db.Model(&model.Trace{}). Where("id = ? AND updated_at = ?", traceID, originalUpdatedAt). Updates(updates) @@ -220,7 +224,7 @@ func tryUpdateTraceStateCore(ctx context.Context, traceID, taskID string, newSta } // buildLevelStatistics constructs level statistics from task list -func buildLevelStatistics(tasks []database.Task, treeHeight int) map[int]*levelStatistics { +func buildLevelStatistics(tasks []model.Task, treeHeight int) map[int]*levelStatistics { stats := make(map[int]*levelStatistics) // Initialize statistics for each level @@ -255,7 +259,7 @@ func buildLevelStatistics(tasks []database.Task, treeHeight int) map[int]*levelS // hasEarlyTerminationEvent checks if any CollectResult task has completed with early termination events // Now also checks the streamEvent to accurately determine if it's truly an early termination -func hasEarlyTerminationEvent(tasks []database.Task, streamEvent *dto.TraceStreamEvent) bool { +func hasEarlyTerminationEvent(tasks []model.Task, streamEvent *dto.TraceStreamEvent) bool { // Priority 1: Check if streamEvent explicitly indicates early termination if streamEvent != nil && streamEvent.EventName != "" { // These events indicate early termination - no further processing needed @@ -297,7 +301,7 @@ func hasEarlyTerminationEvent(tasks []database.Task, streamEvent *dto.TraceStrea // findEarlyTerminationEvent finds and returns the early termination event from completed CollectResult tasks // Now uses streamEvent to accurately return the correct event type -func findEarlyTerminationEvent(tasks []database.Task, streamEvent *dto.TraceStreamEvent) consts.EventType { +func findEarlyTerminationEvent(tasks []model.Task, streamEvent *dto.TraceStreamEvent) consts.EventType { // Priority 1: If streamEvent is provided with early termination events, use it directly if streamEvent != nil && streamEvent.EventName != "" { if streamEvent.EventName == consts.EventDatapackNoAnomaly || @@ -335,7 +339,7 @@ func findEarlyTerminationEvent(tasks []database.Task, streamEvent *dto.TraceStre } // selectBestLastEvent selects the most appropriate last event from completed leaf tasks -func selectBestLastEvent(tasks []database.Task, leafLevel int, streamEvent *dto.TraceStreamEvent) consts.EventType { +func selectBestLastEvent(tasks []model.Task, leafLevel int, streamEvent *dto.TraceStreamEvent) consts.EventType { // Event priority map: higher value = higher priority eventPriority := map[consts.EventType]int{ consts.EventFaultInjectionCompleted: 80, @@ -387,7 +391,7 @@ func selectBestLastEvent(tasks []database.Task, leafLevel int, streamEvent *dto. // inferTraceState infers trace state and last event from all tasks // streamEvent parameter helps distinguish between early termination vs continuation scenarios -func inferTraceState(trace *database.Trace, tasks []database.Task, streamEvent *dto.TraceStreamEvent) (consts.TraceState, consts.EventType) { +func inferTraceState(trace *model.Trace, tasks []model.Task, streamEvent *dto.TraceStreamEvent) (consts.TraceState, consts.EventType) { treeHeight := traceTypeHeightMap[trace.Type] stats := buildLevelStatistics(tasks, treeHeight) @@ -493,6 +497,20 @@ func inferTraceState(trace *database.Trace, tasks []database.Task, streamEvent * return consts.TracePending, consts.EventTaskStateUpdate } +func getTraceByID(db *gorm.DB, traceID string) (*model.Trace, error) { + var trace model.Trace + if err := db.Model(&model.Trace{}). + Preload("Project"). + Preload("Tasks", func(db *gorm.DB) *gorm.DB { + return db.Order("level ASC, sequence ASC") + }). + Where("id = ? AND status != ?", traceID, consts.CommonDeleted). + First(&trace).Error; err != nil { + return nil, err + } + return &trace, nil +} + // isOptimisticLockError checks if an error is due to optimistic lock failure func isOptimisticLockError(err error) bool { return err != nil && err.Error() == "optimistic lock conflict: trace was modified by another job" @@ -501,17 +519,17 @@ func isOptimisticLockError(err error) bool { // publishGroupStreamEvent publishes a lightweight event to the group-level Redis stream // when a trace reaches a terminal state (Completed/Failed). // This enables real-time SSE updates for group progress tracking on the frontend. -func publishGroupStreamEvent(ctx context.Context, groupID, traceID string, state consts.TraceState, lastEvent consts.EventType) { +func publishGroupStreamEvent(redisGateway *redisinfra.Gateway, ctx context.Context, groupID, traceID string, state consts.TraceState, lastEvent consts.EventType) { streamKey := fmt.Sprintf(consts.StreamGroupLogKey, groupID) logEntry := logrus.WithField("group_id", groupID).WithField("trace_id", traceID) - event := &dto.GroupStreamEvent{ + event := &groupmodule.GroupStreamEvent{ TraceID: traceID, State: state, LastEvent: lastEvent, } - if err := client.RedisXAdd(ctx, streamKey, event.ToRedisStream()); err != nil { + if err := publishRedisStreamEvent(redisGateway, ctx, streamKey, event); err != nil { logEntry.Errorf("failed to publish group stream event: %v", err) return } diff --git a/src/service/initialization/bootstrap_store.go b/src/service/initialization/bootstrap_store.go new file mode 100644 index 00000000..eb744297 --- /dev/null +++ b/src/service/initialization/bootstrap_store.go @@ -0,0 +1,207 @@ +package initialization + +import ( + "errors" + "fmt" + + "aegis/consts" + "aegis/model" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ( + resourceOmitFields = "Parent" + roleOmitFields = "ActiveName" + permissionOmitFields = "ActiveName,Resource" + userOmitFields = "active_username" + teamOmitFields = "ActiveName" + projectOmitFields = "ActiveName" + userTeamOmitFields = "active_user_team" +) + +type bootstrapStore struct { + db *gorm.DB +} + +func newBootstrapStore(db *gorm.DB) *bootstrapStore { + return &bootstrapStore{db: db} +} + +func (s *bootstrapStore) listExistingConfigs() ([]model.DynamicConfig, error) { + var configs []model.DynamicConfig + if err := s.db.Order("config_key ASC").Find(&configs).Error; err != nil { + return nil, fmt.Errorf("failed to list all existing configs: %w", err) + } + return configs, nil +} + +func (s *bootstrapStore) upsertResources(resources []model.Resource) error { + if len(resources) == 0 { + return fmt.Errorf("no resources to upsert") + } + if err := s.db.Omit(resourceOmitFields).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{}), + }).Create(&resources).Error; err != nil { + return fmt.Errorf("failed to batch upsert resources: %w", err) + } + return nil +} + +func (s *bootstrapStore) listResourcesByNames(names []consts.ResourceName) ([]model.Resource, error) { + if len(names) == 0 { + return nil, fmt.Errorf("no resource names provided") + } + var resources []model.Resource + if err := s.db.Where("name IN ?", names).Find(&resources).Error; err != nil { + return nil, fmt.Errorf("failed to list resources by names: %w", err) + } + return resources, nil +} + +func (s *bootstrapStore) upsertPermissions(permissions []model.Permission) error { + if len(permissions) == 0 { + return fmt.Errorf("no permissions to upsert") + } + if err := s.db.Omit(permissionOmitFields).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{}), + }).Create(&permissions).Error; err != nil { + return fmt.Errorf("failed to batch upsert permissions: %w", err) + } + return nil +} + +func (s *bootstrapStore) upsertRoles(roles []model.Role) error { + if len(roles) == 0 { + return fmt.Errorf("no roles to upsert") + } + if err := s.db.Omit(roleOmitFields).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "name"}}, + DoUpdates: clause.AssignmentColumns([]string{}), + }).Create(&roles).Error; err != nil { + return fmt.Errorf("failed to batch upsert roles: %w", err) + } + return nil +} + +func (s *bootstrapStore) getRoleByName(name string) (*model.Role, error) { + var role model.Role + if err := s.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&role).Error; err != nil { + return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) + } + return &role, nil +} + +func (s *bootstrapStore) listSystemPermissions() ([]model.Permission, error) { + var permissions []model.Permission + if err := s.db.Where("is_system = ? AND status = ?", true, consts.CommonEnabled).Find(&permissions).Error; err != nil { + return nil, fmt.Errorf("failed to get system permissions: %w", err) + } + return permissions, nil +} + +func (s *bootstrapStore) listPermissionsByNames(names []string) ([]model.Permission, error) { + if len(names) == 0 { + return []model.Permission{}, nil + } + var permissions []model.Permission + if err := s.db.Where("name IN ? AND status = ?", names, consts.CommonEnabled).Find(&permissions).Error; err != nil { + return nil, fmt.Errorf("failed to query permissions: %w", err) + } + return permissions, nil +} + +func (s *bootstrapStore) createRolePermissions(rolePermissions []model.RolePermission) error { + if len(rolePermissions) == 0 { + return nil + } + if err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&rolePermissions).Error; err != nil { + return fmt.Errorf("failed to batch create role permissions: %w", err) + } + return nil +} + +func (s *bootstrapStore) createUser(user *model.User) error { + if err := s.db.Omit(userOmitFields).Create(user).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: user %s already exists", consts.ErrAlreadyExists, user.Username) + } + return fmt.Errorf("failed to create user: %w", err) + } + return nil +} + +func (s *bootstrapStore) createUserRole(userRole *model.UserRole) error { + if err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(userRole).Error; err != nil { + return fmt.Errorf("failed to create user-role association: %w", err) + } + return nil +} + +func (s *bootstrapStore) createTeam(team *model.Team) error { + if err := s.db.Omit(teamOmitFields).Create(team).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: team %s already exists", consts.ErrAlreadyExists, team.Name) + } + return fmt.Errorf("failed to create team: %w", err) + } + return nil +} + +func (s *bootstrapStore) createProject(project *model.Project) error { + if err := s.db.Omit(projectOmitFields).Create(project).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: project %s already exists", consts.ErrAlreadyExists, project.Name) + } + return fmt.Errorf("failed to create project: %w", err) + } + return nil +} + +func (s *bootstrapStore) getTeamByName(name string) (*model.Team, error) { + var team model.Team + if err := s.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&team).Error; err != nil { + return nil, fmt.Errorf("failed to find team with name %s: %w", name, err) + } + return &team, nil +} + +func (s *bootstrapStore) getProjectByName(name string) (*model.Project, error) { + var project model.Project + if err := s.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { + return nil, fmt.Errorf("failed to find project with name %s: %w", name, err) + } + return &project, nil +} + +func (s *bootstrapStore) saveProject(project *model.Project) error { + if err := s.db.Omit(projectOmitFields).Save(project).Error; err != nil { + return fmt.Errorf("failed to update project: %w", err) + } + return nil +} + +func (s *bootstrapStore) createUserTeam(userTeam *model.UserTeam) error { + if err := s.db.Omit(userTeamOmitFields).Clauses(clause.OnConflict{DoNothing: true}).Create(userTeam).Error; err != nil { + return fmt.Errorf("failed to create user-team association: %w", err) + } + return nil +} + +func (s *bootstrapStore) createUserProject(userProject *model.UserProject) error { + if err := s.db.Clauses(clause.OnConflict{DoNothing: true}).Create(userProject).Error; err != nil { + return fmt.Errorf("failed to create user-project association: %w", err) + } + return nil +} + +func (s *bootstrapStore) listEnabledSystems() ([]model.System, error) { + var systems []model.System + if err := s.db.Where("status = ?", consts.CommonEnabled).Find(&systems).Error; err != nil { + return nil, fmt.Errorf("failed to list enabled systems: %w", err) + } + return systems, nil +} diff --git a/src/service/initialization/common.go b/src/service/initialization/common.go index c007d702..4c26451f 100644 --- a/src/service/initialization/common.go +++ b/src/service/initialization/common.go @@ -4,35 +4,26 @@ import ( "aegis/config" "aegis/consts" "aegis/service/common" - "context" + "fmt" "github.com/sirupsen/logrus" ) -func registerHandlers(ctx context.Context, scope consts.ConfigScope, handlerFunc func()) { - // Register global-scope handlers (idempotent via sync.Once) - common.RegisterGlobalHandlers() - if handlerFunc != nil { - handlerFunc() - } - - // Ensure etcd listener covers required scopes (each call is idempotent) - listener := common.GetConfigUpdateListener(ctx) - +func activateConfigScope(scope consts.ConfigScope, listener *common.ConfigUpdateListener) error { if err := listener.EnsureScope(consts.ConfigScopeGlobal); err != nil { - logrus.Fatalf("Failed to activate global config listener: %v", err) + return fmt.Errorf("failed to activate global config listener: %w", err) } if scope == consts.ConfigScopeConsumer { if err := listener.EnsureScope(consts.ConfigScopeConsumer); err != nil { - logrus.Fatalf("Failed to activate consumer config listener: %v", err) + return fmt.Errorf("failed to activate consumer config listener: %w", err) } } logrus.Infof("Config handlers registered for scope %s, %d total handler(s)", consts.GetConfigScopeName(scope), len(common.ListRegisteredConfigKeys(nil))) - // Sync atomic vars from viper (listener has loaded configs from etcd) config.SetDetectorName(config.GetString(consts.DetectorKey)) logrus.Infof("Global detector name initialized: %s", config.GetDetectorName()) + return nil } diff --git a/src/service/initialization/consumer.go b/src/service/initialization/consumer.go index 47d6fa1f..e6c19a6d 100644 --- a/src/service/initialization/consumer.go +++ b/src/service/initialization/consumer.go @@ -5,11 +5,11 @@ import ( "fmt" "path/filepath" - "aegis/client/k8s" "aegis/config" "aegis/consts" - "aegis/database" - "aegis/repository" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + "aegis/model" "aegis/service/common" "aegis/service/consumer" @@ -17,55 +17,65 @@ import ( "gorm.io/gorm" ) -var consumerData *configData - -func InitConcurrencyLock(ctx context.Context) { - if err := repository.InitConcurrencyLock(ctx); err != nil { - logrus.Fatalf("error setting concurrency lock to 0: %v", err) +func InitializeConsumer( + ctx context.Context, + db *gorm.DB, + controller *k8sinfra.Controller, + monitor consumer.NamespaceMonitor, + publisher *redisinfra.Gateway, + listener *common.ConfigUpdateListener, + restartLimiter *consumer.TokenBucketRateLimiter, + buildLimiter *consumer.TokenBucketRateLimiter, + algoLimiter *consumer.TokenBucketRateLimiter, +) error { + consumerData, err := newConfigDataWithDB(db, consts.ConfigScopeConsumer) + if err != nil { + return fmt.Errorf("failed to load consumer config metadata: %w", err) } -} - -func InitializeConsumer(ctx context.Context) { - consumerData = newConfigData(consts.ConfigScopeConsumer) if len(consumerData.configs) == 0 { logrus.Info("Seeding initial system data for consumer...") - if err := initializeConsumer(); err != nil { - logrus.Fatalf("Failed to initialize system data for consumer: %v", err) + if err := initializeConsumer(db); err != nil { + return fmt.Errorf("failed to initialize system data for consumer: %w", err) } logrus.Info("Successfully seeded initial system data for consumer") } else { logrus.Info("Initial system data for consumer already seeded, skipping initialization") } - registerHandlers(ctx, consumerData.scope, consumer.RegisterConsumerHandlers) + common.RegisterGlobalHandlers(publisher) + consumer.RegisterConsumerHandlers(controller, monitor, publisher, restartLimiter, buildLimiter, algoLimiter) + if err := activateConfigScope(consumerData.scope, listener); err != nil { + return err + } // Initialize namespaces on startup - critical after restart to re-initialize CRD informers logrus.Info("Initializing namespaces on startup...") - monitor := consumer.GetMonitor() if monitor == nil { logrus.Warn("Monitor not initialized, skipping namespace initialization") + return nil } else { + monitor.SetContext(ctx) initialized, err := monitor.InitializeNamespaces() if err != nil { - logrus.Errorf("Failed to initialize namespaces: %v", err) - return + return fmt.Errorf("failed to initialize namespaces: %w", err) } if len(initialized) == 0 { logrus.Warn("No namespaces to initialize on startup") - return + return nil } logrus.Infof("Initialized namespaces on startup: %v", initialized) - if err := consumer.UpdateK8sController(k8s.GetK8sController(), initialized, []string{}); err != nil { - logrus.Errorf("Failed to update k8s controller: %v", err) - return + if err := consumer.UpdateK8sController(controller, initialized, []string{}); err != nil { + return fmt.Errorf("failed to update k8s controller: %w", err) } } + + return nil } -func initializeConsumer() error { +func initializeConsumer(db *gorm.DB) error { dataPath := config.GetString("initialization.data_path") filePath := filepath.Join(dataPath, consts.InitialFilename) initialData, err := loadInitialDataFromFile(filePath) @@ -73,9 +83,9 @@ func initializeConsumer() error { return fmt.Errorf("failed to load initial data from file: %w", err) } - return withOptimizedDBSettings(func() error { - err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := initializeDynamicConfigs(tx, initialData); err != nil { + return withOptimizedDBSettings(db, func() error { + err := db.Transaction(func(tx *gorm.DB) error { + if _, err := initializeDynamicConfigs(tx, initialData); err != nil { return fmt.Errorf("failed to initialize dynamic configs for consumer: %w", err) } return nil @@ -88,21 +98,20 @@ func initializeConsumer() error { }) } -func initializeDynamicConfigs(tx *gorm.DB, data *InitialData) error { - var configs []database.DynamicConfig +func initializeDynamicConfigs(tx *gorm.DB, data *InitialData) ([]model.DynamicConfig, error) { + var configs []model.DynamicConfig for _, configData := range data.DynamicConfigs { cfg := configData.ConvertToDBDynamicConfig() if err := common.ValidateConfigMetadataConstraints(cfg); err != nil { - return fmt.Errorf("invalid config value for key %s: %w", configData.Key, err) + return nil, fmt.Errorf("invalid config value for key %s: %w", configData.Key, err) } if err := common.CreateConfig(tx, cfg); err != nil { - return fmt.Errorf("failed to create dynamic config %s: %w", configData.Key, err) + return nil, fmt.Errorf("failed to create dynamic config %s: %w", configData.Key, err) } configs = append(configs, *cfg) } - consumerData.configs = configs - return nil + return configs, nil } diff --git a/src/service/initialization/producer.go b/src/service/initialization/producer.go index 06eccc4d..4a12674c 100644 --- a/src/service/initialization/producer.go +++ b/src/service/initialization/producer.go @@ -1,16 +1,18 @@ package initialization import ( - "context" "errors" "fmt" "path/filepath" "aegis/config" "aegis/consts" - "aegis/database" - "aegis/repository" - producer "aegis/service/producer" + redisinfra "aegis/infra/redis" + "aegis/model" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + labelmodule "aegis/module/label" + "aegis/service/common" "aegis/utils" "github.com/sirupsen/logrus" @@ -30,17 +32,16 @@ func (r permMeta) String() string { return fmt.Sprintf("%v %v %v", r.action, r.resourceScope, r.resourceName) } -var producerData *configData - -var resourceIDMap map[consts.ResourceName]int - -func InitializeProducer(ctx context.Context) { - producerData = newConfigData(consts.ConfigScopeProducer) +func InitializeProducer(db *gorm.DB, publisher *redisinfra.Gateway, listener *common.ConfigUpdateListener) error { + producerData, err := newConfigDataWithDB(db, consts.ConfigScopeProducer) + if err != nil { + return fmt.Errorf("failed to load producer config metadata: %w", err) + } if len(producerData.configs) == 0 { logrus.Info("Seeding initial system data for producer...") - if err := initializeProducer(); err != nil { - logrus.Fatalf("Failed to initialize system data for producer: %v", err) + if err := initializeProducer(db); err != nil { + return fmt.Errorf("failed to initialize system data for producer: %w", err) } logrus.Info("Successfully seeded initial system data for producer") } else { @@ -48,12 +49,18 @@ func InitializeProducer(ctx context.Context) { } // Initialize systems (seed builtins, register with chaos-experiment, set MetadataStore) - InitializeSystems() + if err := InitializeSystems(db); err != nil { + return fmt.Errorf("failed to initialize systems: %w", err) + } + common.RegisterGlobalHandlers(publisher) + if err := activateConfigScope(producerData.scope, listener); err != nil { + return err + } - registerHandlers(ctx, producerData.scope, nil) + return nil } -func initializeProducer() error { +func initializeProducer(db *gorm.DB) error { dataPath := config.GetString("initialization.data_path") filePath := filepath.Join(dataPath, consts.InitialFilename) initialData, err := loadInitialDataFromFile(filePath) @@ -62,7 +69,7 @@ func initializeProducer() error { } // System resources (following the order in system.go) - resources := []database.Resource{ + resources := []model.Resource{ {Name: consts.ResourceSystem, Type: consts.ResourceTypeSystem, Category: consts.ResourceCategorySystem}, {Name: consts.ResourceAudit, Type: consts.ResourceTypeTable, Category: consts.ResourceCategorySystem}, {Name: consts.ResourceConfiguration, Type: consts.ResourceTypeTable, Category: consts.ResourceCategorySystem}, @@ -86,9 +93,9 @@ func initializeProducer() error { resources[i].DisplayName = consts.GetResourceDisplayName(resources[i].Name) } - systemRoles := make([]database.Role, 0) + systemRoles := make([]model.Role, 0) for role, displayName := range consts.SystemRoleDisplayNames { - systemRoles = append(systemRoles, database.Role{ + systemRoles = append(systemRoles, model.Role{ Name: role.String(), DisplayName: displayName, IsSystem: true, @@ -96,9 +103,11 @@ func initializeProducer() error { }) } - return withOptimizedDBSettings(func() error { - return database.DB.Transaction(func(tx *gorm.DB) error { - if err := repository.BatchUpsertResources(tx, resources); err != nil { + return withOptimizedDBSettings(db, func() error { + return db.Transaction(func(tx *gorm.DB) error { + txStore := newBootstrapStore(tx) + + if err := txStore.upsertResources(resources); err != nil { return fmt.Errorf("failed to create system resources: %w", err) } @@ -107,7 +116,7 @@ func initializeProducer() error { resourceNames = append(resourceNames, res.Name) } - allResourcesInDB, err := repository.ListResourcesByNames(tx, resourceNames) + allResourcesInDB, err := txStore.listResourcesByNames(resourceNames) if err != nil { return fmt.Errorf("failed to get system resources from database: %w", err) } @@ -116,8 +125,8 @@ func initializeProducer() error { return fmt.Errorf("mismatch in number of resources created and fetched") } - resourceMap := make(map[consts.ResourceName]*database.Resource, len(allResourcesInDB)) - resourceIDMap = make(map[consts.ResourceName]int, len(allResourcesInDB)) + resourceMap := make(map[consts.ResourceName]*model.Resource, len(allResourcesInDB)) + resourceIDMap := make(map[consts.ResourceName]int, len(allResourcesInDB)) for _, res := range allResourcesInDB { resourceIDMap[res.Name] = res.ID resourceMap[res.Name] = &res @@ -126,12 +135,12 @@ func initializeProducer() error { resourceMap[consts.ResourceContainerVersion].ParentID = utils.IntPtr(resourceIDMap[consts.ResourceContainer]) resourceMap[consts.ResourceDatasetVersion].ParentID = utils.IntPtr(resourceIDMap[consts.ResourceDataset]) - toUpdatedResources := []database.Resource{ + toUpdatedResources := []model.Resource{ *resourceMap[consts.ResourceContainerVersion], *resourceMap[consts.ResourceDatasetVersion], } - if err := repository.BatchUpsertResources(tx, toUpdatedResources); err != nil { + if err := txStore.upsertResources(toUpdatedResources); err != nil { return fmt.Errorf("failed to update resource parent IDs: %w", err) } @@ -157,7 +166,7 @@ func initializeProducer() error { } } - var permissionsToCreate []database.Permission + var permissionsToCreate []model.Permission for permName, permData := range uniquePermissions { resource, ok := resourceMap[permData.resourceName] if !ok { @@ -172,7 +181,7 @@ func initializeProducer() error { } } - permission := database.Permission{ + permission := model.Permission{ Name: permName, DisplayName: permData.String(), Action: permData.action, @@ -184,28 +193,28 @@ func initializeProducer() error { permissionsToCreate = append(permissionsToCreate, permission) } - if err := repository.BatchUpsertPermissions(tx, permissionsToCreate); err != nil { + if err := txStore.upsertPermissions(permissionsToCreate); err != nil { return fmt.Errorf("failed to create system permissions: %w", err) } - if err := repository.BatchUpsertRoles(tx, systemRoles); err != nil { + if err := txStore.upsertRoles(systemRoles); err != nil { return fmt.Errorf("failed to create system roles: %w", err) } - if err := assignSystemRolePermissions(tx); err != nil { + if err := assignSystemRolePermissions(txStore); err != nil { return fmt.Errorf("failed to assign system role permissions: %w", err) } - adminUser, err := initializeAdminUser(tx, initialData) + adminUser, err := initializeAdminUser(txStore, initialData) if err != nil { return fmt.Errorf("failed to initialize admin user: %w", err) } - if err := initializeProjectsAndTeams(tx, initialData); err != nil { + if err := initializeProjectsAndTeams(txStore, initialData); err != nil { return fmt.Errorf("failed to initialize admin user, projects and teams: %w", err) } - if err := initializeUsers(tx, initialData); err != nil { + if err := initializeUsers(txStore, initialData); err != nil { return fmt.Errorf("failed to initialize users: %w", err) } @@ -226,9 +235,9 @@ func initializeProducer() error { }) } -func assignSystemRolePermissions(tx *gorm.DB) error { +func assignSystemRolePermissions(store *bootstrapStore) error { for roleName, permissionRules := range consts.SystemRolePermissions { - role, err := repository.GetRoleByName(tx, roleName.String()) + role, err := store.getRoleByName(roleName.String()) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("role %s not found", roleName) @@ -237,20 +246,20 @@ func assignSystemRolePermissions(tx *gorm.DB) error { } if roleName == consts.RoleSuperAdmin { - permissions, err := repository.ListSystemPermissions(tx) + permissions, err := store.listSystemPermissions() if err != nil { return fmt.Errorf("failed to list system permissions: %w", err) } - var rolePermissions []database.RolePermission + var rolePermissions []model.RolePermission for _, perm := range permissions { - rolePermissions = append(rolePermissions, database.RolePermission{ + rolePermissions = append(rolePermissions, model.RolePermission{ RoleID: role.ID, PermissionID: perm.ID, }) } - if err := repository.BatchCreateRolePermissions(tx, rolePermissions); err != nil { + if err := store.createRolePermissions(rolePermissions); err != nil { return fmt.Errorf("failed to assign all permissions to super admin role: %w", err) } } else { @@ -259,20 +268,20 @@ func assignSystemRolePermissions(tx *gorm.DB) error { permissionStrs = append(permissionStrs, rule.String()) } - permissions, err := repository.ListPermissionsByNames(tx, permissionStrs) + permissions, err := store.listPermissionsByNames(permissionStrs) if err != nil { return fmt.Errorf("failed to list permissions for role %s: %w", roleName, err) } - var rolePermissions []database.RolePermission + var rolePermissions []model.RolePermission for _, perm := range permissions { - rolePermissions = append(rolePermissions, database.RolePermission{ + rolePermissions = append(rolePermissions, model.RolePermission{ RoleID: role.ID, PermissionID: perm.ID, }) } - if err := repository.BatchCreateRolePermissions(tx, rolePermissions); err != nil { + if err := store.createRolePermissions(rolePermissions); err != nil { return fmt.Errorf("failed to assign permissions to role %s: %w", roleName, err) } } @@ -281,16 +290,16 @@ func assignSystemRolePermissions(tx *gorm.DB) error { return nil } -func initializeAdminUser(tx *gorm.DB, data *InitialData) (*database.User, error) { +func initializeAdminUser(store *bootstrapStore, data *InitialData) (*model.User, error) { adminUser := data.AdminUser.ConvertToDBUser() - if err := repository.CreateUser(tx, adminUser); err != nil { + if err := store.createUser(adminUser); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { return nil, fmt.Errorf("admin user already exists") } return nil, fmt.Errorf("failed to create admin user: %w", err) } - superAdminRole, err := repository.GetRoleByName(tx, "super_admin") + superAdminRole, err := store.getRoleByName("super_admin") if err != nil { if errors.Is(err, consts.ErrNotFound) { return nil, fmt.Errorf("super_admin role not found, ensure system roles are initialized first") @@ -298,11 +307,11 @@ func initializeAdminUser(tx *gorm.DB, data *InitialData) (*database.User, error) return nil, fmt.Errorf("failed to get super_admin role: %w", err) } - userRole := database.UserRole{ + userRole := model.UserRole{ UserID: adminUser.ID, RoleID: superAdminRole.ID, } - if err := repository.CreateUserRole(tx, &userRole); err != nil { + if err := store.createUserRole(&userRole); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { return nil, fmt.Errorf("admin user already has super_admin role") } @@ -312,10 +321,10 @@ func initializeAdminUser(tx *gorm.DB, data *InitialData) (*database.User, error) return adminUser, nil } -func initializeProjectsAndTeams(tx *gorm.DB, data *InitialData) error { +func initializeProjectsAndTeams(store *bootstrapStore, data *InitialData) error { for _, teamData := range data.Teams { team := teamData.ConvertToDBTeam() - if err := repository.CreateTeam(tx, team); err != nil { + if err := store.createTeam(team); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { return fmt.Errorf("team %s already exists", team.Name) } @@ -325,7 +334,7 @@ func initializeProjectsAndTeams(tx *gorm.DB, data *InitialData) error { for _, projectData := range data.Projects { project := projectData.ConvertToDBProject() - if err := repository.CreateProject(tx, project); err != nil { + if err := store.createProject(project); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { return fmt.Errorf("project %s already exists", project.Name) } @@ -348,12 +357,12 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { } } - versions := make([]database.ContainerVersion, 0, len(containerData.Versions)) + versions := make([]model.ContainerVersion, 0, len(containerData.Versions)) for _, versionData := range containerData.Versions { version := versionData.ConvertToDBContainerVersion() if len(versionData.EnvVars) > 0 { - params := make([]database.ParameterConfig, 0, len(versionData.EnvVars)) + params := make([]model.ParameterConfig, 0, len(versionData.EnvVars)) for _, paramData := range versionData.EnvVars { param := paramData.ConvertToDBParameterConfig() params = append(params, *param) @@ -364,7 +373,7 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { if versionData.HelmConfig != nil { helmConfig := versionData.HelmConfig.ConvertToDBHelmConfig() if len(versionData.HelmConfig.Values) > 0 { - params := make([]database.ParameterConfig, 0, len(versionData.HelmConfig.Values)) + params := make([]model.ParameterConfig, 0, len(versionData.HelmConfig.Values)) for _, paramData := range versionData.HelmConfig.Values { param := paramData.ConvertToDBParameterConfig() params = append(params, *param) @@ -380,17 +389,16 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { container.Versions = versions - createdContainer, err := producer.CreateContainerCore(tx, container, userID) + createdContainer, err := containermodule.CreateContainerCore(tx, container, userID) if err != nil { return fmt.Errorf("failed to create container %s: %w", containerData.Name, err) } if createdContainer.Type == consts.ContainerTypePedestal { - if err := producer.UploadHemlValueFileCore( + if err := containermodule.UploadHelmValueFileFromPath( tx, containerData.Name, container.Versions[0].HelmConfig, - nil, filepath.Join(dataPath, fmt.Sprintf("%s.yaml", createdContainer.Name)), ); err != nil { return fmt.Errorf("failed to upload helm value file for container %s: %w", containerData.Name, err) @@ -405,13 +413,13 @@ func initializeDatasets(tx *gorm.DB, data *InitialData, userID int) error { for _, datasetData := range data.Datasets { dataset := datasetData.ConvertToDBDataset() - versions := make([]database.DatasetVersion, 0, len(datasetData.Versions)) + versions := make([]model.DatasetVersion, 0, len(datasetData.Versions)) for _, versionData := range datasetData.Versions { version := versionData.ConvertToDBDatasetVersion() versions = append(versions, *version) } - _, err := producer.CreateDatasetCore(tx, dataset, versions, userID) + _, err := datasetmodule.CreateDatasetCore(tx, dataset, versions, userID) if err != nil { return fmt.Errorf("failed to create dataset %s: %w", datasetData.Name, err) } @@ -430,7 +438,7 @@ func initializeExecutionLabels(tx *gorm.DB) error { } for _, labelInfo := range sourceLabels { - _, err := producer.CreateLabelCore(tx, &database.Label{ + _, err := labelmodule.CreateLabelCore(tx, &model.Label{ Key: consts.ExecutionLabelSource, Value: labelInfo.value, Category: consts.ExecutionCategory, @@ -445,12 +453,12 @@ func initializeExecutionLabels(tx *gorm.DB) error { return nil } -func initializeUsers(tx *gorm.DB, data *InitialData) error { +func initializeUsers(store *bootstrapStore, data *InitialData) error { if len(data.Users) == 0 { return nil } - role, err := repository.GetRoleByName(tx, consts.RoleUser.String()) + role, err := store.getRoleByName(consts.RoleUser.String()) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("user role not found, ensure system roles are initialized first") @@ -461,7 +469,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { for _, userData := range data.Users { user := userData.ConvertToDBUser() - if err := repository.CreateUser(tx, user); err != nil { + if err := store.createUser(user); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { logrus.Warnf("User %s already exists, skipping", user.Username) continue @@ -469,7 +477,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { return fmt.Errorf("failed to create user %s: %w", user.Username, err) } - if err := repository.CreateUserRole(tx, &database.UserRole{ + if err := store.createUserRole(&model.UserRole{ UserID: user.ID, RoleID: role.ID, }); err != nil { @@ -480,7 +488,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { if len(userData.Teams) > 0 { for _, teamBinding := range userData.Teams { // Get team by name - team, err := repository.GetTeamByName(tx, teamBinding.Name) + team, err := store.getTeamByName(teamBinding.Name) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("team %s not found for user %s", teamBinding.Name, user.Username) @@ -489,7 +497,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { } // Get role by name for user-team binding - teamRole, err := repository.GetRoleByName(tx, teamBinding.Role) + teamRole, err := store.getRoleByName(teamBinding.Role) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("role %s not found for user %s in team %s", teamBinding.Role, user.Username, teamBinding.Name) @@ -498,21 +506,19 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { } // Bind user to team with role - if err := repository.CreateUserTeam(tx, &database.UserTeam{ + if err := store.createUserTeam(&model.UserTeam{ UserID: user.ID, TeamID: team.ID, RoleID: teamRole.ID, Status: consts.CommonEnabled, }); err != nil { - if !errors.Is(err, consts.ErrAlreadyExists) { - return fmt.Errorf("failed to bind user %s to team %s with role %s: %w", user.Username, teamBinding.Name, teamBinding.Role, err) - } + return fmt.Errorf("failed to bind user %s to team %s with role %s: %w", user.Username, teamBinding.Name, teamBinding.Role, err) } // Bind projects to this team and user if specified if len(teamBinding.Projects) > 0 { for _, projectBinding := range teamBinding.Projects { - project, err := repository.GetProjectByName(tx, projectBinding.Name) + project, err := store.getProjectByName(projectBinding.Name) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("project %s not found for team %s", projectBinding.Name, teamBinding.Name) @@ -522,14 +528,14 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { // Update project's team_id to bind project to team project.TeamID = &team.ID - if err := repository.UpdateProject(tx, project); err != nil { + if err := store.saveProject(project); err != nil { return fmt.Errorf("failed to bind project %s to team %s: %w", projectBinding.Name, teamBinding.Name, err) } logrus.Infof("Bound project %s to team %s", projectBinding.Name, teamBinding.Name) // Get role for user-project binding - projectRole, err := repository.GetRoleByName(tx, projectBinding.Role) + projectRole, err := store.getRoleByName(projectBinding.Role) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("role %s not found for user %s in project %s", projectBinding.Role, user.Username, projectBinding.Name) @@ -538,15 +544,13 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { } // Bind user to project with role - if err := repository.CreateUserProject(tx, &database.UserProject{ + if err := store.createUserProject(&model.UserProject{ UserID: user.ID, ProjectID: project.ID, RoleID: projectRole.ID, Status: consts.CommonEnabled, }); err != nil { - if !errors.Is(err, consts.ErrAlreadyExists) { - return fmt.Errorf("failed to bind user %s to project %s with role %s: %w", user.Username, projectBinding.Name, projectBinding.Role, err) - } + return fmt.Errorf("failed to bind user %s to project %s with role %s: %w", user.Username, projectBinding.Name, projectBinding.Role, err) } } } @@ -559,7 +563,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { if len(userData.Projects) > 0 { for _, projectBinding := range userData.Projects { // Get project by name - project, err := repository.GetProjectByName(tx, projectBinding.Name) + project, err := store.getProjectByName(projectBinding.Name) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("project %s not found for user %s", projectBinding.Name, user.Username) @@ -568,7 +572,7 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { } // Get role by name - projectRole, err := repository.GetRoleByName(tx, projectBinding.Role) + projectRole, err := store.getRoleByName(projectBinding.Role) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("role %s not found for user %s in project %s", projectBinding.Role, user.Username, projectBinding.Name) @@ -577,15 +581,13 @@ func initializeUsers(tx *gorm.DB, data *InitialData) error { } // Bind user to project with role - if err := repository.CreateUserProject(tx, &database.UserProject{ + if err := store.createUserProject(&model.UserProject{ UserID: user.ID, ProjectID: project.ID, RoleID: projectRole.ID, Status: consts.CommonEnabled, }); err != nil { - if !errors.Is(err, consts.ErrAlreadyExists) { - return fmt.Errorf("failed to bind user %s to project %s with role %s: %w", user.Username, projectBinding.Name, projectBinding.Role, err) - } + return fmt.Errorf("failed to bind user %s to project %s with role %s: %w", user.Username, projectBinding.Name, projectBinding.Role, err) } } } diff --git a/src/service/initialization/systems.go b/src/service/initialization/systems.go index b4f03eec..0ee73f4d 100644 --- a/src/service/initialization/systems.go +++ b/src/service/initialization/systems.go @@ -3,9 +3,9 @@ package initialization import ( "aegis/config" "aegis/consts" - "aegis/database" - "aegis/repository" + "aegis/model" "aegis/service/common" + "fmt" chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" @@ -13,7 +13,7 @@ import ( ) // builtinSystems defines the 6 built-in systems that are seeded on startup. -var builtinSystems = []database.System{ +var builtinSystems = []model.System{ {Name: "train-ticket", DisplayName: "Train Ticket", NsPattern: `^ts\d+$`, ExtractPattern: `^(ts)(\d+)$`, Count: 1, IsBuiltin: true, Status: consts.CommonEnabled}, {Name: "sock-shop", DisplayName: "Sock Shop", NsPattern: `^ss\d+$`, ExtractPattern: `^(ss)(\d+)$`, Count: 1, IsBuiltin: true, Status: consts.CommonEnabled}, {Name: "social-network", DisplayName: "Social Network", NsPattern: `^sn\d+$`, ExtractPattern: `^(sn)(\d+)$`, Count: 1, IsBuiltin: true, Status: consts.CommonEnabled}, @@ -24,16 +24,16 @@ var builtinSystems = []database.System{ // InitializeSystems seeds built-in systems, registers all enabled systems with // chaos-experiment, and sets the global MetadataStore. -func InitializeSystems() { +func InitializeSystems(db *gorm.DB) error { // Set DB reference for ChaosSystemConfig to query System table - config.SetChaosConfigDB(database.DB) + config.SetChaosConfigDB(db) // Seed built-in systems using FirstOrCreate for _, sys := range builtinSystems { - var existing database.System - result := database.DB.Where("name = ?", sys.Name).First(&existing) + var existing model.System + result := db.Where("name = ?", sys.Name).First(&existing) if result.Error == gorm.ErrRecordNotFound { - if err := database.DB.Create(&sys).Error; err != nil { + if err := db.Create(&sys).Error; err != nil { logrus.Warnf("Failed to seed builtin system %s: %v", sys.Name, err) } else { logrus.Infof("Seeded builtin system: %s", sys.Name) @@ -42,10 +42,9 @@ func InitializeSystems() { } // Load all enabled systems from DB and register with chaos-experiment - systems, err := repository.ListEnabledSystems(database.DB) + systems, err := newBootstrapStore(db).listEnabledSystems() if err != nil { - logrus.Errorf("Failed to load enabled systems: %v", err) - return + return fmt.Errorf("failed to load enabled systems: %w", err) } for _, sys := range systems { @@ -61,7 +60,8 @@ func InitializeSystems() { } // Create and set the global MetadataStore - store := common.NewDBMetadataStore() + store := common.NewDBMetadataStore(db) chaos.SetMetadataStore(store) logrus.Info("Set global DBMetadataStore for chaos-experiment") + return nil } diff --git a/src/service/initialization/types.go b/src/service/initialization/types.go index 27d13c89..4bbc7458 100644 --- a/src/service/initialization/types.go +++ b/src/service/initialization/types.go @@ -2,10 +2,8 @@ package initialization import ( "aegis/consts" - "aegis/database" - "aegis/repository" - - "github.com/sirupsen/logrus" + "aegis/model" + "gorm.io/gorm" ) const AdminUsername = "admin" @@ -24,8 +22,8 @@ type InitialDynamicConfig struct { Options string `yaml:"options"` } -func (c *InitialDynamicConfig) ConvertToDBDynamicConfig() *database.DynamicConfig { - return &database.DynamicConfig{ +func (c *InitialDynamicConfig) ConvertToDBDynamicConfig() *model.DynamicConfig { + return &model.DynamicConfig{ Key: c.Key, DefaultValue: c.DefaultValue, ValueType: c.ValueType, @@ -48,8 +46,8 @@ type InitialDataContainer struct { Versions []InitialContainerVersion `yaml:"versions"` } -func (c *InitialDataContainer) ConvertToDBContainer() *database.Container { - return &database.Container{ +func (c *InitialDataContainer) ConvertToDBContainer() *model.Container { + return &model.Container{ Type: c.Type, Name: c.Name, IsPublic: c.IsPublic, @@ -67,8 +65,8 @@ type InitialContainerVersion struct { HelmConfig *InitialHelmConfig `yaml:"helm_config"` } -func (cv *InitialContainerVersion) ConvertToDBContainerVersion() *database.ContainerVersion { - return &database.ContainerVersion{ +func (cv *InitialContainerVersion) ConvertToDBContainerVersion() *model.ContainerVersion { + return &model.ContainerVersion{ Name: cv.Name, GithubLink: cv.GithubLink, ImageRef: cv.ImageRef, @@ -85,8 +83,8 @@ type InitialHelmConfig struct { Values []InitialParameterConfig `yaml:"values"` } -func (hc *InitialHelmConfig) ConvertToDBHelmConfig() *database.HelmConfig { - return &database.HelmConfig{ +func (hc *InitialHelmConfig) ConvertToDBHelmConfig() *model.HelmConfig { + return &model.HelmConfig{ Version: hc.Version, ChartName: hc.ChartName, RepoName: hc.RepoName, @@ -105,8 +103,8 @@ type InitialParameterConfig struct { Overridable *bool `yaml:"overridable"` } -func (pc *InitialParameterConfig) ConvertToDBParameterConfig() *database.ParameterConfig { - config := &database.ParameterConfig{ +func (pc *InitialParameterConfig) ConvertToDBParameterConfig() *model.ParameterConfig { + config := &model.ParameterConfig{ Key: pc.Key, Type: pc.Type, Category: pc.Category, @@ -133,8 +131,8 @@ type InitialDatasaet struct { Versions []InitialDatasetVersion `yaml:"versions"` } -func (d *InitialDatasaet) ConvertToDBDataset() *database.Dataset { - return &database.Dataset{ +func (d *InitialDatasaet) ConvertToDBDataset() *model.Dataset { + return &model.Dataset{ Name: d.Name, Type: d.Type, Description: d.Description, @@ -148,8 +146,8 @@ type InitialDatasetVersion struct { Status consts.StatusType `yaml:"status"` } -func (dv *InitialDatasetVersion) ConvertToDBDatasetVersion() *database.DatasetVersion { - return &database.DatasetVersion{ +func (dv *InitialDatasetVersion) ConvertToDBDatasetVersion() *model.DatasetVersion { + return &model.DatasetVersion{ Name: dv.Name, Status: dv.Status, } @@ -161,8 +159,8 @@ type InitialDataProject struct { Status consts.StatusType `yaml:"status"` } -func (p *InitialDataProject) ConvertToDBProject() *database.Project { - return &database.Project{ +func (p *InitialDataProject) ConvertToDBProject() *model.Project { + return &model.Project{ Name: p.Name, Description: p.Description, Status: p.Status, @@ -176,8 +174,8 @@ type InitialDataTeam struct { Status consts.StatusType `yaml:"status"` } -func (t *InitialDataTeam) ConvertToDBTeam() *database.Team { - return &database.Team{ +func (t *InitialDataTeam) ConvertToDBTeam() *model.Team { + return &model.Team{ Name: t.Name, Description: t.Description, IsPublic: t.IsPublic, @@ -207,8 +205,8 @@ type InitialDataUser struct { Teams []InitialUserTeam `yaml:"teams"` } -func (u *InitialDataUser) ConvertToDBUser() *database.User { - return &database.User{ +func (u *InitialDataUser) ConvertToDBUser() *model.User { + return &model.User{ Username: u.Username, Email: u.Email, Password: u.Password, @@ -230,17 +228,17 @@ type InitialData struct { type configData struct { scope consts.ConfigScope - configs []database.DynamicConfig + configs []model.DynamicConfig } -func newConfigData(scope consts.ConfigScope) *configData { - configs, err := repository.ListExistingConfigs(database.DB) +func newConfigDataWithDB(db *gorm.DB, scope consts.ConfigScope) (*configData, error) { + configs, err := newBootstrapStore(db).listExistingConfigs() if err != nil { - logrus.Fatalf("Failed to check existing dynamic configs: %v", err) + return nil, err } return &configData{ scope: scope, configs: configs, - } + }, nil } diff --git a/src/service/initialization/utils.go b/src/service/initialization/utils.go index 2a1693d3..6e07309d 100644 --- a/src/service/initialization/utils.go +++ b/src/service/initialization/utils.go @@ -6,10 +6,9 @@ import ( "os" "path/filepath" - "aegis/database" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert/yaml" + "gorm.io/gorm" ) func loadInitialDataFromFile(filePath string) (*InitialData, error) { @@ -35,19 +34,19 @@ func loadInitialDataFromFile(filePath string) (*InitialData, error) { return &initialData, nil } -func withOptimizedDBSettings(fn func() error) error { - if err := database.DB.Exec("SET FOREIGN_KEY_CHECKS=0").Error; err != nil { +func withOptimizedDBSettings(db *gorm.DB, fn func() error) error { + if err := db.Exec("SET FOREIGN_KEY_CHECKS=0").Error; err != nil { logrus.Warnf("Failed to disable foreign key checks: %v", err) } - if err := database.DB.Exec("SET UNIQUE_CHECKS=0").Error; err != nil { + if err := db.Exec("SET UNIQUE_CHECKS=0").Error; err != nil { logrus.Warnf("Failed to disable unique checks: %v", err) } defer func() { - if err := database.DB.Exec("SET FOREIGN_KEY_CHECKS=1").Error; err != nil { + if err := db.Exec("SET FOREIGN_KEY_CHECKS=1").Error; err != nil { logrus.Errorf("Failed to re-enable foreign key checks: %v", err) } - if err := database.DB.Exec("SET UNIQUE_CHECKS=1").Error; err != nil { + if err := db.Exec("SET UNIQUE_CHECKS=1").Error; err != nil { logrus.Errorf("Failed to re-enable unique checks: %v", err) } }() diff --git a/src/service/logreceiver/receiver.go b/src/service/logreceiver/receiver.go index 732570f6..032299d1 100644 --- a/src/service/logreceiver/receiver.go +++ b/src/service/logreceiver/receiver.go @@ -11,7 +11,6 @@ import ( "sync/atomic" "time" - "aegis/client" "aegis/dto" "github.com/sirupsen/logrus" @@ -40,6 +39,7 @@ type OTLPLogReceiver struct { port int maxRequestSize int64 shutdownCh chan struct{} + publisher logPublisher // Metrics receivedTotal atomic.Int64 @@ -47,8 +47,12 @@ type OTLPLogReceiver struct { errorsTotal atomic.Int64 } +type logPublisher interface { + Publish(ctx context.Context, channel string, message any) error +} + // NewOTLPLogReceiver creates a new OTLP log receiver -func NewOTLPLogReceiver(port int, maxRequestSize int64) *OTLPLogReceiver { +func NewOTLPLogReceiver(port int, maxRequestSize int64, publisher logPublisher) *OTLPLogReceiver { if port == 0 { port = DefaultPort } @@ -60,6 +64,7 @@ func NewOTLPLogReceiver(port int, maxRequestSize int64) *OTLPLogReceiver { port: port, maxRequestSize: maxRequestSize, shutdownCh: make(chan struct{}), + publisher: publisher, } } @@ -84,7 +89,7 @@ func (r *OTLPLogReceiver) Start(ctx context.Context) error { case <-ctx.Done(): case <-r.shutdownCh: } - shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) defer cancel() if err := r.server.Shutdown(shutdownCtx); err != nil { logrus.Errorf("OTLP log receiver shutdown error: %v", err) @@ -245,7 +250,10 @@ func (r *OTLPLogReceiver) parseJSONRequest(body []byte, exportReq *collogspb.Exp // publishLogEntry publishes a log entry to Redis Pub/Sub channel keyed by task_id func (r *OTLPLogReceiver) publishLogEntry(ctx context.Context, entry dto.LogEntry) error { channel := fmt.Sprintf("%s:%s", PubSubChannelPrefix, entry.TaskID) - return client.RedisPublish(ctx, channel, entry) + if r.publisher == nil { + return fmt.Errorf("log publisher not initialized") + } + return r.publisher.Publish(ctx, channel, entry) } // parseResourceLog parses a single ResourceLog from JSON diff --git a/src/service/producer/audit.go b/src/service/producer/audit.go deleted file mode 100644 index 74c1b764..00000000 --- a/src/service/producer/audit.go +++ /dev/null @@ -1,150 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// GetAuditLogDetail retrieves detailed information about a specific audit log by ID -func GetAuditLogDetail(id int) (*dto.AuditLogDetailResp, error) { - log, err := repository.GetAuditLogByID(database.DB, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: audit log with ID %d not found", consts.ErrNotFound, id) - } - return nil, fmt.Errorf("failed to get audit log: %w", err) - } - - return dto.NewAuditLogDetailResp(log), nil -} - -// ListAuditLogs retrieves audit logs with pagination and filtering -func ListAuditLogs(req *dto.ListAuditLogReq) (*dto.ListResp[dto.AuditLogResp], error) { - limit, offset := req.ToGormParams() - filterOptions := req.ToFilterOptions() - - logs, total, err := repository.ListAuditLogs(database.DB, limit, offset, filterOptions) - if err != nil { - return nil, fmt.Errorf("failed to list audit logs: %w", err) - } - - logResps := make([]dto.AuditLogResp, 0, len(logs)) - for i := range logs { - logResps = append(logResps, *dto.NewAuditLogResp(&logs[i])) - } - - resp := dto.ListResp[dto.AuditLogResp]{ - Items: logResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// LogFailedAction logs a failed action with error message -func LogFailedAction(ipAddress, userAgent, action, errorMsg string, duration, userID int, resourceName consts.ResourceName) error { - if resourceName == "" { - return fmt.Errorf("resource name cannot be empty") - } - - log := &database.AuditLog{ - IPAddress: ipAddress, - UserAgent: userAgent, - Duration: duration, - Action: action, - ErrorMsg: errorMsg, - UserID: userID, - State: consts.AuditLogStateFailed, - Status: consts.CommonEnabled, - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - resource, err := repository.GetResourceByName(tx, resourceName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: resource %s not found", consts.ErrNotFound, resourceName) - } - return fmt.Errorf("failed to get resource: %w", err) - } - - log.ResourceID = resource.ID - - if err := repository.CreateAuditLog(tx, log); err != nil { - return fmt.Errorf("failed to log failed action: %w", err) - } - return nil - }) -} - -// LogSystemAction logs a system action (no user involved) -func LogSystemAction(action, details string, resourceName consts.ResourceName) error { - if resourceName == "" { - return fmt.Errorf("resource name cannot be empty") - } - - log := &database.AuditLog{ - IPAddress: "127.0.0.1", - UserAgent: "SYSTEM", - Action: action, - Details: details, - State: consts.AuditLogStateSuccess, - Status: consts.CommonEnabled, - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - resource, err := repository.GetResourceByName(tx, resourceName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: resource %s not found", consts.ErrNotFound, resourceName) - } - return fmt.Errorf("failed to get resource: %w", err) - } - - log.ResourceID = resource.ID - - if err := repository.CreateAuditLog(tx, log); err != nil { - return fmt.Errorf("failed to log system action: %w", err) - } - return nil - }) -} - -// LogUserAction logs an action performed by a user -func LogUserAction(ipAddress, userAgent, action, details string, duration, userID int, resourceName consts.ResourceName) error { - if resourceName == "" { - return fmt.Errorf("resource name cannot be empty") - } - - log := &database.AuditLog{ - IPAddress: ipAddress, - UserAgent: userAgent, - Duration: duration, - Action: action, - Details: details, - UserID: userID, - State: consts.AuditLogStateSuccess, - Status: consts.CommonEnabled, - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - resource, err := repository.GetResourceByName(tx, resourceName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: resource %s not found", consts.ErrNotFound, resourceName) - } - return fmt.Errorf("failed to get resource: %w", err) - } - - log.ResourceID = resource.ID - - if err := repository.CreateAuditLog(tx, log); err != nil { - return fmt.Errorf("failed to log user action: %w", err) - } - return nil - }) -} diff --git a/src/service/producer/auth.go b/src/service/producer/auth.go deleted file mode 100644 index 7aa2c681..00000000 --- a/src/service/producer/auth.go +++ /dev/null @@ -1,286 +0,0 @@ -package producer - -import ( - "context" - "errors" - "fmt" - "time" - - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/utils" - - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// Register handles user registration business logic -func Register(req *dto.RegisterReq) (*dto.UserInfo, error) { - if req == nil { - return nil, fmt.Errorf("register request is nil") - } - - var createdUser *database.User - - err := database.DB.Transaction(func(tx *gorm.DB) error { - // Check if user already exists - if _, err := repository.GetUserByUsername(tx, req.Username); err == nil { - return fmt.Errorf("%w: username is already taken", consts.ErrAlreadyExists) - } - - if _, err := repository.GetUserByEmail(tx, req.Email); err == nil { - return fmt.Errorf("%w: email is already registered", consts.ErrAlreadyExists) - } - - // Hash password - hashedPassword, err := utils.HashPassword(req.Password) - if err != nil { - return fmt.Errorf("password hashing failed: %w", err) - } - - user := &database.User{ - Username: req.Username, - Email: req.Email, - Password: hashedPassword, - IsActive: true, - Status: consts.CommonEnabled, - } - - if err := repository.CreateUser(tx, user); err != nil { - return fmt.Errorf("failed to create user: %w", err) - } - - createdUser = user - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewUserInfo(createdUser), nil -} - -// Login handles user authentication business logic -func Login(req *dto.LoginReq) (*dto.LoginResp, error) { - if req == nil { - return nil, fmt.Errorf("login request is nil") - } - - var loginedUser *database.User - var token string - var expiresAt time.Time - - err := database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByUsername(tx, req.Username) - if err != nil { - return fmt.Errorf("%w: invalid username or password", consts.ErrAuthenticationFailed) - } - - if !utils.VerifyPassword(req.Password, user.Password) { - return fmt.Errorf("%w: invalid username or password", consts.ErrAuthenticationFailed) - } - - // Generate token with user roles - token, expiresAt, err = generateTokenWithRoles(tx, user) - if err != nil { - return err - } - - if err := repository.UpdateUserLoginTime(tx, user.ID); err != nil { - logrus.Errorf("failed to update last login time for user %d: %v", user.ID, err) - } - - loginedUser = user - return nil - }) - if err != nil { - return nil, err - } - - roles, err := repository.ListRolesByUserID(database.DB, loginedUser.ID) - if err != nil { - return nil, fmt.Errorf("failed to get user role: %w", err) - } - - if len(roles) == 0 { - return nil, fmt.Errorf("%w: user has no assigned role", consts.ErrPermissionDenied) - } - - info := dto.NewUserInfo(loginedUser) - info.Role = roles[0].Name - - resp := &dto.LoginResp{ - Token: token, - ExpiresAt: expiresAt, - User: *info, - } - return resp, nil -} - -// Logout handles user logout business logic -func Logout(ctx context.Context, claims *utils.Claims) error { - metaData := map[string]any{ - "user_id": claims.UserID, - "reason": "User logout", - } - if err := repository.AddTokenToBlacklist(ctx, claims.ID, claims.ExpiresAt.Time, metaData); err != nil { - logrus.Errorf("failed to add token to blacklist: %v", err) - return fmt.Errorf("failed to blacklist token: %w", err) - } - return nil -} - -// RefreshToken handles JWT token refresh business logic -func RefreshToken(req *dto.TokenRefreshReq) (*dto.TokenRefreshResp, error) { - if req == nil { - return nil, fmt.Errorf("token refresh request is nil") - } - - // Validate refresh token and get user info - refreshClaims, err := utils.ValidateToken(req.Token) - if err != nil { - return nil, fmt.Errorf("token refresh failed: %w", err) - } - - // Fetch fresh user data from database - user, err := repository.GetUserByID(database.DB, refreshClaims.UserID) - if err != nil { - return nil, fmt.Errorf("user not found: %w", err) - } - - // Generate new access token with fresh user data - newToken, expiresAt, err := generateTokenWithRoles(database.DB, user) - if err != nil { - return nil, err - } - - response := &dto.TokenRefreshResp{ - Token: newToken, - ExpiresAt: expiresAt, - } - - return response, nil -} - -// ChangePassword handles password change business logic -func ChangePassword(req *dto.ChangePasswordReq, userID int) error { - if req == nil { - return fmt.Errorf("change password request is nil") - } - - err := database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return fmt.Errorf("failed to get user: %w", err) - } - - if !utils.VerifyPassword(req.OldPassword, user.Password) { - return fmt.Errorf("invalid old password") - } - - hashedPassword, err := utils.HashPassword(req.NewPassword) - if err != nil { - return fmt.Errorf("password hashing failed: %w", err) - } - user.Password = hashedPassword - - if err := repository.UpdateUser(tx, user); err != nil { - return fmt.Errorf("failed to update password: %w", err) - } - - return nil - }) - - return err -} - -// GetProfile handles getting current user profile business logic -func GetProfile(userID int) (*dto.UserProfileResp, error) { - user, err := repository.GetUserByID(database.DB, userID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return nil, fmt.Errorf("failed to get user: %w", err) - } - - resp := dto.NewUserProfileResp(user) - userContainers, userDatasets, userProjects, err := getAllUserResourceRoles(userID) - if err != nil { - return nil, fmt.Errorf("failed to get user resource roles: %w", err) - } - - resp.ContainerRoles = userContainers - resp.DatasetRoles = userDatasets - resp.ProjectRoles = userProjects - - return resp, nil -} - -// getAllUserResourceRoles fetches all container, dataset, project roles assigned to the user -func getAllUserResourceRoles(userID int) ([]dto.UserContainerInfo, []dto.UserDatasetInfo, []dto.UserProjectInfo, error) { - userContainers, err := repository.ListUserContainersByUserID(database.DB, userID) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to list user-container roles: %w", err) - } - var containerRoles []dto.UserContainerInfo - for _, uc := range userContainers { - containerRoles = append(containerRoles, *dto.NewUserContainerInfo(&uc)) - } - - userDatasets, err := repository.ListUserDatasetsByUserID(database.DB, userID) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to list user-dataset roles: %w", err) - } - var datasetRoles []dto.UserDatasetInfo - for _, ud := range userDatasets { - datasetRoles = append(datasetRoles, *dto.NewUserDatasetInfo(&ud)) - } - - userProjects, err := repository.ListUserProjectsByUserID(database.DB, userID) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to list user-project roles: %w", err) - } - var projectRoles []dto.UserProjectInfo - for _, up := range userProjects { - projectRoles = append(projectRoles, *dto.NewUserProjectInfo(&up)) - } - - return containerRoles, datasetRoles, projectRoles, nil -} - -// ============================================================================ -// Helper Functions -// ============================================================================ - -// generateTokenWithRoles fetches user roles and generates a JWT token with role information -func generateTokenWithRoles(db *gorm.DB, user *database.User) (string, time.Time, error) { - // Get user's global roles - roles, err := repository.ListRolesByUserID(db, user.ID) - if err != nil { - return "", time.Time{}, fmt.Errorf("failed to get user roles: %w", err) - } - - // Check if user is system admin and build role names list - isAdmin := false - roleNames := make([]string, 0, len(roles)) - for _, role := range roles { - roleNames = append(roleNames, role.Name) - if role.Name == string(consts.RoleSuperAdmin) || role.Name == string(consts.RoleAdmin) { - isAdmin = true - } - } - - // Generate token with role information - token, expiresAt, err := utils.GenerateToken(user.ID, user.Username, user.Email, user.IsActive, isAdmin, roleNames) - if err != nil { - return "", time.Time{}, fmt.Errorf("failed to generate token: %w", err) - } - - return token, expiresAt, nil -} diff --git a/src/service/producer/container.go b/src/service/producer/container.go deleted file mode 100644 index ed416d75..00000000 --- a/src/service/producer/container.go +++ /dev/null @@ -1,861 +0,0 @@ -package producer - -import ( - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - "context" - "errors" - "fmt" - "mime/multipart" - "os" - "os/exec" - "path/filepath" - "time" - - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// ===================================================================== -// Container Service Layer -// ===================================================================== - -// CreateContainer handles the atomic creation of a new container resource, -// including its initial versions and assigning the creating user as container administrator -func CreateContainer(req *dto.CreateContainerReq, userID int) (*dto.ContainerResp, error) { - if req == nil { - return nil, fmt.Errorf("request cannot be nil") - } - - container := req.ConvertToContainer() - - var createdContainer *database.Container - err := database.DB.Transaction(func(tx *gorm.DB) error { - container, err := CreateContainerCore(tx, container, userID) - - if err != nil { - return fmt.Errorf("failed to create container: %w", err) - } - - createdContainer = container - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to create container: %w", err) - } - - return dto.NewContainerResp(createdContainer), nil -} - -// CreateContainerCore performs the core logic of creating a container within a transaction -func CreateContainerCore(tx *gorm.DB, container *database.Container, userID int) (*database.Container, error) { - role, err := repository.GetRoleByName(tx, consts.RoleContainerAdmin.String()) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: role %v not found", err, consts.RoleContainerAdmin) - } - return nil, fmt.Errorf("failed to get project owner role: %w", err) - } - - if err := repository.CreateContainer(tx, container); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return nil, consts.ErrAlreadyExists - } - return nil, err - } - - if err := repository.CreateUserContainer(tx, &database.UserContainer{ - UserID: userID, - ContainerID: container.ID, - RoleID: role.ID, - Status: consts.CommonEnabled, - }); err != nil { - return nil, fmt.Errorf("failed to associate container with user: %w", err) - } - - if len(container.Versions) > 0 { - for i := range container.Versions { - container.Versions[i].ContainerID = container.ID - container.Versions[i].UserID = userID - } - - _, err = createContainerVersionsCore(tx, container.Versions) - if err != nil { - return nil, fmt.Errorf("failed to create container versions: %w", err) - } - } - - return container, nil -} - -// DeleteContainer deletes an existing container (Service Layer) -func DeleteContainer(containerID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - if _, err := repository.BatchDeleteContainerVersions(tx, containerID); err != nil { - return fmt.Errorf("failed to delete container versions: %w", err) - } - - if _, err := repository.RemoveUsersFromContainer(tx, containerID); err != nil { - return fmt.Errorf("failed to remove all users from container: %w", err) - } - - if err := repository.ClearContainerLabels(tx, []int{containerID}, nil); err != nil { - return fmt.Errorf("failed to clear container labels: %w", err) - } - - rows, err := repository.DeleteContainer(tx, containerID) - if err != nil { - return fmt.Errorf("failed to delete container: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: container id %d not found", consts.ErrNotFound, containerID) - } - - return nil - }) -} - -// GetContainerDetail retrieves detailed information about a specific container, -// including its versions and associated Helm configurations -func GetContainerDetail(containerID int) (*dto.ContainerDetailResp, error) { - container, err := repository.GetContainerByID(database.DB, containerID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) - } - return nil, fmt.Errorf("failed to get container: %w", err) - } - - versions, err := repository.ListContainerVersionsByContainerID(database.DB, container.ID) - if err != nil { - return nil, fmt.Errorf("failed to get container versions: %w", err) - } - - resp := dto.NewContainerDetailResp(container) - for _, version := range versions { - resp.Versions = append(resp.Versions, *dto.NewContainerVersionResp(&version)) - } - - return resp, nil -} - -// ListContainers lists containers based on the provided filters -func ListContainers(req *dto.ListContainerReq) (*dto.ListResp[dto.ContainerResp], error) { - limit, offset := req.ToGormParams() - - containers, total, err := repository.ListContainers(database.DB, limit, offset, req.Type, req.IsPublic, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list containers: %w", err) - } - - containerIDs := make([]int, 0, len(containers)) - for _, c := range containers { - containerIDs = append(containerIDs, c.ID) - } - - labelsMap, err := repository.ListContainerLabels(database.DB, containerIDs) - if err != nil { - return nil, fmt.Errorf("failed to list container labels: %w", err) - } - - containerResps := make([]dto.ContainerResp, 0, len(containers)) - for _, container := range containers { - if labels, exists := labelsMap[container.ID]; exists { - container.Labels = labels - } - containerResps = append(containerResps, *dto.NewContainerResp(&container)) - } - - resp := dto.ListResp[dto.ContainerResp]{ - Items: containerResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateContainer updates an existing container's details -func UpdateContainer(req *dto.UpdateContainerReq, containerID int) (*dto.ContainerResp, error) { - var updatedContainer *database.Container - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingContainer, err := repository.GetContainerByID(tx, containerID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: container with id %d not found", consts.ErrNotFound, containerID) - } - } - - req.PatchContainerModel(existingContainer) - - if err := repository.UpdateContainer(tx, existingContainer); err != nil { - return fmt.Errorf("failed to update container: %w", err) - } - - updatedContainer = existingContainer - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewContainerResp(updatedContainer), nil -} - -// ===================== ContainerLabel ===================== - -// ManageContainerLabels handles adding and removing labels for a container -func ManageContainerLabels(req *dto.ManageContainerLabelReq, containerID int) (*dto.ContainerResp, error) { - if req == nil { - return nil, fmt.Errorf("request cannot be nil") - } - - var managedContainer *database.Container - err := database.DB.Transaction(func(tx *gorm.DB) error { - container, err := repository.GetContainerByID(tx, containerID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: container not found", consts.ErrNotFound) - } - return err - } - - // Add labels - if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - containerLabels := make([]database.ContainerLabel, 0, len(labels)) - for _, label := range labels { - containerLabels = append(containerLabels, database.ContainerLabel{ - ContainerID: containerID, - LabelID: label.ID, - }) - } - - if err := repository.AddContainerLabels(tx, containerLabels); err != nil { - return fmt.Errorf("failed to add container labels: %w", err) - } - } - - // Remove labels - if len(req.RemoveLabels) > 0 { - labelIDs, err := repository.ListLabelIDsByKeyAndContainerID(tx, containerID, req.RemoveLabels) - if err != nil { - return fmt.Errorf("failed to find label IDs: %w", err) - } - - if len(labelIDs) == 0 { - return nil - } - - if err := repository.ClearContainerLabels(tx, []int{containerID}, labelIDs); err != nil { - return fmt.Errorf("failed to delete container-label associations: %w", err) - } - - if err := repository.BatchDecreaseLabelUsages(tx, labelIDs, 1); err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) - } - } - - labels, err := repository.ListLabelsByContainerID(database.DB, container.ID) - if err != nil { - return fmt.Errorf("failed to get container labels: %w", err) - } - - container.Labels = labels - managedContainer = container - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewContainerResp(managedContainer), nil -} - -// ===================================================================== -// ContainerVersion Service Layer -// ===================================================================== - -// CreateContainerVersion creates a new version for an existing container -func CreateContainerVersion(req *dto.CreateContainerVersionReq, containerID, userID int) (*dto.ContainerVersionResp, error) { - if req == nil { - return nil, fmt.Errorf("create container version request is nil") - } - - version := req.ConvertToContainerVersion() - version.ContainerID = containerID - version.UserID = userID - - var createdVersion *database.ContainerVersion - err := database.DB.Transaction(func(tx *gorm.DB) error { - versions, err := createContainerVersionsCore(tx, []database.ContainerVersion{*version}) - if err != nil { - return fmt.Errorf("failed to create container version: %w", err) - } - - createdVersion = &versions[0] - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to create container version: %w", err) - } - - return dto.NewContainerVersionResp(createdVersion), nil -} - -// DeleteContainerVersion deletes a specific version of a container -func DeleteContainerVersion(versionID int) error { - rows, err := repository.DeleteContainer(database.DB, versionID) - if err != nil { - return fmt.Errorf("failed to delete container version: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: container version id %d not found", consts.ErrNotFound, versionID) - } - return nil -} - -// GetContainerVersionDetail retrieves detailed information about a specific container version, -// including its Helm configuration if available -func GetContainerVersionDetail(containerID, versionID int) (*dto.ContainerVersionDetailResp, error) { - _, err := repository.GetContainerByID(database.DB, containerID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) - } - return nil, fmt.Errorf("failed to get container: %w", err) - } - - version, err := repository.GetContainerVersionByID(database.DB, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) - } - return nil, fmt.Errorf("failed to get container version: %w", err) - } - - resp := dto.NewContainerVersionDetailResp(version) - - helmConfig, err := repository.GetHelmConfigByContainerVersionID(database.DB, version.ID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("failed to get helm config: %w", err) - } - if helmConfig != nil { - helmConfigResp, err := dto.NewHelmConfigDetailResp(helmConfig) - if err != nil { - return nil, fmt.Errorf("failed to convert helm config: %w", err) - } - resp.HelmConfig = helmConfigResp - } - - return resp, nil -} - -// ListContainerVersions lists container versions with pagination and optional status filtering -func ListContainerVersions(req *dto.ListContainerVersionReq, containerID int) (*dto.ListResp[dto.ContainerVersionResp], error) { - limit, offset := req.ToGormParams() - - versions, total, err := repository.ListContainerVersions(database.DB, limit, offset, containerID, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list container versions: %w", err) - } - - versionResps := make([]dto.ContainerVersionResp, len(versions)) - for i, v := range versions { - versionResps[i] = *dto.NewContainerVersionResp(&v) - } - - resp := dto.ListResp[dto.ContainerVersionResp]{ - Items: versionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateContainerVersion updates an existing container version's details -func UpdateContainerVersion(req *dto.UpdateContainerVersionReq, containerID, versionID int) (*dto.ContainerVersionResp, error) { - var updatedVersion *database.ContainerVersion - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingVersion, err := repository.GetContainerVersionByID(tx, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) - } - return fmt.Errorf("failed to get container: %w", err) - } - - req.PatchContainerVersionModel(existingVersion) - if err := repository.UpdateContainerVersion(tx, existingVersion); err != nil { - return fmt.Errorf("failed to update container: %w", err) - } - - updatedVersion = existingVersion - - if req.HelmConfigRequest != nil { - existingHelmConfig, err := repository.GetHelmConfigByContainerVersionID(tx, existingVersion.ID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("failed to get helm config: %w", err) - } - - if err := req.HelmConfigRequest.PatchHelmConfigModel(existingHelmConfig); err != nil { - return fmt.Errorf("failed to patch helm config model: %w", err) - } - if err := repository.UpdateHelmConfig(tx, existingHelmConfig); err != nil { - return fmt.Errorf("failed to update helm config: %w", err) - } - } - - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewContainerVersionResp(updatedVersion), nil -} - -// UploadHelmChart handles uploading a Helm chart package to local storage -func UploadHelmChart(fileHeader *multipart.FileHeader, containerID, versionID, userID int) (*dto.UploadHelmChartResp, error) { - filename := fileHeader.Filename - - containerVersion, err := validateHelmConfigVersion(containerID, versionID) - if err != nil { - return nil, err - } - - // Get JuiceFS base path - jfsBasePath := config.GetString("jfs.dataset_path") - if jfsBasePath == "" { - return nil, fmt.Errorf("jfs.dataset_path is not configured") - } - - // Create directory: {jfs.dataset_path}/helm-charts - targetDir := filepath.Join(jfsBasePath, "helm-charts") - if err := os.MkdirAll(targetDir, 0755); err != nil { - return nil, fmt.Errorf("failed to create directory: %w", err) - } - - // Generate target filename with timestamp - timestamp := time.Now().Unix() - ext := filepath.Ext(filename) - targetFilename := fmt.Sprintf("%s_chart_%d%s", containerVersion.Container.Name, timestamp, ext) - targetPath := filepath.Join(targetDir, targetFilename) - - // Save the uploaded file - if err := utils.CopyFileFromFileHeader(fileHeader, targetPath); err != nil { - return nil, fmt.Errorf("failed to save chart file: %w", err) - } - - // Calculate SHA256 checksum - checksum, err := utils.CalculateFileSHA256(targetPath) - if err != nil { - logrus.WithField("file_path", targetPath).Warnf("failed to calculate checksum: %v", err) - checksum = "" - } - - logrus.WithFields(logrus.Fields{ - "file_path": targetPath, - "checksum": checksum, - }).Info("Helm chart package uploaded successfully") - - // Update HelmConfig with local path and checksum - containerVersion.HelmConfig.LocalPath = targetPath - containerVersion.HelmConfig.Checksum = checksum - if err := repository.UpdateHelmConfig(database.DB, containerVersion.HelmConfig); err != nil { - return nil, fmt.Errorf("failed to update helm config: %w", err) - } - - return &dto.UploadHelmChartResp{ - FilePath: targetPath, - FileName: filename, - Checksum: checksum, - }, nil -} - -// UploadHelmValueFile handles uploading a Helm values file to JuiceFS storage -func UploadHelmValueFile(fileHeader *multipart.FileHeader, containerID, versionID, userID int) (*dto.UploadHelmValueFileResp, error) { - filename := fileHeader.Filename - - containerVersion, err := validateHelmConfigVersion(containerID, versionID) - if err != nil { - return nil, err - } - - if err := UploadHemlValueFileCore(database.DB, containerVersion.Container.Name, containerVersion.HelmConfig, fileHeader, ""); err != nil { - return nil, fmt.Errorf("failed to upload helm value file: %w", err) - } - - return &dto.UploadHelmValueFileResp{ - FilePath: containerVersion.HelmConfig.ValueFile, - FileName: filename, - }, nil -} - -// UploadHemlValueFileCore handles the core logic of uploading a Helm values file to JuiceFS storage -func UploadHemlValueFileCore(db *gorm.DB, containerName string, helmConfig *database.HelmConfig, srcFileHeader *multipart.FileHeader, srcFilePath string) error { - jfsBasePath := config.GetString("jfs.dataset_path") - if jfsBasePath == "" { - return fmt.Errorf("jfs.dataset_path is not configured") - } - - // Create directory structure: {jfs.dataset_path}/helm-values - targetDir := filepath.Join(jfsBasePath, "helm-values") - if err := os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - timestamp := time.Now().Unix() - - var ext, targetFilename, targetPath string - if srcFileHeader != nil { - ext = filepath.Ext(srcFileHeader.Filename) - targetFilename = fmt.Sprintf("%s_values_%d%s", containerName, timestamp, ext) - targetPath = filepath.Join(targetDir, targetFilename) - - if err := utils.CopyFileFromFileHeader(srcFileHeader, targetPath); err != nil { - return fmt.Errorf("failed to save file: %w", err) - } - } - - if srcFilePath != "" { - ext = filepath.Ext(srcFilePath) - targetFilename = fmt.Sprintf("%s_values_%d%s", containerName, timestamp, ext) - targetPath = filepath.Join(targetDir, targetFilename) - - if err := utils.CopyFile(srcFilePath, targetPath); err != nil { - return fmt.Errorf("failed to save file: %w", err) - } - } - - logrus.WithFields(logrus.Fields{ - "file_path": targetPath, - }).Info("Helm values file uploaded successfully") - - helmConfig.ValueFile = targetPath - if err := repository.UpdateHelmConfig(db, helmConfig); err != nil { - return fmt.Errorf("failed to update helm config: %w", err) - } - - return nil -} - -// ===================================================================== -// Container Building Task Service Layer -// ===================================================================== - -// ProduceContainerBuildingTask produces a container building task into Redis based on the provided request -func ProduceContainerBuildingTask(ctx context.Context, req *dto.SubmitBuildContainerReq, groupID string, userID int) (*dto.SubmitContainerBuildResp, error) { - if req == nil { - return nil, fmt.Errorf("build container request is nil") - } - - sourcePath, err := processGitHubSource(req) - if err != nil { - return nil, fmt.Errorf("failed to process GitHub source: %w", err) - } - - if err := req.ValidateInfoContent(sourcePath); err != nil { - return nil, fmt.Errorf("invalid container info content: %w", err) - } - if err := req.Options.ValidateRequiredFiles(sourcePath); err != nil { - return nil, fmt.Errorf("invalid container options: %w", err) - } - - imageRef := fmt.Sprintf("%s/%s/%s:%s", config.GetString("harbor.registry"), config.GetString("harbor.namespace"), req.ImageName, req.Tag) - payload := map[string]any{ - consts.BuildImageRef: imageRef, - consts.BuildSourcePath: sourcePath, - consts.BuildBuildOptions: req.Options, - } - - task := &dto.UnifiedTask{ - Type: consts.TaskTypeBuildContainer, - Immediate: true, - Payload: payload, - GroupID: groupID, - UserID: userID, - State: consts.TaskPending, - } - task.SetGroupCtx(ctx) - - err = common.SubmitTask(ctx, task) - if err != nil { - return nil, fmt.Errorf("failed to submit container building task: %w", err) - } - - resp := &dto.SubmitContainerBuildResp{ - GroupID: task.GroupID, - TraceID: task.TraceID, - TaskID: task.TaskID, - } - return resp, nil -} - -// createContainerVersionCore performs the core logic of creating container versions within a transaction -func createContainerVersionsCore(db *gorm.DB, versions []database.ContainerVersion) ([]database.ContainerVersion, error) { - if len(versions) == 0 { - return nil, nil - } - - if err := repository.BatchCreateContainerVersions(db, versions); err != nil { - return nil, fmt.Errorf("failed to create container versions: %w", err) - } - - // Collect all envVars with their corresponding version index - type envVarWithVersionIdx struct { - envVar database.ParameterConfig - versionIdx int - envVarIdx int - } - - envVarsWithIdx := []envVarWithVersionIdx{} - for versionIdx, version := range versions { - for envVarIdx, envVar := range version.EnvVars { - envVarsWithIdx = append(envVarsWithIdx, envVarWithVersionIdx{ - envVar: envVar, - versionIdx: versionIdx, - envVarIdx: envVarIdx, - }) - } - } - - if len(envVarsWithIdx) > 0 { - // Extract envVars for batch creation/upsert - envVars := make([]database.ParameterConfig, len(envVarsWithIdx)) - for i, item := range envVarsWithIdx { - envVars[i] = item.envVar - } - - // Use OnConflict to insert or ignore existing configs - if err := repository.BatchCreateOrFindParameterConfigs(db, envVars); err != nil { - return nil, fmt.Errorf("failed to create parameter configs: %w", err) - } - - // Query back the actual IDs from database (including existing ones) - actualEnvVars, err := repository.ListParameterConfigsByKeys(db, envVars) - if err != nil { - return nil, fmt.Errorf("failed to list parameter configs: %w", err) - } - - // Build a map for quick lookup: (key, type, category) -> ID - configMap := make(map[string]int) - for _, cfg := range actualEnvVars { - key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) - configMap[key] = cfg.ID - } - - // Build relations using the actual IDs from database - relations := make([]database.ContainerVersionEnvVar, 0, len(envVarsWithIdx)) - for _, item := range envVarsWithIdx { - cfg := item.envVar - key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) - if paramID, exists := configMap[key]; exists { - relations = append(relations, database.ContainerVersionEnvVar{ - ContainerVersionID: versions[item.versionIdx].ID, - ParameterConfigID: paramID, - }) - } else { - return nil, fmt.Errorf("parameter config not found after creation: %s", key) - } - } - - if err := repository.AddContainerVersionEnvVars(db, relations); err != nil { - return nil, fmt.Errorf("failed to create container version env var relations: %w", err) - } - } - - var helmConfigs []*database.HelmConfig - for versionIdx := range versions { - if versions[versionIdx].HelmConfig != nil { - versions[versionIdx].HelmConfig.ContainerVersionID = versions[versionIdx].ID - helmConfigs = append(helmConfigs, versions[versionIdx].HelmConfig) - } - } - - if len(helmConfigs) == 0 { - return versions, nil - } - - if err := repository.BatchCreateHelmConfigs(db, helmConfigs); err != nil { - return nil, fmt.Errorf("failed to create helm configs: %w", err) - } - - // Collect all helm values with their corresponding helmConfig index - type helmValueWithConfigIdx struct { - value database.ParameterConfig - helmConfigIdx int - valueIdx int - } - - helmValuesWithIdx := []helmValueWithConfigIdx{} - for helmConfigIdx, helmConfig := range helmConfigs { - for valueIdx, value := range helmConfig.DynamicValues { - helmValuesWithIdx = append(helmValuesWithIdx, helmValueWithConfigIdx{ - value: value, - helmConfigIdx: helmConfigIdx, - valueIdx: valueIdx, - }) - } - } - - if len(helmValuesWithIdx) > 0 { - // Extract helm values for batch creation/upsert - helmValues := make([]database.ParameterConfig, len(helmValuesWithIdx)) - for i, item := range helmValuesWithIdx { - helmValues[i] = item.value - } - - // Use OnConflict to insert or ignore existing configs - if err := repository.BatchCreateOrFindParameterConfigs(db, helmValues); err != nil { - return nil, fmt.Errorf("failed to create helm parameter configs: %w", err) - } - - // Query back the actual IDs from database (including existing ones) - actualHelmValues, err := repository.ListParameterConfigsByKeys(db, helmValues) - if err != nil { - return nil, fmt.Errorf("failed to list helm parameter configs: %w", err) - } - - // Build a map for quick lookup: (key, type, category) -> ID - configMap := make(map[string]int) - for _, cfg := range actualHelmValues { - key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) - configMap[key] = cfg.ID - } - - // Build relations using the actual IDs from database - relations := make([]database.HelmConfigValue, 0, len(helmValuesWithIdx)) - for _, item := range helmValuesWithIdx { - cfg := item.value - key := fmt.Sprintf("%s:%d:%d", cfg.Key, cfg.Type, cfg.Category) - if paramID, exists := configMap[key]; exists { - relations = append(relations, database.HelmConfigValue{ - HelmConfigID: helmConfigs[item.helmConfigIdx].ID, - ParameterConfigID: paramID, - }) - } else { - return nil, fmt.Errorf("helm parameter config not found after creation: %s", key) - } - } - - if err := repository.AddHelmConfigValues(db, relations); err != nil { - return nil, fmt.Errorf("failed to create helm config value relations: %w", err) - } - } - - return versions, nil -} - -// fetchContainersMapByIDBatch fetches containers by their IDs and returns a map of container ID to Container -func fetchContainersMapByIDBatch(db *gorm.DB, containerIDs []int) (map[int]database.Container, error) { - if len(containerIDs) == 0 { - return make(map[int]database.Container), nil - } - - containers, err := repository.ListContainersByID(db, utils.ToUniqueSlice(containerIDs)) - if err != nil { - return nil, fmt.Errorf("failed to list containers by IDs: %w", err) - } - - containerMap := make(map[int]database.Container, len(containers)) - for _, c := range containers { - containerMap[c.ID] = c - } - - return containerMap, nil -} - -// processGitHubSource processes the GitHub source for building the container -func processGitHubSource(req *dto.SubmitBuildContainerReq) (string, error) { - targetDir := filepath.Join(config.GetString("jfs.container_path"), req.ImageName, fmt.Sprintf("build_%d", time.Now().Unix())) - if err := os.MkdirAll(targetDir, 0755); err != nil { - return "", fmt.Errorf("failed to create target directory: %w", err) - } - - repoURL := fmt.Sprintf("https://github.com/%s.git", req.GithubRepository) - if req.GithubToken != "" { - repoURL = fmt.Sprintf("https://%s@github.com/%s.git", req.GithubToken, req.GithubRepository) - } - - gitCmd := []string{"git", "clone"} - if req.GithubBranch != "" { - gitCmd = append(gitCmd, repoURL, targetDir) - } else { - gitCmd = append(gitCmd, "--branch", req.GithubBranch, "--single-branch", repoURL, targetDir) - } - - if req.GithubCommit != "" { - cmd := exec.Command(gitCmd[0], gitCmd[1:]...) - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to clone repository: %w", err) - } - - // Checkout specific commit - cmd = exec.Command("git", "-C", targetDir, "checkout", req.GithubCommit) - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to checkout commit %s: %w", req.GithubCommit, err) - } - } else { - cmd := exec.Command(gitCmd[0], gitCmd[1:]...) - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("failed to clone repository: %w", err) - } - } - - // If a specific path is provided, copy only that subdirectory - if req.SubPath != "" { - sourcePath := filepath.Join(targetDir, req.SubPath) - if _, err := os.Stat(sourcePath); os.IsNotExist(err) { - return "", fmt.Errorf("sub path '%s' does not exist in repository", req.SubPath) - } - - newTargetDir := filepath.Join(config.GetString("jfs.container_path"), req.ImageName, fmt.Sprintf("build_final_%d", time.Now().Unix())) - if err := utils.CopyDir(sourcePath, newTargetDir); err != nil { - return "", fmt.Errorf("failed to copy subdirectory: %w", err) - } - - // Clean up the full clone - if err := os.RemoveAll(targetDir); err != nil { - logrus.WithField("target_dir", targetDir).Warnf("failed to remove temporary directory: %v", err) - } - - targetDir = newTargetDir - } - - return targetDir, nil -} - -// validateHelmConfigVersion validates that a container version exists, belongs to the specified container, -// is a pedestal type, and has an associated Helm configuration -func validateHelmConfigVersion(containerID, versionID int) (*database.ContainerVersion, error) { - containerVersion, err := repository.GetContainerVersionByID(database.DB, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: container version %d not found", consts.ErrNotFound, versionID) - } - return nil, fmt.Errorf("failed to get container version: %w", err) - } - - if containerVersion.ContainerID != containerID { - return nil, fmt.Errorf("version %d does not belong to container %d", versionID, containerID) - } - - if containerVersion.Container == nil || containerVersion.Container.Type != consts.ContainerTypePedestal { - return nil, fmt.Errorf("only pedestal container versions support Helm configurations") - } - - if containerVersion.HelmConfig == nil { - return nil, fmt.Errorf("container version %d does not have an associated Helm configuration", versionID) - } - - return containerVersion, nil -} diff --git a/src/service/producer/dataset.go b/src/service/producer/dataset.go deleted file mode 100644 index 10a546bc..00000000 --- a/src/service/producer/dataset.go +++ /dev/null @@ -1,636 +0,0 @@ -package producer - -import ( - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - "archive/zip" - "errors" - "fmt" - "io/fs" - "path/filepath" - - "gorm.io/gorm" -) - -// ===================================================================== -// Dataset Service Layer -// ===================================================================== - -// CreateDataset creates a new dataset -func CreateDataset(req *dto.CreateDatasetReq, userID int) (*dto.DatasetResp, error) { - if req == nil { - return nil, fmt.Errorf("request cannot be nil") - } - - dataset := req.ConvertToDataset() - - var version *database.DatasetVersion - if req.VersionReq != nil { - version = req.VersionReq.ConvertToDatasetVersion() - } - - var createdDataset *database.Dataset - err := database.DB.Transaction(func(tx *gorm.DB) error { - var err error - if version != nil { - dataset, err = CreateDatasetCore(tx, dataset, []database.DatasetVersion{*version}, userID) - } else { - dataset, err = CreateDatasetCore(tx, dataset, nil, userID) - } - - if err != nil { - return fmt.Errorf("failed to create dataset: %w", err) - } - - createdDataset = dataset - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to create dataset: %w", err) - } - - return dto.NewDatasetResp(createdDataset), nil -} - -// CreateDatasetCore performs the core logic of creating a dataset within a transaction -func CreateDatasetCore(tx *gorm.DB, dataset *database.Dataset, versions []database.DatasetVersion, userID int) (*database.Dataset, error) { - role, err := repository.GetRoleByName(tx, consts.RoleDatasetAdmin.String()) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: role %v not found", err, consts.RoleDatasetAdmin) - } - return nil, fmt.Errorf("failed to get dataset owner role: %w", err) - } - - if err := repository.CreateDataset(tx, dataset); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return nil, consts.ErrAlreadyExists - } - - return nil, err - } - - if err := repository.CreateUserDataset(tx, &database.UserDataset{ - UserID: userID, - DatasetID: dataset.ID, - RoleID: role.ID, - Status: consts.CommonEnabled, - }); err != nil { - return nil, fmt.Errorf("failed to associate dataset with user: %w", err) - } - - if len(versions) > 0 { - for i := range versions { - versions[i].DatasetID = dataset.ID - versions[i].UserID = userID - } - - _, err = createDatasetVersionsCore(tx, versions) - if err != nil { - return nil, fmt.Errorf("failed to create dataset versions: %w", err) - } - } - - return dataset, nil -} - -func DeleteDataset(datasetID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - if _, err := repository.BatchDeleteDatasetVersions(tx, datasetID); err != nil { - return fmt.Errorf("failed to delete dataset versions: %w", err) - } - - if _, err := repository.RemoveUsersFromDataset(tx, datasetID); err != nil { - return fmt.Errorf("failed to remove all users from dataset: %w", err) - } - - rows, err := repository.DeleteDataset(tx, datasetID) - if err != nil { - return fmt.Errorf("failed to delete dataset: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: dataset id %d not found", consts.ErrNotFound, datasetID) - } - - return nil - }) -} - -func GetDatasetDetail(datasetID int) (*dto.DatasetDetailResp, error) { - dataset, err := repository.GetDatasetByID(database.DB, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) - } - return nil, fmt.Errorf("failed to get dataset: %w", err) - } - - versions, err := repository.ListDatasetVersionsByDatasetID(database.DB, dataset.ID) - if err != nil { - return nil, fmt.Errorf("failed to get dataset versions: %w", err) - } - - resp := dto.NewDatasetDetailResp(dataset) - - for _, version := range versions { - resp.Versions = append(resp.Versions, *dto.NewDatasetVersionResp(&version)) - } - - return dto.NewDatasetDetailResp(dataset), nil -} - -// ListDatasets lists datasets with pagination and optional filtering -func ListDatasets(req *dto.ListDatasetReq) (*dto.ListResp[dto.DatasetResp], error) { - limit, offset := req.ToGormParams() - - datasets, total, err := repository.ListDatasets(database.DB, limit, offset, req.Type, req.IsPublic, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list datasets: %w", err) - } - - datasetIDs := make([]int, 0, len(datasets)) - for _, d := range datasets { - datasetIDs = append(datasetIDs, d.ID) - } - - labelsMap, err := repository.ListDatasetLabels(database.DB, datasetIDs) - if err != nil { - return nil, fmt.Errorf("failed to list dataset labels: %w", err) - } - - datasetResps := make([]dto.DatasetResp, 0, len(datasets)) - for _, dataset := range datasets { - if labels, exists := labelsMap[dataset.ID]; exists { - dataset.Labels = labels - } - datasetResps = append(datasetResps, *dto.NewDatasetResp(&dataset)) - } - - resp := dto.ListResp[dto.DatasetResp]{ - Items: datasetResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// SearchDataset searches datasets based on the provided search request -func SearchDatasets(req *dto.SearchDatasetReq) (*dto.ListResp[dto.DatasetDetailResp], error) { - if req == nil { - return nil, fmt.Errorf("search dataset request is nil") - } - - searchReq := req.ConvertToSearchReq() - dataests, total, err := repository.ExecuteSearch(database.DB, searchReq, database.Dataset{}, consts.DatasetAllowedFields) - if err != nil { - return nil, fmt.Errorf("failed to search datasets: %w", err) - } - - datasetResps := make([]dto.DatasetDetailResp, 0, len(dataests)) - for _, dataset := range dataests { - datasetResps = append(datasetResps, *dto.NewDatasetDetailResp(&dataset)) - } - - resp := dto.ListResp[dto.DatasetDetailResp]{ - Items: datasetResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -func UpdateDataset(req *dto.UpdateDatasetReq, datasetID int) (*dto.DatasetResp, error) { - var updatedDataset *database.Dataset - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingDataset, err := repository.GetDatasetByID(tx, datasetID) - if err != nil { - return fmt.Errorf("failed to get dataset: %w", err) - } - - req.PatchDatasetModel(existingDataset) - - if err := repository.UpdateDataset(tx, existingDataset); err != nil { - return fmt.Errorf("failed to update dataset: %w", err) - } - - updatedDataset = existingDataset - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewDatasetResp(updatedDataset), nil -} - -// ===================== Dataset-Label ===================== - -func ManageDatasetLabels(req *dto.ManageDatasetLabelReq, datasetID int) (*dto.DatasetResp, error) { - if req == nil { - return nil, fmt.Errorf("manage dataset labels request is nil") - } - - var managedDataset *database.Dataset - err := database.DB.Transaction(func(tx *gorm.DB) error { - dataset, err := repository.GetDatasetByID(tx, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) - } - return fmt.Errorf("failed to get dataset: %w", err) - } - - if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - datasetLabels := make([]database.DatasetLabel, 0, len(labels)) - for _, label := range labels { - datasetLabels = append(datasetLabels, database.DatasetLabel{ - DatasetID: datasetID, - LabelID: label.ID, - }) - } - - if err := repository.AddDatasetLabels(tx, datasetLabels); err != nil { - return fmt.Errorf("failed to add dataset labels: %w", err) - } - } - - if len(req.RemoveLabels) > 0 { - labelIDs, err := repository.ListLabelIDsByKeyAndDatasetID(tx, datasetID, req.RemoveLabels) - if err != nil { - return fmt.Errorf("failed to find label ids by keys: %w", err) - } - - if len(labelIDs) > 0 { - if err := repository.ClearDatasetLabels(tx, []int{datasetID}, labelIDs); err != nil { - return fmt.Errorf("failed to clear dataset labels: %w", err) - } - - if err := repository.BatchDecreaseLabelUsages(tx, labelIDs, 1); err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) - } - } - } - - labels, err := repository.ListLabelsByDatasetID(database.DB, dataset.ID) - if err != nil { - return fmt.Errorf("failed to get dataset labels: %w", err) - } - - dataset.Labels = labels - managedDataset = dataset - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewDatasetResp(managedDataset), nil -} - -// ===================================================================== -// DatasetVersion Service Layer -// ===================================================================== - -func CreateDatasetVersion(req *dto.CreateDatasetVersionReq, datasetID, userID int) (*dto.DatasetVersionResp, error) { - if req == nil { - return nil, fmt.Errorf("create dataset version request is nil") - } - - version := req.ConvertToDatasetVersion() - version.DatasetID = datasetID - version.UserID = userID - - var createdVersion *database.DatasetVersion - err := database.DB.Transaction(func(tx *gorm.DB) error { - versions, err := createDatasetVersionsCore(tx, []database.DatasetVersion{*version}) - if err != nil { - return fmt.Errorf("failed to create dataset version: %w", err) - } - - version := versions[0] - if len(req.Datapacks) > 0 { - if err := linkDatapacksToDatasetVersion(tx, version.ID, req.Datapacks); err != nil { - return fmt.Errorf("failed to link datapacks to dataset version: %w", err) - } - } - - createdVersion = &version - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to create dataset version: %w", err) - } - - return dto.NewDatasetVersionResp(createdVersion), nil -} - -// DeleteDatasetVersion deletes a specific version of a dataset -func DeleteDatasetVersion(versionID int) error { - rows, err := repository.DeleteDatasetVersion(database.DB, versionID) - if err != nil { - return fmt.Errorf("failed to delete dataset version: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: dataset version id %d not found", consts.ErrNotFound, versionID) - } - return nil -} - -// GetDatasetVersionDetail retrieves the details of a specific dataset version by its ID -func GetDatasetVersionDetail(datasetID, versionID int) (*dto.DatasetVersionDetailResp, error) { - _, err := repository.GetDatasetByID(database.DB, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) - } - return nil, fmt.Errorf("failed to get dataset: %w", err) - } - - version, err := repository.GetDatasetVersionByID(database.DB, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) - } - return nil, fmt.Errorf("failed to get dataset version: %w", err) - } - - return dto.NewDatasetVersionDetailResp(version), nil -} - -// ListDatasetVersions lists dataset versions with pagination and optional status filtering -func ListDatasetVersions(req *dto.ListDatasetVersionReq, datasetID int) (*dto.ListResp[dto.DatasetVersionResp], error) { - limit, offset := req.ToGormParams() - - versions, total, err := repository.ListDatasetVersions(database.DB, limit, offset, datasetID, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list dataset versions: %w", err) - } - - versionResps := make([]dto.DatasetVersionResp, 0, len(versions)) - for _, version := range versions { - versionResps = append(versionResps, *dto.NewDatasetVersionResp(&version)) - } - - resp := dto.ListResp[dto.DatasetVersionResp]{ - Items: versionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateDatasetVersion updates the details of a specific dataset version -func UpdateDatasetVersion(req *dto.UpdateDatasetVersionReq, datasetID, versionID int) (*dto.DatasetVersionResp, error) { - var updatedVersion *database.DatasetVersion - - err := database.DB.Transaction(func(tx *gorm.DB) error { - version, err := repository.GetDatasetVersionByID(tx, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) - } - return fmt.Errorf("failed to get dataset version: %w", err) - } - - req.PatchDatasetVersionModel(version) - - if err := repository.UpdateDatasetVersion(tx, version); err != nil { - return fmt.Errorf("failed to update dataset version: %w", err) - } - - updatedVersion = version - return nil - }) - if err != nil { - return nil, fmt.Errorf("failed to update dataset version: %w", err) - } - - return dto.NewDatasetVersionResp(updatedVersion), nil -} - -// GetDatasetVersionFilename generates a filename for the dataset version download -func GetDatasetVersionFilename(datasetID, versionID int) (string, error) { - dataset, err := repository.GetDatasetByID(database.DB, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return "", fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) - } - return "", fmt.Errorf("failed to get dataset: %w", err) - } - - version, err := repository.GetDatasetVersionByID(database.DB, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return "", fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) - } - return "", fmt.Errorf("failed to get dataset version: %w", err) - } - - return fmt.Sprintf("%s-%s", dataset.Name, version.Name), nil -} - -// DownloadDatasetVersion handles the downloading of a specific dataset version -func DownloadDatasetVersion(zipWriter *zip.Writer, excludeRules []utils.ExculdeRule, versionID int) error { - if zipWriter == nil { - return fmt.Errorf("zip writer cannot be nil") - } - - datapacks, err := repository.ListInjectionsByDatasetVersionID(database.DB, versionID, false) - if err != nil { - return fmt.Errorf("failed to list datapacks for dataset version: %w", err) - } - - if err := packageDatasetVersionToZip(zipWriter, datapacks, excludeRules); err != nil { - return fmt.Errorf("failed to package dataset to zip: %w", err) - } - - return nil -} - -// ===================== DatasetVersion-Injection ===================== - -func ManageDatasetVersionInjections(req *dto.ManageDatasetVersionInjectionReq, versionID int) (*dto.DatasetVersionDetailResp, error) { - if req == nil { - return nil, fmt.Errorf("manage dataset version injections request is nil") - } - - var managedVersion *database.DatasetVersion - err := database.DB.Transaction(func(tx *gorm.DB) error { - version, err := repository.GetDatasetVersionByID(tx, versionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: dataset version id: %d", consts.ErrNotFound, versionID) - } - return fmt.Errorf("failed to get dataset version: %w", err) - } - - if len(req.AddDatapacks) > 0 { - if err := linkDatapacksToDatasetVersion(tx, versionID, req.AddDatapacks); err != nil { - return fmt.Errorf("failed to link datapacks to dataset version: %w", err) - } - } - - if len(req.RemoveDatapacks) > 0 { - injectionIDMap, err := repository.ListInjectionIDsByNames(tx, req.AddDatapacks) - if err != nil { - return fmt.Errorf("failed to list injections by names: %w", err) - } - - if len(injectionIDMap) != len(req.RemoveDatapacks) { - return fmt.Errorf("some datapacks to remove were not found") - } - - injectionIDs := make([]int, 0, len(req.RemoveDatapacks)) - for _, datapack := range req.RemoveDatapacks { - injectionID, exists := injectionIDMap[datapack] - if !exists { - return fmt.Errorf("injection not found: %s", datapack) - } - injectionIDs = append(injectionIDs, injectionID) - } - - if err := repository.ClearDatasetVersionInjections(tx, []int{version.ID}, injectionIDs); err != nil { - return fmt.Errorf("failed to remove dataset version datapacks: %w", err) - } - } - - datapacks, err := repository.ListInjectionsByDatasetVersionID(tx, version.ID, false) - if err != nil { - return fmt.Errorf("failed to list datapacks for dataset version: %w", err) - } - - version.Datapacks = datapacks - version.FileCount = version.FileCount + len(req.AddDatapacks) - len(req.RemoveDatapacks) - if err := repository.UpdateDatasetVersion(tx, version); err != nil { - return fmt.Errorf("failed to update dataset version file count: %w", err) - } - - managedVersion = version - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewDatasetVersionDetailResp(managedVersion), nil -} - -// createDatasetVersionCore performs the core logic of creating dataset versions within a transaction -func createDatasetVersionsCore(db *gorm.DB, versions []database.DatasetVersion) ([]database.DatasetVersion, error) { - if len(versions) == 0 { - return nil, nil - } - - if err := repository.BatchCreateDatasetVersions(db, versions); err != nil { - return nil, fmt.Errorf("failed to create dataset versions: %w", err) - } - - return versions, nil -} - -// fetchDatasetsMapByIDBatch fetches datasets by their IDs and returns a map of dataset ID to Dataset -func fetchDatasetsMapByIDBatch(db *gorm.DB, datasetIDs []int) (map[int]database.Dataset, error) { - if len(datasetIDs) == 0 { - return make(map[int]database.Dataset), nil - } - - datasets, err := repository.ListDatasetsByID(db, utils.ToUniqueSlice(datasetIDs)) - if err != nil { - return nil, fmt.Errorf("failed to list datasets by IDs: %w", err) - } - - datasetMap := make(map[int]database.Dataset, len(datasets)) - for _, d := range datasets { - datasetMap[d.ID] = d - } - - return datasetMap, nil -} - -// linkDatapacksToDatasetVersion links the specified datapacks to the given dataset version -func linkDatapacksToDatasetVersion(db *gorm.DB, versionID int, datapacks []string) error { - injectionIDMap, err := repository.ListInjectionIDsByNames(db, datapacks) - if err != nil { - return fmt.Errorf("failed to list injections by names: %w", err) - } - - datasetVersionInjections := make([]database.DatasetVersionInjection, 0, len(datapacks)) - for _, datapack := range datapacks { - injectionID, exists := injectionIDMap[datapack] - if !exists { - return fmt.Errorf("injection not found: %s", datapack) - } - datasetVersionInjections = append(datasetVersionInjections, database.DatasetVersionInjection{ - DatasetVersionID: versionID, - InjectionID: injectionID, - }) - } - - if err := repository.AddDatasetVersionInjections(db, datasetVersionInjections); err != nil { - return fmt.Errorf("failed to add dataset version injections: %w", err) - } - - return nil -} - -// packageDatasetVersionToZip packages the specified datapacks into a zip archive, applying exclusion rules -func packageDatasetVersionToZip(zipWriter *zip.Writer, datapacks []database.FaultInjection, excludeRules []utils.ExculdeRule) error { - for _, datapack := range datapacks { - if err := packageDatapackToZip(zipWriter, &datapack, excludeRules); err != nil { - return err - } - } - return nil -} - -// packageDatapackToZip packages a single datapack into a zip archive, applying exclusion rules -func packageDatapackToZip(zipWriter *zip.Writer, datapack *database.FaultInjection, excludeRules []utils.ExculdeRule) error { - if datapack.State < consts.DatapackBuildSuccess { - return fmt.Errorf("datapack %s is not in a downloadable state", datapack.Name) - } - - workDir := filepath.Join(config.GetString("jfs.dataset_path"), datapack.Name) - if !utils.IsAllowedPath(workDir) { - return fmt.Errorf("invalid path access to %s", workDir) - } - - err := filepath.WalkDir(workDir, func(path string, dir fs.DirEntry, err error) error { - if err != nil || dir.IsDir() { - return err - } - - relPath, _ := filepath.Rel(workDir, path) - fullRelPath := filepath.Join(consts.DownloadFilename, filepath.Base(workDir), relPath) - fileName := filepath.Base(path) - - // Apply exclusion rules - for _, rule := range excludeRules { - if utils.MatchFile(fileName, rule) { - return nil - } - } - - // Get file info to read modification time - fileInfo, err := dir.Info() - if err != nil { - return err - } - - // Convert path separators to "/" - zipPath := filepath.ToSlash(fullRelPath) - return utils.AddToZip(zipWriter, fileInfo, path, zipPath) - }) - if err != nil { - return fmt.Errorf("failed to package datapack %s: %w", datapack.Name, err) - } - - return nil -} diff --git a/src/service/producer/dynamic_config.go b/src/service/producer/dynamic_config.go deleted file mode 100644 index 50452d34..00000000 --- a/src/service/producer/dynamic_config.go +++ /dev/null @@ -1,544 +0,0 @@ -package producer - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "aegis/client" - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// ===================================================================== -// Private Types (migrated from common for producer-only usage) -// ===================================================================== - -// configUpdateContext holds context information for a configuration update -type configUpdateContext struct { - ChangeField consts.ConfigHistoryChangeField - OldValue string - NewValue string - Reason string - OperatorID int - IpAddress string - UserAgent string -} - -// configHistoryParams encapsulates parameters for creating config history entries -type configHistoryParams struct { - ConfigID int - ChangeType consts.ConfigHistoryChangeType - RollbackFromID *int - - ConfigUpdateContext configUpdateContext -} - -// ===================================================================== -// Configuration Service Layer -// ===================================================================== - -// etcdPrefixForScope returns the etcd key prefix for the given config scope. -func etcdPrefixForScope(scope consts.ConfigScope) string { - switch scope { - case consts.ConfigScopeProducer: - return consts.ConfigEtcdProducerPrefix - case consts.ConfigScopeConsumer: - return consts.ConfigEtcdConsumerPrefix - case consts.ConfigScopeGlobal: - return consts.ConfigEtcdGlobalPrefix - } - return "" -} - -// GetConfigDetail retrieves detailed information about a configuration by its key -func GetConfigDetail(containerID int) (*dto.ConfigDetailResp, error) { - config, err := repository.GetConfigByID(database.DB, containerID, true) - if err != nil { - return nil, fmt.Errorf("failed to get config detail: %w", err) - } - - histories, err := repository.ListConfigHistoriesByConfigID(database.DB, config.ID) - if err != nil { - return nil, fmt.Errorf("failed to get config histories: %w", err) - } - - resp := dto.NewConfigDetailResp(config) - for _, history := range histories { - resp.Histories = append(resp.Histories, *dto.NewConfigHistoryResp(&history)) - } - - return resp, nil -} - -// ListConfigs lists configurations based on the provided filters -func ListConfigs(req *dto.ListConfigReq) (*dto.ListResp[dto.ConfigResp], error) { - limit, offset := req.ToGormParams() - - configs, total, err := repository.ListConfigs(database.DB, limit, offset, req.ValueType, req.Category, req.IsSecret, req.UpdatedBy) - if err != nil { - return nil, fmt.Errorf("failed to list configs: %w", err) - } - - configResps := make([]dto.ConfigResp, 0, len(configs)) - for _, config := range configs { - configResps = append(configResps, *dto.NewConfigResp(&config)) - } - - resp := dto.ListResp[dto.ConfigResp]{ - Items: configResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// RollbackConfigValue rolls back a configuration value from history -func RollbackConfigValue(ctx context.Context, req *dto.RollbackConfigReq, configID, operatorID int, ipAddress, userAgent string) error { - // Get the history entry to rollback to - history, err := repository.GetConfigHistory(database.DB, req.HistoryID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) - } - return fmt.Errorf("failed to get config history: %w", err) - } - - // Validate this is a value change history - if history.ChangeField != consts.ChangeFieldValue { - return fmt.Errorf("history entry %d is not a value change (field: %v)", req.HistoryID, history.ChangeField) - } - - // Get existing config - existingConfig, err := repository.GetConfigByID(database.DB, configID, false) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) - } - return fmt.Errorf("failed to get config: %w", err) - } - - oldValue, err := client.EtcdGet(ctx, fmt.Sprintf("%s%s", etcdPrefixForScope(existingConfig.Scope), existingConfig.Key)) - if err != nil { - return fmt.Errorf("failed to get current config value from etcd: %w", err) - } - - newValue := history.OldValue - - if err := common.ValidateConfig(existingConfig, newValue); err != nil { - return fmt.Errorf("invalid config after rollback: %w", err) - } - - if err := setViperIfNeeded(existingConfig, newValue); err != nil { - return fmt.Errorf("failed to set config value in viper: %w", err) - } - - if _, err := createConfigRollback(existingConfig, utils.IntPtr(history.ID), configUpdateContext{ - ChangeField: consts.ChangeFieldValue, - OldValue: oldValue, - NewValue: newValue, - Reason: req.Reason, - OperatorID: operatorID, - IpAddress: ipAddress, - UserAgent: userAgent, - }); err != nil { - return err - } - - return propagateValueChange(ctx, existingConfig, newValue, "rollback") -} - -// RollbackConfigMetadata rolls back a configuration metadata field from history -func RollbackConfigMetadata(req *dto.RollbackConfigReq, configID, operatorID int, ipAddress, userAgent string) (*dto.ConfigResp, error) { - // Get the history entry to rollback to - history, err := repository.GetConfigHistory(database.DB, req.HistoryID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) - } - return nil, fmt.Errorf("failed to get config history: %w", err) - } - - // Validate this is a metadata change history - if history.ChangeField == consts.ChangeFieldValue { - return nil, fmt.Errorf("history entry %d is a value change, use RollbackConfigValue instead", req.HistoryID) - } - - // Get existing config - existingConfig, err := repository.GetConfigByID(database.DB, configID, false) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) - } - return nil, fmt.Errorf("failed to get config: %w", err) - } - - // Rollback the metadata field - oldValue, newValue, err := rollbackMetaFieldValue(existingConfig, history.ChangeField, history.OldValue) - if err != nil { - return nil, fmt.Errorf("failed to rollback metadata field: %w", err) - } - - // Validate the configuration after metadata rollback - if err := common.ValidateConfigMetadataConstraints(existingConfig); err != nil { - return nil, fmt.Errorf("invalid config after metadata rollback: %w", err) - } - - // Save to database with rollback history - updatedConfig, err := createConfigRollback(existingConfig, utils.IntPtr(history.ID), configUpdateContext{ - ChangeField: history.ChangeField, - OldValue: oldValue, - NewValue: newValue, - Reason: req.Reason, - OperatorID: operatorID, - IpAddress: ipAddress, - UserAgent: userAgent, - }) - if err != nil { - return nil, err - } - - return dto.NewConfigResp(updatedConfig), nil -} - -// UpdateConfigValue updates the value of a configuration and handles propagation based on its scope -func UpdateConfigValue(ctx context.Context, req *dto.UpdateConfigValueReq, configID, operatorID int, ipAddress, userAgent string) error { - existingConfig, err := repository.GetConfigByID(database.DB, configID, false) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) - } - } - - oldValue, err := client.EtcdGet(ctx, fmt.Sprintf("%s%s", etcdPrefixForScope(existingConfig.Scope), existingConfig.Key)) - if err != nil { - return fmt.Errorf("failed to get current config value from etcd: %w", err) - } - - newValue := req.Value - - if err := common.ValidateConfig(existingConfig, newValue); err != nil { - return fmt.Errorf("invalid config value: %w", err) - } - - if err := setViperIfNeeded(existingConfig, newValue); err != nil { - return fmt.Errorf("failed to set config value in viper: %w", err) - } - - if err := createConfigHistory(database.DB, configHistoryParams{ - ConfigID: existingConfig.ID, - ChangeType: consts.ChangeTypeUpdate, - ConfigUpdateContext: configUpdateContext{ - ChangeField: consts.ChangeFieldValue, - OldValue: oldValue, - NewValue: newValue, - Reason: req.Reason, - OperatorID: operatorID, - IpAddress: ipAddress, - UserAgent: userAgent, - }, - }); err != nil { - return fmt.Errorf("failed to create config history: %w", err) - } - - return propagateValueChange(ctx, existingConfig, newValue, "update") -} - -// UpdateConfigMetadata updates the metadata of a configuration -func UpdateConfigMetadata(req *dto.UpdateConfigMetadataReq, configID, operatorID int, ipAddress, userAgent string) (*dto.ConfigResp, error) { - existingConfig, err := repository.GetConfigByID(database.DB, configID, false) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) - } - } - - oldValue, newValue := req.PatchConfigModel(existingConfig) - - // Validate the configuration after metadata update - if err := common.ValidateConfigMetadataConstraints(existingConfig); err != nil { - return nil, fmt.Errorf("invalid config after metadata update: %w", err) - } - - var updatedConfig *database.DynamicConfig - err = database.DB.Transaction(func(tx *gorm.DB) error { - existingConfig.UpdatedBy = utils.IntPtr(operatorID) - - if err := repository.UpdateConfig(tx, existingConfig); err != nil { - return fmt.Errorf("failed to update config: %w", err) - } - - updatedConfig = existingConfig - - if err := createConfigHistory(tx, configHistoryParams{ - ConfigID: updatedConfig.ID, - ChangeType: consts.ChangeTypeUpdate, - ConfigUpdateContext: configUpdateContext{ - ChangeField: req.GetChangeField(), - OldValue: oldValue, - NewValue: newValue, - Reason: req.Reason, - OperatorID: operatorID, - IpAddress: ipAddress, - UserAgent: userAgent, - }, - }); err != nil { - return fmt.Errorf("failed to create config history: %w", err) - } - - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewConfigResp(updatedConfig), nil -} - -// ===================== ConfigHistory ===================== - -func ListConfigHistories(req *dto.ListConfigHistoryReq, configID int) (*dto.ListResp[dto.ConfigHistoryResp], error) { - limit, offset := req.ToGormParams() - - histories, total, err := repository.ListConfigHistories(database.DB, limit, offset, configID, req.ChangeType, req.OperatorID) - if err != nil { - return nil, fmt.Errorf("failed to list config histories: %w", err) - } - - historyResps := make([]dto.ConfigHistoryResp, 0, len(histories)) - for _, history := range histories { - historyResps = append(historyResps, *dto.NewConfigHistoryResp(&history)) - } - - resp := dto.ListResp[dto.ConfigHistoryResp]{ - Items: historyResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ===================== Helper Functions ===================== - -// createConfigHistory creates a ConfigHistory entry from a config update (private version) -func createConfigHistory(db *gorm.DB, params configHistoryParams) error { - entry := &database.ConfigHistory{ - ChangeType: params.ChangeType, - OldValue: params.ConfigUpdateContext.OldValue, - NewValue: params.ConfigUpdateContext.NewValue, - Reason: params.ConfigUpdateContext.Reason, - ConfigID: params.ConfigID, - OperatorID: utils.IntPtr(params.ConfigUpdateContext.OperatorID), - IPAddress: params.ConfigUpdateContext.IpAddress, - UserAgent: params.ConfigUpdateContext.UserAgent, - RolledBackFromID: params.RollbackFromID, - ChangeField: params.ConfigUpdateContext.ChangeField, - } - if err := repository.CreateConfigHistory(db, entry); err != nil { - return fmt.Errorf("failed to create config history: %w", err) - } - return nil -} - -// createConfigRollback updates config and creates a rollback history entry -// This wraps the common history creation logic but with rollback-specific parameters -func createConfigRollback(config *database.DynamicConfig, historyID *int, updateContext configUpdateContext) (*database.DynamicConfig, error) { - var updatedConfig *database.DynamicConfig - - err := database.DB.Transaction(func(tx *gorm.DB) error { - // Update the config in database - if err := repository.UpdateConfig(tx, config); err != nil { - return fmt.Errorf("failed to update config: %w", err) - } - - updatedConfig = config - - // Create rollback history entry using common function - if err := createConfigHistory(tx, configHistoryParams{ - ConfigID: config.ID, - ChangeType: consts.ChangeTypeRollback, - ConfigUpdateContext: updateContext, - RollbackFromID: historyID, - }); err != nil { - return fmt.Errorf("failed to create rollback history: %w", err) - } - - return nil - }) - if err != nil { - return nil, err - } - - return updatedConfig, nil -} - -// rollbackMetaFieldValue rolls back a specific field in the config based on the change field type -// Returns the old value (before rollback) and new value (after rollback) -func rollbackMetaFieldValue(config *database.DynamicConfig, changeField consts.ConfigHistoryChangeField, targetValue string) (oldValue string, newValue string, err error) { - newValue = targetValue - - switch changeField { - case consts.ChangeFieldDefaultValue: - oldValue = config.DefaultValue - config.DefaultValue = newValue - - case consts.ChangeFieldDescription: - oldValue = config.Description - config.Description = newValue - - case consts.ChangeFieldMinValue: - if config.MinValue != nil { - oldValue = fmt.Sprintf("%f", *config.MinValue) - } - if newValue == "" { - config.MinValue = nil - } else { - var minVal float64 - if _, err := fmt.Sscanf(newValue, "%f", &minVal); err != nil { - return "", "", fmt.Errorf("failed to parse min value: %w", err) - } - config.MinValue = &minVal - } - - case consts.ChangeFieldMaxValue: - if config.MaxValue != nil { - oldValue = fmt.Sprintf("%f", *config.MaxValue) - } - if newValue == "" { - config.MaxValue = nil - } else { - var maxVal float64 - if _, err := fmt.Sscanf(newValue, "%f", &maxVal); err != nil { - return "", "", fmt.Errorf("failed to parse max value: %w", err) - } - config.MaxValue = &maxVal - } - - case consts.ChangeFieldPattern: - oldValue = config.Pattern - config.Pattern = newValue - - case consts.ChangeFieldOptions: - oldValue = config.Options - config.Options = newValue - - default: - return "", "", fmt.Errorf("unknown change field: %d", changeField) - } - - return oldValue, newValue, nil -} - -// setViperIfNeeded updates the local Viper cache for scopes that need immediate local reflection -// (producer and global). Consumer configs live only in etcd and are applied remotely. -func setViperIfNeeded(cfg *database.DynamicConfig, newValue string) error { - if cfg.Scope == consts.ConfigScopeConsumer { - return nil - } - return config.SetViperValue(cfg.Key, newValue, cfg.ValueType) -} - -// propagateValueChange publishes the new value to etcd and, for consumer scope, waits for ack. -// Producer scope requires no network propagation, so this is a no-op for that scope. -func propagateValueChange(ctx context.Context, cfg *database.DynamicConfig, newValue, opDesc string) error { - if cfg.Scope != consts.ConfigScopeGlobal && cfg.Scope != consts.ConfigScopeConsumer { - return nil - } - - etcdKey := fmt.Sprintf("%s%s", etcdPrefixForScope(cfg.Scope), cfg.Key) - if err := publishConfigToEtcdWithRetry(etcdKey, newValue, 3); err != nil { - return fmt.Errorf("config saved to database but failed to publish to etcd: %w", err) - } - - if cfg.Scope == consts.ConfigScopeConsumer { - logrus.Infof("Waiting for consumer config %s response...", opDesc) - resp, err := waitForConfigUpdateResponse(10 * time.Second) - if err != nil { - return fmt.Errorf("config %s but consumer did not respond: %w", opDesc, err) - } - if !resp.Success { - return fmt.Errorf("consumer failed to process config %s: %s", opDesc, resp.Error) - } - logrus.Infof("Config %s successfully processed by consumer", opDesc) - } - - return nil -} - -// publishConfigToEtcdWithRetry publishes configuration to etcd with exponential backoff retry -func publishConfigToEtcdWithRetry(key, value string, maxRetries int) error { - var lastErr error - baseDelay := 500 * time.Millisecond - - for attempt := range maxRetries { - if attempt > 0 { - delay := baseDelay * time.Duration(1< 0 { - logrus.Infof("Successfully published config to etcd after %d retries", attempt) - } - return nil - } - - lastErr = err - logrus.Warnf("Failed to publish config to etcd (attempt %d/%d): %v", attempt+1, maxRetries, err) - } - - return fmt.Errorf("failed to publish config to etcd after %d attempts: %w", maxRetries, lastErr) -} - -// waitForConfigUpdateResponse uses Redis Pub/Sub to synchronously wait for a response to a configuration update with timeout -func waitForConfigUpdateResponse(timeout time.Duration) (*dto.ConfigUpdateResponse, error) { - redisClient := client.GetRedisClient() - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - pubsub := redisClient.Subscribe(ctx, consts.ConfigUpdateResponseChannel) - defer func() { _ = pubsub.Close() }() - - if _, err := pubsub.Receive(ctx); err != nil { - return nil, fmt.Errorf("failed to confirm subscription: %w", err) - } - - msgChan := pubsub.Channel() - for { - select { - case <-ctx.Done(): - return nil, fmt.Errorf("timeout waiting for config update response after %v", timeout) - - case msg, ok := <-msgChan: - if !ok { - return nil, fmt.Errorf("subscription channel closed unexpectedly") - } - - var response dto.ConfigUpdateResponse - if err := json.Unmarshal([]byte(msg.Payload), &response); err != nil { - logrus.Warnf("failed to parse response message: %v", err) - continue - } - - logrus.WithFields(logrus.Fields{ - "response_id": response.ID, - "success": response.Success, - }).Info("Received matching config update response") - return &response, nil - } - } -} diff --git a/src/service/producer/evaluation.go b/src/service/producer/evaluation.go deleted file mode 100644 index 9b366e83..00000000 --- a/src/service/producer/evaluation.go +++ /dev/null @@ -1,44 +0,0 @@ -package producer - -import ( - "aegis/database" - "aegis/dto" - "aegis/repository" - "fmt" -) - -// ListEvaluations lists evaluations with pagination -func ListEvaluations(req *dto.ListEvaluationReq) (*dto.ListResp[dto.EvaluationResp], error) { - limit, offset := req.ToGormParams() - - evaluations, total, err := repository.ListEvaluations(database.DB, limit, offset) - if err != nil { - return nil, fmt.Errorf("failed to list evaluations: %w", err) - } - - evalResps := make([]dto.EvaluationResp, 0, len(evaluations)) - for _, eval := range evaluations { - evalResps = append(evalResps, *dto.NewEvaluationResp(&eval)) - } - - resp := dto.ListResp[dto.EvaluationResp]{ - Items: evalResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// GetEvaluation retrieves a single evaluation by ID -func GetEvaluation(id int) (*dto.EvaluationResp, error) { - eval, err := repository.GetEvaluationByID(database.DB, id) - if err != nil { - return nil, err - } - - return dto.NewEvaluationResp(eval), nil -} - -// DeleteEvaluation soft-deletes an evaluation by ID -func DeleteEvaluation(id int) error { - return repository.DeleteEvaluation(database.DB, id) -} diff --git a/src/service/producer/execution.go b/src/service/producer/execution.go deleted file mode 100644 index e20f8dfc..00000000 --- a/src/service/producer/execution.go +++ /dev/null @@ -1,421 +0,0 @@ -package producer - -import ( - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - "context" - "errors" - "fmt" - "strings" - "time" - - "gorm.io/gorm" -) - -// BatchCreateDetectorResults saves multiple detector results for a given execution -func BatchCreateDetectorResults(req *dto.UploadDetectorResultReq, executionID int) (*dto.UploadExecutionResultResp, error) { - err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := updateExecutionDuration(tx, executionID, req.Duration); err != nil { - return err - } - - var detectorResults []database.DetectorResult - for _, item := range req.Results { - detectorResults = append(detectorResults, *item.ConvertToDetectorResult(executionID)) - } - - if err := repository.SaveDetectorResults(tx, detectorResults); err != nil { - return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) - } - - return nil - }) - if err != nil { - return nil, err - } - - resp := &dto.UploadExecutionResultResp{ - ResultCount: len(req.Results), - UploadedAt: time.Now(), - HasAnomalies: req.HasAnomalies(), - } - return resp, nil -} - -// BatchCreateGranularityResults saves multiple granularity results for a given execution -func BatchCreateGranularityResults(req *dto.UploadGranularityResultReq, executionID int) (*dto.UploadExecutionResultResp, error) { - err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := updateExecutionDuration(tx, executionID, req.Duration); err != nil { - return err - } - - var granularityResults []database.GranularityResult - for _, item := range req.Results { - granularityResults = append(granularityResults, *item.ConvertToGranularityResult(executionID)) - } - - if err := repository.SaveGranularityResults(tx, granularityResults); err != nil { - return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) - } - - return nil - }) - if err != nil { - return nil, err - } - - resp := &dto.UploadExecutionResultResp{ - ResultCount: len(req.Results), - UploadedAt: time.Now(), - } - return resp, nil -} - -// BatchDeleteExecutions deletes multiple executions by their IDs -func BatchDeleteExecutionsByIDs(executionIDs []int) error { - if len(executionIDs) == 0 { - return nil - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - return batchDeleteExecutionsCore(tx, executionIDs) - }) -} - -// BatchDeleteExecutionsByLabels deletes fault executions based on label conditions -func BatchDeleteExecutionsByLabels(labelItems []dto.LabelItem) error { - if len(labelItems) == 0 { - return nil - } - - labelConditions := make([]map[string]string, 0, len(labelItems)) - for _, item := range labelItems { - labelConditions = append(labelConditions, map[string]string{ - "key": item.Key, - "value": item.Value, - }) - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - executionIDs, err := repository.ListExecutionIDsByLabels(database.DB, labelConditions) - if err != nil { - return fmt.Errorf("failed to list execution ids by labels: %w", err) - } - - return batchDeleteExecutionsCore(tx, executionIDs) - }) -} - -// GetExecutionDetail retrieves detailed information about a specific execution -func GetExecutionDetail(executionID int) (*dto.ExecutionDetailResp, error) { - execution, err := repository.GetExecutionByID(database.DB, executionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: execution id: %d", consts.ErrNotFound, executionID) - } - return nil, fmt.Errorf("failed to get execution: %w", err) - } - - labels, err := repository.ListLabelsByExecutionID(database.DB, execution.ID) - if err != nil { - return nil, fmt.Errorf("failed to get execution labels: %w", err) - } - - resp := dto.NewExecutionDetailResp(execution, labels) - - if execution.AlgorithmVersion.Container.Name == config.GetDetectorName() { - detectorResults, err := repository.ListDetectorResultsByExecutionID(database.DB, execution.ID) - if err != nil { - return nil, fmt.Errorf("failed to get detector results: %w", err) - } - - items := make([]dto.DetectorResultItem, 0, len(detectorResults)) - for _, result := range detectorResults { - items = append(items, dto.NewDetectorResultItem(&result)) - } - - resp.DetectorResults = items - } else { - granularityResults, err := repository.ListGranularityResultsByExecutionID(database.DB, execution.ID) - if err != nil { - return nil, fmt.Errorf("failed to get granularity results: %w", err) - } - - items := make([]dto.GranularityResultItem, 0, len(granularityResults)) - for _, result := range granularityResults { - items = append(items, dto.NewGranularityResultItem(&result)) - } - - resp.GranularityResults = items - } - - return resp, err -} - -// ListExecutions lists executions based on the provided request parameters -func ListExecutions(req *dto.ListExecutionReq) (*dto.ListResp[dto.ExecutionResp], error) { - limit, offset := req.ToGormParams() - - labelConditions := make([]map[string]string, 0, len(req.Labels)) - for _, item := range req.Labels { - parts := strings.SplitN(item, ":", 2) - labelConditions = append(labelConditions, map[string]string{ - "key": parts[0], - "value": parts[1], - }) - } - - executions, total, err := repository.ListExecutions(database.DB, limit, offset, req.State, req.Status, labelConditions) - if err != nil { - return nil, fmt.Errorf("failed to list executions: %w", err) - } - - executionIDs := make([]int, 0, len(executions)) - for _, execution := range executions { - executionIDs = append(executionIDs, execution.ID) - } - - labelsMap, err := repository.ListExecutionLabels(database.DB, executionIDs) - if err != nil { - return nil, fmt.Errorf("failed to list execution labels: %w", err) - } - - executionResps := make([]dto.ExecutionResp, 0, len(executions)) - for _, execution := range executions { - labels := labelsMap[execution.ID] - executionResps = append(executionResps, *dto.NewExecutionResp(&execution, labels)) - } - - resp := dto.ListResp[dto.ExecutionResp]{ - Items: executionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ListAvaliableExecutionLabels lists all available labels for executions -func ListAvaliableExecutionLabels() ([]dto.LabelItem, error) { - labelsMap, err := repository.ListLabelsGroupByCategory(database.DB) - if err != nil { - return nil, fmt.Errorf("failed to list labels grouped by category: %w", err) - } - - if _, exists := labelsMap[consts.ExecutionCategory]; !exists { - return []dto.LabelItem{}, nil - } - - labels := labelsMap[consts.ExecutionCategory] - labelItems := make([]dto.LabelItem, 0, len(labels)) - for _, label := range labels { - labelItems = append(labelItems, dto.LabelItem{ - Key: label.Key, - Value: label.Value, - }) - } - - return labelItems, nil -} - -// ManageExecutionLabels adds or removes labels for a specific execution -func ManageExecutionLabels(req *dto.ManageExecutionLabelReq, executionID int) (*dto.ExecutionResp, error) { - if req == nil { - return nil, fmt.Errorf("manage execution labels request is nil") - } - - var managedExecution *database.Execution - var managedLabels []database.Label - err := database.DB.Transaction(func(tx *gorm.DB) error { - execution, err := repository.GetExecutionByID(database.DB, executionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: execution id: %d", consts.ErrNotFound, executionID) - } - return fmt.Errorf("failed to get execution: %w", err) - } - - if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - labelIDs := make([]int, 0, len(labels)) - for _, label := range labels { - labelIDs = append(labelIDs, label.ID) - } - - if err := repository.AddExecutionLabels(tx, execution.ID, labelIDs); err != nil { - return fmt.Errorf("failed to add execution labels: %w", err) - } - } - - if len(req.RemoveLabels) > 0 { - labelIDs, err := repository.ListLabelIDsByKeyAndExecutionID(tx, execution.ID, req.RemoveLabels) - if err != nil { - return fmt.Errorf("failed to find label ids by keys: %w", err) - } - - if len(labelIDs) == 0 { - if err := repository.ClearExecutionLabels(tx, []int{executionID}, labelIDs); err != nil { - return fmt.Errorf("failed to clear execution labels: %w", err) - } - - if err := repository.BatchDecreaseLabelUsages(tx, labelIDs, 1); err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) - } - } - } - - labels, err := repository.ListLabelsByExecutionID(database.DB, executionID) - if err != nil { - return fmt.Errorf("failed to get execution labels: %w", err) - } - - managedExecution = execution - managedLabels = labels - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewExecutionResp(managedExecution, managedLabels), nil -} - -// ProduceAlgorithmExeuctionTasks produces execution tasks into Redis based on the submission request -func ProduceAlgorithmExeuctionTasks(ctx context.Context, req *dto.SubmitExecutionReq, groupID string, userID int) (*dto.SubmitExecutionResp, error) { - project, err := repository.GetProjectByName(database.DB, req.ProjectName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - - refs := make([]*dto.ContainerRef, 0, len(req.Specs)) - for _, spec := range req.Specs { - refs = append(refs, &spec.Algorithm.ContainerRef) - } - - algorithmVersionResults, err := common.MapRefsToContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) - if err != nil { - return nil, fmt.Errorf("failed to map container refs to versions: %w", err) - } - if len(algorithmVersionResults) == 0 { - return nil, fmt.Errorf("no valid algorithm versions found for the provided specs") - } - - var allExecutionItems []dto.SubmitExecutionItem - for idx, spec := range req.Specs { - datapacks, datasetID, err := extractDatapacks(database.DB, spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) - if err != nil { - return nil, fmt.Errorf("failed to extract datapacks: %w", err) - } - - algorithmVersion, exists := algorithmVersionResults[refs[idx]] - if !exists { - return nil, fmt.Errorf("algorithm version not found for %v", spec.Algorithm) - } - - var executionItems []dto.SubmitExecutionItem - for _, datapack := range datapacks { - if datapack.StartTime == nil || datapack.EndTime == nil { - return nil, fmt.Errorf("datapack %s does not have valid start_time and end_time", datapack.Name) - } - - algorithmItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := common.ListContainerVersionEnvVars(spec.Algorithm.EnvVars, &algorithmVersion) - if err != nil { - return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) - } - - algorithmItem.EnvVars = envVars - - payload := map[string]any{ - consts.ExecuteAlgorithm: algorithmItem, - consts.ExecuteDatapack: dto.NewInjectionItem(&datapack), - consts.ExecuteDatasetVersionID: utils.GetIntValue(datasetID, consts.DefaultInvalidID), - consts.ExecuteLabels: req.Labels, - } - - task := &dto.UnifiedTask{ - Type: consts.TaskTypeRunAlgorithm, - Immediate: true, - Payload: payload, - GroupID: groupID, - ProjectID: project.ID, - UserID: userID, - State: consts.TaskPending, - } - task.SetGroupCtx(ctx) - - err = common.SubmitTask(ctx, task) - if err != nil { - return nil, fmt.Errorf("failed to submit task: %w", err) - } - - executionItem := dto.SubmitExecutionItem{ - Index: idx, - TraceID: task.TraceID, - TaskID: task.TaskID, - AlgorithmID: algorithmVersion.ContainerID, - AlgorithmVersionID: algorithmVersion.ID, - DatapackID: &datapack.ID, - } - executionItems = append(executionItems, executionItem) - } - - allExecutionItems = append(allExecutionItems, executionItems...) - } - - resp := &dto.SubmitExecutionResp{ - GroupID: groupID, - Items: allExecutionItems, - } - return resp, nil -} - -// updateExecutionDuration updates the duration of an execution -func updateExecutionDuration(db *gorm.DB, executionID int, duration float64) error { - execution, err := repository.GetExecutionByID(db, executionID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) - } - return fmt.Errorf("execution %d not found: %w", executionID, err) - } - - if execution.Status != consts.CommonEnabled { - return fmt.Errorf("must upload results for an active execution %d", executionID) - } - - if execution.State == consts.ExecutionSuccess { - return fmt.Errorf("cannot upload results for a successful execution %d", executionID) - } - - if err := repository.UpdateExecution(db, executionID, map[string]any{ - "duration": duration, - }); err != nil { - return fmt.Errorf("failed to update execution %d duration: %w", executionID, err) - } - - return nil -} - -// batchDeleteExecutionsCore is the core logic for batch deleting executions -func batchDeleteExecutionsCore(db *gorm.DB, executionIDs []int) error { - if err := repository.RemoveLabelsFromExecutions(db, executionIDs); err != nil { - return fmt.Errorf("failed to delete execution labels: %w", err) - } - - if err := repository.BatchDeleteExecutions(db, executionIDs); err != nil { - return fmt.Errorf("failed to batch delete executions: %w", err) - } - - return nil -} diff --git a/src/service/producer/injection.go b/src/service/producer/injection.go deleted file mode 100644 index d63b6419..00000000 --- a/src/service/producer/injection.go +++ /dev/null @@ -1,1447 +0,0 @@ -package producer - -import ( - "aegis/client" - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - "archive/zip" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "time" - - chaos "github.com/OperationsPAI/chaos-experiment/handler" - "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -// injectionProcessItem represents a batch of parallel fault injections -type injectionProcessItem struct { - index int // Batch index in the original request - faultDuration int // Maximum duration among all faults in this batch - nodes []chaos.Node // Multiple fault nodes to be injected in parallel - executeTime time.Time // Execution time for this batch -} - -// BatchDeleteInjectionsByIDs deletes fault injections based on their IDs -func BatchDeleteInjectionsByIDs(injectionIDs []int) error { - if len(injectionIDs) == 0 { - return nil - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - return batchDeleteExecutionsCore(tx, injectionIDs) - }) -} - -// BatchDeleteInjectionsByLabels deletes fault injections based on label conditions -func BatchDeleteInjectionsByLabels(labelItems []dto.LabelItem) error { - if len(labelItems) == 0 { - return nil - } - - labelConditions := make([]map[string]string, 0, len(labelItems)) - for _, item := range labelItems { - labelConditions = append(labelConditions, map[string]string{ - "key": item.Key, - "value": item.Value, - }) - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - injectionIDs, err := repository.ListInjectionIDsByLabels(tx, labelConditions) - if err != nil { - return fmt.Errorf("failed to list injection ids by labels: %w", err) - } - - return batchDeleteInjectionsCore(tx, injectionIDs) - }) -} - -// CreateInjection creates a new fault injection along with its associated project-container relationships and labels -func CreateInjection(injection *database.FaultInjection, labelItems []dto.LabelItem) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - if err := repository.CreateInjection(tx, injection); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, injection.Name) - } - return fmt.Errorf("failed to create injection: %w", err) - } - - if len(labelItems) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, labelItems, consts.InjectionCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - // Collect label IDs - labelIDs := make([]int, 0, len(labels)) - for _, label := range labels { - labelIDs = append(labelIDs, label.ID) - } - - // AddInjectionLabels now takes injectionID and labelIDs (stores as TaskLabel internally) - if err := repository.AddInjectionLabels(tx, injection.ID, labelIDs); err != nil { - return fmt.Errorf("failed to add injection labels: %w", err) - } - } - - return nil - }) -} - -// UpdateGroundtruth updates the ground truth for an existing injection -func UpdateGroundtruth(id int, req *dto.UpdateGroundtruthReq) error { - // Verify injection exists - _, err := repository.GetInjectionByID(database.DB, id) - if err != nil { - return err - } - return repository.UpdateGroundtruth(database.DB, id, req.Groundtruths, consts.GroundtruthSourceManual) -} - -// GetInjectionDetail retrieves detailed information about a specific fault injection -func GetInjectionDetail(injectionID int) (*dto.InjectionDetailResp, error) { - logEntry := logrus.WithFields(logrus.Fields{ - "injectionID": injectionID, - }) - - injection, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - logEntry.Error("failed to get injection from repository: %w", err) - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return nil, fmt.Errorf("failed to get injection: %w", err) - } - - labels, err := repository.ListLabelsByInjectionID(database.DB, injection.ID) - if err != nil { - logEntry.Error("failed to get injection labels from repository: %w", err) - return nil, fmt.Errorf("failed to get injection labels: %w", err) - } - - injection.Labels = labels - resp := dto.NewInjectionDetailResp(injection) - - return resp, err -} - -// CloneInjection clones an existing injection with a new name -func CloneInjection(injectionID int, req *dto.CloneInjectionReq) (*dto.InjectionDetailResp, error) { - original, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return nil, fmt.Errorf("failed to get injection: %w", err) - } - - cloned := &database.FaultInjection{ - Name: req.Name, - FaultType: original.FaultType, - Category: original.Category, - Description: original.Description, - DisplayConfig: original.DisplayConfig, - EngineConfig: original.EngineConfig, - Groundtruths: original.Groundtruths, - PreDuration: original.PreDuration, - StartTime: original.StartTime, - EndTime: original.EndTime, - BenchmarkID: original.BenchmarkID, - PedestalID: original.PedestalID, - State: consts.DatapackInitial, - Status: consts.CommonEnabled, - } - - if err := CreateInjection(cloned, req.Labels); err != nil { - return nil, err - } - - labels, err := repository.ListLabelsByInjectionID(database.DB, cloned.ID) - if err != nil { - return nil, fmt.Errorf("failed to get cloned injection labels: %w", err) - } - - cloned.Labels = labels - return dto.NewInjectionDetailResp(cloned), nil -} - -// GetInjectionLogs retrieves execution logs for an injection from Loki -func GetInjectionLogs(injectionID int) (*dto.InjectionLogsResp, error) { - injection, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return nil, fmt.Errorf("failed to get injection: %w", err) - } - - resp := &dto.InjectionLogsResp{ - InjectionID: injectionID, - Logs: []string{}, - } - - if injection.TaskID != nil { - resp.TaskID = *injection.TaskID - - // Query historical logs from Loki - lokiCtx, lokiCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer lokiCancel() - - task, taskErr := repository.GetTaskByID(database.DB, *injection.TaskID) - if taskErr != nil { - logrus.Warnf("Failed to get task %s for log retrieval: %v", *injection.TaskID, taskErr) - return resp, nil - } - - lokiClient := client.NewLokiClient() - queryOpts := client.QueryOpts{ - Start: task.CreatedAt, - Direction: "forward", - } - - logEntries, lokiErr := lokiClient.QueryJobLogs(lokiCtx, *injection.TaskID, queryOpts) - if lokiErr != nil { - logrus.Warnf("Failed to query Loki for injection %d logs: %v", injectionID, lokiErr) - return resp, nil - } - - logs := make([]string, 0, len(logEntries)) - for _, entry := range logEntries { - logs = append(logs, entry.Line) - } - resp.Logs = logs - } - - return resp, nil -} - -// ListInjections lists fault injections based on the provided filters -func ListInjections(req *dto.ListInjectionReq) (*dto.ListResp[dto.InjectionResp], error) { - limit, offset := req.ToGormParams() - fitlerOptions := req.ToFilterOptions() - - injections, total, err := repository.ListInjections(database.DB, limit, offset, fitlerOptions) - if err != nil { - return nil, fmt.Errorf("failed to list injections: %w", err) - } - - injectionIDs := make([]int, 0, len(injections)) - for _, injection := range injections { - injectionIDs = append(injectionIDs, injection.ID) - } - - labelsMap, err := repository.ListInjectionLabels(database.DB, injectionIDs) - if err != nil { - return nil, fmt.Errorf("failed to list injection labels: %w", err) - } - - injectionResps := make([]dto.InjectionResp, 0, len(injections)) - for _, injection := range injections { - if labels, exists := labelsMap[injection.ID]; exists { - injection.Labels = labels - } - injectionResps = append(injectionResps, *dto.NewInjectionResp(&injection)) - } - - resp := dto.ListResp[dto.InjectionResp]{ - Items: injectionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// SearchInjections performs advanced search on fault injections -func SearchInjections(req *dto.SearchInjectionReq, projectID *int) (*dto.SearchResp[dto.InjectionDetailResp], error) { - if req == nil { - return nil, fmt.Errorf("search injection request is nil") - } - - searchReq := req.ConvertToSearchReq() - - // Add project filter if projectID is provided - if projectID != nil { - searchReq.AddFilter("project_id", dto.OpEqual, *projectID) - } - - injections, total, err := repository.ExecuteSearch(database.DB, searchReq, database.FaultInjection{}, consts.InjectionAllowedFields) - if err != nil { - return nil, fmt.Errorf("failed to search injections: %w", err) - } - - labelConditions := make([]map[string]string, 0, len(req.Labels)) - for _, item := range req.Labels { - labelConditions = append(labelConditions, map[string]string{ - "key": item.Key, - "value": item.Value, - }) - } - - filteredInjections := []database.FaultInjection{} - if len(labelConditions) > 0 { - injectionIDs, err := repository.ListInjectionIDsByLabels(database.DB, labelConditions) - if err != nil { - return nil, fmt.Errorf("failed to list injection ids by labels: %w", err) - } - - injectionIDMap := make(map[int]struct{}, len(injectionIDs)) - for _, id := range injectionIDs { - injectionIDMap[id] = struct{}{} - } - - for _, injection := range injections { - if _, exists := injectionIDMap[injection.ID]; exists { - filteredInjections = append(filteredInjections, injection) - } - } - } else { - filteredInjections = injections - } - - // Convert to response format - injectionResps := make([]dto.InjectionDetailResp, 0, len(filteredInjections)) - for _, injection := range filteredInjections { - injectionResps = append(injectionResps, *dto.NewInjectionDetailResp(&injection)) - } - - resp := &dto.SearchResp[dto.InjectionDetailResp]{ - Pagination: req.ConvertToPaginationInfo(total), - } - - if len(req.GroupBy) > 0 { - resp.Groups = dto.BuildGroupTree(injectionResps, req.GroupBy) - } else { - resp.Items = injectionResps - } - - return resp, nil -} - -// GetDatapackFilename returns the filename for downloading a datapack -func GetDatapackFilename(injectionID int) (string, error) { - injection, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return "", fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return "", fmt.Errorf("failed to get injection: %w", err) - } - - if injection.State < consts.DatapackBuildSuccess { - return "", fmt.Errorf("datapack for injection id %d is not ready for download", injectionID) - } - - return injection.Name, nil -} - -// DownloadDatapack handles the downloading of a specific datapack -func DownloadDatapack(zipWriter *zip.Writer, excludeRules []utils.ExculdeRule, injectionID int) error { - if zipWriter == nil { - return fmt.Errorf("zip writer cannot be nil") - } - - injection, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return fmt.Errorf("failed to get injection: %w", err) - } - - if err := packageDatapackToZip(zipWriter, injection, excludeRules); err != nil { - return fmt.Errorf("failed to package injection to zip: %w", err) - } - - return nil -} - -// GetDatapackFiles retrieves the file structure of a datapack in tree format -func GetDatapackFiles(datapackID int, baseURL string) (*dto.DatapackFilesResp, error) { - datapack, err := repository.GetInjectionByID(database.DB, datapackID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: datapack id: %d", consts.ErrNotFound, datapackID) - } - return nil, fmt.Errorf("failed to get datapack: %w", err) - } - - if datapack.State < consts.DatapackBuildSuccess { - return nil, fmt.Errorf("datapack %d is not ready", datapackID) - } - - workDir := filepath.Join(config.GetString("jfs.dataset_path"), datapack.Name) - if !utils.IsAllowedPath(workDir) { - return nil, fmt.Errorf("invalid path access to %s", workDir) - } - - // Check if directory exists - if _, err := os.Stat(workDir); os.IsNotExist(err) { - return nil, fmt.Errorf("datapack directory not found for datapack id %d", datapackID) - } - - resp := &dto.DatapackFilesResp{ - Files: []dto.DatapackFileItem{}, - FileCount: 0, - DirCount: 0, - } - - // Build tree structure - rootItems, err := buildFileTree(workDir, "", baseURL, datapackID, resp) - if err != nil { - return nil, fmt.Errorf("failed to build file tree: %w", err) - } - - resp.Files = rootItems - return resp, nil -} - -// DownloadDatapackFile downloads a specific file from a datapack -func DownloadDatapackFile(datapackID int, filePath string) (string, string, int64, io.ReadSeekCloser, error) { - fullPath, err := getFileFullPath(datapackID, filePath) - if err != nil { - return "", "", 0, nil, fmt.Errorf("invalid file path: %w", err) - } - - file, err := os.Open(fullPath) - if err != nil { - return "", "", 0, nil, fmt.Errorf("failed to open file: %w", err) - } - - stat, err := file.Stat() - if err != nil { - _ = file.Close() - return "", "", 0, nil, fmt.Errorf("failed to stat file: %w", err) - } - - fileName := filepath.Base(fullPath) - contentType := "application/octet-stream" - - // Determine content type based on file extension - switch filepath.Ext(fileName) { - case ".json": - contentType = "application/json" - case ".yaml", ".yml": - contentType = "application/x-yaml" - case ".txt", ".log": - contentType = "text/plain" - case ".csv": - contentType = "text/csv" - case ".xml": - contentType = "application/xml" - case ".html", ".htm": - contentType = "text/html" - case ".pdf": - contentType = "application/pdf" - case ".zip": - contentType = "application/zip" - case ".tar", ".gz", ".tgz": - contentType = "application/x-tar" - } - - return fileName, contentType, stat.Size(), file, nil -} - -// ListInjectionsNoissues handles the request to list fault injections without issues -func ListInjectionsNoIssues(req *dto.ListInjectionNoIssuesReq, projectID *int) ([]dto.InjectionNoIssuesResp, error) { - if len(req.Labels) == 0 { - return nil, nil - } - - labelConditions := make([]map[string]string, 0, len(req.Labels)) - for _, item := range req.Labels { - parts := strings.SplitN(item, ":", 2) - labelConditions = append(labelConditions, map[string]string{ - "key": parts[0], - "value": parts[1], - }) - } - - opts, err := req.Convert() - if err != nil { - return nil, fmt.Errorf("invalid time range: %w", err) - } - - records, err := repository.ListInjectionsNoIssues(database.DB, labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) - if err != nil { - return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) - } - - var items []dto.InjectionNoIssuesResp - for i, record := range records { - resp, err := dto.NewInjectionNoIssuesResp(record) - if err != nil { - return nil, fmt.Errorf("failed to create InjectionNoIssuesResp at index %d: %w", i, err) - } - - items = append(items, *resp) - } - - return items, nil -} - -// ListInjectionsNoissues handles the request to list fault injections without issues -func ListInjectionsWithIssues(req *dto.ListInjectionWithIssuesReq, projectID *int) ([]dto.InjectionWithIssuesResp, error) { - if len(req.Labels) == 0 { - return nil, nil - } - - labelConditions := make([]map[string]string, 0, len(req.Labels)) - for _, item := range req.Labels { - parts := strings.SplitN(item, ":", 2) - labelConditions = append(labelConditions, map[string]string{ - "key": parts[0], - "value": parts[1], - }) - } - - opts, err := req.Convert() - if err != nil { - return nil, fmt.Errorf("invalid time range: %w", err) - } - - records, err := repository.ListInjectionsWithIssues(database.DB, labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) - if err != nil { - return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) - } - - var items []dto.InjectionWithIssuesResp - for _, record := range records { - resp, err := dto.NewInjectionWithIssuesResp(record) - if err != nil { - return nil, fmt.Errorf("failed to create InjectionNoIssuesResp: %w", err) - } - - items = append(items, *resp) - } - - return items, nil -} - -// ManageInjectionTags manages labels associated with a fault injection -func ManageInjectionLabels(req *dto.ManageInjectionLabelReq, injectionID int) (*dto.InjectionResp, error) { - if req == nil { - return nil, fmt.Errorf("manage injection labels request is nil") - } - - var managedInjection *database.FaultInjection - - err := database.DB.Transaction(func(tx *gorm.DB) error { - injection, err := repository.GetInjectionByID(database.DB, injectionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, injectionID) - } - return fmt.Errorf("failed to get injection: %w", err) - } - - if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - // Collect label IDs - labelIDs := make([]int, 0, len(labels)) - for _, label := range labels { - labelIDs = append(labelIDs, label.ID) - } - - // AddInjectionLabels now takes injectionID and labelIDs (stores as TaskLabel internally) - if err := repository.AddInjectionLabels(tx, injection.ID, labelIDs); err != nil { - return fmt.Errorf("failed to add injection labels: %w", err) - } - } - - if len(req.RemoveLabels) > 0 { - labelIDs, err := repository.ListLabelIDsByKeyAndInjectionID(tx, injection.ID, req.RemoveLabels) - if err != nil { - return fmt.Errorf("failed to find label ids by keys: %w", err) - } - - if len(labelIDs) > 0 { - if err := repository.ClearInjectionLabels(tx, []int{injectionID}, labelIDs); err != nil { - return fmt.Errorf("failed to clear injection labels: %w", err) - } - - if err := repository.BatchDecreaseLabelUsages(tx, labelIDs, 1); err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) - } - } - } - - labels, err := repository.ListLabelsByInjectionID(database.DB, injectionID) - if err != nil { - return fmt.Errorf("failed to get injection labels: %w", err) - } - - injection.Labels = labels - managedInjection = injection - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewInjectionResp(managedInjection), nil -} - -// BatchManageInjectionLabels adds or removes labels from multiple injections -// Each injection can have its own set of label operations -func BatchManageInjectionLabels(req *dto.BatchManageInjectionLabelReq) (*dto.BatchManageInjectionLabelResp, error) { - if req == nil { - return nil, fmt.Errorf("batch manage injection labels request is nil") - } - - resp := &dto.BatchManageInjectionLabelResp{ - FailedCount: 0, - FailedItems: []string{}, - SuccessCount: 0, - SuccessItems: []dto.InjectionResp{}, - } - - if len(req.Items) == 0 { - return resp, nil - } - - // Process all operations in a single transaction - return resp, database.DB.Transaction(func(tx *gorm.DB) error { - // Step 1: Collect all injection IDs and verify they exist (batch query) - allInjectionIDs := make([]int, 0, len(req.Items)) - operationMap := make(map[int]*dto.InjectionLabelOperation) - - for i := range req.Items { - item := &req.Items[i] - allInjectionIDs = append(allInjectionIDs, item.InjectionID) - operationMap[item.InjectionID] = item - } - - injections, err := repository.ListFaultInjectionsByID(tx, allInjectionIDs) - if err != nil { - return fmt.Errorf("failed to list injections: %w", err) - } - - foundIDMap := make(map[int]*database.FaultInjection) - for i := range injections { - foundIDMap[injections[i].ID] = &injections[i] - } - - // Track which IDs were not found - validIDs := make([]int, 0, len(foundIDMap)) - for _, id := range allInjectionIDs { - if _, found := foundIDMap[id]; !found { - resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d not found", id)) - resp.FailedCount++ - delete(operationMap, id) // Remove from operations - } else { - validIDs = append(validIDs, id) - } - } - - if len(validIDs) == 0 { - return fmt.Errorf("no valid injection IDs found") - } - - // Step 2: Collect all unique labels from all operations and create them in batch - allAddLabels := make([]dto.LabelItem, 0) - allRemoveLabels := make([]dto.LabelItem, 0) - labelKeySet := make(map[string]bool) - - for _, op := range operationMap { - for _, label := range op.AddLabels { - key := label.Key + ":" + label.Value - if !labelKeySet[key] { - labelKeySet[key] = true - allAddLabels = append(allAddLabels, label) - } - } - for _, label := range op.RemoveLabels { - key := label.Key + ":" + label.Value - if !labelKeySet[key] { - labelKeySet[key] = true - allRemoveLabels = append(allRemoveLabels, label) - } - } - } - - // Create or update all labels in batch - var labelMap map[string]int // key:value -> label_id - if len(allAddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - labelMap = make(map[string]int) - for _, label := range labels { - key := label.Key + ":" + label.Value - labelMap[key] = label.ID - } - } - - // Get label IDs for removal labels - var removeLabelMap map[string]int // key:value -> label_id - if len(allRemoveLabels) > 0 { - labelConditions := make([]map[string]string, 0, len(allRemoveLabels)) - for _, item := range allRemoveLabels { - labelConditions = append(labelConditions, map[string]string{ - "key": item.Key, - "value": item.Value, - }) - } - - labelIDs, err := repository.ListLabelIDsByConditions(tx, labelConditions, consts.InjectionCategory) - if err != nil { - return fmt.Errorf("failed to find labels to remove: %w", err) - } - - // Map them back for quick lookup - if len(labelIDs) > 0 { - labels, err := repository.ListLabelsByID(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to list labels by IDs: %w", err) - } - - removeLabelMap = make(map[string]int) - for _, label := range labels { - key := label.Key + ":" + label.Value - removeLabelMap[key] = label.ID - } - } - } - - // Step 3: Process each injection's operations - for _, injectionID := range validIDs { - op := operationMap[injectionID] - - if len(op.AddLabels) > 0 { - labelIDsToAdd := make([]int, 0, len(op.AddLabels)) - for _, label := range op.AddLabels { - key := label.Key + ":" + label.Value - if labelID, exists := labelMap[key]; exists { - labelIDsToAdd = append(labelIDsToAdd, labelID) - } - } - - if len(labelIDsToAdd) > 0 { - if err := repository.AddInjectionLabels(tx, injectionID, labelIDsToAdd); err != nil { - resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to add labels - %s", injectionID, err.Error())) - resp.FailedCount++ - delete(foundIDMap, injectionID) - continue - } - } - } - - if len(op.RemoveLabels) > 0 && removeLabelMap != nil { - labelIDsToRemove := make([]int, 0, len(op.RemoveLabels)) - for _, label := range op.RemoveLabels { - key := label.Key + ":" + label.Value - if labelID, exists := removeLabelMap[key]; exists { - labelIDsToRemove = append(labelIDsToRemove, labelID) - } - } - - if len(labelIDsToRemove) > 0 { - if err := repository.ClearInjectionLabels(tx, []int{injectionID}, labelIDsToRemove); err != nil { - resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to remove labels - %s", injectionID, err.Error())) - resp.FailedCount++ - delete(foundIDMap, injectionID) - continue - } - } - } - } - - // Step 4: Fetch updated injection data with labels (batch query) - if len(foundIDMap) > 0 { - successIDs := make([]int, 0, len(foundIDMap)) - for id := range foundIDMap { - successIDs = append(successIDs, id) - } - - updatedInjections, err := repository.ListFaultInjectionsByID(tx, successIDs) - if err != nil { - return fmt.Errorf("failed to fetch updated injections: %w", err) - } - - labelsMap, err := repository.ListInjectionLabels(tx, successIDs) - if err != nil { - return fmt.Errorf("failed to list injection labels: %w", err) - } - - for i := range updatedInjections { - injection := &updatedInjections[i] - if labels, exists := labelsMap[injection.ID]; exists { - injection.Labels = labels - } - injectionResp := dto.NewInjectionResp(injection) - resp.SuccessItems = append(resp.SuccessItems, *injectionResp) - resp.SuccessCount++ - } - } - - return nil - }) -} - -// ProduceRestartPedestalTasks produces pedestal restart tasks with support for parallel fault injection -func ProduceRestartPedestalTasks(ctx context.Context, req *dto.SubmitInjectionReq, groupID string, userID int, projectID *int) (*dto.SubmitInjectionResp, error) { - if req == nil { - return nil, fmt.Errorf("submit injection request is nil") - } - - if projectID == nil { - project, err := repository.GetProjectByName(database.DB, req.ProjectName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - projectID = &project.ID - } - - pedestalVersionResults, err := common.MapRefsToContainerVersions([]*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) - if err != nil { - return nil, fmt.Errorf("failed to map pedestal container ref to version: %w", err) - } - - pedestalVersion, exists := pedestalVersionResults[&req.Pedestal.ContainerRef] - if !exists { - return nil, fmt.Errorf("pedestal version not found for container: %s (version: %s)", req.Pedestal.Name, req.Pedestal.Version) - } - - helmConfig, err := repository.GetHelmConfigByContainerVersionID(database.DB, pedestalVersion.ID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: helm config not found for pedestal version id %d", consts.ErrNotFound, pedestalVersion.ID) - } - return nil, fmt.Errorf("failed to get helm config: %w", err) - } - - params := flattenYAMLToParameters(req.Pedestal.Payload, "") - helmValues, err := common.ListHelmConfigValues(params, helmConfig) - if err != nil { - return nil, fmt.Errorf("failed to render pedestal helm values: %w", err) - } - - helmConfigItem := dto.NewHelmConfigItem(helmConfig) - helmConfigItem.DynamicValues = helmValues - - pedestalItem := dto.NewContainerVersionItem(&pedestalVersion) - pedestalItem.Extra = helmConfigItem - - benchmarkVersionResults, err := common.MapRefsToContainerVersions([]*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) - if err != nil { - return nil, fmt.Errorf("failed to map benchmark container ref to version: %w", err) - } - - benchmarkVersion, exists := benchmarkVersionResults[&req.Benchmark.ContainerRef] - if !exists { - return nil, fmt.Errorf("benchmark version not found for container: %s (version: %s)", req.Benchmark.Name, req.Benchmark.Version) - } - - benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := common.ListContainerVersionEnvVars(req.Benchmark.EnvVars, &benchmarkVersion) - if err != nil { - return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) - } - - benchmarkVersionItem.EnvVars = envVars - - // Parse each batch and collect items - processedItems := make([]injectionProcessItem, 0, len(req.Specs)) - var parseWarnings []string - for i := range req.Specs { - item, warning, err := parseBatchInjectionSpecs(ctx, pedestalItem.ContainerName, i, req.Specs[i]) - if err != nil { - return nil, fmt.Errorf("failed to parse injection spec batch %d: %w", i, err) - } - - if warning != "" { - parseWarnings = append(parseWarnings, warning) - } else { - processedItems = append(processedItems, *item) - } - } - - // Remove duplicated batches - uniqueItems, duplicatedInRequest, alreadyExisted, err := removeDuplicated(processedItems) - if err != nil { - return nil, fmt.Errorf("failed to remove duplicated batches: %w", err) - } - - // Collect warnings about duplications - var warnings *dto.InjectionWarnings - if len(parseWarnings) > 0 || len(duplicatedInRequest) > 0 || len(alreadyExisted) > 0 { - warnings = &dto.InjectionWarnings{ - DuplicateServicesInBatch: parseWarnings, - DuplicateBatchesInRequest: duplicatedInRequest, - BatchesExistInDatabase: alreadyExisted, - } - } - - if len(req.Algorithms) > 0 { - refs := make([]*dto.ContainerRef, 0, len(req.Algorithms)) - for i := range req.Algorithms { - refs = append(refs, &req.Algorithms[i].ContainerRef) - } - - algorithmVersionsResults, err := common.MapRefsToContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) - if err != nil { - return nil, fmt.Errorf("failed to map container refs to versions: %w", err) - } - - var algorithmVersionItems []dto.ContainerVersionItem - for i := range req.Algorithms { - spec := &req.Algorithms[i] - algorithmVersion, exists := algorithmVersionsResults[&spec.ContainerRef] - if !exists { - return nil, fmt.Errorf("algorithm version not found for %v", spec) - } - - algorithmVersionItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := common.ListContainerVersionEnvVars(spec.EnvVars, &algorithmVersion) - if err != nil { - return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) - } - - algorithmVersionItem.EnvVars = envVars - algorithmVersionItems = append(algorithmVersionItems, algorithmVersionItem) - } - - if len(algorithmVersionItems) > 0 { - if err := client.SetHashField(ctx, consts.InjectionAlgorithmsKey, groupID, algorithmVersionItems); err != nil { - return nil, fmt.Errorf("failed to store injection algorithms: %w", err) - } - } - } - - injectionItems := make([]dto.SubmitInjectionItem, 0, len(uniqueItems)) - for _, item := range uniqueItems { - payload := map[string]any{ - consts.RestartPedestal: pedestalItem, - consts.RestartHelmConfig: helmConfig, - consts.RestartIntarval: req.Interval, - consts.RestartFaultDuration: item.faultDuration, - consts.RestartInjectPayload: map[string]any{ - consts.InjectBenchmark: benchmarkVersionItem, - consts.InjectPreDuration: req.PreDuration, - consts.InjectNodes: item.nodes, - consts.InjectLabels: req.Labels, - consts.InjectSystem: chaos.SystemType(pedestalItem.ContainerName), - }, - } - - task := &dto.UnifiedTask{ - Type: consts.TaskTypeRestartPedestal, - Immediate: false, - ExecuteTime: item.executeTime.Unix(), - Payload: payload, - GroupID: groupID, - ProjectID: *projectID, - UserID: userID, - State: consts.TaskPending, - Extra: map[consts.TaskExtra]any{ - consts.TaskExtraInjectionAlgorithms: len(req.Algorithms), - }, - } - task.SetGroupCtx(ctx) - - err := common.SubmitTask(ctx, task) - if err != nil { - return nil, fmt.Errorf("failed to submit fault injection task: %w", err) - } - - injectionItems = append(injectionItems, dto.SubmitInjectionItem{ - Index: item.index, - TraceID: task.TraceID, - TaskID: task.TaskID, - }) - } - - sort.Slice(injectionItems, func(i, j int) bool { - return injectionItems[i].Index < injectionItems[j].Index - }) - - return &dto.SubmitInjectionResp{ - GroupID: groupID, - Items: injectionItems, - OriginalCount: len(processedItems), - Warnings: warnings, - }, nil -} - -// ProduceDatapackBuildingTasks produces datapack building tasks into Redis based on the request specifications -func ProduceDatapackBuildingTasks(ctx context.Context, req *dto.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*dto.SubmitDatapackBuildingResp, error) { - if req == nil { - return nil, fmt.Errorf("submit datapack building request is nil") - } - - if projectID == nil { - // Use project name from request - project, err := repository.GetProjectByName(database.DB, req.ProjectName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - projectID = &project.ID - } - - refs := make([]*dto.ContainerRef, 0, len(req.Specs)) - for _, spec := range req.Specs { - refs = append(refs, &spec.Benchmark.ContainerRef) - } - - benchmarkVersionResults, err := common.MapRefsToContainerVersions(refs, consts.ContainerTypeBenchmark, userID) - if err != nil { - return nil, fmt.Errorf("failed to map container refs to versions: %w", err) - } - - var allBuildingItems []dto.SubmitBuildingItem - for idx, spec := range req.Specs { - datapacks, datasetVersionID, err := extractDatapacks(database.DB, spec.Datapack, spec.Dataset, userID, consts.TaskTypeBuildDatapack) - if err != nil { - return nil, fmt.Errorf("failed to extract datapacks: %w", err) - } - - benchmarkVersion, exists := benchmarkVersionResults[refs[idx]] - if !exists { - return nil, fmt.Errorf("benchmark version not found for %v", spec.Benchmark) - } - - benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := common.ListContainerVersionEnvVars(spec.Benchmark.EnvVars, &benchmarkVersion) - if err != nil { - return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) - } - - benchmarkVersionItem.EnvVars = envVars - - var buildingItems []dto.SubmitBuildingItem - for _, datapack := range datapacks { - if datapack.StartTime == nil || datapack.EndTime == nil { - return nil, fmt.Errorf("datapack %s does not have valid start_time and end_time", datapack.Name) - } - - payload := map[string]any{ - consts.BuildBenchmark: benchmarkVersionItem, - consts.BuildDatapack: dto.NewInjectionItem(&datapack), - consts.BuildDatasetVersionID: datasetVersionID, - consts.BuildLabels: req.Labels, - } - - task := &dto.UnifiedTask{ - Type: consts.TaskTypeBuildDatapack, - Immediate: true, - Payload: payload, - GroupID: groupID, - ProjectID: *projectID, - UserID: userID, - State: consts.TaskPending, - } - task.SetGroupCtx(ctx) - - err = common.SubmitTask(ctx, task) - if err != nil { - return nil, fmt.Errorf("failed to submit datapack building task: %w", err) - } - - buildingItems = append(buildingItems, dto.SubmitBuildingItem{ - Index: idx, - TraceID: task.TraceID, - TaskID: task.TaskID, - }) - } - - allBuildingItems = append(allBuildingItems, buildingItems...) - } - - resp := &dto.SubmitDatapackBuildingResp{ - GroupID: groupID, - Items: allBuildingItems, - } - return resp, nil -} - -func batchDeleteInjectionsCore(db *gorm.DB, injectionIDs []int) error { - executions, err := repository.ListExecutionsByDatapackIDs(db, injectionIDs) - if err != nil { - return fmt.Errorf("failed to list executions by datapack ids: %w", err) - } - - if len(executions) == 0 { - return fmt.Errorf("no executions found for the given injection ids") - } - - executionIDs := make([]int, 0, len(executions)) - for _, execution := range executions { - executionIDs = append(executionIDs, execution.ID) - } - - if err := batchDeleteExecutionsCore(db, executionIDs); err != nil { - return fmt.Errorf("failed to batch delete executions: %v", err) - } - - if err := repository.ClearInjectionLabels(db, injectionIDs, nil); err != nil { - return fmt.Errorf("failed to clear injection labels: %w", err) - } - - if err := repository.BatchDeleteInjections(db, injectionIDs); err != nil { - return fmt.Errorf("failed to delete injections: %w", err) - } - - return nil -} - -// parseBatchInjectionSpecs parses a single batch of fault injection specifications for parallel execution -// Returns the processed item, a warning message (if any), and an error -func parseBatchInjectionSpecs(ctx context.Context, pedestal string, batchIndex int, specs []chaos.Node) (*injectionProcessItem, string, error) { - if len(specs) == 0 { - return nil, "", fmt.Errorf("empty fault injection batch at index %d", batchIndex) - } - - // Extract fault duration - use the maximum duration among all faults in the batch - maxDuration := 0 - nodes := make([]chaos.Node, 0, len(specs)) - - for idx, spec := range specs { - childNode, exists := spec.Children[strconv.Itoa(spec.Value)] - if !exists { - return nil, "", fmt.Errorf("failed to find key %d in the children at index %d", spec.Value, idx) - } - - if len(childNode.Children) < 3 { - return nil, "", fmt.Errorf("no child nodes found for fault spec at index %d", idx) - } - - faultDuration := childNode.Children[consts.DurationNodeKey].Value - if faultDuration > maxDuration { - maxDuration = faultDuration - } - - systemIdx := childNode.Children[consts.SystemNodeKey].Value - system := chaos.GetAllSystemTypes()[systemIdx] - if pedestal != system.String() { - return nil, "", fmt.Errorf("mismatched system type %s for pedestal %s at index %d", system.String(), pedestal, idx) - } - - nodes = append(nodes, spec) - } - - uniqueServices := make(map[string]int, len(nodes)) - var duplicateServiceWarnings []string - for idx, node := range nodes { - conf, err := chaos.NodeToStruct[chaos.InjectionConf](&node) - if err != nil { - return nil, "", fmt.Errorf("failed to convert node to InjectionConf at index %d: %w", idx, err) - } - - groundtruth, err := conf.GetGroundtruth() - if err != nil { - return nil, "", fmt.Errorf("failed to get groundtruth from InjectionConf at index %d: %w", idx, err) - } - - for _, service := range groundtruth.Service { - if service != "" { - if oldIdx, exists := uniqueServices[service]; exists { - duplicateServiceWarnings = append(duplicateServiceWarnings, - fmt.Sprintf("service '%s' at positions %d and %d", service, oldIdx, idx)) - continue - } - uniqueServices[service] = idx - } - } - } - - // Sort nodes to ensure consistent ordering - nodes = sortNodes(nodes) - - var warning string - if len(duplicateServiceWarnings) > 0 { - warning = fmt.Sprintf("Batch %d contains duplicate service injections: %s", - batchIndex, strings.Join(duplicateServiceWarnings, "; ")) - } - - return &injectionProcessItem{ - index: batchIndex, - faultDuration: maxDuration, - nodes: nodes, - }, warning, nil -} - -// flattenYAMLToParameters converts nested YAML map to flat parameter specs -func flattenYAMLToParameters(data map[string]any, prefix string) []dto.ParameterSpec { - var params []dto.ParameterSpec - - for key, value := range data { - fullKey := key - if prefix != "" { - fullKey = prefix + "." + key - } - - switch v := value.(type) { - case map[string]any: - // Recursively flatten nested structures - params = append(params, flattenYAMLToParameters(v, fullKey)...) - case []any: - // Convert array to JSON string - jsonBytes, err := json.Marshal(v) - if err != nil { - logrus.Warnf("Failed to marshal array for key %s: %v", fullKey, err) - continue - } - params = append(params, dto.ParameterSpec{ - Key: fullKey, - Value: string(jsonBytes), - }) - default: - // Primitive values (string, int, bool, etc.) - params = append(params, dto.ParameterSpec{ - Key: fullKey, - Value: v, - }) - } - } - - return params -} - -// removeDuplicated filters out batches that already exist in DB and removes duplicates within the request -func removeDuplicated(items []injectionProcessItem) ([]injectionProcessItem, []int, []int, error) { - engineConfigStrs := make([]string, len(items)) - for i, item := range items { - if len(item.nodes) == 0 { - engineConfigStrs[i] = "" - continue - } - - // Marshal the entire batch of nodes as the engine config - b, err := json.Marshal(item.nodes) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to marshal engine config at batch index %d: %w", i, err) - } - - engineConfigStrs[i] = string(b) - } - - orderedUniqueIdx := make([]int, 0, len(engineConfigStrs)) - seen := make(map[string]struct{}, len(engineConfigStrs)) - duplicatedInRequest := make([]int, 0) - for i, key := range engineConfigStrs { - if key == "" { - orderedUniqueIdx = append(orderedUniqueIdx, i) - continue - } - if _, ok := seen[key]; ok { - duplicatedInRequest = append(duplicatedInRequest, items[i].index) - continue - } - - seen[key] = struct{}{} - orderedUniqueIdx = append(orderedUniqueIdx, i) - } - - existed := make(map[string]struct{}) - keys := make([]string, 0, len(seen)) - for k := range seen { - if k != "" { - keys = append(keys, k) - } - } - - batchSize := 100 - for start := 0; start < len(keys); start += batchSize { - end := min(start+batchSize, len(keys)) - - batch := keys[start:end] - existing, err := repository.ListExistingEngineConfigs(database.DB, batch) - if err != nil { - return nil, nil, nil, err - } - - for _, v := range existing { - existed[v] = struct{}{} - } - } - - out := make([]injectionProcessItem, 0, len(orderedUniqueIdx)) - alreadyExisted := make([]int, 0) // Track batch indices that already exist in DB - for _, idx := range orderedUniqueIdx { - key := engineConfigStrs[idx] - if key == "" { - out = append(out, items[idx]) - continue - } - if _, ok := existed[key]; ok { - alreadyExisted = append(alreadyExisted, items[idx].index) - continue - } - - items[idx].executeTime = time.Now().Add(time.Duration(idx*2) * time.Second) - out = append(out, items[idx]) - } - - return out, duplicatedInRequest, alreadyExisted, nil -} - -// sortNodes sorts chaos nodes by their Value field and then by their JSON representation for consistency -func sortNodes(nodes []chaos.Node) []chaos.Node { - if len(nodes) <= 1 { - return nodes - } - - // Create a copy to avoid modifying the original slice - sortedNodes := make([]chaos.Node, len(nodes)) - copy(sortedNodes, nodes) - - // Sort nodes by their Value field first, then by serialized representation for consistency - // Using a stable sort to maintain relative order for equal elements - for i := 0; i < len(sortedNodes)-1; i++ { - for j := i + 1; j < len(sortedNodes); j++ { - // Primary sort: by Value field - if sortedNodes[i].Value > sortedNodes[j].Value { - sortedNodes[i], sortedNodes[j] = sortedNodes[j], sortedNodes[i] - continue - } - - // Secondary sort: if Values are equal, sort by JSON representation for consistency - if sortedNodes[i].Value == sortedNodes[j].Value { - iJSON, _ := json.Marshal(sortedNodes[i]) - jJSON, _ := json.Marshal(sortedNodes[j]) - if string(iJSON) > string(jJSON) { - sortedNodes[i], sortedNodes[j] = sortedNodes[j], sortedNodes[i] - } - } - } - } - - return sortedNodes -} - -// buildFileTree recursively builds a tree structure of files and directories -func buildFileTree(workDir, relPath string, baseURL string, datapackID int, resp *dto.DatapackFilesResp) ([]dto.DatapackFileItem, error) { - currentPath := filepath.Join(workDir, relPath) - entries, err := os.ReadDir(currentPath) - if err != nil { - return nil, err - } - - var items []dto.DatapackFileItem - for _, entry := range entries { - itemRelPath := filepath.Join(relPath, entry.Name()) - - fileInfo, err := entry.Info() - if err != nil { - return nil, err - } - - item := dto.DatapackFileItem{ - Name: entry.Name(), - Path: filepath.ToSlash(itemRelPath), - } - - if entry.IsDir() { - children, err := buildFileTree(workDir, itemRelPath, baseURL, datapackID, resp) - if err != nil { - return nil, err - } - item.Children = children - - // Count direct subfolders and files - subFolderCount := 0 - fileCount := 0 - for _, child := range children { - if len(child.Children) > 0 { - subFolderCount++ - } else { - fileCount++ - } - } - - item.Size = fmt.Sprintf("%d subfolders, %d files", subFolderCount, fileCount) - resp.DirCount++ - } else { - fileSize := fileInfo.Size() - item.Size = formatFileSize(fileSize) - modTime := fileInfo.ModTime() - item.ModTime = &modTime - resp.FileCount++ - } - - items = append(items, item) - } - - return items, nil -} - -// formatFileSize formats bytes to human readable format (KB or MB) with one decimal place. -func formatFileSize(bytes int64) string { - const ( - KB = 1024 - MB = 1024 * 1024 - ) - - if bytes < MB { - return fmt.Sprintf("%.1fKB", float64(bytes)/float64(KB)) - } - return fmt.Sprintf("%.1fMB", float64(bytes)/float64(MB)) -} - -func getFileFullPath(datapackID int, filePath string) (string, error) { - datapack, err := repository.GetInjectionByID(database.DB, datapackID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return "", fmt.Errorf("%w: datapack id: %d", consts.ErrNotFound, datapackID) - } - return "", fmt.Errorf("failed to get datapack: %w", err) - } - - if datapack.State < consts.DatapackBuildSuccess { - return "", fmt.Errorf("datapack %d is not ready for download", datapackID) - } - - workDir := filepath.Join(config.GetString("jfs.dataset_path"), datapack.Name) - if !utils.IsAllowedPath(workDir) { - return "", fmt.Errorf("invalid path access to %s", workDir) - } - - cleanPath := filepath.Clean(filePath) - fullPath := filepath.Join(workDir, cleanPath) - - if !strings.HasPrefix(fullPath, workDir) { - return "", fmt.Errorf("invalid file path: path traversal detected") - } - if !utils.IsAllowedPath(fullPath) { - return "", fmt.Errorf("invalid file path access") - } - - fileInfo, err := os.Stat(fullPath) - if err != nil { - if os.IsNotExist(err) { - return "", fmt.Errorf("%w: file not found: %s", consts.ErrNotFound, cleanPath) - } - return "", fmt.Errorf("failed to stat file: %w", err) - } - - if fileInfo.IsDir() { - return "", fmt.Errorf("path is a directory, not a file: %s", cleanPath) - } - - return fullPath, nil -} diff --git a/src/service/producer/label.go b/src/service/producer/label.go deleted file mode 100644 index 0758280f..00000000 --- a/src/service/producer/label.go +++ /dev/null @@ -1,352 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// BatchDeleteLabels deletes multiple labels and their associations in a transaction -func BatchDeleteLabels(labelIDs []int) error { - if len(labelIDs) == 0 { - return nil - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - labels, err := repository.ListLabelsByID(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to list labels by IDs: %w", err) - } - - if len(labels) == 0 { - return fmt.Errorf("no labels found for the provided IDs") - } - if len(labels) != len(labelIDs) { - return fmt.Errorf("some labels not found for the provided IDs") - } - - labelMap := make(map[int]*database.Label, len(labels)) - for _, label := range labels { - labelMap[label.ID] = &label - } - - containerCountMap, err := removeContainersFromLabels(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to delete container-label associations: %v", err) - } - - datasetCountMap, err := removeDatasetsFromLabels(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to delete dataset-label associations: %v", err) - } - - projectCountMap, err := removeProjectsFromLabels(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to delete project-label associations: %v", err) - } - - injectionCountMap, err := removeInjectionsFromLabels(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to delete injection-label associations: %v", err) - } - - executionCountMap, err := removeExecutionsFromLabels(tx, labelIDs) - if err != nil { - return fmt.Errorf("failed to delete execution-label associations: %v", err) - } - - toUpdatedLabels := make([]database.Label, 0, len(labelIDs)) - for labelID, label := range labelMap { - totalDecrement := int64(0) - - if count, exists := containerCountMap[labelID]; exists { - totalDecrement += count - } - if count, exists := datasetCountMap[labelID]; exists { - totalDecrement += count - } - if count, exists := projectCountMap[labelID]; exists { - totalDecrement += count - } - if count, exists := injectionCountMap[labelID]; exists { - totalDecrement += count - } - if count, exists := executionCountMap[labelID]; exists { - totalDecrement += count - } - - label.Usage = max(label.Usage-int(totalDecrement), 0) - toUpdatedLabels = append(toUpdatedLabels, *label) - } - - if err := repository.BatchUpdateLabels(tx, toUpdatedLabels); err != nil { - return fmt.Errorf("failed to update label usages: %v", err) - } - - if err := repository.BatchDeleteLabels(tx, labelIDs); err != nil { - return fmt.Errorf("failed to batch delete labels: %v", err) - } - - return nil - }) -} - -// CreateLabel creates a new label or reactivates an existing deleted one -func CreateLabel(req *dto.CreateLabelReq) (*dto.LabelResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("label validation failed: %w", err) - } - - label := req.ConvertToLabel() - - var createdLabel *database.Label - err := database.DB.Transaction(func(tx *gorm.DB) error { - label, err := CreateLabelCore(tx, label) - if err != nil { - return fmt.Errorf("failed to create label: %w", err) - } - - createdLabel = label - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewLabelResp(createdLabel), nil -} - -// CreateLabelCore performs the core logic of creating a label within a transaction -func CreateLabelCore(db *gorm.DB, label *database.Label) (*database.Label, error) { - existingLabel, err := repository.GetLabelByKeyAndValue(db, label.Key, label.Value) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("failed to check existing label: %w", err) - } - - if existingLabel == nil { - if err := repository.CreateLabel(db, label); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return nil, fmt.Errorf("%w: label with key %s and value %s already exists", consts.ErrAlreadyExists, label.Key, label.Value) - } - return nil, fmt.Errorf("failed to create label: %w", err) - } - - return label, nil - } - - existingLabel.Category = label.Category - existingLabel.Description = label.Description - existingLabel.Color = label.Color - existingLabel.Status = consts.CommonEnabled - - if err := repository.UpdateLabel(db, existingLabel); err != nil { - return nil, fmt.Errorf("failed to update existing label: %w", err) - } - - return existingLabel, nil -} - -// DeleteLabel deletes a label by its ID -func DeleteLabel(labelID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - label, err := repository.GetLabelByID(tx, labelID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: label with id %d not found", consts.ErrNotFound, labelID) - } - return fmt.Errorf("failed to get label: %v", err) - } - - // Delete all related associations - containerRows, err := repository.RemoveContainersFromLabel(tx, label.ID) - if err != nil { - return fmt.Errorf("failed to delete container-label associations: %v", err) - } - - datasetRows, err := repository.RemoveDatasetsFromLabel(tx, label.ID) - if err != nil { - return fmt.Errorf("failed to delete dataset-label associations: %v", err) - } - - projectRows, err := repository.RemoveProjectsFromLabel(tx, label.ID) - if err != nil { - return fmt.Errorf("failed to delete project-label associations: %v", err) - } - - injectionRows, err := repository.RemoveInjectionsFromLabel(tx, label.ID) - if err != nil { - return fmt.Errorf("failed to delete injection-label associations: %v", err) - } - - executionRows, err := repository.RemoveExecutionsFromLabel(tx, label.ID) - if err != nil { - return fmt.Errorf("failed to delete execution-label associations: %v", err) - } - - totalRows := int(containerRows + datasetRows + projectRows + injectionRows + executionRows) - if err := repository.BatchDecreaseLabelUsages(tx, []int{label.ID}, totalRows); err != nil { - return fmt.Errorf("failed to decrease label usage: %v", err) - } - - // Delete the label itself - rows, err := repository.DeleteLabel(tx, labelID) - if err != nil { - return fmt.Errorf("failed to delete label: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: label id %d not found", consts.ErrNotFound, labelID) - } - - return nil - }) -} - -// GetLabelDetail retrieves detailed information about a label by its ID -func GetLabelDetail(labelID int) (*dto.LabelDetailResp, error) { - label, err := repository.GetLabelByID(database.DB, labelID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: label with ID %d not found", consts.ErrNotFound, labelID) - } - return nil, fmt.Errorf("failed to get label: %w", err) - } - - return dto.NewLabelDetailResp(label), nil -} - -// ListLabels lists labels based on the provided filters -func ListLabels(req *dto.ListLabelReq) (*dto.ListResp[dto.LabelResp], error) { - limit, offset := req.ToGormParams() - fitlerOptions := req.ToFilterOptions() - - labels, total, err := repository.ListLabels(database.DB, limit, offset, fitlerOptions) - if err != nil { - return nil, fmt.Errorf("failed to list labels: %w", err) - } - - labelResps := make([]dto.LabelResp, 0, len(labels)) - for i := range labels { - labelResps = append(labelResps, *dto.NewLabelResp(&labels[i])) - } - - resp := dto.ListResp[dto.LabelResp]{ - Items: labelResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateLabel updates an existing label's details -func UpdateLabel(req *dto.UpdateLabelReq, labelID int) (*dto.LabelResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - var updatedLabel *database.Label - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingLabel, err := repository.GetLabelByID(tx, labelID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: label with ID %d not found", consts.ErrNotFound, labelID) - } - return fmt.Errorf("failed to get label: %w", err) - } - - req.PatchLabelModel(existingLabel) - - if err := repository.UpdateLabel(tx, existingLabel); err != nil { - return fmt.Errorf("failed to update label: %w", err) - } - - updatedLabel = existingLabel - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewLabelResp(updatedLabel), nil -} - -// labelRemovalOps defines the operations needed to remove associations for a specific entity type -type labelRemovalOps struct { - countFunc func(*gorm.DB, []int) (map[int]int64, error) - removeFunc func(*gorm.DB, []int) (int64, error) - entityName string -} - -// removeAssociationsFromLabels is a generic function to remove entity associations from labels -func removeAssociationsFromLabels(db *gorm.DB, labelIDs []int, ops labelRemovalOps) (map[int]int64, error) { - if len(labelIDs) == 0 { - return nil, nil - } - - countsMap, err := ops.countFunc(db, labelIDs) - if err != nil { - return nil, fmt.Errorf("failed to get %s-label counts: %w", ops.entityName, err) - } - if len(countsMap) == 0 { - return nil, nil - } - - rows, err := ops.removeFunc(db, labelIDs) - if err != nil { - return nil, fmt.Errorf("failed to remove %ss from labels: %w", ops.entityName, err) - } - if rows == 0 { - return nil, nil - } - - return countsMap, nil -} - -// removeContainersFromLabels removes container associations from multiple labels and returns the total usage count removed -func removeContainersFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - return removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ - countFunc: repository.ListContainerLabelCounts, - removeFunc: repository.RemoveContainersFromLabels, - entityName: "container", - }) -} - -// removeDatasetsFromLabels removes dataset associations from multiple labels and returns the count map -func removeDatasetsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - return removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ - countFunc: repository.ListDatasetLabelCounts, - removeFunc: repository.RemoveDatasetsFromLabels, - entityName: "dataset", - }) -} - -// removeProjectsFromLabels removes project associations from multiple labels and returns the count map -func removeProjectsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - return removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ - countFunc: repository.ListProjectLabelCounts, - removeFunc: repository.RemoveProjectsFromLabels, - entityName: "project", - }) -} - -// removeInjectionsFromLabels removes injection associations from multiple labels and returns the count map -func removeInjectionsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - return removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ - countFunc: repository.ListInjectionLabelCounts, - removeFunc: repository.RemoveInjectionsFromLabels, - entityName: "injection", - }) -} - -// removeExecutionsFromLabels removes execution associations from multiple labels and returns the count map -func removeExecutionsFromLabels(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - return removeAssociationsFromLabels(db, labelIDs, labelRemovalOps{ - countFunc: repository.ListExecutionLabelCounts, - removeFunc: repository.RemoveExecutionsFromLabels, - entityName: "execution", - }) -} diff --git a/src/service/producer/notification.go b/src/service/producer/notification.go deleted file mode 100644 index ee4cee7e..00000000 --- a/src/service/producer/notification.go +++ /dev/null @@ -1,23 +0,0 @@ -package producer - -import ( - "aegis/client" - "context" - "fmt" - "time" - - "github.com/redis/go-redis/v9" -) - -// ReadNotificationStreamMessages reads messages from the notification stream -func ReadNotificationStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { - if lastID == "" { - lastID = "0" - } - - messages, err := client.RedisXRead(ctx, []string{streamKey, lastID}, count, block) - if err != nil { - return nil, fmt.Errorf("failed to read notification stream messages: %w", err) - } - return messages, nil -} diff --git a/src/service/producer/permission.go b/src/service/producer/permission.go deleted file mode 100644 index 1434b3e3..00000000 --- a/src/service/producer/permission.go +++ /dev/null @@ -1,114 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// CheckUserPermission checks if user has specific permission using a params struct -func CheckUserPermission(params *dto.CheckPermissionParams) (bool, error) { - if err := params.Validate(); err != nil { - return false, fmt.Errorf("invalid request: %w", err) - } - - permission, err := repository.GetPermissionByActionAndResource(database.DB, params.Action, params.Scope, params.ResourceName) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, fmt.Errorf("failed to find target permission: %w", err) - } - - return repository.CheckUserHasPermission(database.DB, params, permission.ID) -} - -// GetPermissionDetail retrieves detailed information about a permission by its ID -func GetPermissionDetail(permissionID int) (*dto.PermissionDetailResp, error) { - permission, err := repository.GetPermissionByID(database.DB, permissionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: permission not found", consts.ErrNotFound) - } - return nil, fmt.Errorf("failed to get permission: %w", err) - } - - return dto.NewPermissionDetailResp(permission), nil -} - -// ListPermissions lists permissions based on the provided request parameters -func ListPermissions(req *dto.ListPermissionReq) (*dto.ListResp[dto.PermissionResp], error) { - limit, offset := req.ToGormParams() - - permissions, total, err := repository.ListPermissions(database.DB, limit, offset, req.Action, req.IsSystem, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list roles: %w", err) - } - - permissionResps := make([]dto.PermissionResp, len(permissions)) - for i, permission := range permissions { - permissionResps[i] = *dto.NewPermissionResp(&permission) - } - - resp := dto.ListResp[dto.PermissionResp]{ - Items: permissionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -func ListRolesFromPermission(permissionID int) ([]dto.RoleResp, error) { - permission, err := repository.GetPermissionByID(database.DB, permissionID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: permission not found", consts.ErrNotFound) - } - return nil, fmt.Errorf("failed to get permission: %w", err) - } - - roles, err := repository.ListRolesByPermissionID(database.DB, permission.ID) - if err != nil { - return nil, fmt.Errorf("failed to get permission roles: %w", err) - } - - var roleResps []dto.RoleResp - for _, role := range roles { - roleResps = append(roleResps, *dto.NewRoleResp(&role)) - } - - return roleResps, nil -} - -// fetchPermissionsMapByIDBatch fetches permissions by their IDs and returns a map of permission ID to Permission -func fetchPermissionsMapByIDBatch(db *gorm.DB, permissionIDs []int) (map[int]database.Permission, error) { - if len(permissionIDs) == 0 { - return make(map[int]database.Permission), nil - } - - uniqueIDs := make(map[int]struct{}) - for _, id := range permissionIDs { - uniqueIDs[id] = struct{}{} - } - - deduplicatedIDs := make([]int, 0, len(uniqueIDs)) - for id := range uniqueIDs { - deduplicatedIDs = append(deduplicatedIDs, id) - } - - permissions, err := repository.ListPermissionsByID(db, deduplicatedIDs) - if err != nil { - return nil, fmt.Errorf("failed to list permissions by IDs: %w", err) - } - - permissionMap := make(map[int]database.Permission, len(permissions)) - for _, perm := range permissions { - permissionMap[perm.ID] = perm - } - - return permissionMap, nil -} diff --git a/src/service/producer/project.go b/src/service/producer/project.go deleted file mode 100644 index d051e2cc..00000000 --- a/src/service/producer/project.go +++ /dev/null @@ -1,399 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "aegis/service/common" - "aegis/utils" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// CreateProject handles the business logic for creating a new project -func CreateProject(req *dto.CreateProjectReq, userID int) (*dto.ProjectResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - project := req.ConvertToProject() - - var createdProject *database.Project - err := database.DB.Transaction(func(tx *gorm.DB) error { - role, err := repository.GetRoleByName(tx, consts.RoleProjectAdmin.String()) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role %v not found", err, consts.RoleProjectAdmin) - } - return fmt.Errorf("failed to get project owner role: %w", err) - } - - if err := repository.CreateProject(tx, project); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: project with name %s already exists", consts.ErrAlreadyExists, project.Name) - } - return err - } - - if err := repository.CreateUserProject(tx, &database.UserProject{ - UserID: userID, - ProjectID: project.ID, - RoleID: role.ID, - Status: consts.CommonEnabled, - }); err != nil { - return fmt.Errorf("failed to assign project owner: %w", err) - } - - createdProject = project - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewProjectResp(createdProject, nil), nil -} - -// DeleteProject deletes an existing project by marking its status as deleted -func DeleteProject(projectID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - if _, err := repository.RemoveUsersFromProject(tx, projectID); err != nil { - return fmt.Errorf("failed to remove users from project: %w", err) - } - - rows, err := repository.DeleteProject(tx, projectID) - if err != nil { - return fmt.Errorf("failed to delete project: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) - } - - return nil - }) -} - -// GetProjectDetail retrieves detailed information about a project by its ID -func GetProjectDetail(projectID int) (*dto.ProjectDetailResp, error) { - project, err := repository.GetProjectByID(database.DB, projectID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: project with ID %d not found", consts.ErrNotFound, projectID) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - - // Get project statistics - statsMap, err := repository.BatchGetProjectStatistics(database.DB, []int{project.ID}) - if err != nil { - return nil, fmt.Errorf("failed to get project statistics: %w", err) - } - - stats := statsMap[project.ID] - resp := dto.NewProjectDetailResp(project, stats) - - userCount, err := repository.GetProjectUserCount(database.DB, project.ID) - if err != nil { - return nil, fmt.Errorf("failed to get project user count: %w", err) - } - resp.UserCount = userCount - - // TODO add more project details if needed (container, dataset, etc.) - - return resp, nil -} - -// ListProjects lists projects based on the provided filters -func ListProjects(req *dto.ListProjectReq) (*dto.ListResp[dto.ProjectResp], error) { - if req == nil { - return nil, fmt.Errorf("list project request is nil") - } - - limit, offset := req.ToGormParams() - - projects, total, err := repository.ListProjects(database.DB, limit, offset, req.IsPublic, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list projects: %w", err) - } - - projectIDs := make([]int, 0, len(projects)) - for _, p := range projects { - projectIDs = append(projectIDs, p.ID) - } - - labelsMap, err := repository.ListProjectLabels(database.DB, projectIDs) - if err != nil { - return nil, fmt.Errorf("failed to list project labels: %w", err) - } - - // Batch get statistics for all projects - statsMap, err := repository.BatchGetProjectStatistics(database.DB, projectIDs) - if err != nil { - return nil, fmt.Errorf("failed to batch get project statistics: %w", err) - } - - projectResps := make([]dto.ProjectResp, 0, len(projects)) - for i := range projects { - // Convert repository stats to dto stats - var stats *dto.ProjectStatistics - if repoStats, exists := statsMap[projects[i].ID]; exists { - stats = &dto.ProjectStatistics{ - InjectionCount: repoStats.InjectionCount, - ExecutionCount: repoStats.ExecutionCount, - LastInjectionAt: repoStats.LastInjectionAt, - LastExecutionAt: repoStats.LastExecutionAt, - } - } - - if labels, exists := labelsMap[projects[i].ID]; exists { - projects[i].Labels = labels - } - projectResps = append(projectResps, *dto.NewProjectResp(&projects[i], stats)) - } - - resp := dto.ListResp[dto.ProjectResp]{ - Items: projectResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateProject updates an existing project's details -func UpdateProject(req *dto.UpdateProjectReq, projectID int) (*dto.ProjectResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - var updatedProject *database.Project - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingProject, err := repository.GetProjectByID(tx, projectID) - if err != nil { - return fmt.Errorf("failed to get project: %w", err) - } - - req.PatchProjectModel(existingProject) - - if err := repository.UpdateProject(tx, existingProject); err != nil { - return fmt.Errorf("failed to update project: %w", err) - } - - updatedProject = existingProject - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewProjectResp(updatedProject, nil), nil -} - -// ===================== Project-Label ===================== - -// ManageProjectLabels manages project labels (key-value pairs) -func ManageProjectLabels(req *dto.ManageProjectLabelReq, projectID int) (*dto.ProjectResp, error) { - if req == nil { - return nil, fmt.Errorf("manage project labels request is nil") - } - - var managedProject *database.Project - err := database.DB.Transaction(func(tx *gorm.DB) error { - project, err := repository.GetProjectByID(tx, projectID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: project id: %d", consts.ErrNotFound, projectID) - } - return fmt.Errorf("failed to get project: %w", err) - } - - if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - projectLabels := make([]database.ProjectLabel, 0, len(labels)) - for _, label := range labels { - projectLabels = append(projectLabels, database.ProjectLabel{ - ProjectID: projectID, - LabelID: label.ID, - }) - } - - if err := repository.AddProjectLabels(tx, projectLabels); err != nil { - return fmt.Errorf("failed to add project labels: %w", err) - } - } - - if len(req.RemoveLabels) > 0 { - labelIDs, err := repository.ListLabelIDsByKeyAndProjectID(tx, projectID, req.RemoveLabels) - if err != nil { - return fmt.Errorf("failed to find label ids by keys: %w", err) - } - - if len(labelIDs) == 0 { - if err := repository.ClearProjectLabels(tx, []int{projectID}, labelIDs); err != nil { - return fmt.Errorf("failed to clear project labels: %w", err) - } - - if err := repository.BatchDecreaseLabelUsages(tx, labelIDs, 1); err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) - } - } - } - - labels, err := repository.ListLabelsByProjectID(database.DB, project.ID) - if err != nil { - return fmt.Errorf("failed to get project labels: %w", err) - } - - project.Labels = labels - managedProject = project - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewProjectResp(managedProject, nil), nil -} - -func fetchProjectsMapByIDBatch(db *gorm.DB, projectIDs []int) (map[int]database.Project, error) { - if len(projectIDs) == 0 { - return make(map[int]database.Project), nil - } - - projects, err := repository.ListProjectsByID(db, utils.ToUniqueSlice(projectIDs)) - if err != nil { - return nil, fmt.Errorf("failed to list projects by IDs: %w", err) - } - - projectMap := make(map[int]database.Project, len(projectIDs)) - for _, p := range projects { - projectMap[p.ID] = p - } - - return projectMap, nil -} - -// ===================== Project-Injection ===================== - -// ListProjectInjections lists all fault injections for a specific project -func ListProjectInjections(req *dto.ListInjectionReq, projectID int) (*dto.ListResp[dto.InjectionResp], error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - // Verify project exists - if _, err := repository.GetProjectByID(database.DB, projectID); err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - - limit, offset := req.ToGormParams() - - injections, total, err := repository.ListInjectionsByProjectID(database.DB, projectID, limit, offset) - if err != nil { - return nil, fmt.Errorf("failed to list injections for project %d: %w", projectID, err) - } - - injectionResps := make([]dto.InjectionResp, 0, len(injections)) - for _, injection := range injections { - injectionResps = append(injectionResps, *dto.NewInjectionResp(&injection)) - } - - resp := dto.ListResp[dto.InjectionResp]{ - Items: injectionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ===================== Project-Execution ===================== - -// ListProjectExecutions lists all algorithm executions for a specific project -func ListProjectExecutions(req *dto.ListExecutionReq, projectID int) (*dto.ListResp[dto.ExecutionResp], error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - // Verify project exists - if _, err := repository.GetProjectByID(database.DB, projectID); err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, projectID) - } - return nil, fmt.Errorf("failed to get project: %w", err) - } - - limit, offset := req.ToGormParams() - - executions, total, err := repository.ListExecutionsByProjectID(database.DB, projectID, limit, offset) - if err != nil { - return nil, fmt.Errorf("failed to list executions for project %d: %w", projectID, err) - } - - executionResps := make([]dto.ExecutionResp, 0, len(executions)) - for _, execution := range executions { - executionResps = append(executionResps, *dto.NewExecutionResp(&execution, nil)) - } - - resp := dto.ListResp[dto.ExecutionResp]{ - Items: executionResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ============================================================================ -// Project Permission Check Helper Functions (exported for middleware) -// ============================================================================ - -// IsUserInProject checks if a user is a member of a project -func IsUserInProject(userID int, projectID int) (bool, error) { - up, err := repository.GetUserProjectRole(database.DB, userID, projectID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return up != nil, nil -} - -// IsUserProjectAdmin checks if a user has project admin role in a specific project -func IsUserProjectAdmin(userID int, projectID int) (bool, error) { - up, err := repository.GetUserProjectRole(database.DB, userID, projectID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return up != nil && up.Role != nil && up.Role.Name == consts.RoleProjectAdmin.String(), nil -} - -// IsProjectPublic checks if a project is publicly accessible -func IsProjectPublic(projectID int) (bool, error) { - project, err := repository.GetProjectByID(database.DB, projectID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return project.IsPublic, nil -} - -// GetProjectTeamID gets the team ID for a project -func GetProjectTeamID(projectID int) (int, error) { - teamID, err := repository.GetProjectTeamID(database.DB, projectID) - if err != nil { - return 0, err - } - return teamID, nil -} diff --git a/src/service/producer/query_datapack_noarrow.go b/src/service/producer/query_datapack_noarrow.go deleted file mode 100644 index b7378a62..00000000 --- a/src/service/producer/query_datapack_noarrow.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !duckdb_arrow - -package producer - -import ( - "context" - "fmt" - "io" -) - -// QueryDatapackFileContent requires the duckdb_arrow build tag because duckdb's Arrow API -// is compiled behind that tag in github.com/duckdb/duckdb-go/v2. -func QueryDatapackFileContent(ctx context.Context, datapackID int, filePath string) (string, int64, io.ReadCloser, error) { - return "", 0, nil, fmt.Errorf("QueryDatapackFileContent requires building with -tags duckdb_arrow") -} diff --git a/src/service/producer/relation.go b/src/service/producer/relation.go deleted file mode 100644 index d436e517..00000000 --- a/src/service/producer/relation.go +++ /dev/null @@ -1,515 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// ===================== User-Role ===================== - -// AssignRoleToUser assigns a role to a user -func AssignRoleToUser(userID, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - // Assign role to user - if err := repository.CreateUserRole(tx, &database.UserRole{ - UserID: user.ID, - RoleID: role.ID, - }); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: user already has this role", consts.ErrAlreadyExists) - } - return err - } - - return nil - }) -} - -// RemoveRoleFromUser removes a role from a user -func RemoveRoleFromUser(userID, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if err := repository.DeleteUserRole(tx, user.ID, role.ID); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: failed to delete user role association (%d, %d)", err, userID, roleID) - } - return err - } - return nil - }) -} - -// ===================== User-Permission ===================== - -// BatchAssignUserPermissions assigns multiple permissions to a user -func BatchAssignUserPermissions(req *dto.AssignUserPermissionReq, userID int) error { - permissionIDs := make([]int, len(req.Items)) - for i, up := range req.Items { - permissionIDs[i] = up.PermissionID - } - - containerIDs := make([]int, 0, len(req.Items)) - datasetIDs := make([]int, 0, len(req.Items)) - projectIDs := make([]int, 0, len(req.Items)) - for _, item := range req.Items { - if item.ContainerID != nil { - containerIDs = append(containerIDs, *item.ContainerID) - } - if item.DatasetID != nil { - datasetIDs = append(datasetIDs, *item.DatasetID) - } - if item.ProjectID != nil { - projectIDs = append(projectIDs, *item.ProjectID) - } - } - - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - permissionResults, err := fetchPermissionsMapByIDBatch(tx, permissionIDs) - if err != nil { - return fmt.Errorf("failed to fetch permissions: %w", err) - } - - containerResults, err := fetchContainersMapByIDBatch(tx, containerIDs) - if err != nil { - return fmt.Errorf("failed to fetch containers: %w", err) - } - - datasetResults, err := fetchDatasetsMapByIDBatch(tx, datasetIDs) - if err != nil { - return fmt.Errorf("failed to fetch datasets: %w", err) - } - - projectResults, err := fetchProjectsMapByIDBatch(tx, projectIDs) - if err != nil { - return fmt.Errorf("failed to fetch projects: %w", err) - } - - var userPermissons []database.UserPermission - for _, item := range req.Items { - if _, exists := permissionResults[item.PermissionID]; !exists { - return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, item.PermissionID) - } - - if item.ContainerID != nil { - if _, exists := containerResults[*item.ContainerID]; !exists { - return fmt.Errorf("%w: container id %d not found", consts.ErrNotFound, *item.ContainerID) - } - } - - if item.DatasetID != nil { - if _, exists := datasetResults[*item.DatasetID]; !exists { - return fmt.Errorf("%w: dataset id %d not found", consts.ErrNotFound, *item.DatasetID) - } - } - - if item.ProjectID != nil { - if _, exists := projectResults[*item.ProjectID]; !exists { - return fmt.Errorf("%w: project id %d not found", consts.ErrNotFound, *item.ProjectID) - } - } - - userPermisson := item.ConvertToUserPermission() - userPermisson.UserID = user.ID - userPermissons = append(userPermissons, *userPermisson) - } - - if err := repository.BatchCreateUserPermissions(tx, userPermissons); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: user already has one or more of these permissions", consts.ErrAlreadyExists) - } - return fmt.Errorf("failed to assgin permissions to user: %w", err) - } - - return nil - }) -} - -// BatchRemoveUserPermissions removes multiple permissions from a user -func BatchRemoveUserPermissions(req *dto.RemoveUserPermissionReq, userID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - permissionResults, err := fetchPermissionsMapByIDBatch(tx, req.PermissionIDs) - if err != nil { - return fmt.Errorf("failed to fetch permissions: %w", err) - } - - for _, permissionID := range req.PermissionIDs { - if _, exists := permissionResults[permissionID]; !exists { - return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, permissionID) - } - } - - if err := repository.BatchDeleteUserPermisssions(tx, user.ID, req.PermissionIDs); err != nil { - return fmt.Errorf("") - } - return nil - }) -} - -// ===================== Role-Permission ===================== - -// AssginPermissionsToRole assigns multiple permissions to a role -func BatchAssignRolePermissions(permissionIDs []int, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if role.IsSystem { - return fmt.Errorf("%w: cannot assign permissions to system role", consts.ErrPermissionDenied) - } - - permissionResults, err := fetchPermissionsMapByIDBatch(tx, permissionIDs) - if err != nil { - return fmt.Errorf("failed to fetch permissions: %w", err) - } - - var rolePermissions []database.RolePermission - for _, permissionID := range permissionIDs { - if _, exists := permissionResults[permissionID]; !exists { - return fmt.Errorf("%w: permission id %d not found", consts.ErrNotFound, permissionID) - } - - rolePermissions = append(rolePermissions, database.RolePermission{ - RoleID: role.ID, - PermissionID: permissionID, - }) - } - - if err := repository.BatchCreateRolePermissions(tx, rolePermissions); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: role already has one or more of these permissions", consts.ErrAlreadyExists) - } - return fmt.Errorf("failed to assign permissions to role: %w", err) - } - - return nil - }) -} - -// RemovePermissionsFromRole removes permissions from a role -func RemovePermissionsFromRole(permissionIDs []int, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if role.IsSystem { - return fmt.Errorf("%w: cannot remove permissions of system role", consts.ErrPermissionDenied) - } - - if err := repository.BatchDeleteRolePermisssions(tx, roleID, permissionIDs); err != nil { - return fmt.Errorf("") - } - - return nil - }) -} - -// ListUsersFromRole lists users assigned to a specific role -func ListUsersFromRole(roleID int) ([]dto.UserResp, error) { - role, err := repository.GetRoleByID(database.DB, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return nil, err - } - - users, err := repository.ListUsersByRoleID(database.DB, role.ID) - if err != nil { - return nil, fmt.Errorf("failed to get role users: %w", err) - } - - var userResps []dto.UserResp - for _, user := range users { - userResps = append(userResps, *dto.NewUserResp(&user)) - } - - return userResps, nil -} - -// ===================== User-Container ===================== - -// AssignContainerToUser assigns a user to a container with a specific role -func AssignContainerToUser(userID, containerID, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - container, err := repository.GetContainerByID(tx, containerID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: container not found", consts.ErrNotFound) - } - return err - } - - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if err := repository.CreateUserContainer(tx, &database.UserContainer{ - UserID: user.ID, - ContainerID: container.ID, - RoleID: role.ID, - }); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: user already assigned to this container", consts.ErrAlreadyExists) - } - return err - } - - return nil - }) -} - -// RemoveContainerFromUser removes a user from a container -func RemoveContainerFromUser(userID, containerID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - container, err := repository.GetContainerByID(tx, containerID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: container not found", consts.ErrNotFound) - } - return err - } - - row, err := repository.DeleteUserContainer(tx, user.ID, container.ID) - if err != nil { - return fmt.Errorf("failed to remove user from container: %w", err) - } - if row == 0 { - return fmt.Errorf("%w: user is not assigned to this container", consts.ErrNotFound) - } - - return nil - }) -} - -// ===================== User-Dataset ===================== - -// AssignDatasetToUser assigns a user to a dataset with a specific role -func AssignDatasetToUser(userID, datasetID, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - dataset, err := repository.GetDatasetByID(tx, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: dataset not found", consts.ErrNotFound) - } - return err - } - - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if err := repository.CreateUserDataset(tx, &database.UserDataset{ - UserID: user.ID, - DatasetID: dataset.ID, - RoleID: role.ID, - }); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: user already assigned to this dataset", consts.ErrAlreadyExists) - } - return err - } - - return nil - }) -} - -// RemoveDatasetFromUser removes a user from a dataset -func RemoveDatasetFromUser(userID, datasetID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - dataset, err := repository.GetDatasetByID(tx, datasetID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: dataset not found", consts.ErrNotFound) - } - return err - } - - row, err := repository.DeleteUserDataset(tx, user.ID, dataset.ID) - if err != nil { - return fmt.Errorf("failed to remove user from dataset: %w", err) - } - if row == 0 { - return fmt.Errorf("%w: user is not assigned to this dataset", consts.ErrNotFound) - } - - return nil - }) -} - -// ===================== User-Project ===================== - -// AssignProjectToUser assigns a user to a project with a specific role -func AssignProjectToUser(userID, projectID, roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - project, err := repository.GetProjectByID(tx, projectID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: project not found", consts.ErrNotFound) - } - return err - } - - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return err - } - - if err := repository.CreateUserProject(tx, &database.UserProject{ - UserID: user.ID, - ProjectID: project.ID, - RoleID: role.ID, - }); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: user already assigned to this project", consts.ErrAlreadyExists) - } - return err - } - - return nil - }) -} - -// RemoveProjectFromUser removes a user from a project -func RemoveProjectFromUser(userID, projectID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return err - } - - project, err := repository.GetProjectByID(tx, projectID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: project not found", consts.ErrNotFound) - } - return err - } - - row, err := repository.DeleteUserProject(tx, project.ID, user.ID) - if err != nil { - return fmt.Errorf("failed to remove user from project: %w", err) - } - if row == 0 { - return fmt.Errorf("%w: user is not assigned to this project", consts.ErrNotFound) - } - - return nil - }) -} diff --git a/src/service/producer/resource.go b/src/service/producer/resource.go deleted file mode 100644 index d240d39e..00000000 --- a/src/service/producer/resource.go +++ /dev/null @@ -1,69 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// GetResourceDetail retrieves detailed information about a resource by its ID -func GetResourceDetail(resourceID int) (*dto.ResourceResp, error) { - resource, err := repository.GetResourceByID(database.DB, resourceID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: resource with ID %d not found", consts.ErrNotFound, resourceID) - } - return nil, fmt.Errorf("failed to get resource: %w", err) - } - - return dto.NewResourceResp(resource), nil -} - -// ListResources lists resources based on the provided filters -func ListResources(req *dto.ListResourceReq) (*dto.ListResp[dto.ResourceResp], error) { - limit, offset := req.ToGormParams() - - resources, total, err := repository.ListResources(database.DB, limit, offset, req.Type, req.Category) - if err != nil { - return nil, fmt.Errorf("failed to list resources: %w", err) - } - - resourceResps := make([]dto.ResourceResp, 0, len(resources)) - for i := range resources { - resourceResps = append(resourceResps, *dto.NewResourceResp(&resources[i])) - } - - resp := dto.ListResp[dto.ResourceResp]{ - Items: resourceResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ListResourcePermissions lists permissions associated with a specific resource -func ListResourcePermissions(resourceID int) ([]dto.PermissionResp, error) { - resource, err := repository.GetResourceByID(database.DB, resourceID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: resource with ID %d not found", consts.ErrNotFound, resourceID) - } - return nil, err - } - - permissions, err := repository.GetPermissionsByResource(database.DB, resource.ID) - if err != nil { - return nil, err - } - - var permissionResps []dto.PermissionResp - for _, permission := range permissions { - permissionResps = append(permissionResps, *dto.NewPermissionResp(&permission)) - } - - return permissionResps, nil -} diff --git a/src/service/producer/role.go b/src/service/producer/role.go deleted file mode 100644 index d613ef02..00000000 --- a/src/service/producer/role.go +++ /dev/null @@ -1,164 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "gorm.io/gorm" -) - -// CreateRole handles the business logic for creating a new role -func CreateRole(req *dto.CreateRoleReq) (*dto.RoleResp, error) { - role := req.ConvertToRole() - - var createdRole *database.Role - err := database.DB.Transaction(func(tx *gorm.DB) error { - if err := repository.CreateRole(tx, role); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: role with name %s already exists", consts.ErrAlreadyExists, role.Name) - } - return err - } - - createdRole = role - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewRoleResp(createdRole), nil -} - -// DeleteRole deletes an existing role by marking its status as deleted -func DeleteRole(roleID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - role, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return fmt.Errorf("failed to get role: %w", err) - } - - if role.IsSystem { - return fmt.Errorf("%w: cannot delete system role", consts.ErrPermissionDenied) - } - - if _, err := repository.RemoveContainersFromRole(tx, role.ID); err != nil { - return fmt.Errorf("failed to remove containers with role: %w", err) - } - if _, err := repository.RemoveDatasetsFromRole(tx, role.ID); err != nil { - return fmt.Errorf("failed to remove datasets with role: %w", err) - } - if _, err := repository.RemoveProjectsFromRole(tx, role.ID); err != nil { - return fmt.Errorf("failed to remove projects with role: %w", err) - } - - if err := repository.RemovePermissionsFromRole(tx, role.ID); err != nil { - return fmt.Errorf("failed to remove permissions with role: %w", err) - } - if err := repository.RemoveUsersFromRole(tx, role.ID); err != nil { - return fmt.Errorf("failed to remove users with role: %w", err) - } - - row, err := repository.DeleteRole(tx, role.ID) - if err != nil { - return fmt.Errorf("failed to delete role: %w", err) - } - if row == 0 { - return fmt.Errorf("%w: role id %d not found", consts.ErrNotFound, roleID) - } - - return nil - }) -} - -// GetRoleDetail retrieves detailed information about a role by its ID -func GetRoleDetail(roleID int) (*dto.RoleDetailResp, error) { - role, err := repository.GetRoleByID(database.DB, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: role with ID %d not found", consts.ErrNotFound, roleID) - } - return nil, fmt.Errorf("failed to get role: %w", err) - } - - resp := dto.NewRoleDetailResp(role) - - userCount, err := repository.GetRoleUserCount(database.DB, role.ID) - if err != nil { - return nil, fmt.Errorf("failed to get role user count: %w", err) - } - resp.UserCount = userCount - - permissions, err := repository.GetRolePermissions(database.DB, role.ID) - if err != nil { - return nil, fmt.Errorf("failed to get role permissions: %w", err) - } - - resp.Permissions = make([]dto.PermissionResp, len(permissions)) - for _, permission := range permissions { - resp.Permissions = append(resp.Permissions, *dto.NewPermissionResp(&permission)) - } - - return resp, nil -} - -// ListRoles lists roles based on the provided filters -func ListRoles(req *dto.ListRoleReq) (*dto.ListResp[dto.RoleResp], error) { - limit, offset := req.ToGormParams() - - roles, total, err := repository.ListRoles(database.DB, limit, offset, req.IsSystem, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list roles: %w", err) - } - - roleResps := make([]dto.RoleResp, len(roles)) - for i, role := range roles { - roleResps[i] = *dto.NewRoleResp(&role) - } - - resp := dto.ListResp[dto.RoleResp]{ - Items: roleResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateRole updates an existing role -func UpdateRole(req *dto.UpdateRoleReq, roleID int) (*dto.RoleResp, error) { - var updatedRole *database.Role - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingRole, err := repository.GetRoleByID(tx, roleID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: role not found", consts.ErrNotFound) - } - return fmt.Errorf("failed to get role: %w", err) - } - - if existingRole.IsSystem { - return fmt.Errorf("%w: cannot update system role", consts.ErrPermissionDenied) - } - - req.PatchRoleModel(existingRole) - - if err := repository.UpdateRole(tx, existingRole); err != nil { - return fmt.Errorf("failed to update role: %w", err) - } - - updatedRole = existingRole - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewRoleResp(updatedRole), nil -} diff --git a/src/service/producer/sdk_evaluation.go b/src/service/producer/sdk_evaluation.go deleted file mode 100644 index de77db09..00000000 --- a/src/service/producer/sdk_evaluation.go +++ /dev/null @@ -1,57 +0,0 @@ -package producer - -import ( - "fmt" - - "aegis/database" - "aegis/dto" - "aegis/repository" -) - -// ListSDKEvaluations lists SDK evaluation samples with pagination and filtering. -func ListSDKEvaluations(req *dto.ListSDKEvaluationReq) (*dto.ListResp[database.SDKEvaluationSample], error) { - limit, offset := req.ToGormParams() - - items, total, err := repository.ListSDKEvaluations(database.DB, req.ExpID, req.Stage, limit, offset) - if err != nil { - return nil, fmt.Errorf("failed to list SDK evaluations: %w", err) - } - - return &dto.ListResp[database.SDKEvaluationSample]{ - Items: items, - Pagination: req.ConvertToPaginationInfo(total), - }, nil -} - -// GetSDKEvaluation retrieves a single SDK evaluation sample by ID. -func GetSDKEvaluation(id int) (*database.SDKEvaluationSample, error) { - item, err := repository.GetSDKEvaluationByID(database.DB, id) - if err != nil { - return nil, err - } - return item, nil -} - -// ListSDKExperiments returns all distinct experiment IDs. -func ListSDKExperiments() (*dto.SDKExperimentListResp, error) { - expIDs, err := repository.ListSDKExperiments(database.DB) - if err != nil { - return nil, fmt.Errorf("failed to list SDK experiments: %w", err) - } - return &dto.SDKExperimentListResp{Experiments: expIDs}, nil -} - -// ListSDKDatasetSamples lists SDK dataset samples with pagination and filtering. -func ListSDKDatasetSamples(req *dto.ListSDKDatasetSampleReq) (*dto.ListResp[database.SDKDatasetSample], error) { - limit, offset := req.ToGormParams() - - items, total, err := repository.ListSDKDatasetSamples(database.DB, req.Dataset, limit, offset) - if err != nil { - return nil, fmt.Errorf("failed to list SDK dataset samples: %w", err) - } - - return &dto.ListResp[database.SDKDatasetSample]{ - Items: items, - Pagination: req.ConvertToPaginationInfo(total), - }, nil -} diff --git a/src/service/producer/system.go b/src/service/producer/system.go deleted file mode 100644 index 45219462..00000000 --- a/src/service/producer/system.go +++ /dev/null @@ -1,283 +0,0 @@ -package producer - -import ( - "aegis/client" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "context" - "encoding/json" - "errors" - "fmt" - "runtime" - "strconv" - "time" - - "github.com/redis/go-redis/v9" - "github.com/shirou/gopsutil/v3/cpu" - "github.com/shirou/gopsutil/v3/disk" - "github.com/shirou/gopsutil/v3/mem" -) - -// InspectLock retrieves the current lock status of all namespaces -func InspectLock(ctx context.Context) (*dto.ListNamespaceLockResp, error) { - redisClient := client.GetRedisClient() - - // Get all namespaces - namespaces, err := redisClient.SMembers(ctx, consts.NamespacesKey).Result() - if err != nil { - return nil, fmt.Errorf("failed to get namespaces from Redis: %v", err) - } - - nsMap := make(map[string]dto.NsMonitorItem, len(namespaces)) - - // Get data for each namespace - for _, ns := range namespaces { - nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, ns) - values, err := redisClient.HGetAll(ctx, nsKey).Result() - if err != nil { - return nil, fmt.Errorf("failed to get data for namespace %s: %v", ns, err) - } - - endTimeUnix, err := strconv.ParseInt(values["end_time"], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid end_time format for namespace %s: %v", ns, err) - } - - // Get status, default to enabled for backward compatibility - status := consts.CommonEnabled - if statusStr, ok := values["status"]; ok { - statusInt, err := strconv.Atoi(statusStr) - if err == nil { - status = consts.StatusType(statusInt) - } - } - - nsMap[ns] = dto.NsMonitorItem{ - LockedBy: values["trace_id"], - EndTime: time.Unix(endTimeUnix, 0), - Status: consts.GetStatusTypeName(status), - } - } - - resp := &dto.ListNamespaceLockResp{ - Items: nsMap, - } - return resp, nil -} - -// ListQueuedTasks lists tasks currently in the ready and delayed queues -func ListQueuedTasks(ctx context.Context) (*dto.QueuedTasksResp, error) { - readyTaskDatas, err := repository.ListReadyTasks(ctx) - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("%w: no ready tasks found", consts.ErrNotFound) - } - return nil, err - } - - readyTask := make([]dto.TaskResp, 0, len(readyTaskDatas)) - for _, taskData := range readyTaskDatas { - var task database.Task - if err := json.Unmarshal([]byte(taskData), &task); err != nil { - return nil, err - } - - readyTask = append(readyTask, *dto.NewTaskResp(&task)) - } - - delayedTaskDatas, err := repository.ListDelayedTasks(ctx, 1000) - if err != nil { - if errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("%w: no delayed tasks found", consts.ErrNotFound) - } - return nil, err - } - - delayedTask := make([]dto.TaskResp, 0, len(delayedTaskDatas)) - for _, taskData := range delayedTaskDatas { - var task database.Task - if err := json.Unmarshal([]byte(taskData), &task); err != nil { - return nil, err - } - - delayedTask = append(delayedTask, *dto.NewTaskResp(&task)) - } - - resp := &dto.QueuedTasksResp{ - ReadyTasks: readyTask, - DelayedTasks: delayedTask, - } - return resp, nil -} - -// GetSystemMetrics retrieves current system metrics -func GetSystemMetrics(ctx context.Context) (*dto.SystemMetricsResp, error) { - now := time.Now() - - // Get CPU usage - cpuPercent, err := cpu.PercentWithContext(ctx, time.Second, false) - if err != nil { - return nil, fmt.Errorf("failed to get CPU usage: %v", err) - } - cpuUsage := 0.0 - if len(cpuPercent) > 0 { - cpuUsage = cpuPercent[0] - } - - // Get memory usage - memInfo, err := mem.VirtualMemoryWithContext(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get memory usage: %v", err) - } - - // Get disk usage - diskInfo, err := disk.UsageWithContext(ctx, "/") - if err != nil { - return nil, fmt.Errorf("failed to get disk usage: %v", err) - } - - resp := &dto.SystemMetricsResp{ - CPU: dto.MetricValue{ - Value: cpuUsage, - Timestamp: now, - Unit: "%", - }, - Memory: dto.MetricValue{ - Value: memInfo.UsedPercent, - Timestamp: now, - Unit: "%", - }, - Disk: dto.MetricValue{ - Value: diskInfo.UsedPercent, - Timestamp: now, - Unit: "%", - }, - } - - return resp, nil -} - -// GetSystemMetricsHistory retrieves historical system metrics (24 hours) -func GetSystemMetricsHistory(ctx context.Context) (*dto.SystemMetricsHistoryResp, error) { - redisClient := client.GetRedisClient() - now := time.Now() - - // Get last 24 hours of metrics from Redis - startTime := now.Add(-24 * time.Hour).Unix() - endTime := now.Unix() - - cpuKey := "system:metrics:cpu" - memKey := "system:metrics:memory" - - // Get CPU history - cpuData, err := redisClient.ZRangeByScore(ctx, cpuKey, &redis.ZRangeBy{ - Min: fmt.Sprintf("%d", startTime), - Max: fmt.Sprintf("%d", endTime), - }).Result() - if err != nil && !errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("failed to get CPU history: %v", err) - } - - // Get memory history - memData, err := redisClient.ZRangeByScore(ctx, memKey, &redis.ZRangeBy{ - Min: fmt.Sprintf("%d", startTime), - Max: fmt.Sprintf("%d", endTime), - }).Result() - if err != nil && !errors.Is(err, redis.Nil) { - return nil, fmt.Errorf("failed to get memory history: %v", err) - } - - // Parse CPU data - cpuMetrics := make([]dto.MetricValue, 0, len(cpuData)) - for _, data := range cpuData { - var metric dto.MetricValue - if err := json.Unmarshal([]byte(data), &metric); err == nil { - cpuMetrics = append(cpuMetrics, metric) - } - } - - // Parse memory data - memMetrics := make([]dto.MetricValue, 0, len(memData)) - for _, data := range memData { - var metric dto.MetricValue - if err := json.Unmarshal([]byte(data), &metric); err == nil { - memMetrics = append(memMetrics, metric) - } - } - - // If no historical data, generate current metrics - if len(cpuMetrics) == 0 || len(memMetrics) == 0 { - current, err := GetSystemMetrics(ctx) - if err != nil { - return nil, err - } - - if len(cpuMetrics) == 0 { - cpuMetrics = []dto.MetricValue{current.CPU} - } - if len(memMetrics) == 0 { - memMetrics = []dto.MetricValue{current.Memory} - } - } - - resp := &dto.SystemMetricsHistoryResp{ - CPU: cpuMetrics, - Memory: memMetrics, - } - - return resp, nil -} - -// StoreSystemMetrics stores current system metrics in Redis for historical tracking -func StoreSystemMetrics(ctx context.Context) error { - metrics, err := GetSystemMetrics(ctx) - if err != nil { - return err - } - - redisClient := client.GetRedisClient() - now := time.Now().Unix() - - // Store CPU metric - cpuData, _ := json.Marshal(metrics.CPU) - if err := redisClient.ZAdd(ctx, "system:metrics:cpu", redis.Z{ - Score: float64(now), - Member: cpuData, - }).Err(); err != nil { - return fmt.Errorf("failed to store CPU metric: %v", err) - } - - // Store memory metric - memData, _ := json.Marshal(metrics.Memory) - if err := redisClient.ZAdd(ctx, "system:metrics:memory", redis.Z{ - Score: float64(now), - Member: memData, - }).Err(); err != nil { - return fmt.Errorf("failed to store memory metric: %v", err) - } - - // Clean up old metrics (older than 24 hours) - oldTime := time.Now().Add(-24 * time.Hour).Unix() - redisClient.ZRemRangeByScore(ctx, "system:metrics:cpu", "0", fmt.Sprintf("%d", oldTime)) - redisClient.ZRemRangeByScore(ctx, "system:metrics:memory", "0", fmt.Sprintf("%d", oldTime)) - - return nil -} - -func init() { - // Start background goroutine to collect metrics every minute - go func() { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for range ticker.C { - ctx := context.Background() - if err := StoreSystemMetrics(ctx); err != nil { - // Log error but don't crash - runtime.Gosched() - } - } - }() -} diff --git a/src/service/producer/task.go b/src/service/producer/task.go deleted file mode 100644 index 202ea660..00000000 --- a/src/service/producer/task.go +++ /dev/null @@ -1,368 +0,0 @@ -package producer - -import ( - "aegis/client" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "context" - "encoding/json" - "errors" - "fmt" - "sync" - "time" - - "github.com/gorilla/websocket" - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" -) - -const ( - // WebSocket timing configuration - writeWait = 10 * time.Second - pongWait = 60 * time.Second - pingPeriod = 54 * time.Second // Must be less than pongWait - maxMsgSize = 512 // Max size of incoming messages (control frames) - - // Task polling interval for completion detection - taskPollInterval = 5 * time.Second - - // Flush delay after task completion to catch remaining logs - completionFlushDelay = 5 * time.Second -) - -// TaskLogStreamer manages WebSocket-based real-time log streaming for a task. -type TaskLogStreamer struct { - conn *websocket.Conn - mu sync.Mutex - log *logrus.Entry - taskID string -} - -// NewTaskLogStreamer creates a new TaskLogStreamer for the given WebSocket connection and task. -func NewTaskLogStreamer(conn *websocket.Conn, taskID string) *TaskLogStreamer { - return &TaskLogStreamer{ - conn: conn, - taskID: taskID, - log: logrus.WithField("task_id", taskID), - } -} - -// BatchDeleteTasks deletes multiple tasks by their IDs -func BatchDeleteTasks(taskIDs []string) error { - if len(taskIDs) == 0 { - return nil - } - - if err := repository.BatchDeleteTasks(database.DB, taskIDs); err != nil { - return err - } - return nil -} - -// GetTaskDetail retrieves detailed information about a specific task, including historical logs from Loki -func GetTaskDetail(taskID string) (*dto.TaskDetailResp, error) { - task, err := repository.GetTaskByID(database.DB, taskID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: task id: %s", consts.ErrNotFound, taskID) - } - return nil, fmt.Errorf("failed to get task: %w", err) - } - - // Query historical logs from Loki - var logs []string - lokiCtx, lokiCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer lokiCancel() - - lokiClient := client.NewLokiClient() - queryOpts := client.QueryOpts{ - Start: task.CreatedAt, - Direction: "forward", - } - - logEntries, lokiErr := lokiClient.QueryJobLogs(lokiCtx, taskID, queryOpts) - if lokiErr != nil { - logrus.Warnf("Failed to query Loki for task %s logs: %v", taskID, lokiErr) - } else { - logs = make([]string, 0, len(logEntries)) - for _, entry := range logEntries { - logs = append(logs, entry.Line) - } - } - - if logs == nil { - logs = []string{} - } - - resp := dto.NewTaskDetailResp(task, logs) - return resp, nil -} - -// ListTasks lists tasks based on filter options and pagination -func ListTasks(req *dto.ListTaskReq) (*dto.ListResp[dto.TaskResp], error) { - if req == nil { - return nil, fmt.Errorf("list tasks request is nil") - } - - limit, offset := req.ToGormParams() - fitlerOptions := req.ToFilterOptions() - - tasks, total, err := repository.ListTasks(database.DB, limit, offset, fitlerOptions) - if err != nil { - return nil, fmt.Errorf("failed to list tasks: %w", err) - } - - taskResps := make([]dto.TaskResp, 0, len(tasks)) - for _, task := range tasks { - taskResps = append(taskResps, *dto.NewTaskResp(&task)) - } - - resp := dto.ListResp[dto.TaskResp]{ - Items: taskResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// StreamLogs sets up the WebSocket lifecycle, queries Loki for historical logs, subscribes to Redis Pub/Sub for real-time logs, -// and polls for task completion. It blocks until the context is cancelled or the task completes. -func (s *TaskLogStreamer) StreamLogs(ctx context.Context, task *database.Task) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Setup WebSocket connection parameters - s.conn.SetReadLimit(maxMsgSize) - _ = s.conn.SetReadDeadline(time.Now().Add(pongWait)) - s.conn.SetPongHandler(func(string) error { - _ = s.conn.SetReadDeadline(time.Now().Add(pongWait)) - return nil - }) - - // Read pump — handles client messages and detects disconnection - go func() { - defer cancel() - for { - _, _, err := s.conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) { - s.log.Warnf("WebSocket unexpected close: %v", err) - } - return - } - } - }() - - // Ping ticker for keepalive - go s.runPingLoop(ctx, cancel) - - // Step 1: Subscribe to Redis Pub/Sub first (before querying Loki to avoid gaps) - pubsubChannel := "joblogs:" + s.taskID - pubsub := client.GetRedisClient().Subscribe(ctx, pubsubChannel) - defer func() { _ = pubsub.Close() }() - - if _, err := pubsub.Receive(ctx); err != nil { - s.log.Errorf("Failed to subscribe to Redis Pub/Sub channel %s: %v", pubsubChannel, err) - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeError, - Message: "failed to subscribe to log stream", - }) - return - } - s.log.Info("Subscribed to Redis Pub/Sub for real-time logs") - - // Step 2: Query Loki for historical logs - lastHistoricalTime := s.sendHistoricalLogs(task) - - // Step 3: Check if task is already completed - if isTaskTerminal(task.State) { - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeEnd, - Message: "task already completed", - }) - s.closeNormal("task completed") - return - } - - // Step 4: Forward real-time logs from Redis Pub/Sub - s.streamRealtime(ctx, pubsub.Channel(), lastHistoricalTime) -} - -// WriteMessage sends a WSLogMessage to the WebSocket connection with thread-safe locking. -func (s *TaskLogStreamer) WriteMessage(msg dto.WSLogMessage) { - s.mu.Lock() - defer s.mu.Unlock() - - _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := s.conn.WriteJSON(msg); err != nil { - s.log.Warnf("WebSocket write error: %v", err) - } -} - -// ForwardRedisLog parses a Redis Pub/Sub payload and forwards it as a realtime log entry. -// It deduplicates against lastHistoricalTime to avoid sending overlapping logs. -func (s *TaskLogStreamer) ForwardRedisLog(payload string, lastHistoricalTime time.Time) { - var entry dto.LogEntry - if err := json.Unmarshal([]byte(payload), &entry); err != nil { - s.log.Warnf("Failed to unmarshal Redis log message: %v", err) - return - } - - // Deduplicate: skip entries that are before or equal to the last historical log - if !lastHistoricalTime.IsZero() && !entry.Timestamp.After(lastHistoricalTime) { - return - } - - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeRealtime, - Logs: []dto.LogEntry{entry}, - }) -} - -// runPingLoop sends periodic WebSocket ping messages to keep the connection alive. -func (s *TaskLogStreamer) runPingLoop(ctx context.Context, cancel context.CancelFunc) { - ticker := time.NewTicker(pingPeriod) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - s.mu.Lock() - _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) - err := s.conn.WriteMessage(websocket.PingMessage, nil) - s.mu.Unlock() - if err != nil { - cancel() - return - } - } - } -} - -// sendHistoricalLogs queries Loki for historical logs and sends them to the client. -// Returns the timestamp of the last historical entry for deduplication. -func (s *TaskLogStreamer) sendHistoricalLogs(task *database.Task) time.Time { - // Use a dedicated context with timeout for the Loki query. - // The parent ctx is tied to the WebSocket lifecycle (Hijack'd connection), - // which may get cancelled prematurely and abort the HTTP request. - lokiCtx, lokiCancel := context.WithTimeout(context.Background(), 15*time.Second) - defer lokiCancel() - - lokiClient := client.NewLokiClient() - queryOpts := client.QueryOpts{ - Start: task.CreatedAt, - Direction: "forward", - } - - historicalLogs, err := lokiClient.QueryJobLogs(lokiCtx, s.taskID, queryOpts) - if err != nil { - s.log.Warnf("Failed to query Loki for historical logs: %v", err) - return time.Time{} - } - - if len(historicalLogs) > 0 { - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeHistory, - Logs: historicalLogs, - Total: len(historicalLogs), - }) - s.log.Infof("Sent %d historical log entries", len(historicalLogs)) - return historicalLogs[len(historicalLogs)-1].Timestamp - } - - return time.Time{} -} - -// streamRealtime forwards real-time logs from Redis Pub/Sub and polls for task completion. -func (s *TaskLogStreamer) streamRealtime(ctx context.Context, redisCh <-chan *redis.Message, lastHistoricalTime time.Time) { - // Task completion polling - taskDoneCh := make(chan struct{}) - go func() { - ticker := time.NewTicker(taskPollInterval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - t, err := repository.GetTaskByID(database.DB, s.taskID) - if err != nil { - s.log.Warnf("Failed to poll task state: %v", err) - continue - } - if isTaskTerminal(t.State) { - s.log.Info("Task detected as terminal, initiating close") - close(taskDoneCh) - return - } - } - } - }() - - for { - select { - case <-ctx.Done(): - s.log.Info("Context cancelled, closing WebSocket") - return - - case <-taskDoneCh: - s.flushAndClose(redisCh, lastHistoricalTime) - return - - case msg, ok := <-redisCh: - if !ok { - s.log.Warn("Redis Pub/Sub channel closed") - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeError, - Message: "log stream interrupted", - }) - return - } - s.ForwardRedisLog(msg.Payload, lastHistoricalTime) - } - } -} - -// flushAndClose drains remaining Redis messages after task completion, then closes. -func (s *TaskLogStreamer) flushAndClose(redisCh <-chan *redis.Message, lastHistoricalTime time.Time) { - s.log.Info("Task completed, flushing remaining logs...") - flushTimer := time.NewTimer(completionFlushDelay) - -flushLoop: - for { - select { - case msg, ok := <-redisCh: - if !ok { - break flushLoop - } - s.ForwardRedisLog(msg.Payload, lastHistoricalTime) - case <-flushTimer.C: - break flushLoop - } - } - flushTimer.Stop() - - s.WriteMessage(dto.WSLogMessage{ - Type: consts.WSLogTypeEnd, - Message: "task completed", - }) - s.closeNormal("task completed") -} - -// closeNormal sends a WebSocket close frame with NormalClosure status. -func (s *TaskLogStreamer) closeNormal(reason string) { - s.mu.Lock() - defer s.mu.Unlock() - - _ = s.conn.SetWriteDeadline(time.Now().Add(writeWait)) - _ = s.conn.WriteMessage(websocket.CloseMessage, - websocket.FormatCloseMessage(websocket.CloseNormalClosure, reason)) -} - -// isTaskTerminal checks if a task state represents a terminal (completed/error/cancelled) state. -func isTaskTerminal(state consts.TaskState) bool { - return state == consts.TaskCompleted || state == consts.TaskError || state == consts.TaskCancelled -} diff --git a/src/service/producer/team.go b/src/service/producer/team.go deleted file mode 100644 index c3ea4846..00000000 --- a/src/service/producer/team.go +++ /dev/null @@ -1,381 +0,0 @@ -package producer - -import ( - "errors" - "fmt" - - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - - "gorm.io/gorm" -) - -// CreateTeam creates a new team -func CreateTeam(req *dto.CreateTeamReq, userID int) (*dto.TeamResp, error) { - team := req.ConvertToTeam() - - // Get super_admin role - superAdminRole, err := repository.GetRoleByName(database.DB, consts.RoleSuperAdmin.String()) - if err != nil { - return nil, fmt.Errorf("failed to get super_admin role: %w", err) - } - - err = database.DB.Transaction(func(tx *gorm.DB) error { - if err := repository.CreateTeam(tx, team); err != nil { - if errors.Is(err, consts.ErrAlreadyExists) { - return consts.ErrAlreadyExists - } - return fmt.Errorf("failed to create team: %w", err) - } - - // Add creator as team admin - userTeam := &database.UserTeam{ - UserID: userID, - TeamID: team.ID, - RoleID: superAdminRole.ID, - Status: consts.CommonEnabled, - } - if err := repository.CreateUserTeam(tx, userTeam); err != nil { - return fmt.Errorf("failed to add creator to team: %w", err) - } - - return nil - }) - - if err != nil { - return nil, err - } - - return dto.NewTeamResp(team), nil -} - -// DeleteTeam soft deletes a team -func DeleteTeam(teamID int) error { - rowsAffected, err := repository.DeleteTeam(database.DB, teamID) - if err != nil { - return err - } - if rowsAffected == 0 { - return consts.ErrNotFound - } - return nil -} - -// GetTeamDetail retrieves detailed team information -func GetTeamDetail(teamID int) (*dto.TeamDetailResp, error) { - team, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, consts.ErrNotFound - } - return nil, err - } - - resp := dto.NewTeamDetailResp(team) - - // Get user count - userCount, err := repository.GetTeamUserCount(database.DB, teamID) - if err != nil { - return nil, fmt.Errorf("failed to get team user count: %w", err) - } - resp.UserCount = userCount - - // Get project count - projectCount, err := repository.GetTeamProjectCount(database.DB, teamID) - if err != nil { - return nil, fmt.Errorf("failed to get team project count: %w", err) - } - resp.ProjectCount = projectCount - - return resp, nil -} - -// ListTeams lists teams with pagination and filtering -func ListTeams(req *dto.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[dto.TeamResp], error) { - var teamIDs []int - if !isAdmin { - userTeams, err := repository.ListUserTeamsByUserID(database.DB, userID, consts.CommonEnabled) - if err != nil { - return nil, fmt.Errorf("failed to get user teams: %w", err) - } - for _, ut := range userTeams { - teamIDs = append(teamIDs, ut.TeamID) - } - } - - limit, offset := req.ToGormParams() - teams, total, err := repository.ListTeams(database.DB, limit, offset, req.IsPublic, req.Status, teamIDs) - if err != nil { - return nil, err - } - - items := make([]dto.TeamResp, len(teams)) - for i, team := range teams { - items[i] = *dto.NewTeamResp(&team) - } - - resp := dto.ListResp[dto.TeamResp]{ - Items: items, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateTeam updates team information -func UpdateTeam(req *dto.UpdateTeamReq, teamID int) (*dto.TeamResp, error) { - team, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, consts.ErrNotFound - } - return nil, err - } - - req.PatchTeamModel(team) - - if err := repository.UpdateTeam(database.DB, team); err != nil { - return nil, err - } - - return dto.NewTeamResp(team), nil -} - -// ListTeamProjects lists all projects belonging to a team -func ListTeamProjects(req *dto.ListProjectReq, teamID int) (*dto.ListResp[dto.ProjectResp], error) { - // Get paginated projects - limit, offset := req.ToGormParams() - projects, total, err := repository.ListProjectsByTeamID(database.DB, teamID, limit, offset, req.IsPublic, req.Status) - if err != nil { - return nil, err - } - - projectIDs := make([]int, 0, len(projects)) - for _, p := range projects { - projectIDs = append(projectIDs, p.ID) - } - - statsMap, err := repository.BatchGetProjectStatistics(database.DB, projectIDs) - if err != nil { - return nil, fmt.Errorf("failed to batch get project statistics: %w", err) - } - - projectResps := make([]dto.ProjectResp, 0, len(projects)) - for i := range projects { - stats := statsMap[projects[i].ID] - projectResps = append(projectResps, *dto.NewProjectResp(&projects[i], stats)) - } - - resp := dto.ListResp[dto.ProjectResp]{ - Items: projectResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// AddTeamMember adds a user to team -func AddTeamMember(req *dto.AddTeamMemberReq, teamID int) error { - // Verify team exists - _, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return consts.ErrNotFound - } - return err - } - - // Get user by username - user, err := repository.GetUserByUsername(database.DB, req.Username) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("user not found: %s", req.Username) - } - return err - } - - // Verify role exists - _, err = repository.GetRoleByID(database.DB, req.RoleID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("role not found") - } - return err - } - - userTeam := &database.UserTeam{ - UserID: user.ID, - TeamID: teamID, - RoleID: req.RoleID, - Status: consts.CommonEnabled, - } - - if err := repository.CreateUserTeam(database.DB, userTeam); err != nil { - if errors.Is(err, consts.ErrAlreadyExists) { - return consts.ErrAlreadyExists - } - return err - } - - return nil -} - -// RemoveTeamMember removes a user from team (only admin can remove others, cannot remove self) -func RemoveTeamMember(teamID, currentUserID, targetUserID int) error { - // Cannot remove self - if targetUserID == currentUserID { - return fmt.Errorf("cannot remove yourself from the team") - } - - // Verify team exists - _, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return consts.ErrNotFound - } - return err - } - - // Remove user from team - rowsAffected, err := repository.DeleteUserTeam(database.DB, targetUserID, teamID) - if err != nil { - return err - } - if rowsAffected == 0 { - return fmt.Errorf("user is not a member of this team") - } - - return nil -} - -// UpdateTeamMemberRole updates a team member's role (only admin can do this) -func UpdateTeamMemberRole(req *dto.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { - // Verify team exists - _, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return consts.ErrNotFound - } - return err - } - - // Verify new role exists - _, err = repository.GetRoleByID(database.DB, req.RoleID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("role not found") - } - return err - } - - // Get existing user-team association - userTeams, err := repository.ListUserTeamsByUserID(database.DB, targetUserID) - if err != nil { - return err - } - - var targetUserTeam *database.UserTeam - for i := range userTeams { - if userTeams[i].TeamID == teamID { - targetUserTeam = &userTeams[i] - break - } - } - - if targetUserTeam == nil { - return fmt.Errorf("user is not a member of this team") - } - - // Update role - targetUserTeam.RoleID = req.RoleID - if err := database.DB.Save(targetUserTeam).Error; err != nil { - return fmt.Errorf("failed to update team member role: %w", err) - } - - return nil -} - -// ListTeamMembers lists all members of a team with pagination -func ListTeamMembers(req *dto.ListTeamMemberReq, teamID int) (*dto.ListResp[dto.TeamMemberResp], error) { - // Get paginated team members - limit, offset := req.ToGormParams() - users, total, err := repository.ListUsersByTeamID(database.DB, teamID, limit, offset) - if err != nil { - return nil, err - } - - // Build response - members := make([]dto.TeamMemberResp, 0, len(users)) - for _, user := range users { - userTeams, err := repository.ListUserTeamsByUserID(database.DB, user.ID) - if err != nil { - return nil, err - } - - for _, ut := range userTeams { - if ut.TeamID == teamID && ut.Status == consts.CommonEnabled { - member := dto.TeamMemberResp{ - UserID: user.ID, - Username: user.Username, - FullName: user.FullName, - Email: user.Email, - RoleID: ut.RoleID, - JoinedAt: ut.CreatedAt, - } - - if ut.Role != nil { - member.RoleName = ut.Role.DisplayName - } - - members = append(members, member) - break - } - } - } - - resp := dto.ListResp[dto.TeamMemberResp]{ - Items: members, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ============================================================================ -// Team Permission Check Helper Functions (exported for middleware) -// ============================================================================ - -// IsUserInTeam checks if a user is a member of a team -func IsUserInTeam(userID, teamID int) (bool, error) { - ut, err := repository.GetUserTeamRole(database.DB, userID, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return ut != nil, nil -} - -// IsUserTeamAdmin checks if a user has team admin role in a specific team -func IsUserTeamAdmin(userID, teamID int) (bool, error) { - ut, err := repository.GetUserTeamRole(database.DB, userID, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return ut != nil && ut.Role != nil && ut.Role.Name == consts.RoleTeamAdmin.String(), nil -} - -// IsTeamPublic checks if a team is publicly accessible -func IsTeamPublic(teamID int) (bool, error) { - team, err := repository.GetTeamByID(database.DB, teamID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return false, nil - } - return false, err - } - return team.IsPublic, nil -} diff --git a/src/service/producer/trace.go b/src/service/producer/trace.go deleted file mode 100644 index 5408de7e..00000000 --- a/src/service/producer/trace.go +++ /dev/null @@ -1,283 +0,0 @@ -package producer - -import ( - "aegis/client" - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "context" - "encoding/json" - "errors" - "fmt" - "reflect" - "strconv" - "strings" - "time" - - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var payloadTypeRegistry = map[consts.EventType]reflect.Type{ - // Algorithm execution events - consts.EventAlgoRunStarted: reflect.TypeFor[dto.ExecutionInfo](), - consts.EventAlgoRunSucceed: reflect.TypeFor[dto.ExecutionResult](), - consts.EventAlgoRunFailed: reflect.TypeFor[dto.ExecutionResult](), - - // Dataset Build events - consts.EventDatapackBuildStarted: reflect.TypeFor[dto.DatapackInfo](), - consts.EventDatapackBuildSucceed: reflect.TypeFor[dto.DatapackResult](), - consts.EventDatapackBuildFailed: reflect.TypeFor[dto.DatapackResult](), - - // K8s Job events - consts.EventJobSucceed: reflect.TypeFor[dto.JobMessage](), - consts.EventJobFailed: reflect.TypeFor[dto.JobMessage](), -} - -// ===================== Trace Service ===================== - -// GetTraceDetail retrieves detailed information about a specific trace -func GetTraceDetail(traceID string) (*dto.TraceDetailResp, error) { - trace, err := repository.GetTraceByID(database.DB, traceID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: trace id: %s", consts.ErrNotFound, traceID) - } - return nil, fmt.Errorf("failed to get trace: %w", err) - } - - resp := dto.NewTraceDetailResp(trace) - return resp, nil -} - -// ListTraces lists traces based on filter options and pagination -func ListTraces(req *dto.ListTraceReq) (*dto.ListResp[dto.TraceResp], error) { - if req == nil { - return nil, fmt.Errorf("list traces request is nil") - } - - limit, offset := req.ToGormParams() - filterOptions := req.ToFilterOptions() - - traces, total, err := repository.ListTraces(database.DB, limit, offset, filterOptions) - if err != nil { - return nil, fmt.Errorf("failed to list traces: %w", err) - } - - traceResps := make([]dto.TraceResp, 0, len(traces)) - for i := range traces { - traceResps = append(traceResps, *dto.NewTraceResp(&traces[i])) - } - - resp := dto.ListResp[dto.TraceResp]{ - Items: traceResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// ===================== Trace Stream Service ===================== - -type StreamProcessor struct { - isCompleted bool - algorithmMap map[string]struct{} - finishedCount int -} - -func NewStreamProcessor(algorithms []dto.ContainerVersionItem) *StreamProcessor { - algorithmMap := make(map[string]struct{}, len(algorithms)) - for _, algorithm := range algorithms { - algorithmMap[algorithm.ContainerName] = struct{}{} - } - - return &StreamProcessor{ - isCompleted: false, - algorithmMap: algorithmMap, - finishedCount: 0, - } -} - -func (sp *StreamProcessor) IsCompleted() bool { - return sp.isCompleted -} - -func (sp *StreamProcessor) ProcessMessageForSSE(msg redis.XMessage) (string, *dto.TraceStreamEvent, error) { - streamEvent, err := parseStreamEvent(msg.ID, msg.Values) - if err != nil { - return "", nil, fmt.Errorf("failed to parse stream message value: %v", err) - } - - switch streamEvent.EventName { - case consts.EventImageBuildSucceed: - sp.isCompleted = true - - case consts.EventRestartPedestalFailed, consts.EventFaultInjectionFailed, consts.EventDatapackBuildFailed: - sp.isCompleted = true - - case consts.EventDatapackNoAnomaly, consts.EventDatapackNoDetectorData: - sp.isCompleted = true - - case consts.EventDatapackResultCollection: - sp.isCompleted = len(sp.algorithmMap) == 0 - - case consts.EventAlgoResultCollection, consts.EventAlgoRunFailed: - payload, ok := streamEvent.Payload.(*dto.ExecutionResult) - if !ok { - return "", nil, fmt.Errorf("invalid payload type for task status update event: %T", streamEvent.Payload) - } - - if payload.Algorithm != config.GetDetectorName() { - if _, exists := sp.algorithmMap[payload.Algorithm]; exists { - sp.finishedCount++ - if sp.finishedCount >= len(sp.algorithmMap) { - sp.isCompleted = true - } - } - } else { - sp.isCompleted = true - } - } - - return msg.ID, streamEvent, nil -} - -// GetTraceStreamProcessor creates and initializes a stream processor for the given trace -func GetTraceStreamProcessor(ctx context.Context, traceID string) (*StreamProcessor, error) { - trace, err := repository.GetTraceByID(database.DB, traceID) - if err != nil { - return nil, fmt.Errorf("failed to fetch trace: %w", err) - } - - var algorithms []dto.ContainerVersionItem - if trace.Type == consts.TraceTypeFullPipeline { - if client.CheckCachedField(ctx, consts.InjectionAlgorithmsKey, trace.GroupID) { - err = client.GetHashField(ctx, consts.InjectionAlgorithmsKey, trace.GroupID, &algorithms) - if err != nil { - return nil, fmt.Errorf("failed to get algorithms from Redis: %w", err) - } - } - } - - return NewStreamProcessor(algorithms), nil -} - -// ReadTraceStreamMessages reads messages from the trace stream -func ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { - if lastID == "" { - lastID = "0" - } - - messages, err := client.RedisXRead(ctx, []string{streamKey, lastID}, count, block) - if err != nil { - return nil, fmt.Errorf("failed to read stream messages: %w", err) - } - return messages, err -} - -// parseStreamEvent parses a Redis stream message values into a StreamEvent -func parseStreamEvent(id string, values map[string]any) (*dto.TraceStreamEvent, error) { - message := "missing or invalid key %s in redis stream message values" - - taskID, ok := values[consts.RdbEventTaskID].(string) - if !ok || taskID == "" { - return nil, fmt.Errorf(message, consts.RdbEventTaskID) - } - - timeStamp, err := strconv.Atoi(strings.Split(id, "-")[0]) - if err != nil { - return nil, err - } - - event := &dto.TraceStreamEvent{ - TimeStamp: timeStamp, - TaskID: taskID, - } - - if _, exists := values[consts.RdbEventTaskType]; exists { - taskTypeStr, ok := values[consts.RdbEventTaskType].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventTaskType) - } - - taskTypePtr := consts.GetTaskTypeByName(taskTypeStr) - if taskTypePtr == nil { - return nil, fmt.Errorf("unknown task type name: %s", taskTypeStr) - } - - event.TaskType = *taskTypePtr - } - - if _, exists := values[consts.RdbEventFn]; exists { - fnName, ok := values[consts.RdbEventFn].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventFn) - } - event.FnName = fnName - } - - if _, exists := values[consts.RdbEventFileName]; exists { - fileName, ok := values[consts.RdbEventFileName].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventTaskID) - } - - event.FileName = fileName - } - - if _, exists := values[consts.RdbEventLine]; exists { - lineInt64, ok := values[consts.RdbEventLine].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventLine) - } - - line, err := strconv.Atoi(lineInt64) - if err != nil { - return nil, fmt.Errorf("invalid line number: %w", err) - } - event.Line = line - } - - if _, exists := values[consts.RdbEventName]; exists { - eventName, ok := values[consts.RdbEventName].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventName) - } - event.EventName = consts.EventType(eventName) - } - - if _, exists := values[consts.RdbEventPayload]; exists { - if values[consts.RdbEventPayload] != nil { - payloadStr, ok := values[consts.RdbEventPayload].(string) - if !ok { - return nil, fmt.Errorf(message, consts.RdbEventPayload) - } - - payload, err := parsePayloadByEventType(event.EventName, payloadStr) - if err != nil { - return nil, fmt.Errorf(message, consts.RdbEventPayload) - } - event.Payload = payload - } - } - - return event, nil -} - -// parsePayloadByEventType dynamically parses payload based on event type and -// returns the parsed payload as any, caller should do type assertion -func parsePayloadByEventType(eventType consts.EventType, payloadStr string) (any, error) { - payloadType, exists := payloadTypeRegistry[eventType] - if !exists { - return nil, nil - } - - valuePtr := reflect.New(payloadType) - - if err := json.Unmarshal([]byte(payloadStr), valuePtr.Interface()); err != nil { - return nil, fmt.Errorf("failed to unmarshal payload for event %s: %w", eventType, err) - } - - return valuePtr.Interface(), nil -} diff --git a/src/service/producer/upload.go b/src/service/producer/upload.go deleted file mode 100644 index ca814489..00000000 --- a/src/service/producer/upload.go +++ /dev/null @@ -1,262 +0,0 @@ -package producer - -import ( - "archive/zip" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "aegis/config" - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - - chaos "github.com/OperationsPAI/chaos-experiment/handler" - "github.com/sirupsen/logrus" -) - -// validParquetFiles is the set of recognized parquet files in a datapack archive -var validParquetFiles = map[string]bool{ - "abnormal_traces.parquet": true, - "abnormal_metrics.parquet": true, - "abnormal_logs.parquet": true, - "normal_traces.parquet": true, - "normal_metrics.parquet": true, - "normal_logs.parquet": true, -} - -// UploadDatapack handles the business logic for uploading a manual datapack -func UploadDatapack(req *dto.UploadDatapackReq, file io.Reader, fileSize int64) (*dto.UploadDatapackResp, error) { - // Parse labels and groundtruths from request - labels, err := req.ParseLabels() - if err != nil { - return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) - } - - groundtruths, err := req.ParseGroundtruths() - if err != nil { - return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) - } - - // Check name uniqueness - existing, _ := repository.GetInjectionByName(database.DB, req.Name, false) - if existing != nil { - return nil, fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, req.Name) - } - - // Save uploaded file to temp location - tmpFile, err := os.CreateTemp("", "datapack-upload-*.zip") - if err != nil { - return nil, fmt.Errorf("failed to create temp file: %w", err) - } - tmpPath := tmpFile.Name() - defer func() { _ = os.Remove(tmpPath) }() - - if _, err := io.Copy(tmpFile, file); err != nil { - _ = tmpFile.Close() - return nil, fmt.Errorf("failed to save uploaded file: %w", err) - } - _ = tmpFile.Close() - - // Validate archive contents - if err := validateDatapackArchive(tmpPath); err != nil { - return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) - } - - // Get target directory - datasetPath := config.GetString("jfs.dataset_path") - if datasetPath == "" { - return nil, fmt.Errorf("dataset path not configured") - } - targetDir := filepath.Join(datasetPath, req.Name) - - // Ensure target directory does not already exist - if _, err := os.Stat(targetDir); err == nil { - return nil, fmt.Errorf("%w: directory %s already exists", consts.ErrAlreadyExists, req.Name) - } - - // Extract zip to target directory - if err := extractZipToDir(tmpPath, targetDir); err != nil { - // Clean up on failure - _ = os.RemoveAll(targetDir) - return nil, fmt.Errorf("failed to extract archive: %w", err) - } - - // Determine ground truth source - groundtruthSource := "" - if len(groundtruths) > 0 { - // Ground truth was provided in the request - groundtruthSource = consts.GroundtruthSourceManual - } else { - // Try to extract ground truth from injection.json if not provided in request - groundtruths = extractGroundtruthFromInjectionJSON(targetDir) - if len(groundtruths) > 0 { - groundtruthSource = consts.GroundtruthSourceImported - } - } - - // Create FaultInjection record - category := chaos.SystemType("") - if req.Category != "" { - category = chaos.SystemType(req.Category) - } - - injection := &database.FaultInjection{ - Name: req.Name, - Source: consts.DatapackSourceManual, - FaultType: chaos.ChaosType(0), - Category: category, - Description: req.Description, - EngineConfig: "", - Groundtruths: groundtruths, - GroundtruthSource: groundtruthSource, - PreDuration: 0, - BenchmarkID: nil, - PedestalID: nil, - State: consts.DatapackBuildSuccess, - Status: consts.CommonEnabled, - } - - if err := CreateInjection(injection, labels); err != nil { - // Clean up extracted files on DB failure - _ = os.RemoveAll(targetDir) - return nil, err - } - - return &dto.UploadDatapackResp{ - ID: injection.ID, - Name: injection.Name, - }, nil -} - -// validateDatapackArchive checks that the zip archive contains at least one recognized parquet file -func validateDatapackArchive(zipPath string) error { - r, err := zip.OpenReader(zipPath) - if err != nil { - return fmt.Errorf("failed to open zip archive: %w", err) - } - defer func() { _ = r.Close() }() - - for _, f := range r.File { - name := filepath.Base(f.Name) - if validParquetFiles[name] { - return nil - } - } - - return fmt.Errorf("archive must contain at least one parquet file from: abnormal_traces.parquet, abnormal_metrics.parquet, abnormal_logs.parquet, normal_traces.parquet, normal_metrics.parquet, normal_logs.parquet") -} - -// extractZipToDir extracts a zip archive to the target directory with path traversal protection -func extractZipToDir(zipPath, targetDir string) error { - r, err := zip.OpenReader(zipPath) - if err != nil { - return fmt.Errorf("failed to open zip archive: %w", err) - } - defer func() { _ = r.Close() }() - - // Create target directory - if err := os.MkdirAll(targetDir, 0755); err != nil { - return fmt.Errorf("failed to create target directory: %w", err) - } - - for _, f := range r.File { - // Path traversal protection - destPath := filepath.Join(targetDir, f.Name) - if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(targetDir)+string(os.PathSeparator)) && - filepath.Clean(destPath) != filepath.Clean(targetDir) { - return fmt.Errorf("illegal file path in archive: %s", f.Name) - } - - if f.FileInfo().IsDir() { - if err := os.MkdirAll(destPath, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", f.Name, err) - } - continue - } - - // Ensure parent directory exists - if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { - return fmt.Errorf("failed to create parent directory for %s: %w", f.Name, err) - } - - outFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", f.Name, err) - } - - rc, err := f.Open() - if err != nil { - _ = outFile.Close() - return fmt.Errorf("failed to open file in archive %s: %w", f.Name, err) - } - - _, err = io.Copy(outFile, rc) - _ = rc.Close() - _ = outFile.Close() - if err != nil { - return fmt.Errorf("failed to extract file %s: %w", f.Name, err) - } - } - - return nil -} - -// injectionJSONGroundtruth represents the ground truth structure in injection.json -type injectionJSONGroundtruth struct { - Service []string `json:"service,omitempty"` - Pod []string `json:"pod,omitempty"` - Container []string `json:"container,omitempty"` - Metric []string `json:"metric,omitempty"` - Function []string `json:"function,omitempty"` - Span []string `json:"span,omitempty"` -} - -type injectionJSONFile struct { - Groundtruths []injectionJSONGroundtruth `json:"ground_truths"` - GroundTruth []injectionJSONGroundtruth `json:"ground_truth"` -} - -// extractGroundtruthFromInjectionJSON tries to read ground truth from injection.json in the directory -func extractGroundtruthFromInjectionJSON(dir string) []database.Groundtruth { - jsonPath := filepath.Join(dir, "injection.json") - data, err := os.ReadFile(jsonPath) - if err != nil { - logrus.Debugf("No injection.json found in %s: %v", dir, err) - return nil - } - - var parsed injectionJSONFile - if err := json.Unmarshal(data, &parsed); err != nil { - logrus.Warnf("Failed to parse injection.json in %s: %v", dir, err) - return nil - } - - // Try ground_truths first, then ground_truth - rawGTs := parsed.Groundtruths - if len(rawGTs) == 0 { - rawGTs = parsed.GroundTruth - } - - if len(rawGTs) == 0 { - return nil - } - - result := make([]database.Groundtruth, 0, len(rawGTs)) - for _, gt := range rawGTs { - result = append(result, database.Groundtruth{ - Service: gt.Service, - Pod: gt.Pod, - Container: gt.Container, - Metric: gt.Metric, - Function: gt.Function, - Span: gt.Span, - }) - } - - return result -} diff --git a/src/service/producer/user.go b/src/service/producer/user.go deleted file mode 100644 index a57f3cd8..00000000 --- a/src/service/producer/user.go +++ /dev/null @@ -1,213 +0,0 @@ -package producer - -import ( - "aegis/consts" - "aegis/database" - "aegis/dto" - "aegis/repository" - "errors" - "fmt" - - "golang.org/x/crypto/bcrypt" - "gorm.io/gorm" -) - -// CreateUser handles the business logic for creating a new user -func CreateUser(req *dto.CreateUserReq) (*dto.UserResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("failed to hash password: %w", err) - } - - user := &database.User{ - Username: req.Username, - Email: req.Email, - Password: string(hashedPassword), - FullName: req.FullName, - Phone: req.Phone, - Avatar: req.Avatar, - Status: consts.CommonEnabled, - IsActive: true, - } - - var createdUser *database.User - err = database.DB.Transaction(func(tx *gorm.DB) error { - if _, err := repository.GetUserByUsername(tx, user.Username); err == nil { - return fmt.Errorf("%w: username %s already exists", consts.ErrAlreadyExists, user.Username) - } - - if _, err := repository.GetUserByEmail(tx, user.Email); err == nil { - return fmt.Errorf("%w: email %s already exists", consts.ErrAlreadyExists, user.Email) - } - - if err := repository.CreateUser(tx, user); err != nil { - return err - } - - createdUser = user - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewUserResp(createdUser), nil -} - -// DeleteUser deletes an existing user by marking their status as deleted -func DeleteUser(userID int) error { - return database.DB.Transaction(func(tx *gorm.DB) error { - user, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return fmt.Errorf("failed to get user: %w", err) - } - - // Remove all associations with containers, datasets, and projects - if _, err := repository.RemoveContainersFromUser(tx, user.ID); err != nil { - return fmt.Errorf("failed to remove containers from user: %w", err) - } - if _, err = repository.RemoveDatasetsFromUser(tx, user.ID); err != nil { - return fmt.Errorf("failed to remove datasets from user: %w", err) - } - if _, err = repository.RemoveProjectsFromUser(tx, user.ID); err != nil { - return fmt.Errorf("failed to remove projects from user: %w", err) - } - - // Remove associated permissions and roles - if err := repository.RemovePermissionsFromUser(tx, user.ID); err != nil { - return fmt.Errorf("failed to remove projects from user: %w", err) - } - if err := repository.RemoveRolesFromUser(tx, user.ID); err != nil { - return fmt.Errorf("failed to remove roles from user: %w", err) - } - - rows, err := repository.DeleteUser(tx, userID) - if err != nil { - return fmt.Errorf("failed to delete user: %w", err) - } - if rows == 0 { - return fmt.Errorf("%w: user id %d not found", consts.ErrNotFound, userID) - } - - return nil - }) -} - -// GetUserDetail retrieves detailed information about a user by their ID -func GetUserDetail(userID int) (*dto.UserDetailResp, error) { - user, err := repository.GetUserByID(database.DB, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return nil, fmt.Errorf("%w: user with ID %d not found", consts.ErrNotFound, userID) - } - return nil, fmt.Errorf("failed to get user: %w", err) - } - - resp := dto.NewUserDetailResp(user) - - globalRoles, err := repository.ListRolesByUserID(database.DB, user.ID) - if err != nil { - return nil, fmt.Errorf("failed to get user global roles: %w", err) - } - - resp.GlobalRoles = make([]dto.RoleResp, len(globalRoles)) - for i, role := range globalRoles { - roleResp := *dto.NewRoleResp(&role) - resp.GlobalRoles[i] = roleResp - } - - permissions, err := repository.ListPermissionsByUserID(database.DB, user.ID) - if err != nil { - return nil, fmt.Errorf("failed to get user permissions: %w", err) - } - - resp.Permissions = make([]dto.PermissionResp, len(permissions)) - for i, permission := range permissions { - resp.Permissions[i] = *dto.NewPermissionResp(&permission) - } - - userContainers, userDatasets, userProjects, err := getAllUserResourceRoles(userID) - if err != nil { - return nil, fmt.Errorf("failed to get user resource roles: %w", err) - } - - resp.ContainerRoles = userContainers - resp.DatasetRoles = userDatasets - resp.ProjectRoles = userProjects - - return resp, nil -} - -// ListUsers lists users based on the provided filters -func ListUsers(req *dto.ListUserReq) (*dto.ListResp[dto.UserResp], error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - limit, offset := req.ToGormParams() - - users, total, err := repository.ListUsers(database.DB, limit, offset, req.IsActive, req.Status) - if err != nil { - return nil, fmt.Errorf("failed to list users: %w", err) - } - - userResps := make([]dto.UserResp, len(users)) - for i, u := range users { - userResps[i] = *dto.NewUserResp(&u) - } - - resp := dto.ListResp[dto.UserResp]{ - Items: userResps, - Pagination: req.ConvertToPaginationInfo(total), - } - return &resp, nil -} - -// UpdateUser updates an existing user's details -func UpdateUser(req *dto.UpdateUserReq, userID int) (*dto.UserResp, error) { - if err := req.Validate(); err != nil { - return nil, fmt.Errorf("validation failed: %w", err) - } - - var updatedUser *database.User - - err := database.DB.Transaction(func(tx *gorm.DB) error { - existingUser, err := repository.GetUserByID(tx, userID) - if err != nil { - if errors.Is(err, consts.ErrNotFound) { - return fmt.Errorf("%w: user not found", consts.ErrNotFound) - } - return fmt.Errorf("failed to get user: %w", err) - } - - req.PatchUserModel(existingUser) - - if err := repository.UpdateUser(tx, existingUser); err != nil { - return fmt.Errorf("failed to update user: %w", err) - } - - updatedUser = existingUser - return nil - }) - if err != nil { - return nil, err - } - - return dto.NewUserResp(updatedUser), nil -} - -func SearchUsers(req *dto.SearchReq[string]) (*dto.SearchResp[dto.UserResp], error) { - return nil, nil -} - -// IsUserSystemAdmin checks if a user has system admin role -func IsUserSystemAdmin(userID int) (bool, error) { - return repository.IsSystemAdmin(database.DB, userID) -} diff --git a/src/testutil/redisstub.go b/src/testutil/redisstub.go new file mode 100644 index 00000000..60fca6ad --- /dev/null +++ b/src/testutil/redisstub.go @@ -0,0 +1,132 @@ +package testutil + +import ( + "bufio" + "fmt" + "io" + "net" + "strconv" + "strings" + "testing" +) + +func StartRedisStub(tb testing.TB) (string, func()) { + tb.Helper() + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + tb.Fatalf("listen redis stub: %v", err) + } + + done := make(chan struct{}) + go func() { + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-done: + return + default: + return + } + } + + go handleRedisStubConn(conn) + } + }() + + cleanup := func() { + close(done) + _ = ln.Close() + } + + return ln.Addr().String(), cleanup +} + +func handleRedisStubConn(conn net.Conn) { + defer conn.Close() + + reader := bufio.NewReader(conn) + writer := bufio.NewWriter(conn) + + for { + cmd, err := readRESPArray(reader) + if err != nil { + if err == io.EOF { + return + } + _, _ = writer.WriteString("-ERR invalid request\r\n") + _ = writer.Flush() + return + } + if len(cmd) == 0 { + continue + } + + switch strings.ToUpper(cmd[0]) { + case "PING": + _, _ = writer.WriteString("+PONG\r\n") + case "HELLO": + _, _ = writer.WriteString("%7\r\n+server\r\n+redis\r\n+version\r\n+7.0.0\r\n+proto\r\n:3\r\n+id\r\n:1\r\n+mode\r\n+standalone\r\n+role\r\n+master\r\n+modules\r\n*0\r\n") + case "CLIENT", "AUTH", "SELECT", "QUIT": + _, _ = writer.WriteString("+OK\r\n") + case "COMMAND": + _, _ = writer.WriteString("*0\r\n") + case "LPUSH", "HSET", "ZADD": + _, _ = writer.WriteString(":1\r\n") + default: + _, _ = writer.WriteString("+OK\r\n") + } + + if err := writer.Flush(); err != nil { + return + } + } +} + +func readRESPArray(reader *bufio.Reader) ([]string, error) { + prefix, err := reader.ReadByte() + if err != nil { + return nil, err + } + if prefix != '*' { + return nil, fmt.Errorf("unexpected prefix %q", prefix) + } + + countLine, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + count, err := strconv.Atoi(strings.TrimSpace(countLine)) + if err != nil { + return nil, err + } + + items := make([]string, 0, count) + for i := 0; i < count; i++ { + bulkPrefix, err := reader.ReadByte() + if err != nil { + return nil, err + } + if bulkPrefix != '$' { + return nil, fmt.Errorf("unexpected bulk prefix %q", bulkPrefix) + } + + sizeLine, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + size, err := strconv.Atoi(strings.TrimSpace(sizeLine)) + if err != nil { + return nil, err + } + + buf := make([]byte, size+2) + if _, err := io.ReadFull(reader, buf); err != nil { + return nil, err + } + items = append(items, string(buf[:size])) + } + + return items, nil +} diff --git a/src/utils/access_key_crypto.go b/src/utils/access_key_crypto.go new file mode 100644 index 00000000..182b47b1 --- /dev/null +++ b/src/utils/access_key_crypto.go @@ -0,0 +1,78 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" +) + +func EncryptAccessKeySecret(secret string) (string, error) { + block, err := aes.NewCipher(accessKeyCryptoKey()) + if err != nil { + return "", fmt.Errorf("failed to initialize cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to initialize GCM: %w", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", fmt.Errorf("failed to generate nonce: %w", err) + } + + ciphertext := gcm.Seal(nonce, nonce, []byte(secret), nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func DecryptAccessKeySecret(ciphertext string) (string, error) { + raw, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("failed to decode ciphertext: %w", err) + } + + block, err := aes.NewCipher(accessKeyCryptoKey()) + if err != nil { + return "", fmt.Errorf("failed to initialize cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to initialize GCM: %w", err) + } + + nonceSize := gcm.NonceSize() + if len(raw) < nonceSize { + return "", fmt.Errorf("ciphertext is too short") + } + + nonce, encrypted := raw[:nonceSize], raw[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, encrypted, nil) + if err != nil { + return "", fmt.Errorf("failed to decrypt ciphertext: %w", err) + } + + return string(plaintext), nil +} + +func SignAccessKeyRequest(secret, payload string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(payload)) + return hex.EncodeToString(mac.Sum(nil)) +} + +func VerifyAccessKeyRequestSignature(secret, payload, signature string) bool { + expected := SignAccessKeyRequest(secret, payload) + return hmac.Equal([]byte(expected), []byte(signature)) +} + +func accessKeyCryptoKey() []byte { + sum := sha256.Sum256([]byte(JWTSecret)) + return sum[:] +} diff --git a/src/utils/jwt.go b/src/utils/jwt.go index 490ddf6a..e10f6e6a 100644 --- a/src/utils/jwt.go +++ b/src/utils/jwt.go @@ -21,12 +21,14 @@ const ( // Claims represents JWT claims structure type Claims struct { - UserID int `json:"user_id"` - Username string `json:"username"` - Email string `json:"email"` - IsActive bool `json:"is_active"` - IsAdmin bool `json:"is_admin"` // System admin flag (super_admin or admin) - Roles []string `json:"roles"` // Global role names + UserID int `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + IsActive bool `json:"is_active"` + IsAdmin bool `json:"is_admin"` // System admin flag (super_admin or admin) + Roles []string `json:"roles"` // Global role names + AuthType string `json:"auth_type,omitempty"` + AccessKeyID int `json:"access_key_id,omitempty"` jwt.RegisteredClaims } @@ -45,17 +47,27 @@ type ServiceClaims struct { // GenerateToken generates a new JWT token for the given user func GenerateToken(userID int, username, email string, isActive, isAdmin bool, roles []string) (string, time.Time, error) { + return generateUserToken(userID, username, email, isActive, isAdmin, roles, "user", 0) +} + +func GenerateAccessKeyToken(userID int, username, email string, isActive, isAdmin bool, roles []string, accessKeyID int) (string, time.Time, error) { + return generateUserToken(userID, username, email, isActive, isAdmin, roles, "access_key", accessKeyID) +} + +func generateUserToken(userID int, username, email string, isActive, isAdmin bool, roles []string, authType string, accessKeyID int) (string, time.Time, error) { expirationTime := time.Now().Add(TokenExpiration) claims := &Claims{ - UserID: userID, - Username: username, - Email: email, - IsActive: isActive, - IsAdmin: isAdmin, - Roles: roles, + UserID: userID, + Username: username, + Email: email, + IsActive: isActive, + IsAdmin: isAdmin, + Roles: roles, + AuthType: authType, + AccessKeyID: accessKeyID, RegisteredClaims: jwt.RegisteredClaims{ - ID: fmt.Sprintf("jwt_%d_%d", userID, time.Now().Unix()), // JWT ID (jti) + ID: fmt.Sprintf("jwt_%s_%d_%d", authType, userID, time.Now().Unix()), ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), From bd4a72786633bbc1ed6fabd41e2f719e804a3861 Mon Sep 17 00:00:00 2001 From: rainystevn1 Date: Sat, 18 Apr 2026 17:07:18 +0800 Subject: [PATCH 2/4] refactor(architecture): split monolith into six service boundaries with gRPC - introduce six dedicated service entry points: api-gateway, iam-service, resource-service, orchestrator-service, runtime-worker-service, system-service - add gateway layer in src/app/gateway/ with typed service clients for each boundary - add internal gRPC clients in src/internalclient/* for synchronous inter-service calls - add proto definitions and generated code for iam, orchestrator, resource, runtime, system services - add src/cmd/*/main.go entry points for each dedicated service mode - add handler_service.go to each module for clean handler-to-service wiring - add runtime injection matrix to README documenting mode vs service tradeoffs - update helm templates and docker-compose for multi-service deployment - move module-owned repositories into src/module/*/repository.go per service boundary - add Redis gateway.go for distributed rate limiting and session state - remove legacy spec docs replaced by runtime injection matrix documentation --- README.md | 329 ++- config.dev.toml | 30 + docker-compose.microservices.yaml | 109 + docs/access-key-signature-spec.md | 168 -- docs/aegisctl-cli-spec.md | 1131 -------- docs/backend-fx-refactor-plan.md | 650 ----- docs/log-streaming-plan.md | 527 ---- docs/model-dto-refactor-todo.md | 179 -- docs/report-index.md | 730 +++++ docs/swagger-audience-marking-report.md | 201 -- docs/todo.md | 114 +- helm/templates/configmap.yaml | 42 +- helm/templates/deployment.yaml | 388 ++- helm/templates/service.yaml | 176 +- helm/values.yaml | 25 +- manifests/microservices/README.md | 20 + .../microservices/aegislab-microservices.yaml | 330 +++ src/app/app.go | 38 +- src/app/both.go | 32 +- src/app/compat_options.go | 49 + src/app/consumer.go | 26 +- src/app/gateway/auth_services.go | 129 + src/app/gateway/metric_services.go | 118 + src/app/gateway/metric_services_test.go | 120 + src/app/gateway/middleware_service.go | 80 + src/app/gateway/options.go | 175 ++ src/app/gateway/orchestrator_services.go | 339 +++ src/app/gateway/orchestrator_services_test.go | 187 ++ src/app/gateway/rbac_services.go | 129 + src/app/gateway/remote_required.go | 7 + src/app/gateway/resource_services.go | 233 ++ src/app/gateway/resource_services_test.go | 104 + src/app/gateway/system_services.go | 97 + src/app/gateway/team_services.go | 97 + src/app/gateway/team_services_test.go | 60 + src/app/gateway/user_services.go | 137 + src/app/http_modules.go | 10 +- src/app/iam/options.go | 35 + src/app/orchestrator/options.go | 29 + src/app/producer.go | 16 +- src/app/remote_require.go | 58 + src/app/remote_require_test.go | 52 + src/app/resource/options.go | 38 + src/app/runtime/options.go | 25 + src/app/runtime_stack.go | 47 + src/app/service_entrypoints_test.go | 390 +++ src/app/startup_smoke_test.go | 8 +- src/app/system/options.go | 33 + src/cmd/aegisctl/client/client.go | 4 +- src/cmd/aegisctl/client/sse.go | 4 +- src/cmd/aegisctl/client/ws.go | 8 +- src/cmd/aegisctl/cmd/inject.go | 8 +- src/cmd/aegisctl/cmd/wait.go | 5 - src/cmd/api-gateway/main.go | 17 + src/cmd/iam-service/main.go | 16 + src/cmd/orchestrator-service/main.go | 16 + src/cmd/resource-service/main.go | 16 + src/cmd/runtime-worker-service/main.go | 16 + src/cmd/system-service/main.go | 16 + src/config.dev.toml | 30 + src/consts/consts.go | 9 + src/dto/common.go | 73 - src/httpx/request_id.go | 119 + src/httpx/request_id_test.go | 48 + src/infra/redis/gateway.go | 43 + src/infra/redis/task_queue.go | 86 +- src/interface/controller/module.go | 31 +- src/interface/grpciam/lifecycle.go | 94 + src/interface/grpciam/module.go | 11 + src/interface/grpciam/service.go | 979 +++++++ src/interface/grpciam/service_test.go | 288 ++ src/interface/grpcorchestrator/lifecycle.go | 94 + src/interface/grpcorchestrator/module.go | 18 + .../grpcorchestrator/project_statistics.go | 22 + src/interface/grpcorchestrator/service.go | 843 ++++++ .../grpcorchestrator/service_test.go | 888 +++++++ src/interface/grpcresource/lifecycle.go | 94 + src/interface/grpcresource/module.go | 11 + src/interface/grpcresource/service.go | 543 ++++ src/interface/grpcresource/service_test.go | 465 ++++ src/interface/grpcruntime/lifecycle.go | 94 + src/interface/grpcruntime/module.go | 11 + src/interface/grpcruntime/service.go | 231 ++ src/interface/grpcruntime/service_test.go | 52 + src/interface/grpcsystem/lifecycle.go | 94 + src/interface/grpcsystem/module.go | 11 + src/interface/grpcsystem/service.go | 233 ++ src/interface/grpcsystem/service_test.go | 171 ++ src/interface/worker/module.go | 4 + src/internalclient/iamclient/client.go | 1024 +++++++ src/internalclient/iamclient/module.go | 7 + .../orchestratorclient/client.go | 664 +++++ .../orchestratorclient/module.go | 7 + src/internalclient/resourceclient/client.go | 472 ++++ src/internalclient/resourceclient/module.go | 7 + src/internalclient/runtimeclient/client.go | 122 + src/internalclient/runtimeclient/module.go | 7 + src/internalclient/systemclient/client.go | 242 ++ src/internalclient/systemclient/module.go | 7 + src/main.go | 38 +- src/middleware/auth.go | 20 +- src/middleware/deps.go | 98 +- src/middleware/middleware.go | 14 + src/middleware/permission.go | 17 +- src/module/auth/handler.go | 4 +- src/module/auth/handler_service.go | 29 + src/module/auth/middleware_adapter.go | 7 + src/module/auth/module.go | 2 + src/module/auth/repository.go | 12 - src/module/auth/service.go | 37 +- src/module/auth/token_store.go | 13 + src/module/chaossystem/handler.go | 4 +- src/module/chaossystem/handler_service.go | 22 + src/module/chaossystem/module.go | 1 + src/module/container/core.go | 12 +- src/module/container/handler.go | 4 +- src/module/container/handler_service.go | 31 + src/module/container/module.go | 1 + src/module/container/repository.go | 149 +- .../container/resolve.go} | 158 +- src/module/container/service.go | 105 +- src/module/dataset/api_types.go | 77 +- src/module/dataset/core.go | 8 +- src/module/dataset/handler.go | 4 +- src/module/dataset/handler_service.go | 32 + src/module/dataset/module.go | 1 + src/module/dataset/repository.go | 91 +- .../dataset.go => module/dataset/resolve.go} | 28 +- src/module/dataset/service.go | 100 +- src/module/evaluation/execution_query.go | 76 + src/module/evaluation/handler.go | 4 +- src/module/evaluation/handler_service.go | 20 + src/module/evaluation/module.go | 2 + src/module/evaluation/service.go | 91 +- src/module/evaluation/service_test.go | 21 + src/module/execution/api_types.go | 23 + src/module/execution/handler.go | 4 +- src/module/execution/handler_service.go | 24 + src/module/execution/module.go | 1 + src/module/execution/repository.go | 164 +- src/module/execution/runtime_types.go | 21 + src/module/execution/service.go | 171 +- src/module/group/handler.go | 6 +- src/module/group/handler_service.go | 19 + src/module/group/module.go | 1 + src/module/group/service.go | 27 +- src/module/injection/handler.go | 4 +- src/module/injection/handler_service.go | 38 + src/module/injection/module.go | 1 + src/module/injection/repository.go | 93 +- .../injection/resolve.go} | 32 +- src/module/injection/runtime_types.go | 42 + src/module/injection/service.go | 242 +- src/module/injection/submit.go | 2 +- src/module/label/core.go | 70 +- src/module/label/handler.go | 4 +- src/module/label/handler_service.go | 21 + src/module/label/module.go | 1 + src/module/label/repository.go | 54 +- src/module/label/service.go | 10 +- src/module/metric/handler.go | 4 +- src/module/metric/handler_service.go | 14 + src/module/metric/module.go | 1 + src/module/metric/service.go | 3 + src/module/notification/handler.go | 4 +- src/module/notification/handler_service.go | 17 + src/module/notification/module.go | 1 + src/module/project/api_types.go | 9 +- src/module/project/handler.go | 4 +- src/module/project/handler_service.go | 21 + src/module/project/module.go | 2 + src/module/project/project_statistics.go | 61 + src/module/project/repository.go | 224 +- src/module/project/service.go | 61 +- src/module/project/service_test.go | 26 +- src/module/rbac/handler.go | 4 +- src/module/rbac/handler_service.go | 29 + src/module/rbac/module.go | 1 + src/module/rbac/repository.go | 70 +- src/module/rbac/service.go | 24 +- src/module/system/handler.go | 30 +- src/module/system/handler_service.go | 29 + src/module/system/module.go | 10 +- src/module/system/repository.go | 26 +- src/module/system/runtime_query.go | 71 + src/module/system/service.go | 98 +- src/module/system/service_test.go | 13 +- src/module/systemmetric/handler.go | 4 +- src/module/systemmetric/handler_service.go | 13 + src/module/systemmetric/module.go | 9 +- src/module/systemmetric/service.go | 34 +- src/module/task/handler.go | 4 +- src/module/task/handler_service.go | 23 + src/module/task/log_types.go | 9 + src/module/task/module.go | 1 + src/module/task/service.go | 30 + src/module/team/handler.go | 4 +- src/module/team/handler_service.go | 25 + src/module/team/module.go | 2 + src/module/team/project_reader.go | 127 + src/module/team/repository.go | 143 +- src/module/team/service.go | 51 +- src/module/team/service_test.go | 16 +- src/module/trace/handler.go | 4 +- src/module/trace/handler_service.go | 22 + src/module/trace/module.go | 1 + src/module/trace/service.go | 26 +- src/module/user/handler.go | 4 +- src/module/user/handler_service.go | 30 + src/module/user/module.go | 1 + src/module/user/repository.go | 49 +- src/module/user/service.go | 76 +- src/proto/iam/v1/iam.pb.go | 2209 +++++++++++++++ src/proto/iam/v1/iam.proto | 246 ++ src/proto/iam/v1/iam_grpc.pb.go | 2364 +++++++++++++++++ src/proto/orchestrator/v1/orchestrator.pb.go | 1903 +++++++++++++ src/proto/orchestrator/v1/orchestrator.proto | 192 ++ .../orchestrator/v1/orchestrator_grpc.pb.go | 1185 +++++++++ src/proto/resource/v1/resource.pb.go | 976 +++++++ src/proto/resource/v1/resource.proto | 105 + src/proto/resource/v1/resource_grpc.pb.go | 1034 +++++++ src/proto/runtime/v1/runtime.pb.go | 821 ++++++ src/proto/runtime/v1/runtime.proto | 84 + src/proto/runtime/v1/runtime_grpc.pb.go | 311 +++ src/proto/system/v1/system.pb.go | 466 ++++ src/proto/system/v1/system.proto | 51 + src/proto/system/v1/system_grpc.pb.go | 539 ++++ src/repository/container.go | 581 ---- src/repository/dataset.go | 485 ---- src/repository/detector.go | 33 - src/repository/execution.go | 522 ---- src/repository/granularity.go | 42 - src/repository/injection.go | 533 ---- src/repository/label.go | 303 --- src/repository/scope.go | 51 - src/router/router.go | 5 +- src/{repository => searchx}/query_builder.go | 172 +- src/service/common/label.go | 110 - src/service/common/template.go | 62 - src/service/consumer/algo_execution.go | 60 +- src/service/consumer/build_datapack.go | 5 - src/service/consumer/collect_result.go | 28 +- src/service/consumer/fault_injection.go | 66 +- src/service/consumer/k8s_handler.go | 25 +- src/service/consumer/owner_adapter.go | 171 ++ src/service/consumer/rate_limiter.go | 23 + src/service/consumer/rate_limiter_store.go | 8 + src/service/consumer/runtime_deps.go | 2 + src/service/consumer/runtime_snapshot.go | 175 ++ src/service/consumer/state_store.go | 113 +- src/service/initialization/producer.go | 9 +- src/testutil/redisstub.go | 4 +- 252 files changed, 28951 insertions(+), 7518 deletions(-) create mode 100644 docker-compose.microservices.yaml delete mode 100644 docs/access-key-signature-spec.md delete mode 100644 docs/aegisctl-cli-spec.md delete mode 100644 docs/backend-fx-refactor-plan.md delete mode 100644 docs/log-streaming-plan.md delete mode 100644 docs/model-dto-refactor-todo.md create mode 100644 docs/report-index.md delete mode 100644 docs/swagger-audience-marking-report.md create mode 100644 manifests/microservices/README.md create mode 100644 manifests/microservices/aegislab-microservices.yaml create mode 100644 src/app/compat_options.go create mode 100644 src/app/gateway/auth_services.go create mode 100644 src/app/gateway/metric_services.go create mode 100644 src/app/gateway/metric_services_test.go create mode 100644 src/app/gateway/middleware_service.go create mode 100644 src/app/gateway/options.go create mode 100644 src/app/gateway/orchestrator_services.go create mode 100644 src/app/gateway/orchestrator_services_test.go create mode 100644 src/app/gateway/rbac_services.go create mode 100644 src/app/gateway/remote_required.go create mode 100644 src/app/gateway/resource_services.go create mode 100644 src/app/gateway/resource_services_test.go create mode 100644 src/app/gateway/system_services.go create mode 100644 src/app/gateway/team_services.go create mode 100644 src/app/gateway/team_services_test.go create mode 100644 src/app/gateway/user_services.go create mode 100644 src/app/iam/options.go create mode 100644 src/app/orchestrator/options.go create mode 100644 src/app/remote_require.go create mode 100644 src/app/remote_require_test.go create mode 100644 src/app/resource/options.go create mode 100644 src/app/runtime/options.go create mode 100644 src/app/runtime_stack.go create mode 100644 src/app/service_entrypoints_test.go create mode 100644 src/app/system/options.go create mode 100644 src/cmd/api-gateway/main.go create mode 100644 src/cmd/iam-service/main.go create mode 100644 src/cmd/orchestrator-service/main.go create mode 100644 src/cmd/resource-service/main.go create mode 100644 src/cmd/runtime-worker-service/main.go create mode 100644 src/cmd/system-service/main.go create mode 100644 src/httpx/request_id.go create mode 100644 src/httpx/request_id_test.go create mode 100644 src/interface/grpciam/lifecycle.go create mode 100644 src/interface/grpciam/module.go create mode 100644 src/interface/grpciam/service.go create mode 100644 src/interface/grpciam/service_test.go create mode 100644 src/interface/grpcorchestrator/lifecycle.go create mode 100644 src/interface/grpcorchestrator/module.go create mode 100644 src/interface/grpcorchestrator/project_statistics.go create mode 100644 src/interface/grpcorchestrator/service.go create mode 100644 src/interface/grpcorchestrator/service_test.go create mode 100644 src/interface/grpcresource/lifecycle.go create mode 100644 src/interface/grpcresource/module.go create mode 100644 src/interface/grpcresource/service.go create mode 100644 src/interface/grpcresource/service_test.go create mode 100644 src/interface/grpcruntime/lifecycle.go create mode 100644 src/interface/grpcruntime/module.go create mode 100644 src/interface/grpcruntime/service.go create mode 100644 src/interface/grpcruntime/service_test.go create mode 100644 src/interface/grpcsystem/lifecycle.go create mode 100644 src/interface/grpcsystem/module.go create mode 100644 src/interface/grpcsystem/service.go create mode 100644 src/interface/grpcsystem/service_test.go create mode 100644 src/internalclient/iamclient/client.go create mode 100644 src/internalclient/iamclient/module.go create mode 100644 src/internalclient/orchestratorclient/client.go create mode 100644 src/internalclient/orchestratorclient/module.go create mode 100644 src/internalclient/resourceclient/client.go create mode 100644 src/internalclient/resourceclient/module.go create mode 100644 src/internalclient/runtimeclient/client.go create mode 100644 src/internalclient/runtimeclient/module.go create mode 100644 src/internalclient/systemclient/client.go create mode 100644 src/internalclient/systemclient/module.go create mode 100644 src/module/auth/handler_service.go create mode 100644 src/module/auth/middleware_adapter.go create mode 100644 src/module/chaossystem/handler_service.go create mode 100644 src/module/container/handler_service.go rename src/{service/common/container.go => module/container/resolve.go} (59%) create mode 100644 src/module/dataset/handler_service.go rename src/{service/common/dataset.go => module/dataset/resolve.go} (71%) create mode 100644 src/module/evaluation/execution_query.go create mode 100644 src/module/evaluation/handler_service.go create mode 100644 src/module/evaluation/service_test.go create mode 100644 src/module/execution/handler_service.go create mode 100644 src/module/execution/runtime_types.go create mode 100644 src/module/group/handler_service.go create mode 100644 src/module/injection/handler_service.go rename src/{service/common/datapack_resolver.go => module/injection/resolve.go} (81%) create mode 100644 src/module/injection/runtime_types.go create mode 100644 src/module/label/handler_service.go create mode 100644 src/module/metric/handler_service.go create mode 100644 src/module/notification/handler_service.go create mode 100644 src/module/project/handler_service.go create mode 100644 src/module/project/project_statistics.go create mode 100644 src/module/rbac/handler_service.go create mode 100644 src/module/system/handler_service.go create mode 100644 src/module/system/runtime_query.go create mode 100644 src/module/systemmetric/handler_service.go create mode 100644 src/module/task/handler_service.go create mode 100644 src/module/team/handler_service.go create mode 100644 src/module/team/project_reader.go create mode 100644 src/module/trace/handler_service.go create mode 100644 src/module/user/handler_service.go create mode 100644 src/proto/iam/v1/iam.pb.go create mode 100644 src/proto/iam/v1/iam.proto create mode 100644 src/proto/iam/v1/iam_grpc.pb.go create mode 100644 src/proto/orchestrator/v1/orchestrator.pb.go create mode 100644 src/proto/orchestrator/v1/orchestrator.proto create mode 100644 src/proto/orchestrator/v1/orchestrator_grpc.pb.go create mode 100644 src/proto/resource/v1/resource.pb.go create mode 100644 src/proto/resource/v1/resource.proto create mode 100644 src/proto/resource/v1/resource_grpc.pb.go create mode 100644 src/proto/runtime/v1/runtime.pb.go create mode 100644 src/proto/runtime/v1/runtime.proto create mode 100644 src/proto/runtime/v1/runtime_grpc.pb.go create mode 100644 src/proto/system/v1/system.pb.go create mode 100644 src/proto/system/v1/system.proto create mode 100644 src/proto/system/v1/system_grpc.pb.go delete mode 100644 src/repository/container.go delete mode 100644 src/repository/dataset.go delete mode 100644 src/repository/detector.go delete mode 100644 src/repository/execution.go delete mode 100644 src/repository/granularity.go delete mode 100644 src/repository/injection.go delete mode 100644 src/repository/label.go delete mode 100644 src/repository/scope.go rename src/{repository => searchx}/query_builder.go (52%) delete mode 100644 src/service/common/label.go delete mode 100644 src/service/common/template.go create mode 100644 src/service/consumer/owner_adapter.go create mode 100644 src/service/consumer/runtime_snapshot.go diff --git a/README.md b/README.md index 490905d5..d337563c 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,69 @@ RCABench enables researchers and practitioners to: ## 🏗️ Architecture -The platform consists of several key components: +The current backend architecture is a single repository with a single `go.mod`, but it supports both: -- **Core API Server** (Go): REST API for managing experiments, algorithms, and evaluations -- **Python SDK**: Client library for programmatic interaction with the platform -- **Fault Injection Engine**: Kubernetes-native chaos engineering capabilities -- **Algorithm Registry**: Extensible framework for RCA algorithm integration -- **Evaluation Framework**: Automated metrics calculation and comparison tools -- **Observability Stack**: Integration with tracing, metrics, and logging systems +- **local monolith-style development modes** for speed +- **split-service runtime modes** for service-boundary validation + +The main service boundaries are: + +- **`api-gateway`**: external HTTP/OpenAPI entrypoint +- **`iam-service`**: auth, user, RBAC, team, access-key +- **`resource-service`**: project, label, container, dataset, evaluation metadata/query +- **`orchestrator-service`**: submit, task, trace, retry, dead-letter, workflow control-plane +- **`runtime-worker-service`**: Redis async consumption, K8s/BuildKit/Helm/Chaos runtime execution +- **`system-service`**: config, audit, monitor, health, metrics + +Key implementation rules: + +- External APIs are HTTP/OpenAPI. +- Internal synchronous calls are gRPC via `src/internalclient/*`. +- Long-running execution stays asynchronous on Redis; it is not converted into synchronous execution RPC. +- Module-owned DB access lives in `src/module/*/repository.go`. +- Infra connectivity and low-level operations live in `src/infra/*`. + +## 🧩 Runtime Modes And Injection Rules + +The backend now has two categories of startup modes: + +- **local integrated modes**: `producer`, `consumer`, `both` +- **dedicated service modes**: `api-gateway`, `iam-service`, `resource-service`, `orchestrator-service`, `runtime-worker-service`, `system-service` + +### What `both` Actually Means + +`both` is **not** the six-service topology. + +It starts: + +- the local HTTP stack +- the local worker/consumer stack + +It is the fastest option for local end-to-end debugging such as: + +- submit -> queue -> worker -> state update +- task/trace/log flow +- API + async worker integration + +### Injection Matrix + +| Mode / Service | What Starts | Local Owner Implementations Injected | Internal Clients Required | Best For | +| --- | --- | --- | --- | --- | +| `producer` | HTTP server only | Yes, local HTTP-facing modules | No | API, handler/service, Swagger, frontend integration | +| `consumer` | worker/controller/receiver side only | Yes, local runtime-side owners | Optional depending on config | queue/runtime/worker-only debugging | +| `both` | HTTP + worker/controller/receiver | Yes, local owners for integrated debugging | Optional depending on config | full local async loop | +| `api-gateway` | external HTTP gateway | No cross-owner local fallback as main path; service-specific remote wiring is expected | Yes | gateway boundary and remote-first debugging | +| `iam-service` | IAM gRPC service | Yes, IAM-local owners only | Only if a specific cross-service read path needs it | auth/user/rbac/team/access-key | +| `resource-service` | Resource gRPC service | Yes, resource-local owners only | Yes for orchestrator-backed queries like some statistics/evaluation views | project/container/dataset/label/evaluation | +| `orchestrator-service` | Orchestrator gRPC service | Yes, orchestrator-local owners only | Optional runtime/resource dependencies as needed | submit/task/trace/workflow | +| `runtime-worker-service` | runtime worker + runtime gRPC | Yes, runtime-side execution infrastructure only | Yes, especially orchestrator target | Redis consumer, K8s/build/helm runtime | +| `system-service` | system gRPC service | Yes, system-local owners only | Yes, especially runtime target | config/audit/monitor/metrics | + +### Rule Of Thumb + +- Use **`producer`** for normal API development. +- Use **`both`** when you need the local async loop. +- Use the **six dedicated services** when you need to verify service boundaries, internal gRPC, or remote-first behavior. ## 📋 Prerequisites @@ -46,21 +101,62 @@ The platform consists of several key components: ## 🚀 Quick Start -### Option 1: Local Development with Docker Compose +### Option 1: Local Dependencies ```bash # Clone the repository git clone https://github.com/OperationsPAI/AegisLab.git cd AegisLab -# Start local environment -make local-debug +# Start core dependencies +docker compose up -d redis mysql etcd jaeger buildkitd loki prometheus grafana +``` + +### Option 2: Fast Local API Debugging -# The API will be available at http://localhost:8082 -# Swagger documentation at http://localhost:8082/swagger/index.html +```bash +cd src && go run . producer -conf ./config.dev.toml -port 8082 + +# HTTP: http://localhost:8082 +# Health: http://localhost:8082/system/health +# Docs: http://localhost:8082/docs/doc.json ``` -### Option 2: Kubernetes Deployment +### Option 3: Fast Local End-To-End Debugging + +```bash +cd src && go run . both -conf ./config.dev.toml -port 8082 +``` + +Use this mode when you need: + +- HTTP + worker in one local process set +- submit -> queue -> consumer -> query loop +- task / trace / logs integration + +### Option 4: Split-Service Debugging + +```bash +# terminal 1 +cd src && go run ./cmd/iam-service -conf ./config.dev.toml + +# terminal 2 +cd src && go run ./cmd/orchestrator-service -conf ./config.dev.toml + +# terminal 3 +cd src && go run ./cmd/resource-service -conf ./config.dev.toml + +# terminal 4 +cd src && go run ./cmd/runtime-worker-service -conf ./config.dev.toml + +# terminal 5 +cd src && go run ./cmd/system-service -conf ./config.dev.toml + +# terminal 6 +cd src && go run ./cmd/api-gateway -conf ./config.dev.toml -port 8082 +``` + +### Option 5: Kubernetes Deployment ```bash # Check prerequisites @@ -78,11 +174,10 @@ make logs ## 📖 Documentation -- **[User Guide](docs/user-guide.md)**: Complete guide for using RCABench -- **[Installation Guide](docs/installation.md)**: Detailed setup instructions -- **[API Reference](docs/api-reference.md)**: Complete API documentation -- **[Algorithm Development](docs/algorithm-development.md)**: Guide for implementing RCA algorithms -- **[Examples](docs/examples.md)**: Usage examples and tutorials +- **[Report Index](docs/report-index.md)**: Consolidated backend refactor, runtime, governance, SDK/auth, and validation notes +- **[Refactor TODO](docs/todo.md)**: Source-of-truth task list and final acceptance checklist +- **[Frontend Redesign](docs/frontend-redesign.md)**: Frontend redesign plan and IA notes +- **[Frontend UI Guidelines](docs/frontend-ui-guidelines.md)**: Frontend visual/system guidelines ## 🔧 Configuration @@ -110,11 +205,50 @@ host = "localhost:6379" [k8s] namespace = "default" +[clients.iam] +target = "127.0.0.1:9091" + +[clients.resource] +target = "127.0.0.1:9093" + +[clients.orchestrator] +target = "127.0.0.1:9092" + +[clients.runtime] +target = "127.0.0.1:9094" + +[clients.system] +target = "127.0.0.1:9095" + +[iam.grpc] +addr = ":9091" + +[resource.grpc] +addr = ":9093" + +[orchestrator.grpc] +addr = ":9092" + +[runtime_worker.grpc] +addr = ":9094" + +[system.grpc] +addr = ":9095" + [injection] benchmark = ["workload-name"] target_label_key = "app" ``` +Important config rules: + +- `producer` and `both` can use local owner implementations for fast debugging. +- dedicated services should use the appropriate `clients.*.target` values when a remote dependency is required. +- `api-gateway` validates `clients.iam.target`, `clients.resource.target`, `clients.orchestrator.target`, and `clients.system.target`. +- `runtime-worker-service` validates `clients.orchestrator.target`. +- `system-service` validates `clients.runtime.target`. +- `resource-service` validates `clients.orchestrator.target` for remote-backed query paths. + ### Storage Configuration For production deployment, configure persistent volumes: @@ -213,6 +347,128 @@ Access monitoring: ## 🛠️ Development +### Recommended Debug Flow + +Choose the mode first: + +- **API-only debugging** -> `producer` +- **local async loop debugging** -> `both` +- **service-boundary / gRPC debugging** -> six dedicated services + +### Where To Put Breakpoints + +#### HTTP issues + +Start here: + +- `src/router/*` +- `src/module/*/handler.go` +- `src/module/*/service.go` +- `src/module/*/repository.go` + +If the problem only appears in split-service mode, then also check: + +- `src/app/gateway/*` +- `src/internalclient/*` + +#### gRPC / service-boundary issues + +Start here: + +- `src/internalclient/*` +- `src/interface/grpc*/*` +- `src/app/{gateway,iam,resource,orchestrator,runtime,system}/*` + +#### async runtime issues + +Start here: + +- `src/service/consumer/*` +- `src/interface/worker/*` +- `src/interface/controller/*` +- `src/infra/k8s/*` +- `src/infra/buildkit/*` +- `src/infra/helm/*` +- `src/infra/chaos/*` + +### Module-Oriented Debug Map + +#### Auth / User / RBAC / Team + +Check: + +- `src/module/auth/*` +- `src/module/user/*` +- `src/module/rbac/*` +- `src/module/team/*` + +Split-service path: + +- `src/app/gateway/{auth,user,rbac,team}_services.go` +- `src/internalclient/iamclient/*` +- `src/interface/grpciam/*` + +#### Project / Label / Container / Dataset + +Check: + +- `src/module/project/*` +- `src/module/label/*` +- `src/module/container/*` +- `src/module/dataset/*` + +Split-service path: + +- `src/app/gateway/resource_services.go` +- `src/internalclient/resourceclient/*` +- `src/interface/grpcresource/*` + +#### Injection / Execution / Task / Trace / Group / Notification + +Check: + +- `src/module/injection/*` +- `src/module/execution/*` +- `src/module/task/*` +- `src/module/trace/*` +- `src/module/group/*` +- `src/module/notification/*` + +Split-service path: + +- `src/app/gateway/orchestrator_services.go` +- `src/internalclient/orchestratorclient/*` +- `src/interface/grpcorchestrator/*` +- `src/service/consumer/*` + +#### System / Metrics / Monitor / Config / Audit + +Check: + +- `src/module/system/*` +- `src/module/systemmetric/*` + +Split-service path: + +- `src/app/gateway/system_services.go` +- `src/internalclient/systemclient/*` +- `src/internalclient/runtimeclient/*` +- `src/interface/grpcsystem/*` +- `src/interface/grpcruntime/*` + +#### Runtime / K8s / Build / Helm / Chaos + +Check: + +- `src/service/consumer/*` +- `src/interface/worker/*` +- `src/interface/controller/*` +- `src/infra/k8s/*` +- `src/infra/buildkit/*` +- `src/infra/helm/*` +- `src/infra/chaos/*` +- `src/infra/redis/*` + ### Building from Source ```bash @@ -282,11 +538,44 @@ make swagger # Generate API documentation kubectl auth can-i create pods --namespace=default ``` +4. **A Request Works In `producer` But Fails In Split-Service Mode** + + Check in this order: + + - are the dedicated services actually running? + - are the required `clients.*.target` values configured? + - is the request going through `src/internalclient/*` as expected? + - is the destination gRPC service registered and listening? + +5. **Submit Works But Task State Does Not Move** + + Check in this order: + + - Redis queue health + - `src/service/consumer/*` + - runtime infra (`src/infra/k8s/*`, `src/infra/buildkit/*`, `src/infra/helm/*`) + - orchestrator owner write-back path + +### Quick Validation Commands + +```bash +cd src && go test ./... +cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke' +cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions' +cd src && go test ./router ./docs ./interface/http +``` + +Real-cluster K8s validation: + +```bash +cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration +``` + ### Getting Help -- Check the [troubleshooting guide](docs/troubleshooting.md) +- Review the consolidated notes in `docs/report-index.md` - Review application logs with `make logs` -- Verify configuration in `src/config.toml` +- Verify configuration in `src/config.dev.toml` ## 📊 Performance Considerations diff --git a/config.dev.toml b/config.dev.toml index b0b0eedc..265bf1dd 100644 --- a/config.dev.toml +++ b/config.dev.toml @@ -60,6 +60,36 @@ experiment_storage_path = "/tmp/aegislab/experiment_storage" [buildkit] address = "localhost:1234" +[clients.iam] +target = "localhost:9091" + +[clients.orchestrator] +target = "localhost:9092" + +[clients.resource] +target = "localhost:9093" + +[clients.runtime] +target = "localhost:9094" + +[clients.system] +target = "localhost:9095" + +[iam.grpc] +addr = ":9091" + +[orchestrator.grpc] +addr = ":9092" + +[resource.grpc] +addr = ":9093" + +[runtime_worker.grpc] +addr = ":9094" + +[system.grpc] +addr = ":9095" + [loki] address = "http://10.10.10.161:3100" timeout = "10s" diff --git a/docker-compose.microservices.yaml b/docker-compose.microservices.yaml new file mode 100644 index 00000000..ffd6edd7 --- /dev/null +++ b/docker-compose.microservices.yaml @@ -0,0 +1,109 @@ +name: aegislab-microservices +services: + iam-service: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/iam-service", "-conf", "/workspace/src/config.dev.toml"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "9091:9091" + depends_on: + redis: + condition: service_healthy + mysql: + condition: service_healthy + + orchestrator-service: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/orchestrator-service", "-conf", "/workspace/src/config.dev.toml"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "9092:9092" + depends_on: + redis: + condition: service_healthy + mysql: + condition: service_healthy + + resource-service: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/resource-service", "-conf", "/workspace/src/config.dev.toml"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "9093:9093" + depends_on: + mysql: + condition: service_healthy + orchestrator-service: + condition: service_started + + runtime-worker-service: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/runtime-worker-service", "-conf", "/workspace/src/config.dev.toml"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "9094:9094" + depends_on: + redis: + condition: service_healthy + mysql: + condition: service_healthy + etcd: + condition: service_started + orchestrator-service: + condition: service_started + + system-service: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/system-service", "-conf", "/workspace/src/config.dev.toml"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "9095:9095" + depends_on: + redis: + condition: service_healthy + mysql: + condition: service_healthy + etcd: + condition: service_started + runtime-worker-service: + condition: service_started + + api-gateway: + image: golang:1.24-bookworm + working_dir: /workspace/src + command: ["go", "run", "./cmd/api-gateway", "-conf", "/workspace/src/config.dev.toml", "-port", "8082"] + volumes: + - ./:/workspace + environment: + GOPROXY: https://proxy.golang.org,direct + ports: + - "8082:8082" + depends_on: + iam-service: + condition: service_started + orchestrator-service: + condition: service_started + resource-service: + condition: service_started + system-service: + condition: service_started diff --git a/docs/access-key-signature-spec.md b/docs/access-key-signature-spec.md deleted file mode 100644 index 5599cbab..00000000 --- a/docs/access-key-signature-spec.md +++ /dev/null @@ -1,168 +0,0 @@ -# Access Key Signature Spec - -This document defines the canonical AK/SK signing flow used by SDK clients and `aegisctl` to exchange an access key for a short-lived bearer token. - -## Portal Workflow - -Recommended operator flow: - -1. Sign in to the Portal with your normal human account. -2. Open the access-key management page and create an access key for the specific automation use case. -3. Copy the returned `access_key` and one-time `secret_key` immediately and store them in your secret manager. -4. Use the signed-header token exchange flow in SDKs, `aegisctl`, CI, or other automation. -5. Rotate or disable the key from Portal when the automation changes or is no longer needed. - -Portal manages the credential lifecycle, while runtime callers only use: - -- `access_key` -- `secret_key` -- `POST /api/v2/auth/access-key/token` - -## Endpoint - -- `POST /api/v2/auth/access-key/token` - -This endpoint is the only place where `secret_key` is used directly. All normal business APIs still use: - -- `Authorization: Bearer ` - -## Required Headers - -Every token exchange request must include these headers: - -- `X-Access-Key`: the access key identifier, for example `ak_xxx` -- `X-Timestamp`: unix timestamp in seconds, for example `1713333333` -- `X-Nonce`: caller-generated unique nonce, max length `128` -- `X-Signature`: lowercase hex `HMAC-SHA256` - -## Canonical String - -The signature payload is the following newline-joined canonical string: - -```text -METHOD -PATH -ACCESS_KEY -TIMESTAMP -NONCE -``` - -For the token exchange endpoint, the canonical string looks like: - -```text -POST -/api/v2/auth/access-key/token -ak_demo -1713333333 -abc123 -``` - -Rules: - -- `METHOD` must be uppercase, for example `POST` -- `PATH` is the request path only, without scheme, host, or query string -- `ACCESS_KEY`, `TIMESTAMP`, and `NONCE` must exactly match the transmitted headers - -## Signature Algorithm - -Compute the signature as: - -```text -signature = hex(hmac_sha256(secret_key, canonical_string)) -``` - -Details: - -- hash: `SHA-256` -- MAC: `HMAC` -- output encoding: lowercase hexadecimal -- secret material: raw `secret_key` - -## Verification Rules - -The server currently enforces: - -- timestamp must be within `+- 5 minutes` -- nonce is single-use inside the validity window -- repeated nonce submissions are rejected as replay attempts -- disabled, deleted, or expired access keys cannot issue tokens - -Replay protection is implemented with Redis-backed nonce reservation. - -## Request Example - -```http -POST /api/v2/auth/access-key/token HTTP/1.1 -Host: aegislab.example.com -Accept: application/json -Content-Type: application/json -X-Access-Key: ak_demo -X-Timestamp: 1713333333 -X-Nonce: abc123 -X-Signature: 4cf2f2cbb93d... -``` - -The request body is empty. - -## curl Example - -The following example shows a full shell flow from `access_key` / `secret_key` to bearer token: - -```bash -ACCESS_KEY="ak_demo" -SECRET_KEY="sk_demo" -SERVER="http://localhost:8082" -PATH_URI="/api/v2/auth/access-key/token" -TIMESTAMP="$(date +%s)" -NONCE="$(openssl rand -hex 16)" -CANONICAL="POST\n${PATH_URI}\n${ACCESS_KEY}\n${TIMESTAMP}\n${NONCE}" -SIGNATURE="$(printf '%b' "${CANONICAL}" | openssl dgst -sha256 -hmac "${SECRET_KEY}" -hex | awk '{print $2}')" - -curl -X POST "${SERVER}${PATH_URI}" \ - -H "Accept: application/json" \ - -H "X-Access-Key: ${ACCESS_KEY}" \ - -H "X-Timestamp: ${TIMESTAMP}" \ - -H "X-Nonce: ${NONCE}" \ - -H "X-Signature: ${SIGNATURE}" -``` - -After receiving the response, extract `data.token` and use it as: - -```http -Authorization: Bearer -``` - -## Response Usage - -On success, the endpoint returns a bearer token payload similar to: - -```json -{ - "code": 0, - "message": "Access key token issued successfully", - "data": { - "token": "", - "token_type": "Bearer", - "expires_at": "2026-04-17T12:00:00Z", - "auth_type": "access_key", - "access_key": "ak_demo" - } -} -``` - -Clients must use the returned JWT for subsequent business API calls: - -```http -Authorization: Bearer -``` - -Do not send `X-Access-Key` / `X-Signature` headers to normal business endpoints. - -## aegisctl Debug Helpers - -`aegisctl` provides two local debugging commands for signature issues: - -- `aegisctl auth inspect`: inspect the stored auth context, token source, expiry, and access key metadata -- `aegisctl auth sign-debug --access-key ... --secret-key ...`: print the canonical string, signed headers, and a ready-to-run curl example -- `aegisctl auth sign-debug --execute`: execute the signed token exchange request immediately and print the API response -- `aegisctl auth sign-debug --execute --save-context`: execute the signed request and persist the returned bearer token into the current CLI context diff --git a/docs/aegisctl-cli-spec.md b/docs/aegisctl-cli-spec.md deleted file mode 100644 index 1a51a5f7..00000000 --- a/docs/aegisctl-cli-spec.md +++ /dev/null @@ -1,1131 +0,0 @@ -# aegisctl CLI Client Specification - -## Overview - -`aegisctl` is a Go-based command-line client for the AegisLab (RCABench) backend API. It enables AI agents and human operators to drive the full RCA experiment lifecycle from the terminal — fault injection, progress monitoring, algorithm execution, and result inspection — without manual curl commands or browser interaction. - -## Design Principles - -### 1. Name-First, Semantic-Driven - -All resource references use **names** instead of numeric IDs. The CLI internally resolves names to IDs via the API, keeping the interface human-readable and agent-friendly. - -```bash -# Good — semantic -aegisctl inject get pod-kill-ts-order-20260413 -aegisctl execute submit --project train-ticket --spec exec.yaml - -# Bad — opaque IDs -aegisctl inject get 42 -aegisctl execute submit --project 7 --spec exec.yaml -``` - -**Exception**: Resources without semantic names (task IDs, trace IDs, execution IDs) use their UUID/numeric identifiers directly. - -### 2. Machine-Parseable Output - -Every command supports `--output json` (alias `-o json`) for agent consumption. Default output is `table` for human readability. - -### 3. Exit Code Convention - -| Code | Meaning | -|------|---------| -| 0 | Success | -| 1 | Client error (invalid input, missing config, validation failure) | -| 2 | Server error (API returned 4xx/5xx) | -| 3 | Timeout (used by `wait` command) | - -### 4. Stream Separation - -- `stdout`: structured output only (data, tables, JSON) -- `stderr`: errors, warnings, progress messages - -This allows agents to pipe stdout to `jq` while still seeing errors. - ---- - -## Authentication & Configuration - -### Config File - -Location: `~/.aegisctl/config.yaml` - -```yaml -current-context: dev - -contexts: - dev: - server: http://localhost:8082 - token: eyJhbGci... - default-project: train-ticket - token-expiry: 2026-04-14T03:00:00Z - staging: - server: https://aegislab-staging.example.com - token: eyJhbGci... - -preferences: - output: table # Default output format (table|json|wide|yaml) - request-timeout: 30s # HTTP request timeout -``` - -### Token Resolution Priority (highest to lowest) - -1. `--token` command-line flag -2. `AEGIS_TOKEN` environment variable -3. `token` field in the active context of `config.yaml` - -### Environment Variable Overrides - -| Variable | Purpose | -|----------|---------| -| `AEGIS_SERVER` | API server URL | -| `AEGIS_TOKEN` | Authentication token | -| `AEGIS_PROJECT` | Default project name | -| `AEGIS_OUTPUT` | Default output format | -| `AEGIS_TIMEOUT` | Default request timeout | - -### Token Auto-Refresh - -Before each request, check `token-expiry`. If the token will expire within 5 minutes, automatically call `/api/v2/auth/refresh` and update the config file. - ---- - -## Command Tree - -### Global Flags - -Available on all commands: - -| Flag | Short | Env Var | Description | -|------|-------|---------|-------------| -| `--server` | `-s` | `AEGIS_SERVER` | API server URL | -| `--token` | `-t` | `AEGIS_TOKEN` | Authentication token | -| `--project` | `-p` | `AEGIS_PROJECT` | Default project name | -| `--output` | `-o` | `AEGIS_OUTPUT` | Output format: `table`, `json`, `wide`, `yaml` | -| `--request-timeout` | | `AEGIS_TIMEOUT` | HTTP request timeout (default: 30s) | -| `--quiet` | `-q` | | Suppress progress/info messages on stderr | -| `--dry-run` | | | Validate input without submitting (where applicable) | - ---- - -### `aegisctl auth` — Authentication - -#### `aegisctl auth login` - -Authenticate and persist token. - -```bash -# Exchange AK/SK for a bearer token -aegisctl auth login --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo - -# With context name -aegisctl auth login --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --context dev -``` - -**Behavior**: -- Computes the canonical string `METHOD\nPATH\nACCESS_KEY\nTIMESTAMP\nNONCE` -- Signs it with lowercase hex `HMAC-SHA256(secret_key, canonical_string)` -- Calls `POST /api/v2/auth/access-key/token` with `X-Access-Key`, `X-Timestamp`, `X-Nonce`, `X-Signature` -- Saves token + server + expiry to `~/.aegisctl/config.yaml` -- Sets as `current-context` if no context exists yet -- Prints authentication status to stdout -- Does not persist `secret_key` - -**Flags**: - -| Flag | Required | Description | -|------|----------|-------------| -| `--server` | Yes | API server URL | -| `--access-key` | Yes | Access key | -| `--secret-key` | Yes | Secret key | -| `--context` | No | Context name to save as (default: `default`) | - -#### `aegisctl auth status` - -Show current authentication status. - -```bash -aegisctl auth status -# Output: -# Context: dev -# Server: http://localhost:8082 -# User: admin -# Token: eyJh...xyz (expires: 2026-04-14T03:00:00Z) -# Status: valid -``` - -**Behavior**: Calls `GET /api/v2/auth/profile` to verify token validity. - -#### `aegisctl auth inspect` - -Inspect the current local auth context without sending credentials anywhere. - -```bash -aegisctl auth inspect -aegisctl auth inspect -o json -``` - -**Behavior**: -- Reads the active context from `~/.aegisctl/config.yaml` -- Prints `server`, `auth_type`, `access_key`, token preview, and expiry state - -#### `aegisctl auth sign-debug` - -Print the canonical string and signed headers for `AK/SK -> token` debugging. - -```bash -aegisctl auth sign-debug --access-key ak_demo --secret-key sk_demo -aegisctl auth sign-debug --access-key ak_demo --secret-key sk_demo --timestamp 1713333333 --nonce abc123 -aegisctl auth sign-debug --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --execute -aegisctl auth sign-debug --server http://localhost:8082 --access-key ak_demo --secret-key sk_demo --execute --save-context -``` - -**Behavior**: -- Rebuilds the canonical string `METHOD\nPATH\nACCESS_KEY\nTIMESTAMP\nNONCE` -- Prints the computed `X-Access-Key`, `X-Timestamp`, `X-Nonce`, `X-Signature` -- Prints a ready-to-run curl example for `POST /api/v2/auth/access-key/token` -- Optionally executes the request with `--execute` and prints the live response -- Optionally persists the returned bearer token into the active context with `--save-context` - -#### `aegisctl auth token` - -Directly set an API token without login flow. - -```bash -aegisctl auth token --set eyJhbGci... -``` - -**Use case**: CI/CD pipelines or agents that receive tokens from external secret managers. - -#### Portal Access Key Workflow - -Recommended usage: - -1. Create or rotate the access key in Portal. -2. Store `access_key` and one-time `secret_key` in a secret manager. -3. Use `aegisctl auth login --access-key ... --secret-key ...` or direct curl signing to get a bearer token. -4. Use the bearer token for normal API calls. - ---- - -### `aegisctl context` — Multi-Environment Management - -#### `aegisctl context set` - -Create or update a context. - -```bash -aegisctl context set --name staging --server https://aegislab-staging.example.com -aegisctl context set --name dev --default-project train-ticket -``` - -#### `aegisctl context use` - -Switch active context. - -```bash -aegisctl context use staging -``` - -#### `aegisctl context list` - -List all configured contexts. - -```bash -aegisctl context list -# Output: -# NAME SERVER DEFAULT-PROJECT CURRENT -# dev http://localhost:8082 train-ticket * -# staging https://aegislab-staging.example.com - -``` - ---- - -### `aegisctl project` — Project Management - -#### `aegisctl project list` - -```bash -aegisctl project list -aegisctl project list -o json -``` - -**API**: `GET /api/v2/projects` - -#### `aegisctl project get` - -```bash -aegisctl project get train-ticket -aegisctl project get train-ticket -o json -``` - -**API**: `GET /api/v2/projects/:project_id` (resolved from name) - -#### `aegisctl project create` - -```bash -aegisctl project create --name train-ticket --description "Train ticket microservice system" -``` - -**API**: `POST /api/v2/projects` - ---- - -### `aegisctl container` — Container Management - -#### `aegisctl container list` - -```bash -aegisctl container list -aegisctl container list --type algorithm -aegisctl container list --type pedestal -aegisctl container list --type benchmark -``` - -**API**: `GET /api/v2/containers` - -**Flags**: - -| Flag | Description | -|------|-------------| -| `--type` | Filter by container type: `algorithm`, `benchmark`, `pedestal` | - -#### `aegisctl container get` - -```bash -aegisctl container get train-ticket -``` - -**API**: `GET /api/v2/containers/:container_id` - -#### `aegisctl container versions` - -```bash -aegisctl container versions train-ticket -``` - -**API**: `GET /api/v2/containers/:container_id/versions` - -#### `aegisctl container build` - -```bash -aegisctl container build train-ticket --version v1.0.0 -``` - -**API**: `POST /api/v2/containers/build` - ---- - -### `aegisctl inject` — Fault Injection (Core) - -#### `aegisctl inject submit` - -Submit a fault injection experiment. - -```bash -aegisctl inject submit --project train-ticket --spec injection-spec.yaml -aegisctl inject submit --project train-ticket --spec injection-spec.yaml --dry-run -aegisctl inject submit --project train-ticket --spec injection-spec.yaml -o json -``` - -**API**: `POST /api/v2/projects/:project_id/injections/inject` - -**Spec File Format** (`injection-spec.yaml`): - -```yaml -pedestal: - name: train-ticket - version: v1.0.0 -benchmark: - name: jaeger-collector - version: v1.0.0 -interval: 30 # Total experiment interval in minutes -pre_duration: 10 # Normal data collection duration before fault injection (minutes) -specs: - # Each top-level element is a batch; faults within a batch run in parallel - - - type: pod-kill - namespace: ts - target: ts-order-service - duration: 60s - - - type: cpu-stress - namespace: ts - target: ts-payment-service - duration: 120s - - type: network-delay - namespace: ts - target: ts-order-service - duration: 120s -algorithms: # Optional: RCA algorithms to execute after injection - - name: rca-algo-1 - version: v1.0.0 - env_vars: - - key: THRESHOLD - value: "0.5" -labels: # Optional: labels to attach - - key: experiment - value: batch-001 - - key: scenario - value: cascade-failure -``` - -**JSON Output** (on success): - -```json -{ - "trace_id": "abc-123-def-456", - "group_id": "grp-789", - "tasks": [ - {"task_id": "task-001", "type": "RestartPedestal", "state": "Pending"} - ] -} -``` - -**`--dry-run` behavior**: Validate the spec file against the server (check container names exist, spec structure valid) without submitting. Exit 0 if valid, exit 1 with validation errors. - -#### `aegisctl inject list` - -```bash -aegisctl inject list --project train-ticket -aegisctl inject list --project train-ticket --state build_success -aegisctl inject list --project train-ticket --fault-type pod-kill -aegisctl inject list --project train-ticket --labels experiment=batch-001 -aegisctl inject list --project train-ticket --page 1 --size 20 -``` - -**API**: `GET /api/v2/projects/:project_id/injections` - -**Flags**: - -| Flag | Description | -|------|-------------| -| `--state` | Filter by datapack state: `initial`, `inject_failed`, `inject_success`, `build_failed`, `build_success`, `detector_failed`, `detector_success` | -| `--fault-type` | Filter by chaos type | -| `--labels` | Filter by labels (comma-separated `key=value` pairs) | -| `--page` | Page number (default: 1) | -| `--size` | Page size (default: 20) | - -**Table Output**: - -``` -NAME STATE FAULT-TYPE START-TIME LABELS -pod-kill-ts-order-20260413 build_success pod-kill 2026-04-13T10:00:00Z experiment=batch-001 -cpu-stress-ts-payment-20260413 inject_success cpu-stress 2026-04-13T10:05:00Z experiment=batch-001 -``` - -#### `aegisctl inject get` - -```bash -aegisctl inject get pod-kill-ts-order-20260413 -aegisctl inject get pod-kill-ts-order-20260413 -o json -``` - -**API**: `GET /api/v2/injections/:id` - -#### `aegisctl inject search` - -Advanced search with multiple filters. - -```bash -aegisctl inject search --project train-ticket --name-pattern "pod-kill-*" --labels experiment=batch-001 -``` - -**API**: `POST /api/v2/projects/:project_id/injections/search` - -#### `aegisctl inject logs` - -```bash -aegisctl inject logs pod-kill-ts-order-20260413 -``` - -**API**: `GET /api/v2/injections/:id/logs` - -#### `aegisctl inject files` - -```bash -aegisctl inject files pod-kill-ts-order-20260413 -``` - -**API**: `GET /api/v2/injections/:id/files` - -**Table Output**: - -``` -PATH SIZE TYPE -traces/trace.parquet 12.3 MB parquet -metrics/cpu.parquet 5.1 MB parquet -logs/service.log 2.0 MB text -groundtruth.yaml 0.1 KB yaml -``` - -#### `aegisctl inject download` - -```bash -aegisctl inject download pod-kill-ts-order-20260413 -o /tmp/datapack/ -``` - -**API**: `GET /api/v2/injections/:id/download` - -#### `aegisctl inject metadata` - -Show available fault types, resources, and status mappings. - -```bash -aegisctl inject metadata -``` - -**API**: `GET /api/v2/injections/metadata` - -**Output**: - -``` -FAULT TYPES: - pod-kill Kill target pods - cpu-stress Inject CPU stress - memory-stress Inject memory stress - network-delay Add network latency - network-loss Inject packet loss - ... - -DATAPACK STATES: - initial, inject_failed, inject_success, build_failed, - build_success, detector_failed, detector_success -``` - ---- - -### `aegisctl execute` — Algorithm Execution - -#### `aegisctl execute submit` - -```bash -aegisctl execute submit --project train-ticket --spec execution-spec.yaml -aegisctl execute submit --project train-ticket --spec execution-spec.yaml -o json -``` - -**API**: `POST /api/v2/projects/:project_id/executions/execute` - -**Spec File Format** (`execution-spec.yaml`): - -```yaml -specs: - - algorithm: - name: rca-algo-1 - version: v1.0.0 - datapack: pod-kill-ts-order-20260413 # Reference by injection name - - algorithm: - name: rca-algo-2 - version: v2.0.0 - dataset: # Or reference by dataset - name: train-ticket-dataset - version: v1.0.0 -labels: - - key: batch - value: comparison-run -``` - -**Note**: `project_name` is automatically set from `--project` flag; do not include in spec file. - -#### `aegisctl execute list` - -```bash -aegisctl execute list --project train-ticket -``` - -**API**: `GET /api/v2/projects/:project_id/executions` - -#### `aegisctl execute get` - -```bash -aegisctl execute get 123 -aegisctl execute get 123 -o json -``` - -**API**: `GET /api/v2/executions/:execution_id` - ---- - -### `aegisctl task` — Task Monitoring - -#### `aegisctl task list` - -```bash -aegisctl task list -aegisctl task list --state Running -aegisctl task list --type FaultInjection -``` - -**API**: `GET /api/v2/tasks` - -**Flags**: - -| Flag | Description | -|------|-------------| -| `--state` | Filter: `Pending`, `Running`, `Completed`, `Error`, `Cancelled`, `Rescheduled` | -| `--type` | Filter: `BuildContainer`, `RestartPedestal`, `FaultInjection`, `RunAlgorithm`, `BuildDatapack`, `CollectResult`, `CronJob` | - -**Table Output**: - -``` -TASK-ID TYPE STATE TRACE-ID PROJECT CREATED -task-abc123 RestartPedestal Running trace-def456 train-ticket 2m ago -task-xyz789 FaultInjection Pending trace-def456 train-ticket 1m ago -``` - -#### `aegisctl task get` - -```bash -aegisctl task get task-abc123 -aegisctl task get task-abc123 -o json -``` - -**API**: `GET /api/v2/tasks/:task_id` - -#### `aegisctl task logs` - -Stream task logs in real-time. - -```bash -aegisctl task logs task-abc123 -aegisctl task logs task-abc123 --follow # Continuously stream via WebSocket -``` - -**API**: `GET /api/v2/tasks/:task_id/logs/ws` (WebSocket) - -**`--follow` behavior**: Keep WebSocket connection open, print new log lines as they arrive. Ctrl+C to stop. - -**Without `--follow`**: Connect, read available logs, disconnect. - ---- - -### `aegisctl trace` — Experiment Tracing - -#### `aegisctl trace list` - -```bash -aegisctl trace list -aegisctl trace list --project train-ticket -aegisctl trace list --state Running -``` - -**API**: `GET /api/v2/traces` - -**Flags**: - -| Flag | Description | -|------|-------------| -| `--project` | Filter by project name | -| `--state` | Filter: `Pending`, `Running`, `Completed`, `Failed` | -| `--group-id` | Filter by group ID | - -**Table Output**: - -``` -TRACE-ID TYPE STATE PROJECT START-TIME TASKS -trace-abc123 FullPipeline Running train-ticket 2026-04-13T10:00:00Z 3/5 -trace-def456 AlgorithmRun Completed train-ticket 2026-04-13T09:30:00Z 2/2 -``` - -#### `aegisctl trace get` - -```bash -aegisctl trace get trace-abc123 -aegisctl trace get trace-abc123 -o json -``` - -**API**: `GET /api/v2/traces/:trace_id` - -**Detailed Output** (includes child tasks): - -``` -Trace: trace-abc123 -Type: FullPipeline -State: Running -Start: 2026-04-13T10:00:00Z - -Tasks: - TASK-ID TYPE STATE DURATION - task-001 RestartPedestal Completed 45s - task-002 FaultInjection Completed 5m30s - task-003 BuildDatapack Running 2m10s (in progress) - task-004 RunAlgorithm Pending - - task-005 CollectResult Pending - -``` - -#### `aegisctl trace watch` - -Real-time SSE event stream for a trace. - -```bash -aegisctl trace watch trace-abc123 -``` - -**API**: `GET /api/v2/traces/:trace_id/stream` (SSE) - -**Output** (streaming): - -``` -[10:00:05] RestartPedestal task-001 Running Restarting pedestal... -[10:00:45] RestartPedestal task-001 Completed Pedestal restarted successfully -[10:00:46] FaultInjection task-002 Running Injecting pod-kill on ts-order-service -[10:06:16] FaultInjection task-002 Completed Fault injection completed -[10:06:17] BuildDatapack task-003 Running Building datapack... -... -``` - -**Termination**: Stream ends when trace reaches terminal state (`Completed` or `Failed`), or on Ctrl+C. - ---- - -### `aegisctl dataset` — Dataset Management - -#### `aegisctl dataset list` - -```bash -aegisctl dataset list -``` - -**API**: `GET /api/v2/datasets` - -#### `aegisctl dataset get` - -```bash -aegisctl dataset get train-ticket-dataset -``` - -**API**: `GET /api/v2/datasets/:dataset_id` - -#### `aegisctl dataset versions` - -```bash -aegisctl dataset versions train-ticket-dataset -``` - -**API**: `GET /api/v2/datasets/:dataset_id/versions` - ---- - -### `aegisctl eval` — Evaluation Results - -#### `aegisctl eval list` - -```bash -aegisctl eval list -``` - -**API**: `GET /api/v2/evaluations` - -#### `aegisctl eval get` - -```bash -aegisctl eval get 123 -``` - -**API**: `GET /api/v2/evaluations/:id` - ---- - -### `aegisctl wait` — Block Until Completion - -Block execution until a trace or task reaches a terminal state. This is the primary synchronization primitive for agents. - -```bash -aegisctl wait trace-abc123 -aegisctl wait trace-abc123 --timeout 600s -aegisctl wait task-xyz789 --timeout 300s --interval 5s -aegisctl wait trace-abc123 --exit-on error -``` - -**Behavior**: -1. Detect whether the argument is a trace ID or task ID (by format or API probe) -2. Poll the status at `--interval` (default: 5s) -3. Print status line on each poll (to stderr, unless `--quiet`) -4. Exit when terminal state is reached or timeout - -**Flags**: - -| Flag | Default | Description | -|------|---------|-------------| -| `--timeout` | `600s` | Maximum wait time | -| `--interval` | `5s` | Poll interval | -| `--exit-on` | `completed,error` | Which terminal states to exit on | -| `--quiet` | `false` | Suppress polling status output | - -**Exit codes**: -- `0`: Completed successfully -- `2`: Completed with error/failure -- `3`: Timeout - -**JSON output** (`-o json`): On exit, prints the final resource state to stdout. - -```json -{ - "id": "trace-abc123", - "state": "Completed", - "duration": "5m30s", - "tasks_completed": 5, - "tasks_total": 5 -} -``` - -**Polling status** (stderr): - -``` -Waiting for trace-abc123... [Running] BuildDatapack (3/5 tasks) 2m10s elapsed -Waiting for trace-abc123... [Running] RunAlgorithm (4/5 tasks) 4m30s elapsed -Waiting for trace-abc123... [Completed] 5/5 tasks in 5m30s -``` - ---- - -### `aegisctl status` — Global Overview - -```bash -aegisctl status -``` - -**Output**: - -``` -Server: http://localhost:8082 (dev) -User: admin -Connected: yes - -Active Tasks: 3 - Running: 2 (FaultInjection, BuildDatapack) - Pending: 1 (RunAlgorithm) - -Recent Injections (last 24h): - NAME STATE FAULT-TYPE TIME - pod-kill-ts-order-20260413 build_success pod-kill 3h ago - cpu-stress-ts-payment-20260413 inject_success cpu-stress 5h ago - -Recent Traces: - TRACE-ID STATE TYPE DURATION - trace-abc123 Running FullPipeline 2m (in progress) - trace-def456 Completed AlgorithmRun 8m30s -``` - ---- - -### `aegisctl completion` — Shell Completion - -```bash -aegisctl completion bash > /etc/bash_completion.d/aegisctl -aegisctl completion zsh > "${fpath[1]}/_aegisctl" -aegisctl completion fish > ~/.config/fish/completions/aegisctl.fish -``` - ---- - -## Internal Architecture - -### Directory Structure - -``` -src/cmd/aegisctl/ -├── main.go # Entry point -├── cmd/ -│ ├── root.go # Cobra root command + global flags -│ ├── auth.go # auth login, status, token -│ ├── context.go # context set, use, list -│ ├── project.go # project list, get, create -│ ├── container.go # container list, get, versions, build -│ ├── inject.go # inject submit, list, get, search, logs, files, download, metadata -│ ├── execute.go # execute submit, list, get -│ ├── task.go # task list, get, logs -│ ├── trace.go # trace list, get, watch -│ ├── dataset.go # dataset list, get, versions -│ ├── eval.go # eval list, get -│ ├── wait.go # wait (poll trace/task state) -│ ├── status.go # status overview -│ └── completion.go # shell completion generation -├── client/ -│ ├── client.go # Core HTTP client (request/response/error handling) -│ ├── auth.go # Token management + auto-refresh -│ ├── sse.go # SSE streaming (trace watch, group stream) -│ ├── ws.go # WebSocket (task logs --follow) -│ └── resolver.go # Name-to-ID resolution + cache -├── config/ -│ └── config.go # ~/.aegisctl/config.yaml read/write -└── output/ - ├── format.go # Output dispatcher (table/json/wide/yaml) - ├── table.go # Table formatting with column alignment - └── printer.go # stdout/stderr stream separation -``` - -### Name-to-ID Resolver - -The resolver is the core abstraction that makes name-based references work. It maintains a short-lived cache to avoid redundant API calls within a single command session. - -```go -type Resolver struct { - client *Client - cache map[string]int // key format: "resource_type:name" -> ID - ttl time.Duration // Cache TTL (default: 5 minutes) -} - -// Core resolution methods -func (r *Resolver) ProjectID(name string) (int, error) -func (r *Resolver) ContainerID(name string) (int, error) -func (r *Resolver) InjectionID(name string) (int, error) -func (r *Resolver) DatasetID(name string) (int, error) -``` - -**Resolution strategy**: -1. Check local cache -2. Call list API with name filter (e.g., `GET /api/v2/projects?name=train-ticket`) -3. If exactly one match, cache and return ID -4. If zero matches, return error: `project "train-ticket" not found` -5. If multiple matches, return error with disambiguation hint - -### HTTP Client - -```go -type Client struct { - baseURL string - token string - httpClient *http.Client - resolver *Resolver -} - -// APIResponse is the standard response envelope -type APIResponse[T any] struct { - Code int `json:"code"` - Message string `json:"message"` - Data T `json:"data"` - Timestamp string `json:"timestamp"` - Errors []string `json:"errors,omitempty"` -} - -// PaginatedData wraps list responses -type PaginatedData[T any] struct { - Items []T `json:"items"` - Pagination Pagination `json:"pagination"` -} - -type Pagination struct { - Page int `json:"page"` - Size int `json:"size"` - Total int `json:"total"` - Pages int `json:"pages"` -} -``` - -### SSE Reader - -```go -type SSEReader struct { - url string - client *http.Client - token string - lastID string -} - -func (r *SSEReader) Stream(ctx context.Context) (<-chan SSEEvent, error) -``` - -### WebSocket Reader - -```go -type WSReader struct { - url string - token string -} - -func (r *WSReader) Stream(ctx context.Context) (<-chan string, error) -``` - ---- - -## Agent Workflow Examples - -### Example 1: Full Pipeline Experiment - -```bash -#!/bin/bash -set -e - -# Setup -aegisctl auth login --server http://aegislab:8082 --access-key ak_agent --secret-key sk_agent - -# Discover resources -ALGORITHMS=$(aegisctl container list --type algorithm -o json) -PEDESTALS=$(aegisctl container list --type pedestal -o json) - -# Generate spec file (agent generates this programmatically) -cat > /tmp/inject-spec.yaml < /tmp/exec-spec.yaml < 创建日期:2026-04-15 -> 状态:Draft -> 范围:后端模块边界、Fx 依赖装配、生命周期管理、HTTP / worker / controller / receiver 多入口治理 - -## TL;DR - -AegisLab 后端不只是一个 HTTP API 服务。它同时包含: - -- HTTP producer server -- background consumer -- scheduler -- K8s controller -- OTLP log receiver -- DB / Redis / Etcd / K8s / Loki / tracing 等基础设施资源 - -因此当前问题不是单纯缺少依赖注入,而是: - -- 模块边界不清 -- 全局初始化散落 -- 资源生命周期没有统一管理 -- HTTP / worker / controller 等入口互相交叉 -- handler / service / repository 依赖方向不够硬 - -结论:**优先采用 Fx,而不是继续沿旧 DI 骨架扩张。** - -Fx 在这里的价值不是“自动 new 对象”,而是: - -1. 把 app 启动和模块装配收回到 app 层。 -2. 用 module 明确业务域和基础设施边界。 -3. 用 lifecycle 管理 DB、Redis、HTTP server、consumer、scheduler、receiver、controller 的启动和关闭。 -4. 让 producer / consumer / both 三种模式共享基础模块,但启用不同入口。 - -## 1. Current Problems - -### 1.1 全局初始化散落 - -当前启动流程里存在多个全局初始化点: - -- `database.InitDB()` -- `client.InitTraceProvider()` -- `initChaosExperiment()` -- `k8s.GetK8sController()` -- `client.GetRedisClient()` -- `consumer.StartScheduler(ctx)` -- `consumer.ConsumeTasks(ctx)` -- `logreceiver.NewOTLPLogReceiver(...).Start(ctx)` - -这些初始化分散在 `main.go`、`client`、`service`、`repository` 等多个包里。结果是: - -- 启动顺序靠人工记忆。 -- 新人很难判断资源从哪里来。 -- 关闭逻辑不统一。 -- 测试很难替换基础设施。 -- producer / consumer / both 三种模式重复装配逻辑。 - -### 1.2 分层边界不够硬 - -期望依赖方向: - -```text -cmd - -> app - -> interface - -> module - -> domain - -> infra interface - -> infra implementation -``` - -当前实际情况: - -- handler 直接调用 `service/producer` 包级函数。 -- 少数 handler 直接 import `database` / `repository`。 -- service 大量直接使用全局 `database.DB`、Redis、K8s、Loki 等 client。 -- repository 中混入 Redis queue / token blacklist 等非 DB 能力。 -- middleware 直接依赖具体 producer service。 - -### 1.3 多入口没有统一 app 模型 - -当前有三种运行模式: - -- `producer`: HTTP API server -- `consumer`: background worker / scheduler / K8s controller / receiver -- `both`: 同时启动 producer 和 consumer 能力 - -这些模式本质上应该是三套 Fx option: - -```text -CommonOptions + ProducerOptions -CommonOptions + ConsumerOptions -CommonOptions + ProducerOptions + ConsumerOptions -``` - -而不是在 `main.go` 中手写多份初始化流程。 - -## 2. Target Module Boundary - -先确定模块边界,再谈 Fx 注入。 - -### 2.1 App Layer - -职责: - -- 程序启动入口 -- Fx app 创建 -- producer / consumer / both option 选择 -- 生命周期统一管理 -- graceful shutdown - -建议目录: - -```text -src/app/ - app.go - options.go - producer.go - consumer.go - both.go -``` - -### 2.2 Interface Layer - -职责: - -- HTTP / Gin router -- middleware -- handler -- worker entry -- scheduler entry -- K8s controller entry -- OTLP receiver entry - -建议目录: - -```text -src/interface/ - http/ - module.go - router.go - routes_public.go - routes_sdk.go - routes_portal.go - routes_admin.go - routes_system.go - worker/ - module.go - consumer.go - scheduler.go - controller/ - module.go - k8s.go - receiver/ - module.go - otlp.go -``` - -过渡期可以先不移动现有 `handlers/`、`router/`、`service/consumer/` 文件,只在 Fx module 中包装它们。 - -### 2.3 Business Module Layer - -按业务域拆模块,每个模块只暴露 `Module`、`NewService`、`NewHandler`、`NewRepository`、必要接口。 - -建议业务模块: - -```text -src/module/ - auth/ - user/ - rbac/ - team/ - project/ - container/ - dataset/ - injection/ - execution/ - task/ - evaluation/ - trace/ - metrics/ - notification/ - audit/ - system/ - dynamicconfig/ -``` - -每个模块的目标形态: - -```go -var Module = fx.Module("project", - fx.Provide( - NewRepository, - NewService, - NewHandler, - ), -) -``` - -### 2.4 Domain Layer - -职责: - -- 核心业务规则 -- domain entity / value object -- 纯逻辑校验 -- 不依赖 Gin、Gorm、Redis、K8s - -建议目录: - -```text -src/domain/ - project/ - task/ - injection/ - execution/ - permission/ -``` - -过渡期可以先继续使用 `database` entity 和 `dto`,等模块稳定后再抽 domain。 - -### 2.5 Infra Layer - -职责: - -- 配置 -- 日志 -- DB -- Redis -- Etcd -- K8s -- Loki -- Jaeger / tracing -- Harbor -- Helm -- BuildKit -- Chaos client - -建议目录: - -```text -src/infra/ - config/ - logger/ - db/ - redis/ - etcd/ - k8s/ - loki/ - tracing/ - harbor/ - helm/ - buildkit/ - chaos/ -``` - -每个 infra module 要明确: - -- 创建什么资源 -- 返回什么接口或 client -- 是否需要 `fx.Lifecycle` -- `OnStart` 做什么 -- `OnStop` 做什么 - -## 3. Dependency Rules - -### 3.1 允许依赖 - -```text -cmd -> app -app -> interface / module / infra -interface -> module service interface -module -> domain / infra interface -infra implementation -> external libraries -``` - -### 3.2 禁止依赖 - -```text -domain -> gin / gorm / redis / k8s -repository -> handler -repository -> service -handler -> database.DB -handler -> repository implementation -middleware -> concrete producer service -business module -> another module's implementation -``` - -跨业务模块调用优先依赖接口。例如 project 需要 RBAC 能力: - -```go -type PermissionChecker interface { - CheckUserPermission(ctx context.Context, params *dto.CheckPermissionParams) (bool, error) -} -``` - -由 rbac module 提供实现。 - -## 4. Fx App Design - -### 4.1 Common Options - -所有模式共享: - -```go -func CommonOptions() fx.Option { - return fx.Options( - config.Module, - logger.Module, - db.Module, - redis.Module, - tracing.Module, - etcd.Module, - BusinessModules(), - ) -} -``` - -### 4.2 Producer Options - -HTTP server 模式: - -```go -func ProducerOptions() fx.Option { - return fx.Options( - CommonOptions(), - http.Module, - ) -} -``` - -### 4.3 Consumer Options - -后台任务模式: - -```go -func ConsumerOptions() fx.Option { - return fx.Options( - CommonOptions(), - k8s.Module, - chaos.Module, - worker.Module, - controller.Module, - receiver.Module, - ) -} -``` - -### 4.4 Both Options - -本地或一体化部署模式: - -```go -func BothOptions() fx.Option { - return fx.Options( - CommonOptions(), - k8s.Module, - chaos.Module, - http.Module, - worker.Module, - controller.Module, - receiver.Module, - ) -} -``` - -### 4.5 main.go 目标形态 - -```go -func main() { - mode := parseMode() - - var opts fx.Option - switch mode { - case "producer": - opts = app.ProducerOptions() - case "consumer": - opts = app.ConsumerOptions() - case "both": - opts = app.BothOptions() - } - - fx.New(opts).Run() -} -``` - -`main.go` 不再直接初始化 DB、Redis、K8s controller、HTTP server、scheduler。 - -## 5. Lifecycle Plan - -Fx lifecycle 应统一管理这些资源: - -### 5.1 DB - -- `fx.Provide(NewGormDB)` -- `OnStop`: close underlying sql DB - -### 5.2 Redis - -- `fx.Provide(NewRedisClient)` -- `OnStop`: `Close()` - -### 5.3 HTTP Server - -- `fx.Provide(NewGinEngine, NewHTTPServer)` -- `OnStart`: `server.ListenAndServe()` in goroutine -- `OnStop`: `server.Shutdown(ctx)` - -### 5.4 K8s Controller - -- `fx.Provide(NewK8sController)` -- `OnStart`: start controller in goroutine -- `OnStop`: cancel controller context - -### 5.5 Worker / Scheduler - -- `fx.Provide(NewTaskConsumer, NewScheduler)` -- `OnStart`: start goroutines -- `OnStop`: cancel context and wait if needed - -### 5.6 OTLP Receiver - -- `fx.Provide(NewOTLPReceiver)` -- `OnStart`: start receiver -- `OnStop`: shutdown receiver - -### 5.7 Tracing - -- `fx.Provide(NewTraceProvider)` -- `OnStop`: flush / shutdown provider if supported - -## 6. HTTP Boundary Plan - -HTTP routes should be split by audience, not by current file size. - -### 6.1 Public - -- login -- register -- refresh -- health -- docs - -### 6.2 SDK - -Stable programmatic API: - -- project list / get / create -- container / dataset / version query -- submit injection / build / execution -- task status / logs -- injection / execution / evaluation query -- metrics query -- datapack download / query - -### 6.3 Portal - -普通登录用户前端页面 API: - -- profile -- teams / projects -- labels -- notifications -- user-scoped container / dataset / injection / execution -- upload / download / query - -### 6.4 Admin - -系统管理 API: - -- users -- roles -- permissions -- resources -- audit -- system configs -- global injections / executions -- chaos systems -- batch delete - -第一阶段只拆注册函数,不改 URL。 - -## 7. Migration Strategy - -### Phase 0: Stop Legacy DI Expansion - -当前已有的旧 DI 骨架可以视为短期试验。后续不要继续沿旧 provider 骨架深挖。 - -处理方式: - -- 暂时保留也可以,避免立即制造回滚噪音。 -- 开始引入 Fx 后,用 Fx app 替换 `app.InitializeProducerApp()`。 -- 最终删除旧 DI 骨架相关文件和依赖。 - -### Phase 1: Add Fx Skeleton - -目标:引入 Fx,但不重写业务逻辑。 - -任务: - -- 增加 `go.uber.org/fx` -- 新建 `app` Fx options -- 新建 `infra/config`、`infra/logger`、`infra/db` 的 module 草案 -- 先包装现有 `config.Init`、`database.InitDB` -- 保持现有 router / handler / service 行为 - -验收: - -- producer 可以通过 Fx 启动 -- consumer 旧逻辑暂不迁移或只包装 -- 现有 API 路由不变 - -### Phase 2: Move Lifecycle Into Fx - -目标:把启动和停止资源收回 app。 - -迁移顺序: - -1. DB lifecycle -2. Redis lifecycle -3. tracing lifecycle -4. HTTP server lifecycle -5. OTLP receiver lifecycle -6. scheduler lifecycle -7. consumer lifecycle -8. K8s controller lifecycle - -验收: - -- `main.go` 不再手写资源启动顺序 -- producer / consumer / both 使用不同 Fx options -- 资源关闭有 `OnStop` - -### Phase 3: Module Boundary Wrapper - -目标:先建立业务 module 壳,不急着重写内部逻辑。 - -优先模块: - -1. project -2. auth -3. task -4. injection -5. execution - -每个模块先暴露: - -```go -var Module = fx.Module("project", - fx.Provide(NewHandler), -) -``` - -如果 service / repository 尚未 struct 化,可以先由 handler wrapper 调旧函数。 - -验收: - -- router 依赖 module handler -- 新模块入口清晰 -- 旧包级函数逐步减少 - -### Phase 4: Structify Service / Repository - -目标:逐个业务模块把包级函数改成 struct。 - -每个模块执行: - -- `Repository` struct 化 -- `Service` struct 化 -- `Handler` struct 化 -- service 注入 repository / store / gateway -- handler 注入 service -- 移除 handler direct repository / database import -- 移除 service direct global DB usage - -验收: - -- 新迁移模块可单测 -- 依赖从 Fx 图中可见 -- 无新增全局 client 访问 - -### Phase 5: Split Store / Gateway - -目标:把基础设施访问从 repository / service 中抽出。 - -- Redis token blacklist -> `infra/redis` 或 `module/auth.TokenStore` -- Redis task queue -> `module/task.QueueStore` -- Loki -> `infra/loki.Gateway` -- K8s -> `infra/k8s.Gateway` -- Etcd -> `infra/etcd.Client` -- Harbor / Helm / BuildKit -> gateway - -验收: - -- repository 只处理 DB -- 外部系统访问都有接口边界 - -### Phase 6: SDK / Portal / Admin Governance - -目标:让 API 受众边界和 SDK 生成一致。 - -- 拆 route registration -- 审核 OpenAPI3 `x-api-type` audience 扩展 -- 修正 `sdk / portal / admin` 归属 -- 更新 SDK 生成脚本 - -## 8. First PR Scope - -第一批建议只做: - -1. 新增 Fx 依赖。 -2. 新增 `app` Fx options。 -3. 新增 `infra/config`、`infra/db`、`interface/http` module 壳。 -4. producer 模式通过 Fx 启动 HTTP server。 -5. 保持业务 handler/service/repository 不动。 -6. 标记当前旧 DI 骨架文件为待删除,或直接在本 PR 中移除旧骨架。 - -第一批不建议做: - -- 迁移所有 service -- 搬目录 -- 改 URL -- 清理所有 SDK 标记 -- 重写 consumer -- 重写 repository - -## 9. Completion Criteria - -最终完成后应满足: - -1. `main.go` 只负责解析 mode 和启动 Fx app。 -2. producer / consumer / both 由 Fx options 组合。 -3. DB / Redis / HTTP / worker / receiver / controller 都有 lifecycle。 -4. HTTP routes 按 Public / SDK / Portal / Admin / System 拆分。 -5. handler 不直接 import `database` / repository implementation。 -6. service 不直接使用全局 `database.DB`。 -7. repository 不操作 Redis / K8s / Loki / Etcd。 -8. business module 之间依赖接口,不依赖实现。 -9. 新模块只需要暴露 `Module` 和构造函数。 -10. SDK 只包含稳定外部 API。 - -## 10. Open Questions - -1. 是否要物理移动目录到 `module/`、`infra/`、`interface/`,还是先保持旧目录、只用 Fx module 约束? -2. `service/producer` / `service/consumer` 是否改名? -3. Redis task queue 放在 `infra/redis` 还是 `module/task`? -4. Permission checker 接口归属 `module/rbac` 还是 `interface/http/middleware`? -5. 是否在本轮移除已有旧 DI 骨架,还是等 Fx producer 跑通后再删? - -建议:先不做大规模目录迁移。第一阶段用 Fx module 包装旧代码,等启动生命周期稳定后,再逐个业务模块搬迁。 diff --git a/docs/log-streaming-plan.md b/docs/log-streaming-plan.md deleted file mode 100644 index 0275792e..00000000 --- a/docs/log-streaming-plan.md +++ /dev/null @@ -1,527 +0,0 @@ -# 实时 K8s Job 日志流架构方案 - -> 创建日期:2026-02-17 -> 状态:Draft -> 范围:仅 K8s Job 日志(后端自身日志后续迭代) - -## TL;DR - -实现 K8s Job 日志的**生产级**实时流式传输。利用已部署的 Alloy DaemonSet 采集 Job 日志,新增 OTLP HTTP 输出到后端;后端实现 OTLP HTTP 日志接收器,提取 `task_id` 后通过 Redis Pub/Sub 分发到 WebSocket 端点;WebSocket 端点先查询 Loki 获取历史日志,再切换到实时推送。按 `task_id` 维度查询。 - -## 架构总览 - -``` - K8s Job Pods (stdout/stderr) - │ - ▼ - ┌──────────────────────────────┐ - │ Alloy DaemonSet │ - │ /var/log/pods/*.log │ - │ (已部署, 按 rcabench labels │ - │ 过滤 Job pods) │ - └─────────┬────────────────────┘ - │ loki.process "pipeline" - │ forward_to (dual-write) - │ - ┌─────────┴───────────────────────────┐ - ▼ ▼ -┌──────────┐ ┌─────────────────────┐ -│ Loki │ │ 后端 OTLP HTTP │ -│ :3100 │ │ Receiver :4319 │ -│ (持久化) │ │ /v1/logs │ -└────┬─────┘ └──────────┬──────────┘ - │ │ - │ (历史查询) │ 解析 OTLP LogRecord - │ │ 提取 task_id - │ ▼ - │ ┌─────────────────────┐ - │ │ Redis Pub/Sub │ - │ │ channel: joblogs:{task_id} - │ └──────────┬──────────┘ - │ │ - ▼ ▼ -┌────────────────────────────────────────────────────┐ -│ WebSocket Handler │ -│ GET /api/v2/tasks/{task_id}/logs/ws │ -│ │ -│ 连接流程: │ -│ 1. JWT 认证 (query param: ?token=xxx) │ -│ 2. HTTP → WebSocket 升级 │ -│ 3. 查 Loki 历史日志 → 发送 type:"history" │ -│ 4. 订阅 Redis Pub/Sub → 转发 type:"realtime" │ -│ 5. 监听 task 完成 → 发送 type:"end" → 关闭 │ -└────────────────────┬───────────────────────────────┘ - │ ws:// - ▼ - ┌──────────┐ - │ 前端 │ - │ LogsTab │ - └──────────┘ -``` - -## 为什么选择 OTLP 而非 client-go - -| 对比维度 | OTLP (Alloy → 后端) | client-go Pod Log Stream | -| ---------- | --------------------------------------- | -------------------------------- | -| **解耦** | 后端不直接连 K8s API,Alloy 负责采集 | 后端直接维护 Pod log Follow 连接 | -| **可靠性** | Alloy 有重试/缓冲机制,后端重启不丢日志 | 后端重启 = 日志流中断 | -| **扩展性** | 新增日志源只需改 Alloy 配置 | 每种日志源需要新 goroutine | -| **标准化** | OTLP 是 OpenTelemetry 标准协议 | K8s 专有 API | -| **运维** | 与现有 Alloy→Loki 管道一致 | 额外的连接管理和资源清理 | -| **生产级** | 工业标准,可对接任何 OTLP 兼容后端 | 仅适合小规模/开发环境 | - -**结论**:生产环境应使用 OTLP。Alloy 已在采集 Job 日志,只需加一路 OTLP 输出,后端作为标准 OTLP 接收器处理实时分发。 - -## 现有基础设施 - -### 已有 - -- **Alloy DaemonSet**:已部署在 `exp` namespace,通过 `rcabench_app_id` + `job_name` label 过滤 Job pods -- **Loki**:`http://10.10.10.161:3100`,已接收 Alloy 推送的日志,支持 LogQL 查询 -- **Redis**:已有 `client.RedisPublish()` / `client.GetRedisClient().Subscribe()` 方法 -- **Redis Stream**:已用于 SSE 事件推送(`StreamLogKey = "trace:%s:log"`) -- **gorilla/websocket**:`v1.5.4` 已在 go.mod(间接依赖) -- **OTel proto**:`go.opentelemetry.io/proto/otlp v1.5.0` 已在 go.mod(间接依赖) -- **前端 LogsTab**:已有基础 UI 组件 - -### 需要新增 - -- 后端 OTLP HTTP 日志接收器(`/v1/logs`,端口 4319) -- Alloy 配置增加 OTLP 输出(dual-write) -- Loki 查询客户端 -- WebSocket handler + 路由 -- 日志相关 DTO - -## 实现步骤 - -### Phase 1:后端 OTLP HTTP 日志接收器 - -**新建 `src/service/logreceiver/receiver.go`** - -实现标准 OTLP HTTP 日志接收端点,接收 Alloy 推送的 Job 日志。 - -```go -// 核心结构 -type OTLPLogReceiver struct { - server *http.Server - redisClient *redis.Client - port int - shutdownCh chan struct{} -} - -// 接收端点: POST /v1/logs -// 请求体: protobuf (application/x-protobuf) 或 JSON (application/json) -// 响应: 200 OK / 400 Bad Request / 500 Internal Server Error -``` - -**关键实现细节**: - -1. **OTLP 解析** — 使用 `go.opentelemetry.io/proto/otlp/logs/v1` 解析三层结构: - - ``` - ExportLogsServiceRequest - └── ResourceLogs[] - ├── Resource.Attributes (rcabench_app_id, namespace) - └── ScopeLogs[] - └── LogRecords[] - ├── TimeUnixNano - ├── Body.StringValue (日志行) - └── Attributes (task_id, trace_id, job_id) - ``` - -2. **元数据提取** — 从 Resource Attributes 和 Log Attributes 提取: - - `task_id`(必须,用于路由到正确的 Redis Pub/Sub channel) - - `trace_id`(可选,用于关联追踪) - - `job_id`(可选,job 名称) - - `rcabench_app_id`(已在 Alloy relabel 中设置) - -3. **Redis Pub/Sub 发布** — 按 `task_id` 发布到 channel `joblogs:{task_id}`: - - ```go - client.RedisPublish(ctx, fmt.Sprintf("joblogs:%s", taskID), logEntry) - ``` - -4. **生产级要求**: - - 请求体大小限制(默认 5MB) - - Content-Type 校验(支持 protobuf 和 JSON 两种格式) - - 请求超时控制 - - Prometheus metrics(接收速率、错误率、延迟) - - 优雅关闭(`Shutdown(ctx)`) - - 健康检查端点(`GET /health`) - -**依赖提升**(go.mod indirect → direct): - -- `go.opentelemetry.io/proto/otlp v1.5.0` -- `github.com/gorilla/websocket v1.5.4` -- `google.golang.org/protobuf`(已有) - -### Phase 2:修改 Alloy 配置,新增 OTLP 输出 - -**修改 `manifests/dev/exp-dev-setup.yaml`** - -在现有 pipeline 中增加 OTLP dual-write: - -```river -// ============ 新增: OTLP 日志输出到后端 ============ - -// 桥接: Loki 格式 → OpenTelemetry 格式 -otelcol.receiver.loki "backend" { - output { - logs = [otelcol.exporter.otlphttp.backend.input] - } -} - -// OTLP HTTP 导出到后端接收器 -otelcol.exporter.otlphttp "backend" { - client { - endpoint = "http://rcabench-service.exp.svc.cluster.local:4319" - // 本地开发时用: endpoint = "http://host.k3d.internal:4319" - - // 生产级配置 - retry_on_failure { - enabled = true - initial_interval = "1s" - max_interval = "30s" - max_elapsed_time = "5m" - } - - // 发送队列(缓冲 + 批量) - sending_queue { - enabled = true - num_consumers = 4 - queue_size = 1000 - } - } -} -``` - -**修改 pipeline forward_to**: - -```river -// 现有: -forward_to = [loki.write.default.receiver] - -// 改为 dual-write: -forward_to = [loki.write.default.receiver, otelcol.receiver.loki.backend.receiver] -``` - -**修改 DaemonSet args**: - -```yaml -# 现有: -args: - - --stability.level=generally-available - -# 改为 (otelcol.* 组件需要 public-preview): -args: - - --stability.level=public-preview -``` - -**注意事项**: - -- `otelcol.receiver.loki` 将 Loki labels 自动映射为 OTLP Resource Attributes -- `task_id`、`trace_id`、`job_id` 在 Alloy relabel 阶段已设置为 Structured Metadata,会作为 OTLP Attributes 传递 -- 本地开发环境后端不在 K8s 内,需要用 `host.k3d.internal` 或实际 IP - -### Phase 3:后端新增 Loki 查询客户端 - -**新建 `src/client/loki.go`** - -封装 Loki HTTP API,用于 WebSocket 连接时获取历史日志。 - -```go -type LokiClient struct { - baseURL string - httpClient *http.Client -} - -// QueryJobLogs 查询指定 task_id 的 Job 历史日志 -// LogQL: {app="rcabench"} | task_id=`{taskID}` -func (c *LokiClient) QueryJobLogs(ctx context.Context, taskID string, opts QueryOpts) ([]LogEntry, error) - -// QueryOpts 查询参数 -type QueryOpts struct { - Start time.Time // 默认: task 创建时间 - End time.Time // 默认: now - Limit int // 默认: 5000 - Direction string // "forward" (时间正序) -} -``` - -**Loki API 调用**: - -- `GET /loki/api/v1/query_range` -- LogQL: `{app="rcabench"} | task_id="{task_id}"`(Structured Metadata 过滤) -- 分页: `limit` + `start`/`end` 时间范围 - -**配置** (`config.dev.toml` 新增): - -```toml -[loki] -url = "http://10.10.10.161:3100" -timeout = "10s" -max_entries = 5000 -``` - -### Phase 4:定义日志 DTO 和 WebSocket 消息格式 - -**新建 `src/dto/log.go`** - -```go -// LogEntry 统一日志条目(OTLP 接收和 Loki 查询共用) -type LogEntry struct { - Timestamp time.Time `json:"timestamp"` // 日志时间戳 - Line string `json:"line"` // 日志内容 - TaskID string `json:"task_id"` // 关联的 task ID - JobID string `json:"job_id,omitempty"` // K8s Job 名称 - TraceID string `json:"trace_id,omitempty"` // 追踪 ID - Level string `json:"level,omitempty"` // 日志级别 (info/warn/error) -} - -// WSLogMessage WebSocket 推送的消息格式 -type WSLogMessage struct { - Type string `json:"type"` // "history" | "realtime" | "end" | "error" - Logs []LogEntry `json:"logs,omitempty"` // 日志条目 - Message string `json:"message,omitempty"` // 错误信息或结束原因 - Total int `json:"total,omitempty"` // 历史日志总条数 -} -``` - -### Phase 5:实现 WebSocket Handler - -**新建 `src/handlers/v2/task_logs.go`** - -```go -// GetTaskLogsWS WebSocket 端点 - 实时 Job 日志流 -// @Router /api/v2/tasks/{task_id}/logs/ws [get] -// -// 连接流程: -// 1. JWT 认证 (从 query param ?token=xxx 获取) -// 2. HTTP → WebSocket 升级 -// 3. 查询 Loki 历史日志 → type:"history" -// 4. Redis Pub/Sub 订阅 → type:"realtime" -// 5. 监听 task 完成 → type:"end" → 关闭 -func GetTaskLogsWS(c *gin.Context) -``` - -**生产级要求**: - -1. **认证**: - - WebSocket 不支持自定义 HTTP header - - 从 URL query 参数 `?token=xxx` 获取 JWT - - 验证 token 有效性后再升级连接 - -2. **连接管理**: - - 设置读写超时(WriteWait: 10s, PongWait: 60s, PingPeriod: 54s) - - Ping/Pong 心跳保活 - - 最大消息大小限制 - - 客户端断连时清理 Redis 订阅 - -3. **历史 + 实时日志无缝衔接**: - - 先订阅 Redis Pub/Sub(确保不丢失订阅期间的日志) - - 再查询 Loki 历史日志,发送给客户端 - - 然后开始转发 Redis 实时日志 - - 用时间戳去重(Loki 和 Redis 可能有短暂重叠) - -4. **优雅终止**: - - 监听 task 状态变化(轮询 DB 或订阅 Redis Stream 的 task 完成事件) - - Task 完成后等待 5s(flush 最后的日志)再发送 `type:"end"` - - 支持客户端主动关闭 - -5. **并发安全**: - - WebSocket 写操作需要互斥锁(`sync.Mutex`) - - Redis 订阅和 Loki 查询在独立 goroutine 中 - -### Phase 6:路由注册 - -**修改 `src/router/v2.go`** - -```go -// 在 tasks 路由组中添加: -tasks.GET("/:task_id/logs/ws", v2.GetTaskLogsWS) -``` - -- WebSocket 端点**不使用**标准 JWT middleware(因为 token 在 query param) -- 在 handler 内部手动验证 token - -### Phase 7:应用启动集成 - -**修改 `src/main.go`** - -在 `consumer` 和 `both` 模式中启动 OTLP 接收器: - -```go -// consumer/both 模式 -go logreceiver.Start(ctx, config.GetInt("otlp_receiver.port")) -``` - -### Phase 8:配置更新 - -**修改 `src/config.dev.toml`** - -```toml -[loki] -url = "http://10.10.10.161:3100" -timeout = "10s" -max_entries = 5000 - -[otlp_receiver] -port = 4319 -max_request_size = "5MB" - -[logging.job] -dir = "jobs" -log_retention_days = 30 -pubsub_channel_prefix = "joblogs" -``` - -## Redis Channel 设计 - -``` -joblogs:{task_id} # Pub/Sub channel,实时 Job 日志 - # 每条消息: JSON 序列化的 LogEntry - # 生命周期: task 运行期间活跃 - -trace:{trace_id}:log # Redis Stream(已有),task 状态事件 - # 用于监听 task 完成信号 -``` - -与现有 `StreamLogKey = "trace:%s:log"` Redis Stream 独立,不影响现有 SSE 事件推送。 - -## 验证清单 - -### 开发环境验证 - -1. **OTLP 接收器启动**: - - ```bash - # 构建并启动 - cd src && go build -o /tmp/rcabench ./main.go - ENV_MODE=dev /tmp/rcabench both --port 8082 - # 检查日志: "OTLP log receiver started on :4319" - ``` - -2. **OTLP 接收器功能测试**: - - ```bash - # 发送测试 OTLP 日志(JSON 格式) - curl -X POST http://localhost:4319/v1/logs \ - -H "Content-Type: application/json" \ - -d '{"resourceLogs":[{"resource":{"attributes":[{"key":"task_id","value":{"stringValue":"test-123"}}]},"scopeLogs":[{"logRecords":[{"timeUnixNano":"1708100000000000000","body":{"stringValue":"test log line"}}]}]}]}' - # 响应: 200 OK - ``` - -3. **Redis Pub/Sub 验证**: - - ```bash - # 终端 A: 订阅 - redis-cli subscribe joblogs:test-123 - # 终端 B: 发送上面的 OTLP 测试请求 - # 终端 A 应收到 LogEntry JSON - ``` - -4. **WebSocket 端到端测试**: - - ```bash - # 获取 JWT token - TOKEN=$(curl -s -X POST http://localhost:8082/api/v2/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"admin","password":"admin"}' | jq -r '.data.access_token') - - # 连接 WebSocket - websocat "ws://localhost:8082/api/v2/tasks/{task_id}/logs/ws?token=$TOKEN" - ``` - -### K8s 集群验证 - -5. **Alloy 配置更新**: - - ```bash - kubectl apply -f manifests/dev/exp-dev-setup.yaml - # 验证 Alloy pod 重启成功 - kubectl get pods -n exp -l app=alloy - ``` - -6. **端到端 Job 日志流**: - - ```bash - # 创建一个测试 fault injection → 触发 Job - # WebSocket 客户端应实时收到 Job 日志 - ``` - -7. **Loki 历史查询验证**: - ```bash - curl "http://10.10.10.161:3100/loki/api/v1/query_range" \ - --data-urlencode 'query={app="rcabench"} | task_id="xxx"' \ - --data-urlencode 'start=2026-02-17T00:00:00Z' \ - --data-urlencode 'end=2026-02-17T23:59:59Z' \ - --data-urlencode 'limit=100' - ``` - -### 构建和测试 - -8. **Go 构建**:`cd src && go build -o /tmp/rcabench ./main.go` -9. **单元测试**:`cd src && go test ./utils/... -v` -10. **OTLP 接收器单元测试**:`cd src && go test ./service/logreceiver/... -v` - -## 设计决策 - -| 决策项 | 选择 | 原因 | -| --------- | --------------------- | --------------------------------------------------------------- | -| 日志采集 | Alloy OTLP dual-write | 已有 Alloy pipeline,标准化 OTLP 协议,生产级可靠性 | -| 传输协议 | WebSocket | 双向通信,未来可扩展暂停/过滤控制 | -| 实时中转 | Redis Pub/Sub | 多客户端广播,轻量级,与现有 Redis 基础设施复用 | -| 历史日志 | Loki 查询 | 已有完整 Alloy → Loki 管道,LogQL 支持 Structured Metadata 过滤 | -| 日志维度 | 按 task_id | 匹配前端 LogsTab 在任务详情页的展示场景 | -| OTLP 格式 | HTTP (非 gRPC) | 更简单调试,curl 测试友好,防火墙友好 | -| 日志范围 | 仅 K8s Job | 后端自身日志后续迭代添加 | - -## 风险和缓解 - -| 风险 | 影响 | 缓解措施 | -| ------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------- | -| Alloy `--stability.level` 升级 | `public-preview` 组件可能有 breaking changes | 锁定 Alloy 镜像版本 (v1.13.1),升级前测试 | -| Redis Pub/Sub 无持久化 | 连接前的实时日志丢失 | 先订阅再查 Loki 历史,时间戳去重覆盖间隙 (Loki ~1-5s 延迟) | -| OTLP 接收器宕机 | 实时日志丢失 | Alloy `retry_on_failure` 重试 + `sending_queue` 缓冲;历史日志仍走 Loki 不受影响 | -| WebSocket 连接泄漏 | 资源耗尽 | Ping/Pong 心跳 + 读写超时 + task 完成自动关闭 | -| Loki 查询慢 | WebSocket 连接等待时间长 | 分页查询 + 超时控制 + 先发送部分历史再分批补充 | -| OTLP protobuf 解析复杂 | 开发周期长 | 同时支持 JSON 格式,优先用 JSON 开发调试 | - -## 实现优先级 - -``` -Phase 1 (P0): OTLP 接收器 + JSON 格式支持 → 可独立验证 -Phase 2 (P0): Alloy 配置 dual-write → 打通采集链路 -Phase 3 (P1): Loki 查询客户端 → 历史日志 -Phase 4 (P1): DTO + WebSocket Handler → 前端可用 -Phase 5 (P1): 路由 + 启动集成 + 配置 → 完整功能 -Phase 6 (P2): protobuf 格式支持 → 性能优化 -Phase 7 (P2): Prometheus metrics + 监控仪表盘 → 可观测性 -Phase 8 (P3): 后端自身日志采集 → 扩展范围 -``` - -## 文件清单(预期产出) - -``` -src/service/logreceiver/ -├── receiver.go # OTLP HTTP 接收器(核心) -├── parser.go # OTLP LogRecord 解析 + 元数据提取 -├── receiver_test.go # 单元测试 -└── metrics.go # Prometheus 指标 - -src/client/ -└── loki.go # Loki HTTP 查询客户端 - -src/dto/ -└── log.go # LogEntry, WSLogMessage DTO - -src/handlers/v2/ -└── task_logs.go # WebSocket handler - -src/router/v2.go # 路由注册(修改) -src/main.go # 启动集成(修改) -src/config.dev.toml # 配置新增(修改) - -manifests/dev/ -└── exp-dev-setup.yaml # Alloy 配置 dual-write(修改) -``` diff --git a/docs/model-dto-refactor-todo.md b/docs/model-dto-refactor-todo.md deleted file mode 100644 index 90d15aa7..00000000 --- a/docs/model-dto-refactor-todo.md +++ /dev/null @@ -1,179 +0,0 @@ -# Model / DTO Refactor TODO - -> 创建日期:2026-04-17 -> 目标:把原 `database` 语义收缩为持久化模型层 `model`,并把当前全局 `dto` 逐步拆回各模块,避免存储模型和接口契约继续混在一起。 - -## 设计原则 - -- `src/model` 只放持久化模型、GORM hook、scanner / valuer、只读 view model。 -- `src/infra/db` 负责连接、迁移、生命周期、view 创建。 -- 不把 `dto` 直接并入 `model`。 -- `dto` 优先按模块下沉到 `src/module/*`,只保留极少数真正跨模块共享类型。 -- 先改命名和目录边界,再做更细的 DTO 下沉,避免一轮里同时改太多语义。 - -## 阶段 1:`database` -> `model` - -- [x] 新建 `src/model` -- [x] 将 `src/database/*` 迁到 `src/model/*` -- [x] 将包名从 `database` 改为 `model` -- [x] 批量更新仓库内 `aegis/database` import -- [x] 批量更新 `database.*` 类型引用 -- [x] 跑主链测试确认编译通过 - - 已执行:`cd src && go test ./app -count=1` - - 已执行:`cd src && go test ./module/... ./router/... ./repository ...` - - 备注:沙箱内执行 `cd src && go test ./...` 时,`app` 中两条 loopback smoke test 在整仓并行场景下触发 `listen tcp 127.0.0.1:0: socket: operation not permitted`,单包执行通过,属于环境限制而非本轮重命名回归。 - -## 阶段 2:继续压缩 `src/model` - -- [x] 复查 `src/model` 是否只剩实体 / view model / scanner / valuer -- [x] 将模块专用读模型从 `src/model` 下沉回对应模块 -- [x] 优先处理 SDK 只读模型 - - `src/model/sdk_entities.go` 已删除 - - SDK 只读模型已迁到 `src/module/sdk/models.go` - -## 阶段 3:拆全局 `dto` - -- [x] 明确 `dto` 中每个文件对应的模块归属 - - 当前剩余 `src/dto/*` 已收敛为共享分页/搜索/响应壳与跨模块运行时载荷:`common.go`、`response.go`、`search.go`、`permission.go`、`project.go`、`dynamic_config.go`、`container.go`、`dataset.go`、`injection.go`、`task.go`、`trace.go`、`log.go`、`label.go` -- [x] 优先试点 `auth` / `sdk` / `system` - - `src/module/auth/api_types.go` 已落地,`src/dto/auth.go` 已删除 - - `src/module/sdk/api_types.go` / `src/module/sdk/models.go` 已落地,`src/dto/sdk_evaluation.go` 已删除 - - `src/module/system/api_types.go` 已落地,`src/dto/audit.go` 已删除,并缩减 `src/dto/system.go` / `src/dto/dynamic_config.go` -- [x] 再推进下一批明显模块内聚 DTO - - `src/module/chaossystem/api_types.go` 已落地,`src/dto/chaos_system.go` 已删除 - - `src/module/team/api_types.go` 已落地,`src/dto/team.go` 已删除 - - `src/module/label/api_types.go` 已落地,`src/dto/label.go` 已裁剪为仅保留共享 `LabelItem` - - `src/module/rbac/api_types.go` 已落地,`src/dto/resource.go` 已删除,`src/dto/role.go` 已裁剪掉 role mutation/list 请求类型 - - `src/module/user/api_types.go` 已落地,`src/dto/user.go` 已裁剪掉 user CRUD/detail 请求响应类型 - - `src/module/project/api_types.go` 已落地,`src/dto/project.go` 已裁剪为仅保留共享 search/statistics 结构 - - `src/module/dataset/api_types.go` 已落地,`src/dto/dataset.go` 已裁剪掉 dataset CRUD/detail/label 管理请求响应类型 - - `src/module/container/api_types.go` 已落地,`src/dto/container.go` 已裁剪掉 container CRUD/detail/label 管理请求响应类型 - - `src/module/evaluation/api_types.go` 已落地,`src/dto/evaluation.go` 已删除,并把批量评估逻辑收回 `src/module/evaluation/service.go` - - `src/module/metric/api_types.go` 已落地,`src/dto/metrics.go` 已删除 - - `src/module/task/api_types.go` 已落地,`src/dto/task.go` 已裁剪掉 task list/batch-delete/detail/queue 这批模块内 API 类型;trace 仍复用共享 `TaskResp` - - `src/module/notification/api_types.go` 已落地,`src/dto/notification.go` 已删除 - - `src/module/group/api_types.go` 已落地,`src/dto/group.go` 已删除,并把 group stats/stream 相关类型从 `src/dto/trace.go` 收回模块 - - `src/module/trace/api_types.go` 已落地,`src/dto/trace.go` 已裁剪掉 trace list/detail/stream 请求响应类型;`src/repository/trace.go` 也已去掉对 `dto.ListTraceFilters` 的依赖 - - `src/module/systemmetric/api_types.go` 已落地,`src/dto/system.go` 已删除;system 通过模块别名复用监控响应类型 - - `src/module/execution/api_types.go` 已落地,`src/dto/execution.go` 已删除;evaluation 改为直接复用 execution 模块公开执行引用类型 - - `src/module/execution/result_types.go` 已落地,执行结果上传请求/响应与 detector / granularity 结果项已迁回模块,`src/dto/algorithm_result.go` 已删除 - - `src/module/rbac/api_types.go` 已继续扩充 role / permission 响应与 permission list 查询契约,`src/dto/role.go` 已删除,`src/dto/permission.go` 已裁剪为仅保留 middleware / repository 共享的 `CheckPermissionParams` - - `src/module/injection/api_types.go` 已落地,`src/dto/injection.go` 已裁剪为仅保留 consumer / task 共享的 `InjectionItem` - - `src/module/injection/time_range.go` 已落地,注入分析查询时间窗契约已迁回模块,`src/dto/request.go` 已删除 - - `src/module/dataset/api_types.go` 已继续接管 search / dataset version / datapack relation 契约,`src/dto/dataset.go` 已裁剪为仅保留共享 `DatasetRef` - - `src/module/auth/api_types.go` 已接管 profile 响应契约,`src/dto/user.go` 已删除未使用的 `UserSearchReq` 并移出 `UserProfileResp` - - `src/module/user/api_types.go` 已继续接管 permission assignment / resource-role 视图契约,`src/dto/user.go` 已删除 - - `src/dto/project.go` 已删除未使用的 `SearchProjectReq`,当前仅保留 project/team 共用的 `ProjectStatistics` - - `src/dto/dynamic_config.go` 已删除未使用的 `ConfigStatsResp`,当前仅保留跨 `service/common` / `module/system` 共用的 `ConfigUpdateResponse` - - `src/module/task/log_types.go` 已落地,任务日志 WebSocket 消息已迁回模块,`src/dto/log.go` 已裁剪为仅保留共享 `LogEntry` - - container 构建请求已直接复用共享 `dto.BuildOptions`,模块内重复定义已删除 - - 未被引用的遗留全局 DTO 已继续清理:`src/dto/analyzer.go`、`src/dto/debug.go`、`src/dto/redis.go` 已删除;`src/dto/trace.go` 中未使用的 `TraceQuery` 已移除 -- [x] 将模块专用 request / response 移到 `src/module/*` - - Auth 请求/响应类型已迁到 `src/module/auth/api_types.go` - - SDK 请求/响应类型已迁到 `src/module/sdk/api_types.go` - - System 请求/响应类型已迁到 `src/module/system/api_types.go` - - ChaosSystem 请求/响应类型已迁到 `src/module/chaossystem/api_types.go` - - Team 请求/响应类型已迁到 `src/module/team/api_types.go` - - Label 请求/响应类型已迁到 `src/module/label/api_types.go` - - RBAC 的 role/resource 请求类型与 resource 响应类型已迁到 `src/module/rbac/api_types.go` - - User 的 CRUD/detail 请求响应类型已迁到 `src/module/user/api_types.go` - - Project 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/project/api_types.go` - - Dataset 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/dataset/api_types.go` - - Container 的 CRUD/detail/label 管理请求响应类型已迁到 `src/module/container/api_types.go` - - Evaluation 的 list/detail/batch evaluate 请求响应类型已迁到 `src/module/evaluation/api_types.go` - - Metric 的 query/response 类型已迁到 `src/module/metric/api_types.go` - - Task 的 list/batch-delete/detail/queue 请求响应类型已迁到 `src/module/task/api_types.go` - - Notification 的 stream 请求/事件类型已迁到 `src/module/notification/api_types.go` - - Group 的 stats/stream 请求响应类型已迁到 `src/module/group/api_types.go` - - Trace 的 list/detail/stream 请求响应类型已迁到 `src/module/trace/api_types.go` - - SystemMetric 的 metrics/namespace-lock 请求响应类型已迁到 `src/module/systemmetric/api_types.go` - - Execution 的 list/detail/submit/batch-delete 请求响应类型已迁到 `src/module/execution/api_types.go` - - Execution 的 detector/granularity 结果上传请求响应类型已迁到 `src/module/execution/result_types.go` - - RBAC 的 role / permission 响应类型与 permission list 请求类型已迁到 `src/module/rbac/api_types.go` - - Injection 的 list/search/submit/build/label/file/upload 请求响应类型已迁到 `src/module/injection/api_types.go` - - Injection 的时间窗查询类型已迁到 `src/module/injection/time_range.go` - - Dataset 的 search / version CRUD / datapack relation 请求响应类型已迁到 `src/module/dataset/api_types.go` - - Auth 的 profile 响应类型已迁到 `src/module/auth/api_types.go` - - User 的 permission assignment / resource relation 响应类型已迁到 `src/module/user/api_types.go` -- [x] 保留一个极薄的跨模块共享 DTO 层,避免继续养大而全 `dto` - - 当前共享 DTO 只保留分页/搜索/统一响应、权限检查参数,以及 consumer / runtime / trace / log 等跨模块载荷 - -## 当前进展 - -- [x] `auth` 模块已完成本地 API 类型收口并通过校验 - - 已执行:`cd src && go test ./module/auth ./router ./docs` -- [x] `sdk` / `system` 模块已完成前序下沉并继续保持通过 - - 已执行:`cd src && go test ./module/system ./module/sdk ./module/auth ./router ./docs` -- [x] `chaossystem` 模块已完成本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/chaossystem ./router ./docs` -- [x] `team` 模块已完成本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/team ./router ./docs` -- [x] `label` 模块已完成本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/label ./router ./docs` -- [x] `rbac` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/rbac ./router ./docs` -- [x] `user` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/user ./module/rbac ./router ./docs` -- [x] `project` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/project ./module/team ./router ./docs` -- [x] `dataset` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/dataset ./module/project ./router ./docs` -- [x] `container` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/container ./module/project ./router ./docs` -- [x] `evaluation` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/evaluation ./router ./docs` -- [x] `metric` / `task` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/metric ./module/task ./module/systemmetric ./module/system ./router ./docs` -- [x] `notification` / `group` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/notification ./module/group ./service/consumer ./module/docs ./router ./docs` -- [x] `trace` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/trace ./router ./docs` -- [x] `systemmetric` / `system` 模块已完成一轮本地 API 类型下沉并通过校验 - - 已执行:`cd src && go test ./module/systemmetric ./module/system ./router ./docs` -- [x] `execution` / `rbac` / `user` 模块已继续完成一轮本地 API 类型收缩并通过校验 - - 已执行:`cd src && go test ./module/execution ./module/rbac ./module/user ./module/evaluation ./router ./docs` -- [x] `injection` / `dataset` / `project` / `auth` 模块已继续完成一轮本地 API 类型收缩并通过校验 - - 已执行:`cd src && go test ./module/auth ./module/injection ./module/dataset ./module/project ./router ./docs` -- [x] `execution` / `injection` / `label` / `task` / `container` 已继续完成最后一轮共享 DTO 收缩并通过校验 - - 已执行:`cd src && go test ./module/execution ./module/evaluation ./module/injection ./module/label ./module/container ./module/task ./router ./docs` -- [x] 继续按模块清点 `src/dto/*` 中剩余仅被单模块消费的类型 -- [x] 再清一轮已空心化 `repository` / helper 边界壳 - - `src/repository/project.go`、`src/repository/user.go` 已删除;相关 project/user 访问已完全由模块仓储接管 - - `src/module/injection/repository.go` 已删除仅做 label item 转条件的空包装,service 直接传递 label condition - - 已执行:`cd src && go test ./module/project ./module/user ./module/injection ./module/team ./repository ./router ./docs` -- [x] 继续删旧仓储中已无人引用的模块专用壳文件 - - `src/repository/system.go`、`src/repository/evaluation.go`、`src/repository/role.go`、`src/repository/resource.go`、`src/repository/permission.go`、`src/repository/team.go` 已删除 - - 这些能力已分别由 `src/module/chaossystem`、`src/module/evaluation`、`src/module/rbac`、`src/module/team` 或 middleware / initialization 内聚实现接管 - - 已执行:`cd src && go test ./module/chaossystem ./module/evaluation ./module/rbac ./middleware ./service/initialization ./repository ./router ./docs` - - 已执行:`cd src && go test ./module/team ./middleware ./service/initialization ./repository ./router ./docs` -- [x] 再收一轮 `service/common` / `service/consumer` 直连旧仓储 helper - - `src/repository/dynamic_config.go`、`src/repository/task.go`、`src/repository/trace.go`、`src/repository/system_metadata.go` 已删除 - - 配置创建、task/trace upsert、trace 查询、system metadata 查询已分别内聚回 `src/service/common` / `src/service/consumer` - - 当前 `src/repository/*` 仅剩 container/dataset/execution/injection/label/search builder 等跨模块共享查询能力 - - 已执行:`cd src && go test ./service/common ./service/consumer ./module/system ./module/group ./module/trace ./repository ./router ./docs` -- [x] 最后一轮共享层命名 / 文件抛光 - - `src/repository/common.go` 已删除,剩余共享仓储不再保留无语义公共常量文件 - - container/dataset/injection 共享仓储里的 `active_name` omit 常量已改成各文件自解释命名 - - 修正残余命名/注释噪音:如 `contaierType`、`BatchDelteInjections` - - 已执行:`cd src && go test ./repository ./router ./docs ./service/common ./service/consumer` - -## 边界口径 - -- [x] `src/model` 继续只承载持久化实体 / view model / scanner / valuer -- [x] 跨模块共享的 API 请求/响应暂不并入 `src/model` - - 原因:共享 DTO 仍属于接口契约层,不是持久化模型;直接并入 `model` 会重新把存储边界和 HTTP/API 边界混在一起 - - 后续方向:继续缩小 `src/dto`,必要时再拆成更明确的共享契约包,而不是回灌到 `model` - - 当前保留例子:`src/dto/trace.go` 仍保留 trace 自身 stream/list/detail 契约;group 侧统计/stream DTO 已拆回 `src/module/group` - - 更新:`src/dto/trace.go` 现在只保留 trace stream 事件负载等共享结构,trace handler/service 自身契约已迁回模块,未使用 `TraceQuery` 已删除 - - 更新:`src/dto/task.go` 现在只保留 `UnifiedTask` 这类调度/运行时共享结构;原先重复保留的 `TaskResp` 已删除,trace 直接复用 `src/module/task/api_types.go` - - 更新:`src/dto/log.go` 现在只保留 Loki / OTLP / task log 共用的 `LogEntry`;WebSocket 消息壳已迁回 `src/module/task/log_types.go` - -## 当前决定 - -- [x] DB 初始化、迁移、生命周期已转入 `src/infra/db` -- [x] `scope` 查询辅助已从原 `database` 迁到 `src/repository` -- [x] 明确不采用“把 `dto` 并入 `model`”方案 -- [x] 完成第一阶段目录重命名 -- [x] DTO / model 主线重构已完成 - - 当前保留的 `src/repository/*` 主要是 consumer / service/common / metadata / search builder 等跨模块共享查询能力,不再属于本轮“模块专用旧壳” - - 后续若继续做,只剩增量优化,不再是本轮主线阻塞项 diff --git a/docs/report-index.md b/docs/report-index.md new file mode 100644 index 00000000..34662513 --- /dev/null +++ b/docs/report-index.md @@ -0,0 +1,730 @@ +# Report Index + +> 更新时间:2026-04-18 +> 目的:把本轮后端主线重构、微服务收尾、治理约定、运行口径、SDK/鉴权要点收口到少量总贴文档里。 + +## 1. 最终结论 + +- Fx + module + infra 主线已完成,`producer / consumer / both` 与六服务入口都已跑通。 +- 微服务主线可视为完成,当前已形成 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` 六个明确边界。 +- 旧运行态兼容面已经完成仓库级清扫:`service/producer`、`handlers/system`、`database.DB`、`GetGateway()`、`redisinfra.GetGateway()` 这批模式已退出主线生产代码。 +- 当前仅剩 1 个未勾项:确认 Fx 启动日志是否可接受;这是人工验收,不阻塞代码主线收口。 + +## 2. 保留文档 + +- `docs/todo.md` + - 主 TODO 与最终验收清单,仍作为执行源文档。 +- `docs/report-index.md` + - 当前总索引与汇总版说明。 +- `docs/frontend-redesign.md` + - 前端重设计文档,属于独立主题,未纳入本次后端文档合并。 +- `docs/frontend-ui-guidelines.md` + - 前端 UI 规范,属于独立主题,未纳入本次后端文档合并。 + +## 3. 服务边界与 ownership 总结 + +### 3.1 六服务职责 + +- `api-gateway` + - 对外唯一 HTTP/OpenAPI 入口。 + - 负责 audience、鉴权、参数校验、统一错误壳、聚合响应。 + - 不直接查 DB,不直接做 K8s / Helm / BuildKit 业务判断。 +- `iam-service` + - 承接 `auth / user / rbac / team / access key`。 + - 负责 `AK/SK -> token`、token verify、permission check。 +- `resource-service` + - 承接 `project / label / container / dataset / evaluation` 元数据与查询视图。 +- `orchestrator-service` + - 承接 `execution / injection submit`、`task / trace / retry / dead-letter / cancel` 控制面。 +- `runtime-worker-service` + - 承接 Redis 异步消费、K8s/BuildKit/Helm/Chaos 执行态、limiter、namespace lock、runtime monitor。 + - 异步执行链继续保留 Redis,不改为同步执行 RPC。 +- `system-service` + - 承接 `config / audit / health / monitor / metrics` 运维控制面。 + +### 3.2 owner 约束 + +- `iam-service` + - `users`、`roles`、`permissions`、`resources`、`teams`、`access_keys` 及其授权关系。 +- `resource-service` + - `projects`、`labels`、`containers`、`datasets`、`evaluations` 与资源元数据关系。 +- `orchestrator-service` + - `tasks`、`traces`、`fault_injections`、`executions`、重试/死信/工作流控制面。 +- `system-service` + - `dynamic_configs`、`config_histories`、`audit_logs`、`system_metrics` 等运维数据。 +- `runtime-worker-service` + - Redis runtime state、K8s/build/helm 执行态,不新增跨 owner MySQL 写入。 + +### 3.3 依赖规则 + +- 允许:`cmd -> app -> interface/module/infra/internalclient` +- 允许:`interface -> module/internalclient` +- 允许:`module -> infra/model/本模块 repository` +- 禁止:`gateway -> repository` +- 禁止:`interface -> repository` 直接拼业务 +- 禁止:`module A -> module B repository` +- 禁止:非 owner 服务新增直接写库逻辑 + +## 4. 本地运行与发布口径 + +### 4.1 六服务本地入口 + +| Service | Command | Default Port | +| --- | --- | --- | +| `api-gateway` | `go run ./src/cmd/api-gateway -conf ./src/config.dev.toml -port 8082` | `8082` | +| `iam-service` | `go run ./src/cmd/iam-service -conf ./src/config.dev.toml` | `9091` | +| `orchestrator-service` | `go run ./src/cmd/orchestrator-service -conf ./src/config.dev.toml` | `9092` | +| `resource-service` | `go run ./src/cmd/resource-service -conf ./src/config.dev.toml` | `9093` | +| `runtime-worker-service` | `go run ./src/cmd/runtime-worker-service -conf ./src/config.dev.toml` | `9094` | +| `system-service` | `go run ./src/cmd/system-service -conf ./src/config.dev.toml` | `9095` | + +### 4.2 本地依赖与启动顺序 + +- 先起基础依赖: + - `docker compose up -d redis mysql etcd jaeger buildkitd loki prometheus grafana` +- 如需本地全量六服务: + - `docker compose -f docker-compose.yaml -f docker-compose.microservices.yaml up --build` +- 手动顺序建议: + - `iam-service` + - `orchestrator-service` + - `resource-service` + - `runtime-worker-service` + - `system-service` + - `api-gateway` + +### 4.3 发布骨架 + +- 本地 compose 骨架:`docker-compose.microservices.yaml` +- Kubernetes skeleton:`manifests/microservices/aegislab-microservices.yaml` +- Helm 发布主口径:`helm/templates/{configmap,service,deployment}.yaml` +- 当前 Helm 已按六服务拓扑渲染通过:`helm template aegislab ./helm` + +## 5. Health / Readiness 约定 + +- `api-gateway` + - 协议:HTTP + - 探针:`GET /system/health` + - 默认端口:`8082` +- 内部 gRPC 服务 + - 服务:`iam-service`、`orchestrator-service`、`resource-service`、`runtime-worker-service`、`system-service` + - 协议:gRPC health checking protocol + - 默认端口:`9091` ~ `9095` +- 启动前 target 校验 + - `api-gateway` 校验 `clients.iam.target`、`clients.orchestrator.target`、`clients.resource.target`、`clients.system.target` + - `runtime-worker-service` 校验 `clients.orchestrator.target` + - `resource-service` 校验 `clients.orchestrator.target` + - `system-service` 校验 `clients.runtime.target` +- 口径 + - 缺 target 时直接启动失败,不把配置缺失留给 readiness 长期兜底 + +## 6. 治理约定 + +### 6.1 Request ID + +- 外部 HTTP: + - 优先读取 `X-Request-Id` + - 缺失时由 gateway 生成并回写响应头 +- 内部 gRPC: + - 统一 metadata key:`x-request-id` +- 当前已落地: + - `src/router/router.go` 挂 request-id middleware + - `src/internalclient/*` 统一透传 + - `src/interface/grpc*` 统一提取/补齐并回写 header + +### 6.2 错误码 + +- HTTP: + - `401`、`403`、`400`、`404`、`409`、`500` +- gRPC: + - `Unauthenticated` + - `PermissionDenied` + - `InvalidArgument` + - `NotFound` + - `AlreadyExists` + - 其他统一 `Internal` + +### 6.3 观测与配置 + +- 基础标签: + - `service.name` + - `service.role` + - `request.id` + - `user.id` + - `project.id` + - `trace.id` + - `task.id` + - `group.id` +- 内部 client target 主键: + - `clients.iam.target` + - `clients.resource.target` + - `clients.orchestrator.target` + - `clients.runtime.target` + - `clients.system.target` +- 服务监听主键: + - `iam.grpc.addr` + - `resource.grpc.addr` + - `orchestrator.grpc.addr` + - `runtime_worker.grpc.addr` + - `system.grpc.addr` + +## 7. SDK / 鉴权 / Swagger 总结 + +### 7.1 Swagger audience 现状 + +- 扫描总操作数:`173` +- 已标记操作:`100` +- 空 `@x-api-type {}`:`73` +- 缺失 `@x-api-type`:`0` +- 已标 audience 统计: + - `sdk=5` + - `portal=43` + - `admin=58` + +### 7.2 AK/SK -> token 规范 + +- 交换接口: + - `POST /api/v2/auth/access-key/token` +- 只有这个接口直接使用 `secret_key` +- 业务 API 继续统一使用: + - `Authorization: Bearer ` + +必需请求头: + +- `X-Access-Key` +- `X-Timestamp` +- `X-Nonce` +- `X-Signature` + +canonical string: + +```text +METHOD +PATH +ACCESS_KEY +TIMESTAMP +NONCE +``` + +签名算法: + +```text +signature = hex(hmac_sha256(secret_key, canonical_string)) +``` + +服务端规则: + +- 时间窗:`+- 5 minutes` +- nonce 单次使用 +- disabled / deleted / expired access key 不能换 token +- replay 防护依赖 Redis nonce reservation + +### 7.3 `aegisctl` 鉴权约定 + +- `aegisctl auth login --access-key ... --secret-key ...` + - 走 AK/SK 签名换 token +- `aegisctl auth inspect` + - 查看本地 auth context +- `aegisctl auth sign-debug` + - 输出 canonical string、签名头、curl 示例 +- `aegisctl auth sign-debug --execute` + - 直接发起换 token 请求 +- `aegisctl auth sign-debug --execute --save-context` + - 成功后把 token 落当前 context + +## 8. Model / DTO / repository 收口总结 + +- `src/database` 已整体迁到 `src/model` +- DB 初始化、迁移、生命周期已转到 `src/infra/db` +- 模块专用 API 契约已大量下沉到 `src/module/*/api_types.go` +- 全局 `src/dto/*` 已压缩为极薄共享层,只保留分页/搜索/统一响应/少量跨模块运行时载荷 +- 已删除一批空心化旧仓储文件,模块专用 DB 访问回收到各自 `src/module/*/repository.go` +- 当前 `src/repository/*` 只保留仍有跨模块边界价值的共享查询能力 + +## 9. 当前验收状态 + +- 默认回归: + - `cd src && go test ./...` +- Producer Fx 图校验与 HTTP 主路径: + - `cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke'` +- Consumer / Both 生命周期冒烟: + - `cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions'` +- 路由 / 文档主路径: + - `cd src && go test ./router ./docs ./interface/http` +- 真实 K8s 集群验收: + - `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` + +## 10. 当前唯一未完成人工项 + +- `docs/todo.md` + - `确认 Fx 生成的启动日志是否可接受` + - 性质:人工验收 + - 状态:非阻塞 + - 不影响当前“主线完成”判断 + +## 11. 仓库级补扫结果 + +### 11.1 已确认清空的旧兼容面 + +对 `src` 生产代码补扫后,以下模式当前为 `0` 命中: + +```text +service/producer = 0 +handlers/system = 0 +database.DB = 0 +GetGateway( = 0 +redisinfra.GetGateway = 0 +``` + +说明: + +- 旧 `service/producer` 兼容层已退出运行态代码 +- 旧 `handlers/system` 包级入口已退出运行态代码 +- 全局 `database.DB` 已不再残留在主线包中 +- 旧 infra 全局 gateway fallback 已不再残留在主线包中 + +### 11.2 `context.Background()` 补扫 + +在 `src/app src/interface src/module src/service src/router src/middleware` 范围补扫后: + +- 生产代码未发现新的 `context.Background()` 残留 +- 当前命中均来自测试文件 + +## 12. 微服务主线完成面 + +### 12.1 服务入口 + +当前统一二进制已支持: + +- `producer` +- `consumer` +- `both` +- `api-gateway` +- `iam-service` +- `orchestrator-service` +- `resource-service` +- `runtime-worker-service` +- `system-service` + +### 12.2 internal client 边界 + +当前已落地: + +- gateway -> IAM +- gateway -> Resource +- gateway -> Orchestrator +- gateway -> System +- system -> Runtime +- resource/evaluation -> Orchestrator +- runtime -> Orchestrator + +关键目录: + +- `src/internalclient/iamclient` +- `src/internalclient/resourceclient` +- `src/internalclient/orchestratorclient` +- `src/internalclient/systemclient` +- `src/internalclient/runtimeclient` + +### 12.3 dedicated service 收口状态 + +- dedicated `api-gateway` 已不再静默回退本地 owner service +- `resource-service` 已通过 remote query/source 收口 project statistics 与 evaluation 查询 +- `system-service` 已通过 runtime RPC 收口 namespace locks / queued tasks +- `runtime-worker-service` 已通过 remote owner option 收口 orchestrator owner 操作 +- `api-gateway` 的 team / auth / user / rbac / label / chaos-system / task / trace / group / notification 主路径已收口到内部服务边界 + +## 13. 当前仍剩余,但不阻塞主线 + +- 兼容入口 `producer / consumer / both` 内部仍可继续压缩本地 owner 组合 +- 少量跨服务 DB 直查/直写仍可继续按 owner 深清 +- 发布层后续仍可继续细化环境参数、镜像策略、HPA、Ingress 与 values 编排 + +## 14. 建议下一阶段顺序 + +1. 继续清兼容入口里的本地 owner 组合面 +2. 继续清跨服务 DB 直查/直写 +3. 做版本级环境参数与发布编排抛光 + +## 15. 开发与调试说明 + +### 15.1 先选调试模式 + +日常开发现在建议按下面三种模式选: + +- `producer` + - 适合只调 HTTP/API、Swagger、handler/service 主链 + - 不需要 runtime worker 异步消费时优先用它 +- `both` + - 适合本地联调 submit -> queue -> worker -> query 的完整闭环 + - 一次起 HTTP + worker,最省事 + - 注意:`both` 不是六服务模式,不会同时起 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` +- 六服务模式 + - 适合调试微服务边界、internal client、remote-first 路径、服务 ownership + - 需要确认 gateway 是否真的走 gRPC、某个 dedicated service 是否不再回退本地实现时,用这一套 + +简单建议: + +- 改接口/页面联调:先用 `producer` +- 改异步执行链:先用 `both` +- 改 internal client / gRPC / 服务边界:直接用六服务模式 + +### 15.2 本地基础依赖 + +先起基础依赖: + +```bash +docker compose up -d redis mysql etcd jaeger buildkitd loki prometheus grafana +``` + +配置主文件: + +- `src/config.dev.toml` + +重点配置: + +- MySQL / Redis / Etcd / Loki / BuildKit 连接 +- `clients.iam.target` +- `clients.resource.target` +- `clients.orchestrator.target` +- `clients.runtime.target` +- `clients.system.target` +- `iam.grpc.addr` +- `resource.grpc.addr` +- `orchestrator.grpc.addr` +- `runtime_worker.grpc.addr` +- `system.grpc.addr` + +### 15.3 最常用启动方式 + +#### A. 只调 HTTP + +```bash +cd src && go run . producer -conf ./config.dev.toml -port 8082 +``` + +适合: + +- router / handler / module service +- Swagger / OpenAPI +- Portal / Admin / SDK HTTP 联调 + +#### B. 调完整单机闭环 + +```bash +cd src && go run . both -conf ./config.dev.toml -port 8082 +``` + +适合: + +- execution / injection submit +- queue 消费 +- task / trace / logs 主链 + +#### C. 调微服务边界 + +建议顺序: + +```bash +# terminal 1 +cd src && go run ./cmd/iam-service -conf ./config.dev.toml + +# terminal 2 +cd src && go run ./cmd/orchestrator-service -conf ./config.dev.toml + +# terminal 3 +cd src && go run ./cmd/resource-service -conf ./config.dev.toml + +# terminal 4 +cd src && go run ./cmd/runtime-worker-service -conf ./config.dev.toml + +# terminal 5 +cd src && go run ./cmd/system-service -conf ./config.dev.toml + +# terminal 6 +cd src && go run ./cmd/api-gateway -conf ./config.dev.toml -port 8082 +``` + +如果只想调某一条边界,不需要六个都起: + +- 调 auth/user/rbac/access key:起 `iam-service` + `api-gateway` +- 调 project/container/dataset/evaluation:起 `resource-service` + `api-gateway` +- 调 submit/task/trace:起 `orchestrator-service` + `api-gateway` +- 调 monitor/config/audit:起 `system-service` + `api-gateway` +- 调 worker/runtime:起 `runtime-worker-service`,必要时再带 `orchestrator-service` + +### 15.4 如何判断现在该打在哪一层断点 + +#### HTTP 问题 + +优先看: + +- `src/router/*` +- `src/module/*/handler.go` +- `src/module/*/service.go` + +如果是 dedicated gateway 路径,再看: + +- `src/app/gateway/*` +- `src/internalclient/*` + +判断原则: + +- 请求没进业务:看 router / middleware / handler +- 请求进了业务但结果不对:看 module service / repository +- dedicated gateway 下结果和单体模式不同:看 `app/gateway` remote-aware 装配和 `internalclient/*` + +#### gRPC / 微服务边界问题 + +优先看: + +- `src/internalclient/*` +- `src/interface/grpc*/*` +- 对应 `src/app/{gateway,iam,resource,orchestrator,runtime,system}/*` + +判断原则: + +- 调用没发出去:看 internal client target、dial、interceptor +- 服务收不到:看 grpc service registration / lifecycle +- dedicated service 启动就失败:先查 target 配置是否缺失 + +#### 异步执行链问题 + +优先看: + +- `src/interface/worker/*` +- `src/interface/controller/*` +- `src/service/consumer/*` +- `src/module/task/*` +- `src/module/execution/*` +- `src/module/injection/*` +- `src/infra/k8s/*` + +判断原则: + +- submit 成功但没消费:先看 Redis / consumer +- 消费了但没执行:看 runtime owner、k8s/build/helm gateway +- 执行了但状态没回写:看 orchestrator owner facade / consumer owner adapter + +### 15.5 快速验活命令 + +HTTP: + +```bash +curl -I http://127.0.0.1:8082/docs/doc.json +curl -i http://127.0.0.1:8082/system/health +``` + +gRPC: + +```bash +grpcurl -plaintext 127.0.0.1:9091 list +grpcurl -plaintext 127.0.0.1:9092 list +grpcurl -plaintext 127.0.0.1:9093 list +grpcurl -plaintext 127.0.0.1:9094 list +grpcurl -plaintext 127.0.0.1:9095 list +``` + +### 15.6 推荐调试顺序 + +遇到问题时建议固定按这条顺序排: + +1. 服务有没有起来 +2. 配置 target/addr 对不对 +3. 请求到底走的是本地还是 remote +4. request-id 是否贯通 +5. 业务 service 是否收到正确参数 +6. infra gateway / DB / Redis / K8s 是否返回异常 + +### 15.7 现在最重要的几个判断点 + +- 调 dedicated `api-gateway` 时,不要默认它会静默回退本地 owner service +- 调 `system-service` / `runtime-worker-service` 时,先确认对应 `clients.*.target` 已配 +- 调 submit / task / trace 闭环时,优先用 `both` +- 调 ownership / internal RPC 时,优先用六服务模式 +- 调 repository 逻辑时,优先从各模块 `src/module/*/repository.go` 看,不要再去旧 compat 层找 + +### 15.8 常用回归命令 + +```bash +cd src && go test ./... +cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke' +cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions' +cd src && go test ./router ./docs ./interface/http +``` + +真实 K8s 集群验收: + +```bash +cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration +``` + +### 15.9 一句话建议 + +- 大多数日常功能开发:先 `producer` +- 需要异步闭环:用 `both` +- 需要查微服务边界:直接六服务 +- 查不清时先看 `app/*` 装配,再看 `module/*/service.go`,最后看 `infra/*` + +### 15.10 按模块分类的 debug 路线图 + +#### Auth / User / RBAC / Team + +先看: + +- `src/module/auth/handler.go` +- `src/module/auth/service.go` +- `src/module/auth/repository.go` +- `src/module/user/handler.go` +- `src/module/user/service.go` +- `src/module/user/repository.go` +- `src/module/rbac/handler.go` +- `src/module/rbac/service.go` +- `src/module/rbac/repository.go` +- `src/module/team/handler.go` +- `src/module/team/service.go` +- `src/module/team/repository.go` + +如果是 dedicated gateway 下的认证/权限问题,再看: + +- `src/app/gateway/auth_services.go` +- `src/app/gateway/user_services.go` +- `src/app/gateway/rbac_services.go` +- `src/app/gateway/team_services.go` +- `src/internalclient/iamclient/*` +- `src/interface/grpciam/*` + +常见问题先查: + +- 登录/换 token:`auth/service.go` + `iamclient` +- 权限不对:`middleware/*` + `rbac/service.go` +- team project/member 视图不对:`team/service.go` + remote project reader + +#### Project / Label / Container / Dataset + +先看: + +- `src/module/project/handler.go` +- `src/module/project/service.go` +- `src/module/project/repository.go` +- `src/module/label/handler.go` +- `src/module/label/service.go` +- `src/module/label/repository.go` +- `src/module/container/handler.go` +- `src/module/container/service.go` +- `src/module/container/repository.go` +- `src/module/dataset/handler.go` +- `src/module/dataset/service.go` +- `src/module/dataset/repository.go` + +如果是 dedicated gateway / resource-service 边界问题,再看: + +- `src/app/gateway/resource_services.go` +- `src/internalclient/resourceclient/*` +- `src/interface/grpcresource/*` + +常见问题先查: + +- list/detail 不对:各模块 `repository.go` 查询条件 +- label 关系不对:`project/container/dataset` service 里的 label 管理逻辑 +- 统计字段不对:`project` statistics source 与 orchestrator/resource 边界 + +#### Injection / Execution / Task / Trace / Group / Notification + +先看: + +- `src/module/injection/handler.go` +- `src/module/injection/service.go` +- `src/module/injection/repository.go` +- `src/module/execution/handler.go` +- `src/module/execution/service.go` +- `src/module/execution/repository.go` +- `src/module/task/handler.go` +- `src/module/task/service.go` +- `src/module/task/repository.go` +- `src/module/trace/handler.go` +- `src/module/trace/service.go` +- `src/module/group/handler.go` +- `src/module/group/service.go` +- `src/module/notification/handler.go` +- `src/module/notification/service.go` + +如果是 submit / task / trace / stream 走向问题,再看: + +- `src/app/gateway/orchestrator_services.go` +- `src/internalclient/orchestratorclient/*` +- `src/interface/grpcorchestrator/*` + +如果是异步执行闭环问题,再补看: + +- `src/service/consumer/*` +- `src/interface/worker/*` +- `src/interface/controller/*` + +常见问题先查: + +- submit 成功但 task 不生成:`injection/execution service` -> orchestrator facade +- task 有了但状态不推进:`service/consumer` + owner adapter +- trace/group/notification stream 不对:`orchestrator_services.go` + stream read RPC +- task logs WebSocket 不对:`task/service.go` + orchestrator log poll + +#### System / SystemMetric / Monitor / Config / Audit + +先看: + +- `src/module/system/handler.go` +- `src/module/system/service.go` +- `src/module/system/repository.go` +- `src/module/systemmetric/handler.go` +- `src/module/systemmetric/service.go` + +如果是 dedicated system-service / gateway 边界问题,再看: + +- `src/app/gateway/system_services.go` +- `src/internalclient/systemclient/*` +- `src/internalclient/runtimeclient/*` +- `src/interface/grpcsystem/*` +- `src/interface/grpcruntime/*` + +常见问题先查: + +- config/audit 查询不对:`system/repository.go` +- monitor / queue / lock 不对:`system/service.go` 是否走 runtime RPC +- `/system/health` 异常:`system/handler.go` + service health 依赖 + +#### Runtime / K8s / Build / Helm / Chaos + +先看: + +- `src/service/consumer/*` +- `src/interface/worker/*` +- `src/interface/controller/*` +- `src/infra/k8s/*` +- `src/infra/buildkit/*` +- `src/infra/helm/*` +- `src/infra/chaos/*` +- `src/infra/redis/*` + +如果是 dedicated runtime-worker-service 问题,再看: + +- `src/app/runtime/*` +- `src/internalclient/orchestratorclient/*` +- `src/interface/grpcruntime/*` + +常见问题先查: + +- queue 不消费:`consumer` + Redis +- k8s job 不创建:`infra/k8s` +- build / helm 失败:对应 `infra/buildkit` / `infra/helm` +- 状态回写不到 orchestrator:`consumer owner` + orchestrator client + +#### 看文件顺序的偷懒法 + +如果你一时不确定从哪进,统一按这个顺序看: + +1. `src/router/*` 或 `src/interface/grpc*/*` +2. `src/module/*/handler.go` +3. `src/module/*/service.go` +4. `src/module/*/repository.go` +5. `src/app/*` 装配 +6. `src/internalclient/*` +7. `src/infra/*` diff --git a/docs/swagger-audience-marking-report.md b/docs/swagger-audience-marking-report.md deleted file mode 100644 index 3aeef679..00000000 --- a/docs/swagger-audience-marking-report.md +++ /dev/null @@ -1,201 +0,0 @@ -# Swagger Audience Marking Report - -> Source of truth: Swagger annotations in `src/module/*/handler.go` and `src/httpapi/docs.go`. -> Route position column uses the `@Router` line, then `@x-api-type`, then function line when available. - -## Summary - -- Total operations scanned: **173** -- Marked operations: **100** -- Empty `@x-api-type {}` operations: **73** -- Missing `@x-api-type` operations: **0** -- Audience counts among marked operations: `sdk=5` `portal=43` `admin=58` - -## Marked Operations - -| Method | Path | Audience | Summary | Location | -| --- | --- | --- | --- | --- | -| GET | `/api/v2/access-keys` | `portal` | List access keys | `src/module/auth/handler.go:288` / `src/module/auth/handler.go:289` | -| POST | `/api/v2/access-keys` | `portal` | Create access key | `src/module/auth/handler.go:247` / `src/module/auth/handler.go:248` | -| DELETE | `/api/v2/access-keys/{access_key_id}` | `portal` | Delete access key | `src/module/auth/handler.go:357` / `src/module/auth/handler.go:358` | -| GET | `/api/v2/access-keys/{access_key_id}` | `portal` | Get access key detail | `src/module/auth/handler.go:328` / `src/module/auth/handler.go:329` | -| POST | `/api/v2/access-keys/{access_key_id}/disable` | `portal` | Disable access key | `src/module/auth/handler.go:385` / `src/module/auth/handler.go:386` | -| POST | `/api/v2/access-keys/{access_key_id}/enable` | `portal` | Enable access key | `src/module/auth/handler.go:413` / `src/module/auth/handler.go:414` | -| POST | `/api/v2/access-keys/{access_key_id}/rotate` | `portal` | Rotate access key secret | `src/module/auth/handler.go:441` / `src/module/auth/handler.go:442` | -| POST | `/api/v2/auth/access-key/token` | `sdk` | Exchange access key for token | `src/module/auth/handler.go:472` / `src/module/auth/handler.go:473` | -| POST | `/api/v2/auth/change-password` | `portal, admin` | Change user password | `src/module/auth/handler.go:177` / `src/module/auth/handler.go:178` | -| POST | `/api/v2/auth/login` | `portal, admin` | User login | `src/module/auth/handler.go:36` / `src/module/auth/handler.go:37` | -| POST | `/api/v2/auth/logout` | `portal, admin` | User logout | `src/module/auth/handler.go:139` / `src/module/auth/handler.go:140` | -| GET | `/api/v2/auth/profile` | `portal, admin` | Get current user profile | `src/module/auth/handler.go:216` / `src/module/auth/handler.go:217` | -| POST | `/api/v2/auth/refresh` | `portal, admin` | Refresh JWT token | `src/module/auth/handler.go:106` / `src/module/auth/handler.go:107` | -| POST | `/api/v2/auth/register` | `portal, admin` | User registration | `src/module/auth/handler.go:71` / `src/module/auth/handler.go:72` | -| GET | `/api/v2/labels` | `portal` | List labels | `src/module/label/handler.go:165` / `src/module/label/handler.go:166` | -| POST | `/api/v2/labels` | `portal` | Create label | `src/module/label/handler.go:69` / `src/module/label/handler.go:70` | -| POST | `/api/v2/labels/batch-delete` | `portal` | Batch delete labels | `src/module/label/handler.go:35` / `src/module/label/handler.go:36` | -| DELETE | `/api/v2/labels/{label_id}` | `portal` | Delete label | `src/module/label/handler.go:103` / `src/module/label/handler.go:104` | -| GET | `/api/v2/labels/{label_id}` | `portal` | Get label by ID | `src/module/label/handler.go:131` / `src/module/label/handler.go:132` | -| PATCH | `/api/v2/labels/{label_id}` | `portal` | Update label | `src/module/label/handler.go:201` / `src/module/label/handler.go:202` | -| GET | `/api/v2/permissions` | `admin` | List permissions | `src/module/rbac/handler.go:328` / `src/module/rbac/handler.go:329` | -| GET | `/api/v2/permissions/{id}` | `admin` | Get permission by ID | `src/module/rbac/handler.go:296` / `src/module/rbac/handler.go:297` | -| GET | `/api/v2/permissions/{permission_id}/roles` | `admin` | List roles from permission | `src/module/rbac/handler.go:362` / `src/module/rbac/handler.go:363` | -| GET | `/api/v2/projects` | `portal` | List projects | `src/module/project/handler.go:146` / `src/module/project/handler.go:147` | -| POST | `/api/v2/projects` | `portal` | Create a new project | `src/module/project/handler.go:39` / `src/module/project/handler.go:40` | -| DELETE | `/api/v2/projects/{project_id}` | `portal` | Delete project | `src/module/project/handler.go:82` / `src/module/project/handler.go:83` | -| GET | `/api/v2/projects/{project_id}` | `portal` | Get project by ID | `src/module/project/handler.go:113` / `src/module/project/handler.go:114` | -| PATCH | `/api/v2/projects/{project_id}` | `portal` | Update project | `src/module/project/handler.go:185` / `src/module/project/handler.go:186` | -| GET | `/api/v2/projects/{project_id}/executions` | `portal` | List project executions | `src/module/execution/handler.go:44` / `src/module/execution/handler.go:45` | -| POST | `/api/v2/projects/{project_id}/executions/execute` | `portal` | Submit batch algorithm execution | `src/module/execution/handler.go:88` / `src/module/execution/handler.go:89` | -| GET | `/api/v2/projects/{project_id}/injections` | `portal` | List project fault injections | `src/module/injection/handler.go:51` / `src/module/injection/handler.go:52` | -| GET | `/api/v2/projects/{project_id}/injections/analysis/no-issues` | `portal` | List project fault injections without issues | `src/module/injection/handler.go:125` / `src/module/injection/handler.go:126` | -| GET | `/api/v2/projects/{project_id}/injections/analysis/with-issues` | `portal` | List project fault injections with issues | `src/module/injection/handler.go:155` / `src/module/injection/handler.go:156` | -| POST | `/api/v2/projects/{project_id}/injections/build` | `portal` | Submit project datapack buildings | `src/module/injection/handler.go:211` / `src/module/injection/handler.go:212` | -| POST | `/api/v2/projects/{project_id}/injections/inject` | `portal` | Submit project fault injections | `src/module/injection/handler.go:183` / `src/module/injection/handler.go:184` | -| POST | `/api/v2/projects/{project_id}/injections/search` | `portal` | Search project fault injections | `src/module/injection/handler.go:95` / `src/module/injection/handler.go:96` | -| PATCH | `/api/v2/projects/{project_id}/labels` | `portal` | Manage project custom labels | `src/module/project/handler.go:229` / `src/module/project/handler.go:230` | -| GET | `/api/v2/resources` | `admin` | List resources | `src/module/rbac/handler.go:422` / `src/module/rbac/handler.go:423` | -| GET | `/api/v2/resources/{id}` | `admin` | Get resource by ID | `src/module/rbac/handler.go:391` / `src/module/rbac/handler.go:392` | -| GET | `/api/v2/resources/{id}/permissions` | `admin` | List permissions from resource | `src/module/rbac/handler.go:456` / `src/module/rbac/handler.go:457` / `src/module/rbac/handler.go:470` | -| GET | `/api/v2/roles` | `admin` | List roles | `src/module/rbac/handler.go:127` / `src/module/rbac/handler.go:128` | -| POST | `/api/v2/roles` | `admin` | Create a new role | `src/module/rbac/handler.go:38` / `src/module/rbac/handler.go:39` | -| DELETE | `/api/v2/roles/{id}` | `admin` | Delete role | `src/module/rbac/handler.go:68` / `src/module/rbac/handler.go:69` | -| GET | `/api/v2/roles/{id}` | `admin` | Get role by ID | `src/module/rbac/handler.go:96` / `src/module/rbac/handler.go:97` | -| PATCH | `/api/v2/roles/{id}` | `admin` | Update role | `src/module/rbac/handler.go:159` / `src/module/rbac/handler.go:160` | -| POST | `/api/v2/roles/{role_id}/permissions/assign` | `admin` | Assign permissions to role | `src/module/rbac/handler.go:199` / `src/module/rbac/handler.go:200` | -| POST | `/api/v2/roles/{role_id}/permissions/remove` | `admin` | Remove permissions from role | `src/module/rbac/handler.go:234` / `src/module/rbac/handler.go:235` | -| GET | `/api/v2/roles/{role_id}/users` | `admin` | List users from role | `src/module/rbac/handler.go:267` / `src/module/rbac/handler.go:268` | -| GET | `/api/v2/sdk/datasets` | `sdk` | List SDK dataset samples | `src/module/sdk/handler.go:116` / `src/module/sdk/handler.go:117` | -| GET | `/api/v2/sdk/evaluations` | `sdk` | List SDK evaluation samples | `src/module/sdk/handler.go:36` / `src/module/sdk/handler.go:37` | -| GET | `/api/v2/sdk/evaluations/experiments` | `sdk` | List SDK experiment IDs | `src/module/sdk/handler.go:92` / `src/module/sdk/handler.go:93` | -| GET | `/api/v2/sdk/evaluations/{id}` | `sdk` | Get SDK evaluation sample by ID | `src/module/sdk/handler.go:68` / `src/module/sdk/handler.go:69` | -| GET | `/api/v2/system/metrics` | `admin` | Get current system metrics | `src/module/systemmetric/handler.go:30` / `src/module/systemmetric/handler.go:31` | -| GET | `/api/v2/system/metrics/history` | `admin` | Get historical system metrics | `src/module/systemmetric/handler.go:53` / `src/module/systemmetric/handler.go:54` | -| GET | `/api/v2/systems` | `admin` | List chaos systems | `src/module/chaossystem/handler.go:35` / `src/module/chaossystem/handler.go:36` | -| POST | `/api/v2/systems` | `admin` | Create chaos system | `src/module/chaossystem/handler.go:96` / `src/module/chaossystem/handler.go:97` | -| DELETE | `/api/v2/systems/{id}` | `admin` | Delete chaos system | `src/module/chaossystem/handler.go:158` / `src/module/chaossystem/handler.go:159` | -| GET | `/api/v2/systems/{id}` | `admin` | Get chaos system by ID | `src/module/chaossystem/handler.go:67` / `src/module/chaossystem/handler.go:68` | -| PUT | `/api/v2/systems/{id}` | `admin` | Update chaos system | `src/module/chaossystem/handler.go:126` / `src/module/chaossystem/handler.go:127` | -| GET | `/api/v2/systems/{id}/metadata` | `admin` | List chaos system metadata | `src/module/chaossystem/handler.go:218` / `src/module/chaossystem/handler.go:219` | -| POST | `/api/v2/systems/{id}/metadata` | `admin` | Upsert chaos system metadata | `src/module/chaossystem/handler.go:186` / `src/module/chaossystem/handler.go:187` | -| GET | `/api/v2/teams` | `portal` | List teams | `src/module/team/handler.go:137` / `src/module/team/handler.go:138` | -| POST | `/api/v2/teams` | `portal` | Create a new team | `src/module/team/handler.go:39` / `src/module/team/handler.go:40` | -| DELETE | `/api/v2/teams/{team_id}` | `portal` | Delete team | `src/module/team/handler.go:78` / `src/module/team/handler.go:79` | -| GET | `/api/v2/teams/{team_id}` | `portal` | Get team by ID | `src/module/team/handler.go:106` / `src/module/team/handler.go:107` | -| PATCH | `/api/v2/teams/{team_id}` | `portal` | Update team | `src/module/team/handler.go:178` / `src/module/team/handler.go:179` | -| GET | `/api/v2/teams/{team_id}/members` | `portal` | List team members | `src/module/team/handler.go:391` / `src/module/team/handler.go:392` | -| POST | `/api/v2/teams/{team_id}/members` | `portal` | Add member to team | `src/module/team/handler.go:261` / `src/module/team/handler.go:262` | -| DELETE | `/api/v2/teams/{team_id}/members/{user_id}` | `portal` | Remove member from team | `src/module/team/handler.go:299` / `src/module/team/handler.go:300` | -| PATCH | `/api/v2/teams/{team_id}/members/{user_id}/role` | `portal` | Update team member role | `src/module/team/handler.go:343` / `src/module/team/handler.go:344` | -| GET | `/api/v2/teams/{team_id}/projects` | `portal` | List team projects | `src/module/team/handler.go:220` / `src/module/team/handler.go:221` | -| GET | `/api/v2/users` | `admin` | List users | `src/module/user/handler.go:134` / `src/module/user/handler.go:135` | -| POST | `/api/v2/users` | `admin` | Create a new user | `src/module/user/handler.go:36` / `src/module/user/handler.go:37` | -| DELETE | `/api/v2/users/{id}` | `admin` | Delete user | `src/module/user/handler.go:73` / `src/module/user/handler.go:74` | -| PATCH | `/api/v2/users/{id}` | `admin` | Update user | `src/module/user/handler.go:170` / `src/module/user/handler.go:171` | -| GET | `/api/v2/users/{id}/detail` | `admin` | Get user by ID | `src/module/user/handler.go:101` / `src/module/user/handler.go:102` | -| DELETE | `/api/v2/users/{user_id}/containers/{container_id}` | `admin` | Remove user from container | `src/module/user/handler.go:379` / `src/module/user/handler.go:380` | -| POST | `/api/v2/users/{user_id}/containers/{container_id}/roles/{role_id}` | `admin` | Assign user to container | `src/module/user/handler.go:342` / `src/module/user/handler.go:343` | -| DELETE | `/api/v2/users/{user_id}/datasets/{dataset_id}` | `admin` | Remove user from dataset | `src/module/user/handler.go:450` / `src/module/user/handler.go:451` | -| POST | `/api/v2/users/{user_id}/datasets/{dataset_id}/roles/{role_id}` | `admin` | Assign user to dataset | `src/module/user/handler.go:413` / `src/module/user/handler.go:414` | -| POST | `/api/v2/users/{user_id}/permissions/assign` | `admin` | Assign permission to user | `src/module/user/handler.go:264` / `src/module/user/handler.go:265` | -| POST | `/api/v2/users/{user_id}/permissions/remove` | `admin` | Remove permission from user | `src/module/user/handler.go:303` / `src/module/user/handler.go:304` | -| DELETE | `/api/v2/users/{user_id}/projects/{project_id}` | `admin` | Remove user from project | `src/module/user/handler.go:521` / `src/module/user/handler.go:522` / `src/module/user/handler.go:538` | -| POST | `/api/v2/users/{user_id}/projects/{project_id}/roles/{role_id}` | `admin` | Assign user to project | `src/module/user/handler.go:484` / `src/module/user/handler.go:485` | -| POST | `/api/v2/users/{user_id}/role/{role_id}` | `admin` | Assign global role to user | `src/module/user/handler.go:205` / `src/module/user/handler.go:206` | -| DELETE | `/api/v2/users/{user_id}/roles/{role_id}` | `admin` | Remove role from user | `src/module/user/handler.go:234` / `src/module/user/handler.go:235` | -| GET | `/system/audit` | `admin` | List audit logs | `src/module/system/handler.go:184` / `src/module/system/handler.go:185` | -| GET | `/system/audit/{id}` | `admin` | Get audit log by ID | `src/module/system/handler.go:148` / `src/module/system/handler.go:149` | -| GET | `/system/configs` | `admin` | List configurations | `src/module/system/handler.go:253` / `src/module/system/handler.go:254` | -| GET | `/system/configs/{config_id}` | `admin` | Get configuration | `src/module/system/handler.go:219` / `src/module/system/handler.go:220` | -| PATCH | `/system/configs/{config_id}` | `admin` | Update configuration value | `src/module/system/handler.go:376` / `src/module/system/handler.go:377` | -| GET | `/system/configs/{config_id}/histories` | `admin` | List configuration histories | `src/module/system/handler.go:467` / `src/module/system/handler.go:468` | -| PUT | `/system/configs/{config_id}/metadata` | `admin` | Update configuration metadata | `src/module/system/handler.go:420` / `src/module/system/handler.go:421` | -| POST | `/system/configs/{config_id}/metadata/rollback` | `admin` | Rollback configuration metadata | `src/module/system/handler.go:333` / `src/module/system/handler.go:334` | -| POST | `/system/configs/{config_id}/value/rollback` | `admin` | Rollback configuration value | `src/module/system/handler.go:289` / `src/module/system/handler.go:290` | -| GET | `/system/health` | `admin` | System health check | `src/module/system/handler.go:32` / `src/module/system/handler.go:33` | -| GET | `/system/monitor/info` | `admin` | Get system information | `src/module/system/handler.go:83` / `src/module/system/handler.go:84` | -| POST | `/system/monitor/metrics` | `admin` | Get monitoring metrics | `src/module/system/handler.go:58` / `src/module/system/handler.go:59` | -| GET | `/system/monitor/namespaces/locks` | `admin` | List namespace locks | `src/module/system/handler.go:102` / `src/module/system/handler.go:103` | -| POST | `/system/monitor/tasks/queue` | `admin` | List queued tasks | `src/module/system/handler.go:124` / `src/module/system/handler.go:125` | - -## Empty `@x-api-type {}` Operations - -| Method | Path | Summary | Raw | Location | -| --- | --- | --- | --- | --- | -| GET | `/api/_docs/models` | API Model Definitions | `{}` | `src/httpapi/docs.go:36` / `src/httpapi/docs.go:37` / `src/httpapi/docs.go:38` | -| GET | `/api/v2/containers` | List containers | `{}` | `src/module/container/handler.go:148` / `src/module/container/handler.go:149` | -| POST | `/api/v2/containers` | Create container | `{}` | `src/module/container/handler.go:41` / `src/module/container/handler.go:42` | -| POST | `/api/v2/containers/build` | Submit container building | `{}` | `src/module/container/handler.go:474` / `src/module/container/handler.go:475` | -| DELETE | `/api/v2/containers/{container_id}` | Delete container | `{}` | `src/module/container/handler.go:84` / `src/module/container/handler.go:85` | -| GET | `/api/v2/containers/{container_id}` | Get container by ID | `{}` | `src/module/container/handler.go:114` / `src/module/container/handler.go:115` | -| PATCH | `/api/v2/containers/{container_id}` | Update container | `{}` | `src/module/container/handler.go:187` / `src/module/container/handler.go:188` | -| PATCH | `/api/v2/containers/{container_id}/labels` | Manage container custom labels | `{}` | `src/module/container/handler.go:226` / `src/module/container/handler.go:227` | -| GET | `/api/v2/containers/{container_id}/versions` | List container versions | `{}` | `src/module/container/handler.go:387` / `src/module/container/handler.go:388` | -| POST | `/api/v2/containers/{container_id}/versions` | Create container version | `{}` | `src/module/container/handler.go:270` / `src/module/container/handler.go:271` | -| DELETE | `/api/v2/containers/{container_id}/versions/{version_id}` | Delete container version | `{}` | `src/module/container/handler.go:319` / `src/module/container/handler.go:320` | -| GET | `/api/v2/containers/{container_id}/versions/{version_id}` | Get container version by ID | `{}` | `src/module/container/handler.go:350` / `src/module/container/handler.go:351` | -| PATCH | `/api/v2/containers/{container_id}/versions/{version_id}` | Update container version | `{}` | `src/module/container/handler.go:432` / `src/module/container/handler.go:433` | -| POST | `/api/v2/containers/{container_id}/versions/{version_id}/helm-chart` | Upload Helm chart package | `{}` | `src/module/container/handler.go:521` / `src/module/container/handler.go:522` | -| POST | `/api/v2/containers/{container_id}/versions/{version_id}/helm-values` | Upload Helm values file | `{}` | `src/module/container/handler.go:577` / `src/module/container/handler.go:578` | -| GET | `/api/v2/datasets` | List datasets | `{}` | `src/module/dataset/handler.go:149` / `src/module/dataset/handler.go:150` | -| POST | `/api/v2/datasets` | Create dataset | `{}` | `src/module/dataset/handler.go:42` / `src/module/dataset/handler.go:43` | -| POST | `/api/v2/datasets/search` | Search datasets | `{}` | `src/module/dataset/handler.go:186` / `src/module/dataset/handler.go:187` | -| DELETE | `/api/v2/datasets/{dataset_id}` | Delete dataset | `{}` | `src/module/dataset/handler.go:85` / `src/module/dataset/handler.go:86` | -| GET | `/api/v2/datasets/{dataset_id}` | Get dataset by ID | `{}` | `src/module/dataset/handler.go:115` / `src/module/dataset/handler.go:116` | -| PATCH | `/api/v2/datasets/{dataset_id}` | Update dataset | `{}` | `src/module/dataset/handler.go:225` / `src/module/dataset/handler.go:226` | -| PATCH | `/api/v2/datasets/{dataset_id}/labels` | Manage dataset custom labels | `{}` | `src/module/dataset/handler.go:269` / `src/module/dataset/handler.go:270` | -| PATCH | `/api/v2/datasets/{dataset_id}/version/{version_id}/injections` | Manage dataset injections | `{}` | `src/module/dataset/handler.go:569` / `src/module/dataset/handler.go:570` | -| GET | `/api/v2/datasets/{dataset_id}/versions` | List dataset versions | `{}` | `src/module/dataset/handler.go:430` / `src/module/dataset/handler.go:431` | -| POST | `/api/v2/datasets/{dataset_id}/versions` | Create dataset version | `{}` | `src/module/dataset/handler.go:313` / `src/module/dataset/handler.go:314` | -| DELETE | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Delete dataset version | `{}` | `src/module/dataset/handler.go:362` / `src/module/dataset/handler.go:363` | -| GET | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Get dataset version by ID | `{}` | `src/module/dataset/handler.go:393` / `src/module/dataset/handler.go:394` | -| PATCH | `/api/v2/datasets/{dataset_id}/versions/{version_id}` | Update dataset version | `{}` | `src/module/dataset/handler.go:475` / `src/module/dataset/handler.go:476` | -| GET | `/api/v2/datasets/{dataset_id}/versions/{version_id}/download` | Download dataset version | `{}` | `src/module/dataset/handler.go:521` / `src/module/dataset/handler.go:522` | -| GET | `/api/v2/evaluations` | List evaluations | `{}` | `src/module/evaluation/handler.go:122` / `src/module/evaluation/handler.go:123` | -| POST | `/api/v2/evaluations/datapacks` | List Datapack Evaluation Results | `{}` | `src/module/evaluation/handler.go:37` / `src/module/evaluation/handler.go:38` | -| POST | `/api/v2/evaluations/datasets` | List Dataset Evaluation Results | `{}` | `src/module/evaluation/handler.go:80` / `src/module/evaluation/handler.go:81` | -| DELETE | `/api/v2/evaluations/{id}` | Delete evaluation by ID | `{}` | `src/module/evaluation/handler.go:186` / `src/module/evaluation/handler.go:187` | -| GET | `/api/v2/evaluations/{id}` | Get evaluation by ID | `{}` | `src/module/evaluation/handler.go:157` / `src/module/evaluation/handler.go:158` | -| GET | `/api/v2/executions` | List executions | `{}` | `src/module/execution/handler.go:146` / `src/module/execution/handler.go:147` | -| POST | `/api/v2/executions/batch-delete` | Batch delete executions | `{}` | `src/module/execution/handler.go:271` / `src/module/execution/handler.go:272` | -| GET | `/api/v2/executions/labels` | List execution labels | `{}` | `src/module/execution/handler.go:206` / `src/module/execution/handler.go:207` | -| POST | `/api/v2/executions/{execution_id}/detector_results` | Upload detector results | `{}` | `src/module/execution/handler.go:306` / `src/module/execution/handler.go:307` | -| POST | `/api/v2/executions/{execution_id}/granularity_results` | Upload granularity results | `{}` | `src/module/execution/handler.go:346` / `src/module/execution/handler.go:347` | -| GET | `/api/v2/executions/{id}` | Get execution by ID | `{}` | `src/module/execution/handler.go:180` / `src/module/execution/handler.go:181` | -| PATCH | `/api/v2/executions/{id}/labels` | Manage execution custom labels | `{}` | `src/module/execution/handler.go:233` / `src/module/execution/handler.go:234` | -| GET | `/api/v2/groups/{group_id}/stats` | Get statistics for a group of traces | `{}` | `src/module/group/handler.go:43` / `src/module/group/handler.go:44` | -| GET | `/api/v2/groups/{group_id}/stream` | Stream group trace events in real-time | `{}` | `src/module/group/handler.go:82` / `src/module/group/handler.go:84` | -| GET | `/api/v2/injections` | List injections | `{}` | `src/module/injection/handler.go:242` / `src/module/injection/handler.go:243` | -| GET | `/api/v2/injections/analysis/no-issues` | Query Fault Injection Records Without Issues | `{}` | `src/module/injection/handler.go:333` / `src/module/injection/handler.go:334` | -| GET | `/api/v2/injections/analysis/with-issues` | Query Fault Injection Records With Issues | `{}` | `src/module/injection/handler.go:351` / `src/module/injection/handler.go:352` | -| POST | `/api/v2/injections/batch-delete` | Batch delete injections | `{}` | `src/module/injection/handler.go:522` / `src/module/injection/handler.go:523` | -| POST | `/api/v2/injections/build` | Submit batch datapack buildings | `{}` | `src/module/injection/handler.go:315` / `src/module/injection/handler.go:316` | -| POST | `/api/v2/injections/inject` | Submit batch fault injections | `{}` | `src/module/injection/handler.go:296` / `src/module/injection/handler.go:297` | -| PATCH | `/api/v2/injections/labels/batch` | Batch manage injection labels | `{}` | `src/module/injection/handler.go:488` / `src/module/injection/handler.go:489` | -| GET | `/api/v2/injections/metadata` | Get Injection Metadata | `{}` | `src/module/injection/handler.go:401` / `src/module/injection/handler.go:402` | -| POST | `/api/v2/injections/search` | Search injections | `{}` | `src/module/injection/handler.go:276` / `src/module/injection/handler.go:277` | -| POST | `/api/v2/injections/upload` | Upload a manual datapack | `{}` | `src/module/injection/handler.go:828` / `src/module/injection/handler.go:829` | -| GET | `/api/v2/injections/{id}` | Get injection by ID | `{}` | `src/module/injection/handler.go:372` / `src/module/injection/handler.go:373` | -| POST | `/api/v2/injections/{id}/clone` | Clone injection | `{}` | `src/module/injection/handler.go:556` / `src/module/injection/handler.go:557` | -| GET | `/api/v2/injections/{id}/download` | Download datapack | `{}` | `src/module/injection/handler.go:617` / `src/module/injection/handler.go:618` | -| GET | `/api/v2/injections/{id}/files` | List datapack files | `{}` | `src/module/injection/handler.go:653` / `src/module/injection/handler.go:654` | -| GET | `/api/v2/injections/{id}/files/download` | Download datapack file | `{}` | `src/module/injection/handler.go:691` / `src/module/injection/handler.go:692` | -| GET | `/api/v2/injections/{id}/files/query` | Query datapack file content | `{}` | `src/module/injection/handler.go:745` / `src/module/injection/handler.go:746` | -| PUT | `/api/v2/injections/{id}/groundtruth` | Update datapack ground truth | `{}` | `src/module/injection/handler.go:787` / `src/module/injection/handler.go:788` | -| PATCH | `/api/v2/injections/{id}/labels` | Manage injection custom labels | `{}` | `src/module/injection/handler.go:450` / `src/module/injection/handler.go:451` | -| GET | `/api/v2/injections/{id}/logs` | Get injection logs | `{}` | `src/module/injection/handler.go:589` / `src/module/injection/handler.go:590` | -| GET | `/api/v2/metrics/algorithms` | Get algorithm comparison metrics | `{}` | `src/module/metric/handler.go:99` / `src/module/metric/handler.go:100` | -| GET | `/api/v2/metrics/executions` | Get execution metrics | `{}` | `src/module/metric/handler.go:67` / `src/module/metric/handler.go:68` | -| GET | `/api/v2/metrics/injections` | Get injection metrics | `{}` | `src/module/metric/handler.go:35` / `src/module/metric/handler.go:36` | -| GET | `/api/v2/notifications/stream` | Stream global notifications in real-time | `{}` | `src/module/notification/handler.go:40` / `src/module/notification/handler.go:42` | -| GET | `/api/v2/tasks` | List tasks | `{}` | `src/module/task/handler.go:124` / `src/module/task/handler.go:125` | -| POST | `/api/v2/tasks/batch-delete` | Batch delete tasks | `{}` | `src/module/task/handler.go:48` / `src/module/task/handler.go:49` | -| GET | `/api/v2/tasks/{task_id}` | Get task by ID | `{}` | `src/module/task/handler.go:85` / `src/module/task/handler.go:86` | -| GET | `/api/v2/tasks/{task_id}/logs/ws` | Stream task logs via WebSocket | `{}` | `src/module/task/handler.go:159` / `src/module/task/handler.go:160` | -| GET | `/api/v2/traces` | List traces | `{}` | `src/module/trace/handler.go:81` / `src/module/trace/handler.go:82` | -| GET | `/api/v2/traces/{trace_id}` | Get trace by ID | `{}` | `src/module/trace/handler.go:44` / `src/module/trace/handler.go:45` | -| GET | `/api/v2/traces/{trace_id}/stream` | Stream trace events in real-time | `{}` | `src/module/trace/handler.go:118` / `src/module/trace/handler.go:119` | - -## Missing `@x-api-type` Operations - -| Method | Path | Summary | Location | -| --- | --- | --- | --- | - diff --git a/docs/todo.md b/docs/todo.md index 0a933926..332852e5 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -24,7 +24,8 @@ 验证: -- [x] 阅读 [backend-fx-refactor-plan.md](./backend-fx-refactor-plan.md) +- [x] 阅读汇总文档与主线设计说明 + - 当前总入口已收口到 [report-index.md](./report-index.md) - [x] `cd src && go test ./app ./router ./handlers/v2` - 实际执行:`cd src && go test ./app ./interface/http ./router ./handlers/v2` @@ -436,7 +437,7 @@ Task: - [x] 给 `aegisctl` 增加本地签名排障命令 - 已补 `aegisctl auth inspect` 与 `aegisctl auth sign-debug`;前者可检查当前 context 的 token / auth_type / access_key / expiry,后者可直接打印 canonical string、签名头与 curl 样例,并可通过 `--execute` 直接发起换 token 请求回显响应,或通过 `--save-context` 直接把成功返回的 Bearer token 落盘到当前 CLI context,便于排查 SDK / CLI / 服务端签名不一致问题。 - [x] 补充 AK/SK 头签名规范文档 - - 已新增 `docs/access-key-signature-spec.md`,明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 access key 的使用说明、curl 示例与 `aegisctl` 排障命令说明;同时已回填 `src/handlers/v2/access_keys.go` / `src/dto/auth.go` 的 Swagger/OpenAPI 注释与 schema example,前端与文档站可直接消费。 + - 相关说明现已并入 `docs/report-index.md`:明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 access key 的使用说明与 `aegisctl` 排障命令说明;同时已回填 `src/handlers/v2/access_keys.go` / `src/dto/auth.go` 的 Swagger/OpenAPI 注释与 schema example,前端与文档站可直接消费。 - [x] 补 Portal access key 前端文案与表单提示 - `../AegisLab-frontend/src/pages/settings/Settings.tsx` 已新增 Access Keys 管理页签,覆盖创建 / 轮换 / 启停 / 删除与一次性 secret 提示;`../AegisLab-frontend/src/api/auth.ts` 也已补齐 access key API 封装,页面文案与 OpenAPI 说明保持一致。 - [x] 重新生成 `openapi3` / `sdk.json` 并回填最新统计 @@ -473,7 +474,7 @@ Task: - [x] 删除过渡 handler wrapper - `src/handlers/v2` 现仅保留空的 `doc.go` 占位包以兼容既有测试命令,已不再承担任何运行态 wrapper 职责;`src/handlers/debug.go`、`src/handlers/system/*` 与 `src/handlers/v2/*` 旧兼容入口均已清空或删除。最近一轮又把 `src/app/producer_init.go`、`src/interface/{worker,controller,receiver}/module.go`、`src/interface/http/server.go` 中仅供 Fx 编排使用的注册 helper 全部缩成包内私有实现,启动链公开暴露面继续收口。 - [x] 删除旧包级 service 函数 - - `module/user` CRUD / 资源授权、`module/systemmetric` 指标查询、`module/rbac` 已基本切离 `service/producer`;`handlers/system/monitor.go`、`configs.go`、`audit.go` 主路由入口也已并入 `module/system`。此前已删除旧 `service/producer` 中的 system / metrics / sdk / chaos-system / permission / audit / evaluation / notification / team / trace / group 兼容入口;middleware 也不再直接依赖旧 producer。`module/container` 与 `module/dataset` 现已进一步把 CRUD / detail / list / labels / version 元数据、container build / helm upload、dataset filename / download / version injection 路径下沉到模块 service/repository,并把直接碰 `config` / git / 文件系统的部分收成模块内 gateway/store。旧 `service/producer/container.go` / `dataset.go` 已删除;初始化已改走 `module/container` / `module/dataset` 暴露的 core helper。最近几轮里,`module/injection` 已先后接管 datapack download / files / file query / upload / build 提交流程,以及 injection list / project list / detail / labels / logs / submit fault injection / search / no-issues / with-issues / clone / batch delete 主路径;`src/service/producer/injection.go` 已整体删除。随后又继续按“模块语义留在模块 repo、纯转发尽量删除”的口径收缩:`module/injection` 把 search / list / labels / batch label 管理,以及 project injection list 的标签装配收进 `repository.go`,并继续把 `LoadInjection` / `FindInjectionByName` / `CreateInjectionRecord` / `LoadTask` / `LoadPedestalHelmConfig` / label/execution 删除辅助等一批原子转发写实到模块仓储;最近三轮又把 project resolve、detail with labels、existing injection map、label 条件聚合、project injection list、issue/no-issue 视图、label id by key、fault injection 批量 with labels 这批组合查询继续收成模块内实现。`module/user` 这一轮又把 `CreateUser + EnsureUserUnique`、`Get/Update` 这批基础 CRUD 空包装进一步折成 `CreateUserIfUnique`、`GetUserDetailBase`、`UpdateMutableUser`、`ListUserViews`,并把 `DeleteUserCascade`、global/container/dataset/project 的 assign/remove、permission batch create/delete 这批 relation 逻辑也直接写进模块 repo;随后又把 user detail 关系装配,以及 role/container/dataset/project 的加载 helper 继续改为模块内直接查库;最近又把 permission id 批量校验也直接内聚到模块仓储,并把纯存在性校验提升成公开 `EnsureUserExists(...)` 供 service 组合点复用。`module/rbac` 把 role 详情装配、权限批量校验、角色删除级联、resource/permission 关系查询收进模块 repo,并继续把 role / permission / resource 的基础 list/load/create 查询直接内聚到模块仓储;最近又把 role detail、role->user、permission->role、resource->permission 这批组合视图改成模块内直查;上一轮再把 role delete cascade、mutable update、permission id 批量加载也进一步改成模块仓储自管;这一轮继续把“可写 role”校验收口成模块内 `loadWritableRole(...)`,同时把通用 `LoadPermission` / `LoadResource` 改成更贴业务语义的 `GetPermissionDetail(...)` / `GetResourceDetail(...)`。`module/project` 现已把 create-with-owner、delete cascade、detail/list 视图装配、mutable update、label reload 与按 key 移除标签收进自身 repo,这几轮继续把 project owner role 查询、project statistics 聚合、label 批量装配 / project label id 查找 / usage decrease 一并写实;这一轮再把内部 helper 命名继续往语义侧收紧成 `loadProjectRecord(...)` / `listProjectStatistics(...)`。`module/team` 也把 create-with-creator、detail 聚合、visible list、team project list、member add/remove/update role、team visibility 读取等操作收进 repo,并把 team project statistics 聚合也留在模块内;这一轮又把 team 加载进一步收成 `loadTeam(...)`,用于 detail / mutable update / ensure exists / visibility 读取,同时把 project statistics helper 明确成 `listTeamProjectStatistics(...)`。`module/execution` 现已接管 project list / global list / detail / labels / batch delete / detector result / granularity result / submit execution 全链路,新增自身 `repository.go` 并删除旧 `src/service/producer/execution.go`。由于 project 主路径此前早已由 `module/project` 承接,本轮也同步删除了已空心化的 `src/service/producer/project.go`;同时 `service/producer/label.go` 也已删除,初始化阶段改走 `module/label.CreateLabelCore`。`service/producer/relation.go`、`user.go`、`role.go`、`resource.go`、`auth_helpers.go`、`permission_helpers.go`、`datapack_archive.go` 同样已清掉,producer 侧残余重点进一步收敛到更少的共享逻辑;当前 `src/service/producer` 已无 Go 源文件残留。与此同时,旧 `src/client/loki.go` / `jaeger.go` / `redis_client.go` / `etcd_client.go` / `harbor_client.go` / `helm.go` / `client/k8s/*` 及 Helm 对应测试也已从 root `client` 包清走,真实实现统一并入 `src/infra/*`;上一轮已把 `src/infra/k8s/client.go` 删除,rest/client/dynamic/controller 的单例初始化直接吸回 `src/infra/k8s/gateway.go`;这一轮继续把 `service/consumer` / `service/initialization` 中的 `CurrentK8sController()` fallback 干掉,改成由 Fx 注入 `*k8sinfra.Controller`,同时 `service/common` 的 etcd fallback 改为回落到 `infra/etcd.GetGateway()` 单点入口,并进一步删掉 `service/consumer/deps.go` / `service/common/deps.go` 这类旧全局依赖注册文件。`service/consumer` 中剩余的 K8s / BuildKit / Helm 访问也继续改为直接走 `infra/*` 单点入口:新增 `buildkitinfra.GetGateway()`、`helminfra.GetGateway()`,`CurrentK8sGateway()` / `currentBuildkitGateway()` / `currentHelmGateway()` 已全部清掉;这轮又把 `app/startup.go` 删除,并进一步引入 `app.RegisterProducerInitialization`,把 producer 初始化从 `context.Background()` 改成走 Fx `OnStart` 生命周期上下文。随后又继续把 `interface/controller` / `interface/receiver` / `interface/worker` 的生命周期上下文改成从 Fx `OnStart` 派生,不再在模块注册期直接构造 `context.Background()`;再往下一轮又把 `service/consumer/task.go` / `trace.go` / `jvm_runtime_mutator.go` / `k8s_handler.go` 里残余 `context.Background()` 全部清成 consumer 内部 detached context helper。初始化侧原先带 callback 的 `registerHandlers(...)` 旧 helper 也已改成更窄职责的 `activateConfigScope(...)`,consumer / producer 各自显式注册所需 handlers,再统一激活 listener scope;这一轮再把 `GetConfigUpdateListener(...)` 单例 helper 从启动链收掉,改为在 producer / worker Fx `OnStart` 生命周期里显式创建 `ConfigUpdateListener` 后传给 initialization。`service/consumer` 的 Redis 直连也开始往更窄语义收:新增内部 `currentRedisGateway` / `currentRedisClient` / `publishRedisStreamEvent` / `publishTraceStreamEvent` / `loadCachedInjectionAlgorithms` helper,先把 trace/group stream 发布、detector cache 读取,以及 `monitor` / `rate_limiter` 对 Redis gateway 的获取收进更窄入口;随后又把 monitor 的上下文来源收回 worker lifecycle,并把 namespace SMembers/HGet/HSet/Pipeline 这批读取/写入改为统一走 consumer 内部 Redis helper 取 client,同时 `rate_limiter` 也不再自持 Redis client,而是统一经由 consumer Redis helper 获取连接;最近一轮再把 namespace key / exists / field read / seed / lock write 继续折成 `monitor` 内部更窄 helper,减少 monitor 主流程里散落的 Redis 原语;上一轮则继续把 rate limiter Redis 操作下沉成独立 `tokenBucketStore`,把 token acquire/release 的 Redis 细节与 limiter 配置/调度逻辑分开;这一轮再正式把 monitor 按同一路径拆出独立 `namespaceStore`,把 namespace key/list/exists/read/write/watch/status 这批 Redis 操作从 monitor 主流程里抽走;紧接着又继续深拆成 `namespaceCatalogStore` / `namespaceLockStore` / `namespaceStatusStore` 三个更窄 store,把锁读取/抢占/释放、namespace 注册、status 读写彻底从 `monitor.go` 抽开,并删除已空心化的 `src/service/consumer/namespace_store.go`。这一轮再把 startup / interface 链路里对 monitor 的旧包级获取收一批:`consumer.NewMonitor(...)` 作为 Fx provider 现在直接吃 `*redisinfra.Gateway` 并在内部自取 client,monitor 构造期不再向启动链暴露裸 `*redis.Client`,`initialization.InitializeConsumer(...)`、`RegisterConsumerHandlers(...)`、`interface/controller` 的 K8s callback 构造均改为显式注入 monitor,而不再自己碰 `GetMonitor()`;紧接着又继续把运行时执行主流程里的 monitor 单例拿掉,新增 `consumer.RuntimeDeps` 由 worker lifecycle 显式传入,`dispatchTask(...)` / `executeTaskWithRetry(...)` / `executeFaultInjection(...)` / `executeRestartPedestal(...)` 已不再自己碰 `GetMonitor()`。这一轮继续顺着同一主线把 rate limiter 也从进程级单例收成纯 Fx provider:`NewRestartPedestalRateLimiter(...)` / `NewBuildContainerRateLimiter(...)` / `NewAlgoExecutionRateLimiter(...)` 现在直接吃 `*redisinfra.Gateway` 构造 limiter,不再经过 `Get*RateLimiter()` / `sync.Once`;`executeBuildContainer(...)`、`executeAlgorithm(...)`、`executeRestartPedestal(...)` 与 K8s job 回调里的 algorithm token release 也都改为走显式传入 limiter,不再直接碰旧包级 getter。与此同时,`service/common/config_registry.go` / `config_listener.go` 把配置元数据读取继续收成 `service/common/config_store.go` 本地语义 store,不再穿过公共 `repository` 包;随后又把 producer/worker/controller/receiver 的启动执行体再收成显式可替换的 `ProducerInitializer` / `LifecycleRunner` 依赖,避免 lifecycle 本身直接抱一大串底层依赖,主路径更贴近 Fx;在此基础上,`src/app/startup_validate_test.go` 与 `src/app/startup_smoke_test.go` 现在已经补上 producer / consumer / both 三种 app option 的 Fx 图校验与 start/stop smoke(通过替换重型初始化依赖,验证 HTTP/worker/controller/receiver/producer lifecycle 编排本身可启动可停止)。这一轮继续顺着同一条线,把 `service/common/config_registry.go` 里的 `sync.Once` / `globalHandlersOnce` 再压掉,改成常驻 registry + 幂等注册逻辑,并补上 `config_registry_test.go` 锁住“全局 handlers 多次注册不重复”行为,进一步减少 config startup 主路径上的一次性单例状态;紧接着又继续把 listener / publish 周边的剩余全局依赖再收一层:`ConfigUpdateListener` 现在显式携带 `*gorm.DB`,不再在读取配置元数据和处理变更时回落到 `database.DB`;`RegisterGlobalHandlers(...)` / `RegisterConsumerHandlers(...)` 也开始显式接收 `ConfigPublisher`,`PublishWrapper(...)` 改成走传入 publisher,而不再自己碰 `redisinfra.GetGateway()`。对应地 producer / consumer 初始化与 worker lifecycle 现已把 Redis gateway / DB 一路显式传进 config listener 与 handler 注册主链。顺手也暴露并修复了 producer 模式此前缺少 `k8sinfra.Module`、导致 `chaosinfra.Module` 无法解析 `*rest.Config` 的问题。当前 producer / consumer / both 三种 app options 都已能通过 `go test ./app` 的图校验和启动链 smoke。这一轮继续把 `service/common` 热路径往显式 DB 收:`DBMetadataStore` 改成由 initialization 注入 `*gorm.DB` 创建,`container` / `dataset` / `task` 公共能力补上 `WithDB` 变体,`module/execution` / `module/injection` / `module/container` 的提交与 ref 解析主路径已改用模块 repo 自带 DB,不再回落到 `database.DB`。这一轮又继续把 consumer 运行态主链的 DB 依赖显式化:`consumer.RuntimeDeps` 开始携带 DB,worker/controller 生命周期分别把 DB 显式注入 task runtime 与 K8s handler,build/restart/algo reschedule、fault injection 落库、collect result 查询、K8s job/CRD 回调里的 execution/injection 状态推进与后续 task submit 也都改成优先走注入 DB,而不再默认抓全局 `database.DB`。这一轮继续把状态同步链也收进显式 DB:`taskStateUpdate` 新增 DB 上下文,`updateTaskState(...)` / `updateTraceState(...)` / trace optimistic lock 更新现在优先沿调用链携带的 DB 执行;K8s error context 也开始透传 handler 注入 DB,因此 consumer 主链里剩余 `database.DB` 基本只落在少量兼容 fallback 和 `service/common` 默认 wrapper。这一轮顺手再把 `module/evaluation` -> `service/analyzer` 这条链也切到显式 DB:evaluation service 改用 repo 持有 DB 调 analyzer 的 `WithDB` 版本,container/dataset ref 解析与 evaluation 持久化不再依赖 analyzer 内部全局 DB;同时 `service/common/ExtractDatapacks(...)` 解析 dataset 时也已改走传入 DB 的 `MapRefsToDatasetVersionsWithDB(...)`。再往下一步,consumer 里 `collect_result` / `fault_injection` / `createExecution` 这类原先“nil 就回落全局 DB”的点也开始直接要求 runtime DB 存在,进一步缩小 fallback 面积。这一轮再继续把兼容层直接砍掉:`service/common` 里默认版 `MapRefsToContainerVersions` / `MapRefsToDatasetVersions` / `ListContainerVersionEnvVars` / `ListHelmConfigValues` / `SubmitTask` / `ProduceFaultInjectionTasks` 已删除,`service/analyzer` 里的默认版 evaluation 入口也删掉,只保留显式 `WithDB` 路径;同时 `consumer/task.go` / `trace.go` / `k8s_handler.go` 里的 DB fallback 也改成显式报错,不再默默回落全局 `database.DB`。紧接着又把 `module/system` 里最后一处直接碰 `database.DB` 的 health check 改成走 `repo.DB()`;目前 `module/*`、`service/common`、`service/consumer`、`service/analyzer` 这批主线包内已无 `database.DB` 残留。顺手又把 repository 层里残留的统计/搜索/资源/注入查询改成统一吃显式 `db` 参数,`repository/task.go` 的 `ListTasksByTimeRange(...)` 也不再偷偷回落全局 DB;现在全仓库只剩 `src/infra/db/module.go` 这一处集中持有 `database.DB`,作为 Fx 提供与关闭数据库连接的基础设施边界。最近两轮又继续把 consumer 外部依赖收窄到 Fx 注入:`interface/worker` 把 `*k8sinfra.Gateway` / `*buildkitinfra.Gateway` / `*helminfra.Gateway` / `*consumer.FaultBatchManager` / `*redisinfra.Gateway` 显式塞进 `consumer.RuntimeDeps`,`build container` / `build datapack` / `algo execution` / `restart pedestal` / `collect result` / task retry / trace state update / K8s callback 已不再直接碰 `GetGateway()` 与 fault batch `sync.Once` 单例;`interface/controller` 同步把 K8s gateway、Redis gateway 和 batch manager 显式交给 `consumer.NewHandler(...)`;`service/logreceiver` 也开始由 `interface/receiver` 注入 Redis publisher,OTLP receiver 不再自己抓 `redisinfra.GetGateway()`。这一轮又继续把 HTTP 链路里的 middleware 全局态收掉:`src/middleware/deps.go` 现在提供 `middleware.Service` 与 `InjectService(...)`,`src/router/router.go` 在根路由中显式注入 middleware service,`src/middleware/permission.go` / `audit.go` 改为按请求从 Gin context 读取 checker/logger,不再持有 `currentPermissionChecker` / `currentAuditLogger` 这类包级默认服务;`src/interface/http/module.go` 也不再用 `fx.Invoke(middleware.RegisterDeps)` 做全局注册。最近这一轮再把 startup 初始化链里的隐藏 fatal 收掉:`newConfigDataWithDB(...)`、`activateConfigScope(...)`、`InitializeProducer(...)`、`InitializeConsumer(...)` 全部改成显式返回 `error`,producer/worker 的 Fx `OnStart` 现在会把初始化失败直接上抛,而不再在 helper 内部 `logrus.Fatalf(...)` 提前退出进程。紧接着这一轮又继续把 consumer startup 链里的 Redis 裸 client 收口到 gateway:worker 初始化改为走 `RedisGateway.InitConcurrencyLock(...)`,`monitor` / `rate limiter` provider 也改成只依赖 `*redisinfra.Gateway`。再下一轮又把模块侧剩余 Redis 全局入口清掉:`module/group` / `module/notification` / `module/trace` / `module/injection` / `module/systemmetric` / `module/system` 现在都改为通过构造注入 `*redisinfra.Gateway`,trace/group/notification stream 读取、injection algorithm cache、system config response subscribe、system metric Redis 查询不再直接碰 `redisinfra.GetGateway()`。这一轮继续把任务队列 helper 也收回 gateway:`infra/redis/task_queue.go` 里的 submit/get/reschedule/dead-letter/queue index/concurrency lock/list/remove 操作全部改成 `Gateway` 方法,`service/common.SubmitTaskWithDB(...)`、`service/consumer` 调度与取消链路、`module/systemmetric` 排队任务查询都已改走显式 Redis gateway。紧接着又把 `infra/redis` / `infra/etcd` / `infra/buildkit` / `infra/helm` / `infra/k8s` 里已经没有调用方的 `GetGateway()` 单例 fallback 全部删除,主线现在只剩少量 lifecycle/startup 组织层 wrapper 需要再压。当前 `src/service/consumer` / `src/middleware` 里残余重点已从“全局 gateway fallback / 全局 default service”收缩到更少的流程组织 helper 与 initialization 邻近收尾。 + - `module/user` CRUD / 资源授权、`module/systemmetric` 指标查询、`module/rbac` 已基本切离 `service/producer`;`handlers/system/monitor.go`、`configs.go`、`audit.go` 主路由入口也已并入 `module/system`。此前已删除旧 `service/producer` 中的 system / metrics / sdk / chaos-system / permission / audit / evaluation / notification / team / trace / group 兼容入口;middleware 也不再直接依赖旧 producer。`module/container` 与 `module/dataset` 现已进一步把 CRUD / detail / list / labels / version 元数据、container build / helm upload、dataset filename / download / version injection 路径下沉到模块 service/repository,并把直接碰 `config` / git / 文件系统的部分收成模块内 gateway/store。旧 `service/producer/container.go` / `dataset.go` 已删除;初始化已改走 `module/container` / `module/dataset` 暴露的 core helper。最近几轮里,`module/injection` 已先后接管 datapack download / files / file query / upload / build 提交流程,以及 injection list / project list / detail / labels / logs / submit fault injection / search / no-issues / with-issues / clone / batch delete 主路径;`src/service/producer/injection.go` 已整体删除。随后又继续按“模块语义留在模块 repo、纯转发尽量删除”的口径收缩:`module/injection` 把 search / list / labels / batch label 管理,以及 project injection list 的标签装配收进 `repository.go`,并继续把 `LoadInjection` / `FindInjectionByName` / `CreateInjectionRecord` / `LoadTask` / `LoadPedestalHelmConfig` / label/execution 删除辅助等一批原子转发写实到模块仓储;最近三轮又把 project resolve、detail with labels、existing injection map、label 条件聚合、project injection list、issue/no-issue 视图、label id by key、fault injection 批量 with labels 这批组合查询继续收成模块内实现。`module/user` 这一轮又把 `CreateUser + EnsureUserUnique`、`Get/Update` 这批基础 CRUD 空包装进一步折成 `CreateUserIfUnique`、`GetUserDetailBase`、`UpdateMutableUser`、`ListUserViews`,并把 `DeleteUserCascade`、global/container/dataset/project 的 assign/remove、permission batch create/delete 这批 relation 逻辑也直接写进模块 repo;随后又把 user detail 关系装配,以及 role/container/dataset/project 的加载 helper 继续改为模块内直接查库;最近又把 permission id 批量校验也直接内聚到模块仓储,并把纯存在性校验提升成公开 `EnsureUserExists(...)` 供 service 组合点复用。`module/rbac` 把 role 详情装配、权限批量校验、角色删除级联、resource/permission 关系查询收进模块 repo,并继续把 role / permission / resource 的基础 list/load/create 查询直接内聚到模块仓储;最近又把 role detail、role->user、permission->role、resource->permission 这批组合视图改成模块内直查;上一轮再把 role delete cascade、mutable update、permission id 批量加载也进一步改成模块仓储自管;这一轮继续把“可写 role”校验收口成模块内 `loadWritableRole(...)`,同时把通用 `LoadPermission` / `LoadResource` 改成更贴业务语义的 `GetPermissionDetail(...)` / `GetResourceDetail(...)`。`module/project` 现已把 create-with-owner、delete cascade、detail/list 视图装配、mutable update、label reload 与按 key 移除标签收进自身 repo,这几轮继续把 project owner role 查询、project statistics 聚合、label 批量装配 / project label id 查找 / usage decrease 一并写实;这一轮再把内部 helper 命名继续往语义侧收紧成 `loadProjectRecord(...)` / `listProjectStatistics(...)`。`module/team` 也把 create-with-creator、detail 聚合、visible list、team project list、member add/remove/update role、team visibility 读取等操作收进 repo,并把 team project statistics 聚合也留在模块内;这一轮又把 team 加载进一步收成 `loadTeam(...)`,用于 detail / mutable update / ensure exists / visibility 读取,同时把 project statistics helper 明确成 `listTeamProjectStatistics(...)`。`module/execution` 现已接管 project list / global list / detail / labels / batch delete / detector result / granularity result / submit execution 全链路,新增自身 `repository.go` 并删除旧 `src/service/producer/execution.go`。由于 project 主路径此前早已由 `module/project` 承接,本轮也同步删除了已空心化的 `src/service/producer/project.go`;同时 `service/producer/label.go` 也已删除,初始化阶段改走 `module/label.CreateLabelCore`。`service/producer/relation.go`、`user.go`、`role.go`、`resource.go`、`auth_helpers.go`、`permission_helpers.go`、`datapack_archive.go` 同样已清掉,producer 侧残余重点进一步收敛到更少的共享逻辑;当前 `src/service/producer` 已无 Go 源文件残留。与此同时,旧 `src/client/loki.go` / `jaeger.go` / `redis_client.go` / `etcd_client.go` / `harbor_client.go` / `helm.go` / `client/k8s/*` 及 Helm 对应测试也已从 root `client` 包清走,真实实现统一并入 `src/infra/*`;上一轮已把 `src/infra/k8s/client.go` 删除,rest/client/dynamic/controller 的单例初始化直接吸回 `src/infra/k8s/gateway.go`;这一轮继续把 `service/consumer` / `service/initialization` 中的 `CurrentK8sController()` fallback 干掉,改成由 Fx 注入 `*k8sinfra.Controller`,同时 `service/common` 的 etcd fallback 改为回落到 `infra/etcd.GetGateway()` 单点入口,并进一步删掉 `service/consumer/deps.go` / `service/common/deps.go` 这类旧全局依赖注册文件。`service/consumer` 中剩余的 K8s / BuildKit / Helm 访问也继续改为直接走 `infra/*` 单点入口:新增 `buildkitinfra.GetGateway()`、`helminfra.GetGateway()`,`CurrentK8sGateway()` / `currentBuildkitGateway()` / `currentHelmGateway()` 已全部清掉;这轮又把 `app/startup.go` 删除,并进一步引入 `app.RegisterProducerInitialization`,把 producer 初始化从 `context.Background()` 改成走 Fx `OnStart` 生命周期上下文。随后又继续把 `interface/controller` / `interface/receiver` / `interface/worker` 的生命周期上下文改成从 Fx `OnStart` 派生,不再在模块注册期直接构造 `context.Background()`;再往下一轮又把 `service/consumer/task.go` / `trace.go` / `jvm_runtime_mutator.go` / `k8s_handler.go` 里残余 `context.Background()` 全部清成 consumer 内部 detached context helper。初始化侧原先带 callback 的 `registerHandlers(...)` 旧 helper 也已改成更窄职责的 `activateConfigScope(...)`,consumer / producer 各自显式注册所需 handlers,再统一激活 listener scope;这一轮再把 `GetConfigUpdateListener(...)` 单例 helper 从启动链收掉,改为在 producer / worker Fx `OnStart` 生命周期里显式创建 `ConfigUpdateListener` 后传给 initialization。`service/consumer` 的 Redis 直连也开始往更窄语义收:新增内部 `currentRedisGateway` / `currentRedisClient` / `publishRedisStreamEvent` / `publishTraceStreamEvent` / `loadCachedInjectionAlgorithms` helper,先把 trace/group stream 发布、detector cache 读取,以及 `monitor` / `rate_limiter` 对 Redis gateway 的获取收进更窄入口;随后又把 monitor 的上下文来源收回 worker lifecycle,并把 namespace SMembers/HGet/HSet/Pipeline 这批读取/写入改为统一走 consumer 内部 Redis helper 取 client,同时 `rate_limiter` 也不再自持 Redis client,而是统一经由 consumer Redis helper 获取连接;最近一轮再把 namespace key / exists / field read / seed / lock write 继续折成 `monitor` 内部更窄 helper,减少 monitor 主流程里散落的 Redis 原语;上一轮则继续把 rate limiter Redis 操作下沉成独立 `tokenBucketStore`,把 token acquire/release 的 Redis 细节与 limiter 配置/调度逻辑分开;这一轮再正式把 monitor 按同一路径拆出独立 `namespaceStore`,把 namespace key/list/exists/read/write/watch/status 这批 Redis 操作从 monitor 主流程里抽走;紧接着又继续深拆成 `namespaceCatalogStore` / `namespaceLockStore` / `namespaceStatusStore` 三个更窄 store,把锁读取/抢占/释放、namespace 注册、status 读写彻底从 `monitor.go` 抽开,并删除已空心化的 `src/service/consumer/namespace_store.go`。这一轮再把 startup / interface 链路里对 monitor 的旧包级获取收一批:`consumer.NewMonitor(...)` 作为 Fx provider 现在直接吃 `*redisinfra.Gateway` 并在内部自取 client,monitor 构造期不再向启动链暴露裸 `*redis.Client`,`initialization.InitializeConsumer(...)`、`RegisterConsumerHandlers(...)`、`interface/controller` 的 K8s callback 构造均改为显式注入 monitor,而不再自己碰 `GetMonitor()`;紧接着又继续把运行时执行主流程里的 monitor 单例拿掉,新增 `consumer.RuntimeDeps` 由 worker lifecycle 显式传入,`dispatchTask(...)` / `executeTaskWithRetry(...)` / `executeFaultInjection(...)` / `executeRestartPedestal(...)` 已不再自己碰 `GetMonitor()`。这一轮继续顺着同一主线把 rate limiter 也从进程级单例收成纯 Fx provider:`NewRestartPedestalRateLimiter(...)` / `NewBuildContainerRateLimiter(...)` / `NewAlgoExecutionRateLimiter(...)` 现在直接吃 `*redisinfra.Gateway` 构造 limiter,不再经过 `Get*RateLimiter()` / `sync.Once`;`executeBuildContainer(...)`、`executeAlgorithm(...)`、`executeRestartPedestal(...)` 与 K8s job 回调里的 algorithm token release 也都改为走显式传入 limiter,不再直接碰旧包级 getter。与此同时,`service/common/config_registry.go` / `config_listener.go` 把配置元数据读取继续收成 `service/common/config_store.go` 本地语义 store,不再穿过公共 `repository` 包;随后又把 producer/worker/controller/receiver 的启动执行体再收成显式可替换的 `ProducerInitializer` / `LifecycleRunner` 依赖,避免 lifecycle 本身直接抱一大串底层依赖,主路径更贴近 Fx;在此基础上,`src/app/startup_validate_test.go` 与 `src/app/startup_smoke_test.go` 现在已经补上 producer / consumer / both 三种 app option 的 Fx 图校验与 start/stop smoke(通过替换重型初始化依赖,验证 HTTP/worker/controller/receiver/producer lifecycle 编排本身可启动可停止)。这一轮继续顺着同一条线,把 `service/common/config_registry.go` 里的 `sync.Once` / `globalHandlersOnce` 再压掉,改成常驻 registry + 幂等注册逻辑,并补上 `config_registry_test.go` 锁住“全局 handlers 多次注册不重复”行为,进一步减少 config startup 主路径上的一次性单例状态;紧接着又继续把 listener / publish 周边的剩余全局依赖再收一层:`ConfigUpdateListener` 现在显式携带 `*gorm.DB`,不再在读取配置元数据和处理变更时回落到 `database.DB`;`RegisterGlobalHandlers(...)` / `RegisterConsumerHandlers(...)` 也开始显式接收 `ConfigPublisher`,`PublishWrapper(...)` 改成走传入 publisher,而不再自己碰 `redisinfra.GetGateway()`。对应地 producer / consumer 初始化与 worker lifecycle 现已把 Redis gateway / DB 一路显式传进 config listener 与 handler 注册主链。顺手也暴露并修复了 producer 模式此前缺少 `k8sinfra.Module`、导致 `chaosinfra.Module` 无法解析 `*rest.Config` 的问题。当前 producer / consumer / both 三种 app options 都已能通过 `go test ./app` 的图校验和启动链 smoke。这一轮继续把 `service/common` 热路径往显式 DB 收:`DBMetadataStore` 改成由 initialization 注入 `*gorm.DB` 创建,`container` / `dataset` / `task` 公共能力补上 `WithDB` 变体,`module/execution` / `module/injection` / `module/container` 的提交与 ref 解析主路径已改用模块 repo 自带 DB,不再回落到 `database.DB`。这一轮又继续把 consumer 运行态主链的 DB 依赖显式化:`consumer.RuntimeDeps` 开始携带 DB,worker/controller 生命周期分别把 DB 显式注入 task runtime 与 K8s handler,build/restart/algo reschedule、fault injection 落库、collect result 查询、K8s job/CRD 回调里的 execution/injection 状态推进与后续 task submit 也都改成优先走注入 DB,而不再默认抓全局 `database.DB`。这一轮继续把状态同步链也收进显式 DB:`taskStateUpdate` 新增 DB 上下文,`updateTaskState(...)` / `updateTraceState(...)` / trace optimistic lock 更新现在优先沿调用链携带的 DB 执行;K8s error context 也开始透传 handler 注入 DB,因此 consumer 主链里剩余 `database.DB` 基本只落在少量兼容 fallback 和 `service/common` 默认 wrapper。这一轮顺手再把 `module/evaluation` -> `service/analyzer` 这条链也切到显式 DB:evaluation service 改用 repo 持有 DB 调 analyzer 的 `WithDB` 版本,container/dataset ref 解析与 evaluation 持久化不再依赖 analyzer 内部全局 DB;同时 `module/injection.ExtractDatapacksWithDB(...)` 解析 dataset 时也已改走传入 DB 的 `MapRefsToDatasetVersionsWithDB(...)`。再往下一步,consumer 里 `collect_result` / `fault_injection` / `createExecution` 这类原先“nil 就回落全局 DB”的点也开始直接要求 runtime DB 存在,进一步缩小 fallback 面积。这一轮再继续把兼容层直接砍掉:`service/common` 里默认版 `MapRefsToContainerVersions` / `MapRefsToDatasetVersions` / `ListContainerVersionEnvVars` / `ListHelmConfigValues` / `SubmitTask` / `ProduceFaultInjectionTasks` 已删除,`service/analyzer` 里的默认版 evaluation 入口也删掉,只保留显式 `WithDB` 路径;同时 `consumer/task.go` / `trace.go` / `k8s_handler.go` 里的 DB fallback 也改成显式报错,不再默默回落全局 `database.DB`。紧接着又把 `module/system` 里最后一处直接碰 `database.DB` 的 health check 改成走 `repo.DB()`;目前 `module/*`、`service/common`、`service/consumer`、`service/analyzer` 这批主线包内已无 `database.DB` 残留。顺手又把 repository 层里残留的统计/搜索/资源/注入查询改成统一吃显式 `db` 参数,`repository/task.go` 的 `ListTasksByTimeRange(...)` 也不再偷偷回落全局 DB;现在全仓库只剩 `src/infra/db/module.go` 这一处集中持有 `database.DB`,作为 Fx 提供与关闭数据库连接的基础设施边界。最近两轮又继续把 consumer 外部依赖收窄到 Fx 注入:`interface/worker` 把 `*k8sinfra.Gateway` / `*buildkitinfra.Gateway` / `*helminfra.Gateway` / `*consumer.FaultBatchManager` / `*redisinfra.Gateway` 显式塞进 `consumer.RuntimeDeps`,`build container` / `build datapack` / `algo execution` / `restart pedestal` / `collect result` / task retry / trace state update / K8s callback 已不再直接碰 `GetGateway()` 与 fault batch `sync.Once` 单例;`interface/controller` 同步把 K8s gateway、Redis gateway 和 batch manager 显式交给 `consumer.NewHandler(...)`;`service/logreceiver` 也开始由 `interface/receiver` 注入 Redis publisher,OTLP receiver 不再自己抓 `redisinfra.GetGateway()`。这一轮又继续把 HTTP 链路里的 middleware 全局态收掉:`src/middleware/deps.go` 现在提供 `middleware.Service` 与 `InjectService(...)`,`src/router/router.go` 在根路由中显式注入 middleware service,`src/middleware/permission.go` / `audit.go` 改为按请求从 Gin context 读取 checker/logger,不再持有 `currentPermissionChecker` / `currentAuditLogger` 这类包级默认服务;`src/interface/http/module.go` 也不再用 `fx.Invoke(middleware.RegisterDeps)` 做全局注册。最近这一轮再把 startup 初始化链里的隐藏 fatal 收掉:`newConfigDataWithDB(...)`、`activateConfigScope(...)`、`InitializeProducer(...)`、`InitializeConsumer(...)` 全部改成显式返回 `error`,producer/worker 的 Fx `OnStart` 现在会把初始化失败直接上抛,而不再在 helper 内部 `logrus.Fatalf(...)` 提前退出进程。紧接着这一轮又继续把 consumer startup 链里的 Redis 裸 client 收口到 gateway:worker 初始化改为走 `RedisGateway.InitConcurrencyLock(...)`,`monitor` / `rate limiter` provider 也改成只依赖 `*redisinfra.Gateway`。再下一轮又把模块侧剩余 Redis 全局入口清掉:`module/group` / `module/notification` / `module/trace` / `module/injection` / `module/systemmetric` / `module/system` 现在都改为通过构造注入 `*redisinfra.Gateway`,trace/group/notification stream 读取、injection algorithm cache、system config response subscribe、system metric Redis 查询不再直接碰 `redisinfra.GetGateway()`。这一轮继续把任务队列 helper 也收回 gateway:`infra/redis/task_queue.go` 里的 submit/get/reschedule/dead-letter/queue index/concurrency lock/list/remove 操作全部改成 `Gateway` 方法,`service/common.SubmitTaskWithDB(...)`、`service/consumer` 调度与取消链路、`module/systemmetric` 排队任务查询都已改走显式 Redis gateway。紧接着又把 `infra/redis` / `infra/etcd` / `infra/buildkit` / `infra/helm` / `infra/k8s` 里已经没有调用方的 `GetGateway()` 单例 fallback 全部删除,主线现在只剩少量 lifecycle/startup 组织层 wrapper 需要再压。当前 `src/service/consumer` / `src/middleware` 里残余重点已从“全局 gateway fallback / 全局 default service”收缩到更少的流程组织 helper 与 initialization 邻近收尾。 - [x] 删除旧包级 repository wrapper - 已移除 `repository/task.go` 中 Redis 队列职责与 `repository/token.go` 黑名单兼容层。 - [x] 删除全局 default service @@ -582,3 +583,110 @@ Task: 1. 如需复验真实集群,执行 `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` 2. 常规回归继续跑 `cd src && go test ./...` 3. 如需继续推进,优先进入第 17 节 SDK 标记治理;其余已基本属于文档/命名/测试抛光 + +## 21. 微服务拆分主线 + +目标:在当前 Fx + module + infra 主线已收口的基础上,把运行时进一步演进为“外部 HTTP、内部 gRPC、执行异步队列”的明确微服务架构,而不是继续在单体模式下扩张。 + +- [x] 微服务设计/治理主文档已收口到 `docs/report-index.md` +- [x] 在 `src/app/` 建立第一批服务边界分组:`gateway / runtime / iam / resource / orchestrator / system` +- [x] 新增第一批可运行服务入口:`src/cmd/api-gateway`、`src/cmd/runtime-worker-service`、`src/cmd/iam-service` +- [x] Runtime Worker Service:补 `runtime.proto` 与 gRPC control-plane(`Ping / GetRuntimeStatus / GetQueueStatus / GetLimiterStatus`) +- [x] IAM Service:补 `iam.proto` 与 token verify / permission check / access-key exchange gRPC +- [x] Orchestrator Service:补 `orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service` +- [x] Orchestrator Service:首批 submit / cancel RPC 已收口(`Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask`) +- [x] Gateway -> Orchestrator:execution / injection submit 主路径已支持通过 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 切到内部 gRPC +- [x] Orchestrator Service:workflow state / task query / dead-letter / retry 首批控制面已收口 + - 当前已新增 `GetTask / ListTasks / GetTrace / ListTraces / ListDeadLetterTasks / RetryTask` 六个内部 RPC,并继续保留 Redis 作为执行异步主通道不变。 +- [x] Orchestrator Service:execution owner 的 runtime/evaluation mutation/query facade 已落地 + - 当前已新增 `CreateExecution / CreateInjection / UpdateExecutionState / UpdateInjectionState / UpdateInjectionTimestamps / GetExecution / ListEvaluationExecutionsByDatapack / ListEvaluationExecutionsByDataset` 八个内部 RPC;runtime 状态推进与 evaluation 执行结果查询在配置 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 后已优先走 owner facade。 +- [x] Runtime Worker:已切掉对 orchestrator owner 的共享 repository 直读/直写 + - 当前 `src/service/consumer/*` 已不再直接 import `src/repository/*`;执行创建、fault injection 创建、K8s 回调状态推进、结果收集在未配置 orchestrator gRPC 时也只回退到本地 `executionmodule.Service` / `injectionmodule.Service` owner 实现。 +- [x] `service/common`:首批 container / label 共享 helper 已回收到 owner 模块 + - 当前 `src/service/common/container.go` 与 `src/service/common/label.go` 已删除;container version/parameter 解析与 label upsert 已分别收回 `src/module/container/*`、`src/module/label/*`,`module/{execution,injection,evaluation,project,container,dataset}` 与 consumer K8s 回调已改走 owner 模块实现。 +- [x] `service/common`:dataset version 共享 helper 已回收到 owner 模块 + - 当前 `src/service/common/dataset.go` 已删除;dataset version 解析已收回 `src/module/dataset/resolve.go`,同时 `src/module/dataset/api_types.go` 也已去掉对 `module/injection` 的响应类型依赖,避免再次形成模块循环。 +- [x] Resource / System Service:已补独立启动入口 `src/cmd/resource-service`、`src/cmd/system-service` +- [x] Resource Service:首批资源/评估查询 gRPC 已落地(`Ping / ListProjects / GetProject / ListContainers / GetContainer / ListDatasets / GetDataset / ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation`) +- [x] System Service:首批系统运维 gRPC 已落地(`Ping / GetHealth / GetMetrics / GetSystemInfo / ListConfigs / GetConfig / ListAuditLogs / GetAuditLog / ListNamespaceLocks / ListQueuedTasks / GetSystemMetrics / GetSystemMetricsHistory`) +- [x] Runtime -> System:namespace locks / queued tasks 首批运行态查询已从 Redis 直读收口到 runtime gRPC + - 当前 `src/proto/runtime/v1/runtime.proto` 已新增 `GetNamespaceLocks / GetQueuedTasks`,`src/module/system/service.go` 在配置 `clients.runtime.target` 或 `runtime_worker.grpc.target` 后会优先走 `src/internalclient/runtimeclient/*`,未配置时保留本地回退。 +- [x] Gateway -> Resource:project / container / dataset / evaluation 主路径已支持通过 `clients.resource.target` 或 `resource.grpc.target` 切到内部 gRPC +- [x] Gateway -> System:`system` / `systemmetric` 首批读路径已支持通过 `clients.system.target` 或 `system.grpc.target` 切到内部 gRPC +- [x] `app.CommonOptions()` 已开始按服务边界拆细 + - 当前已落地 `BaseOptions / ObserveOptions / DataOptions / CoordinationOptions / BuildInfraOptions`,`iam/resource/orchestrator/system` 已切到更窄装配口径。 +- [x] 第一轮跨 owner 共享 repository / DB 直查补扫已完成 + - 当前 `src/app` / `src/interface` / `src/internalclient` 侧已无直查 DB;残余主要收敛在 owner 模块内部 repository 和少量本地 fallback 继续压缩。 +- [x] Resource / System Service:资源元数据与运维控制面首轮独立服务边界已落地 + - 当前 `resource-service` 已承接 project / container / dataset / evaluation / label / chaos-system 资源元数据主路径,`system-service` 已承接 health / config / audit / monitor / systemmetric 运维控制面主路径;剩余更细粒度拆分进入后续非阻塞治理阶段,不再阻塞当前主线收口。 + +说明: + +- 当前第一轮“文档 + 骨架 + 可运行入口 + 核心 RPC 主路径”已完成;后续如继续演进,重点转向 ownership 深清与发布治理,而不是主线入口缺失。 +- 当前 `api-gateway` 语义上对应既有 producer HTTP 栈,`runtime-worker-service` 语义上对应既有 consumer 栈;其余服务的核心内部 RPC 与独立启动入口已落地,后续再按边界细化 owner 职责。 +- 本轮已落地 `src/proto/runtime/v1/runtime.proto`、`src/interface/grpcruntime/*` 与 queue/limiter/runtime snapshot 聚合能力,并把 gRPC lifecycle 接入 `ConsumerOptions` / `BothOptions`;默认监听 `:9094`,可通过 `runtime_worker.grpc.addr` 覆盖。 +- 本轮继续扩展 `runtime-worker-service` control-plane:当前额外提供 `GetNamespaceLocks / GetQueuedTasks`,用于承接 runtime Redis 运行态对内查询。 +- 本轮继续落地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/*` 与 `src/cmd/iam-service`,当前 IAM 内部 RPC 已覆盖鉴权、access key、team、user、rbac 五组主路径:除 `VerifyToken / CheckPermission / ExchangeAccessKeyToken` 与 team membership 判定外,也已补齐 `Login / Register / RefreshToken / Logout / ChangePassword / GetProfile / access key CRUD`、`Create/Get/List/Update/Delete user`、user role/permission/resource 绑定、`Create/Get/List/Update/Delete role`、role-permission 绑定以及 permission/resource 查询;默认监听 `:9091`,可通过 `iam.grpc.addr` 覆盖。 +- 本轮继续补上 `src/proto/orchestrator/v1/orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service`,当前 Orchestrator 内部 RPC 已提供 `Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask` 五个入口;默认监听 `:9092`,可通过 `orchestrator.grpc.addr` 覆盖。 +- 本轮继续扩展 `orchestrator-service` 控制面:当前额外已提供 `GetTask / ListTasks / GetTrace / ListTraces / ListDeadLetterTasks / RetryTask` 六个入口,用于 workflow state 查询、dead-letter 补偿与手动 retry;同时执行/消费异步仍保持 Redis queue/event 主链不变。 +- 本轮继续扩展 `orchestrator-service` owner facade:当前又额外提供 `CreateExecution / CreateInjection / UpdateExecutionState / UpdateInjectionState / UpdateInjectionTimestamps / GetExecution / ListEvaluationExecutionsByDatapack / ListEvaluationExecutionsByDataset` 八个入口,分别承接 runtime 状态回写与 evaluation 执行结果查询。 +- Gateway 侧已补 `src/internalclient/orchestratorclient/*`,并通过 `src/app/gateway/options.go` 把 `execution` / `injection` handler 使用的 submit 服务装饰为 remote-aware;配置 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 后,`SubmitAlgorithmExecution / SubmitFaultInjection / SubmitDatapackBuilding` 会优先走内部 gRPC,未配置时继续回退本地实现。 +- `src/app/consumer.go` 当前也已补 execution/injection owner 模块,使 `runtime-worker-service` 在未配置 orchestrator gRPC 时改为回退到本地 owner service,而不再直接碰共享 repository;`interface/worker` / `interface/controller` 会把这两个 owner service 显式注入 consumer runtime deps 与 K8s handler。 +- 同时已把 `src/cmd/resource-service` 与 `src/cmd/system-service` 补齐,后续可以直接在对应服务边界上继续补 `resource.proto` / `system.proto` 和对内 gRPC。 +- 本轮继续补上 `src/proto/resource/v1/resource.proto`、`src/interface/grpcresource/*` 与 `src/app/resource/options.go` 接线,当前 Resource 内部 RPC 已提供 `Ping / ListProjects / GetProject / ListContainers / GetContainer / ListDatasets / GetDataset / ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation` 十二个入口;默认监听 `:9093`,可通过 `resource.grpc.addr` 覆盖。 +- 本轮继续补上 `src/proto/system/v1/system.proto`、`src/interface/grpcsystem/*` 与 `src/app/system/options.go` 接线,当前 System 内部 RPC 已提供 `Ping / GetHealth / GetMetrics / GetSystemInfo / ListConfigs / GetConfig / ListAuditLogs / GetAuditLog / ListNamespaceLocks / ListQueuedTasks / GetSystemMetrics / GetSystemMetricsHistory` 十二个入口;默认监听 `:9095`,可通过 `system.grpc.addr` 覆盖。 +- `src/app/system/options.go` 现已补齐 `k8sinfra.Module` 与 `runtimeclient.Module`,`module/system.Service` 对 `ListNamespaceLocks / ListQueuedTasks` 已优先走 runtime gRPC,把 system/runtime 的首批运行态交互从直接 Redis 读取改成内部 client 边界。 +- Gateway 侧已继续补 `src/internalclient/resourceclient/*`,并通过 `src/app/gateway/options.go` 把 `project` / `container` / `dataset` handler 使用的稳定 list/detail 读服务装饰为 remote-aware;配置 `clients.resource.target` 或 `resource.grpc.target` 后,`ListProjects / GetProjectDetail / ListContainers / GetContainer / ListDatasets / GetDataset` 会优先走内部 gRPC,未配置时继续回退本地实现。 +- Gateway 侧现已补 `src/internalclient/systemclient/*`,并通过 `src/app/gateway/options.go` 把 `system` / `systemmetric` handler 使用的查询服务装饰为 remote-aware;配置 `clients.system.target` 或 `system.grpc.target` 后,`/system/*` 与 `/api/v2/system/metrics*` 会优先走内部 gRPC,未配置时继续回退本地实现。 +- 这一轮继续把 dedicated service 入口往 remote-first 收紧:`src/app/gateway/options.go` 现在会在启动时显式校验 `iam / orchestrator / resource / system` 四类 internal client target,`src/app/runtime/options.go` 会校验 orchestrator target,`src/app/system/options.go` 会校验 runtime target,避免 `api-gateway` / `runtime-worker-service` / `system-service` 这类独立服务入口继续静默回退本地 owner 实现。 +- 这一轮又继续把 standalone runtime 边界再压一层:`src/app/runtime/options.go` 已不再直接复用整套 `ConsumerOptions()`,而是去掉 `executionmodule.Module` / `injectionmodule.Module`,`src/interface/worker/module.go` 与 `src/interface/controller/module.go` 中对应 owner service 依赖已改成可选,避免 `runtime-worker-service` 独立入口继续显式装配本地 execution/injection owner。 +- 这一轮再把独立服务入口的验收补到位:新增 `src/app/service_entrypoints_test.go`,已覆盖 `api-gateway` 的真实 HTTP 冒烟,以及 `runtime-worker-service / resource-service / system-service` 的真实 gRPC 冒烟;`api-gateway` / `runtime-worker-service` 的独立启动与 runtime control-plane 可用性现在都有自动化保护。 +- 这一轮继续把“启动命令 / 配置 / 本地编排”说明收口到 `docs/report-index.md`,并新增 `docker-compose.microservices.yaml` 作为与现有 `docker-compose.yaml` 叠加的多服务本地 compose 骨架;`src/config.dev.toml` 与 `config.dev.toml` 也已补齐 `clients.*.target` 及 `iam/resource/orchestrator/runtime_worker/system` 的 gRPC 默认端口,方便直接按拆分模式起服务。 +- 这一轮继续把“镜像入口 / probe 规范 / K8s skeleton”说明也并入 `docs/report-index.md`:`src/main.go` 已增加 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` 六个新子命令,现有镜像可直接用同一二进制起拆分服务;同时 `manifests/microservices/aegislab-microservices.yaml` 已统一 gateway 的 HTTP `/system/health` probe 与五个 gRPC 服务的 health probe,并补上第一版多服务 Deployment/Service 骨架。 +- 最终仓库级收尾轮已把补扫与收尾结论并入 `docs/report-index.md`:`src` 生产代码里 `service/producer` / `handlers/system` / `database.DB` / `GetGateway()` / `redisinfra.GetGateway()` 这批旧兼容模式已为零命中,`context.Background()` 残留也只在测试中;当前主线可视为完成,剩余主要转入兼容入口 owner 组合继续压缩与少量跨服务 DB 深清。 +- 同一轮里也已把错误码、request-id、观测标签、internal proto、配置命名和 owner 约束统一写实,并收口到 `docs/report-index.md`;治理项已不再是“规范空缺”,当前更多是发布执行层持续收口。 +- 继续执行层收口后,HTTP/gRPC 的 request-id 主路径也已正式落地:`src/router/router.go` 现已统一挂 `X-Request-Id` middleware,`src/internalclient/*` 已统一透传 `x-request-id` metadata,治理规范不再只停留在文档。 +- 同一批收口里,`src/interface/grpc*` 也已统一在 server 入口提取/补齐 request-id;另外 `module/dataset` / `module/injection` 对旧共享 `repository.NewSearchQueryBuilder` 的依赖已清掉,通用搜索装配收到了独立 `src/searchx`。 +- 这一轮又继续把 dedicated `api-gateway` 入口的语义收紧:`src/app/gateway/*` 中经 `iam/resource/orchestrator/system` 的 remote-aware wrapper 已不再静默回退本地 owner service;同时 `src/repository/scope.go` 也已删除,旧共享排序 helper 不再继续扩大。 +- 同一轮里,`src/interface/worker` / `src/interface/controller` 也不再直接依赖 `executionmodule.Service` / `injectionmodule.Service`;runtime 执行 owner 已统一改由 `consumer.ExecutionOwner` / `consumer.InjectionOwner` 注入,owner fallback 面进一步收到了 `src/service/consumer/owner_adapter.go` 单点。 +- 继续收口后,dedicated `api-gateway` / `runtime-worker-service` / `system-service` 这几条入口已基本形成明确的 remote-required 语义;当前残余 local adapter 主要服务于 `producer` / `consumer` / `both` 兼容入口,而不是新的 dedicated service 主路径。 +- 再往下一轮,`resource-service` / `system-service` / `runtime-worker-service` 也已分别通过 `evaluationmodule.RemoteQueryOption()` / `systemmodule.RemoteRuntimeQueryOption()` / `consumer.RemoteOwnerOptions()` 把 dedicated service 路径上的查询/owner 适配器收成 remote-only,进一步减少“同一服务里同时挂本地和远端两套语义”的过渡态。 +- 这一轮继续沿跨服务 DB/owner 深清推进 `team -> project` 这条线:`src/module/team/project_reader.go` 新增 remote-aware project reader,team detail 的 project count 与 team project list 在配置 `clients.resource.target` 或 `resource.grpc.target` 后会优先经 `resource-service` 获取,`iam-service` 也已显式要求 `resource-service` target;对应地 `src/module/project.ListProjectReq` / `src/module/project/service.go` / `src/module/project/repository.go` 已补 `team_id` 与 `include_statistics`,使 team 侧远程 count 可直接复用 `ListProjects` 且可跳过 project statistics 聚合,先把 IAM/gateway 对 `projects`、`fault_injections`、`executions` 的这条直查面压掉一层。 +- 这一轮继续把 `resource-service` 里的 project statistics 主路径也收进 owner facade:新增 `src/module/project/project_statistics.go`,project service 不再直接在资源侧 repo 中拼 execution/injection 统计,而是统一经 `projectStatisticsSource` 获取;`src/app/resource/options.go` 已用 `projectmodule.RemoteStatisticsOption()` 把 dedicated resource 路径强制到 orchestrator RPC。对应地 `src/proto/orchestrator/v1/orchestrator.proto`、`src/interface/grpcorchestrator/*`、`src/internalclient/orchestratorclient/client.go` 已补 `ListProjectStatistics` 内部 RPC,resource 主路径上的 project detail/list statistics 不再直查 owner 表。 +- 这一轮又继续把兼容入口装配层压实到单点:新增 `src/app/compat_options.go`,把 producer 侧 HTTP/K8s/chaos 与 producer init/http server 装配收成 `ProducerCompatibilityOptions / ProducerHTTPEntryOptions`,把 consumer/both 共享的本地 owner runtime 组合继续收成 `CompatibilityRuntimeOptions()`;`src/app/producer.go`、`consumer.go`、`both.go`、`gateway/options.go` 现在不再各自重复拼 `Base/Observe/Data/Coordination/Build + modules + init + http`,同时已删掉 `NormalizeAddr(...)` 与 gateway 专用 `NewProducerInitializerForGateway / RegisterProducerInitializationForGateway` 这类多余壳函数。 +- 这一轮继续把 dedicated `api-gateway` 的 metrics 边界收紧:`src/module/metric` 已补 `HandlerService`,`src/app/gateway/metric_services.go` 新增 remote-aware metrics wrapper,gateway 上的 `/api/v2/metrics/injections|executions|algorithms` 不再直接落本地 `fault_injections / executions / containers` 表;其中 injection/execution metrics 已走新增的 orchestrator RPC `GetInjectionMetrics / GetExecutionMetrics`,algorithm metrics 则由 gateway 经 `resource-service` 拉 algorithm 列表后再按算法向 orchestrator 聚合执行指标,先把 dedicated gateway 这块跨 owner 直查面收掉。 +- 这一轮继续把 dedicated `api-gateway` 的 team 主路径切到 IAM:`src/module/team` 已补 `HandlerService`,`src/app/gateway/team_services.go` 新增 remote-aware team wrapper,gateway 上的 `/api/v2/teams/*` 现在统一经 `iamclient` 转发 `Create/Get/List/Update/Delete`、member 管理、team project/member 列表,而不再直接吃本地 team owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 已补齐 team RPC 面。同时 `src/module/team/project_reader.go` 又新增 `RemoteProjectReaderOption()`,`src/app/iam/options.go` 已把 dedicated `iam-service` 上的 team->project 视图继续收成 resource RPC-only。 +- 这一轮再把 dedicated `api-gateway` 的 IAM 剩余主路径继续收口:`src/module/{auth,user,rbac}` 已补 `HandlerService`,`src/app/gateway/{auth,user,rbac}_services.go` 新增 remote-aware wrapper,gateway 上的 `/api/v2/auth/*`、`/api/v2/access-keys/*`、`/api/v2/users/*`、`/api/v2/roles|permissions|resources/*` 已统一经 `iamclient` 转发,不再在 dedicated `api-gateway` 入口直接吃本地 IAM owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 也已补齐 auth/user/rbac RPC 面。 +- `src/app/app.go` 已开始按服务边界拆装配层:当前新增 `BaseOptions / ObserveOptions / DataOptions / CoordinationOptions / BuildInfraOptions`,独立服务启动链不再统一吃满所有 infra。 +- `src/app/resource/options.go` 这一轮继续把 standalone 边界推进到 `project / label / container / dataset / evaluation`;`resource-service` 已接入 `orchestratorclient.Module` 承接 evaluation -> orchestrator 的远程查询,gateway 对 `evaluation` handler 也已补上 remote-aware 装饰。 +- 这一轮继续把 dedicated `api-gateway` 的 label 主路径切到 Resource:`src/module/label` 已补 `HandlerService`,`src/proto/resource/v1/resource.proto` / `src/interface/grpcresource/service.go` / `src/internalclient/resourceclient/client.go` 已补齐 `Create/Get/List/Update/Delete/BatchDelete label` 对内 RPC;同时 `src/app/gateway/resource_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/labels/*` 改成统一经 `resource-service` 转发,dedicated gateway 不再直接承载 label owner 读写。 +- 这一轮继续把 dedicated `api-gateway` 的 admin systems 主路径切到 Resource:`src/module/chaossystem` 已补 `HandlerService`,`resource-service` 现已纳入 `chaossystemmodule.Module`,并通过 `src/proto/resource/v1/resource.proto` / `src/interface/grpcresource/service.go` / `src/internalclient/resourceclient/client.go` 承接 `List/Get/Create/Update/Delete chaos system` 与 `metadata upsert/list`;同时 `src/app/gateway/resource_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/systems/*` 改成统一经 `resource-service` 转发,继续缩小 dedicated gateway 上的本地 resource owner 面。 +- 这一轮继续把 dedicated `api-gateway` 的 task / trace 查询主路径切到 Orchestrator:`src/module/{task,trace}` 已补 `HandlerService`,`src/internalclient/orchestratorclient/client.go` 已补 `GetTask / ListTasks / GetTrace / ListTraces`,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/tasks/{id}`、`/api/v2/tasks`、`/api/v2/traces/{id}`、`/api/v2/traces` 改成统一经 `orchestrator-service` 转发;日志 WebSocket 与 trace SSE 仍保留本地实现,留待后续流式通道单独收口。 +- 这一轮继续把 dedicated `api-gateway` 的 group stats 查询主路径切到 Orchestrator:`src/module/group` 已补 `HandlerService`,`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已补 `GetGroupStats` 内部 RPC,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/groups/{group_id}/stats` 改成统一经 `orchestrator-service` 转发;group SSE stream 仍保留本地实现,留待后续流式通道单独收口。 +- 这一轮继续把 dedicated `api-gateway` 的 SSE 主路径切到 Orchestrator:`src/module/notification` 已补 `HandlerService`,`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已新增 `GetTraceStreamState / ReadTraceStreamMessages / GetGroupStreamState / ReadGroupStreamMessages / ReadNotificationStreamMessages` 五个内部 RPC,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/traces/{trace_id}/stream`、`/api/v2/groups/{group_id}/stream`、`/api/v2/notifications/stream` 改成统一经 `orchestrator-service` 读取流式批次;当前 dedicated gateway 残余主线只剩 task logs WebSocket 尚未收成内部通道。 +- 这一轮继续把 dedicated `api-gateway` 的 task logs WebSocket 也切到 Orchestrator:`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已新增 `PollTaskLogs` 内部 RPC,`src/module/task/service.go` 新增基于 Loki 的 owner-side log poll facade,`src/app/gateway/orchestrator_services.go` 则把 `/api/v2/tasks/{task_id}/logs/ws` 改成由 gateway 继续负责边缘 WebSocket、但日志历史/轮询数据统一经 `orchestrator-service` 获取;dedicated gateway 主线上的 task/trace/group/notification 读写 owner 残余面已基本清空。 +- 这一轮再把 `module/evaluation` 的查询源约束收紧一层:`src/module/evaluation/service.go` 里 `Execution` 依赖已改成可选,若既没有 orchestrator client、也没有本地 execution owner,会直接显式报错;同时补了 `src/module/evaluation/service_test.go` 锁住这条行为。 +- 这一轮又继续把 gateway -> system 的旧监控接口 fallback 收紧一层:`src/module/system/handler_service.go` / `src/module/system/handler.go` / `src/app/gateway/system_services.go` / `src/interface/grpcsystem/service.go` 里的 `GetMetrics / GetSystemInfo` 已统一改成返回 `(..., error)`;配置 `systemclient` 后不再在 remote 调用失败时静默吞掉错误并回退本地结果。 +- 这一轮继续把 runtime 的 owner fallback 收口成单点适配器:新增 `src/service/consumer/owner_adapter.go`,`collect_result / fault_injection / algo_execution / state_store / k8s_handler` 不再各自散落判断 `orchestratorclient` 与本地 owner,而是统一经 `ExecutionOwner / InjectionOwner` 做 remote-first 路由;`src/interface/{worker,controller}/module.go` 也改为只在装配层创建这两个 owner 适配器,把 fallback 面进一步压缩到 consumer 单点。 +- 这一轮再把 gateway 请求上下文继续贯穿一层:`src/app/gateway/middleware_service.go` 不再用 `context.Background()` 调 IAM client,`middleware.Service` 的 permission helper 已统一改成显式接收 `context.Context`;同时 `src/module/system/*` / `src/interface/grpcsystem/*` 也把 `GetMetrics / GetSystemInfo / GetAuditLog / ListAuditLogs / GetConfig / ListConfigs` 这批读接口改成透传请求上下文,旧 system remote-aware wrapper 不再自己造背景上下文。 +- 这一轮也顺手把 evaluation -> orchestrator 的本地/远程路由收成单点:新增 `src/module/evaluation/execution_query.go`,`module/evaluation.Service` 不再自己持有 `orchestratorclient + execution service` 两套判断,而是统一走 `executionQuerySource` 适配器。 +- 这一轮继续把 evaluation 主路径真正并进 `resource-service`:`src/interface/grpcresource/service.go`、`src/internalclient/resourceclient/client.go`、`src/app/gateway/resource_services.go` 已补齐 `ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation`,gateway 在配置 `clients.resource.target` 或 `resource.grpc.target` 后会优先走 resource gRPC。 +- 这一轮继续把 startup 壳和 system runtime fallback 再压一层:新增 `src/app/runtime_stack.go` 把 runtime worker 的 infra/provider/interface 装配统一抽成共享 stack,`src/app/consumer.go` / `src/app/both.go` / `src/app/runtime/options.go` 不再各自重复拼同一套启动树;同时新增 `src/module/system/runtime_query.go`,`module/system.Service` 对 runtime client / 本地 systemmetric 的切换也已收成单点 `runtimeQuerySource`。 +- 本轮继续把 `service/common` 里的 container/label 共享 helper 回收到 owner 模块:`src/module/container/resolve.go` 与 `src/module/label/core.go` 已承接这批逻辑,`module/{execution,injection,evaluation,project,container,dataset}` 主路径不再经由 `service/common` 读 container 参数/版本或创建 labels。 +- 本轮继续把 `service/common` 里的 dataset version helper 也收回 owner 模块:`src/service/common/dataset.go` 已删除,`src/module/dataset/resolve.go` 负责 dataset version 解析,`src/module/dataset/api_types.go` 同时去掉了对 `module/injection` 的响应耦合。 +- 本轮继续把 `service/common/datapack_resolver.go` 也收回 owner 模块:当前 `src/service/common/datapack_resolver.go` 已删除,datapack 本身与 dataset->datapack 解析已迁入 `src/module/injection/resolve.go`,`module/execution` / `module/injection` 提交主路径不再经由 `service/common`。 +- 这一轮再按“模块 repo 写实、少留裸导出 helper”的口径继续内聚了一批仓储逻辑:`src/module/container/{core,resolve}.go`、`src/module/dataset/{core,resolve}.go`、`src/module/injection/resolve.go`、`src/module/label/core.go` 已改成以 `Repository` 方法为主;`service/initialization`、`module/{execution,evaluation,injection,project,container,dataset}`、`service/consumer/k8s_handler.go` 这批调用点已不再直连 `Create*Core` / `MapRefs*WithDB` / `ExtractDatapacksWithDB` / `CreateOrUpdateLabelsFromItems` 之类裸函数,而是显式走各自模块 repo。 +- 这一轮继续按同一口径压缩 repo/API 面并深清 interface 残余查询:`src/interface/grpcorchestrator/project_statistics.go` 已不再自己持有 Gorm 聚合 SQL,而是改为复用 `src/module/project.Repository.ListProjectStatistics(...)`;`src/module/team/repository.go` 里重复的 `listTeamProjectStatistics(...)` 也已删除,team 本地 project list statistics 改为复用 project 模块仓储。顺手又把 `src/module/{user,team,rbac}/repository.go` 里一批仅供各自 service 使用的 CRUD / assign / remove / batch helper 收成包内私有方法,继续减少模块 repo 对外暴露面。 +- 这一轮再顺着主线补了两处收口:`src/app/compat_options.go` 现已把 consumer 兼容入口里的本地 execution/injection owner 组合显式收成 `CompatibilityOwnerFallbackOptions()` 单点,不再散着写在兼容 runtime 入口里;同时 `src/module/project.Repository.ListProjectStatistics(...)` 已补上对 `fault_injections / executions` 的 `status != deleted` 过滤,和之前 orchestrator interface 的 owner 统计语义重新对齐。`src/module/team` 的 team detail 读取也顺手收成 `loadTeamDetailBase(...)`,避免继续在本地 detail helper 里混入最终由 remote reader 接管的 project count 语义。 +- 这一轮又继续把 repo 暴露面按“没用到就删”收了一层:`src/module/{project,team,user,rbac}/repository.go` 的 `Transaction(...)` 已统一收成包内 `transaction(...)`;顺手补扫了当前 `src/module/*/repository.go`,删除了已无任何调用的 `src/module/execution/repository.go:268` `loadExecutionLabelIDsByItems(...)`。同时 `src/app/http_modules.go` / `src/app/compat_options.go` / `src/app/orchestrator/options.go` 现在通过 `ExecutionInjectionOwnerModules()` 复用 execution/injection owner 组合,compat/orchestrator 邻近不再各自散写相同模块列表。 +- 这一轮再继续按“整个文件没价值就直接删”的口径清理:由于 `src/repository/*.go` 这批旧共享 repository 文件已无任何业务 import 或有效调用(剩余 `repository.DownloadIndexFile()` 仅为 Helm 官方 `repo` 包别名,不是本项目包),当前已整体删除 `src/repository/{container,dataset,detector,execution,granularity,injection,label}.go`。顺手又补扫了 `src/app` / `src/interface` 里的 `Table/Joins/Raw`,目前已无新的“非 owner 层自己拼 DB 查询”残点,残余数据访问基本都收敛在各自 owner 模块 repo 或 runtime owner 内。 +- 这一轮继续顺着你要的两条线往下压:compat 侧新增 `src/app/compat_options.go` 的 `BothCompatibilityOptions(...)`,`src/app/both.go` 不再自己散拼 runtime+HTTP 组合;project/team/user/rbac 这批模块 repo 里又删/折了一批只服务单一路径的内部 helper——例如 `module/project` 把 project label 管理与 label reload、project user count、project list label 装配继续内联回主语义方法,`module/team` 把 team user count、visible team id 查询、team project count / role existence 这批单点 helper 收回主路径或 local reader,`module/user` 把 `ensureUserUnique(...)` 折回 `createUserIfUnique(...)`,`module/rbac` 也继续收掉了旧的 `loadAssignablePermissions(...)` 壳。当前 `project/team/user/rbac` 剩余 repo 方法已基本都对应明确单一 service 语义,不再是“公共但没边界价值”的散 helper。 +- 本轮补扫结果表明,当前最高优先级主线残余已进一步收敛到各服务残余 local fallback 的继续压缩。 +- 同时已继续把 HTTP 鉴权链往 IAM client 收深一轮:`src/middleware/auth.go` 不再直接依赖 `utils.ValidateToken`,而是通过 `middleware.TokenVerifier` 接口走注入实现;当前默认仍由 `authmodule.Service` 提供,本地功能不变。并且 `src/internalclient/iamclient/*` 已继续补齐 team/project 的 member/admin/public 判定 RPC,`src/app/gateway/options.go` 在配置 `clients.iam.target` 或 `iam.grpc.target` 时,已可优先切到 IAM gRPC 做 token verify、`CheckUserPermission(...)` 与 team/project 权限辅助判断。 +- 这一轮继续按“模块内直接写实、删除空包装”的口径再压一层 repo API 面:`src/module/{project,team,user,rbac,execution,injection}/repository.go` 中原先只做 `db.Transaction(...)` / `&Repository{db: tx}` 的 `transaction/Transaction/withDB` 空包装已全部删除,service 组合点统一直接走 `repo.db.Transaction(...)` + `NewRepository(tx)`;同时 `src/module/injection/repository.go` 中只在模块内部使用的一整批方法也已收成包内私有命名,例如 `loadInjection(...)`、`findInjectionByName(...)`、`createInjectionRecord(...)`、`deleteInjectionsCascade(...)`、`listInjectionsView(...)` 等,进一步减少模块仓储对外暴露面并把 compat/local owner 主路径收得更实。 +- 紧接着又把同一口径补到 `src/module/execution/repository.go`:`AddExecutionLabels / ClearExecutionLabels / BatchDecreaseLabelUsages / ListExecutionIDsByLabelItems / BatchDeleteExecutions / UpdateExecutionDuration / LoadExecution / CreateExecutionRecord / UpdateExecutionFields / SaveDetectorResults / SaveGranularityResults` 这批仅供模块内 service/runtime owner 使用的方法已全部私有化,`module/execution/service.go` 也同步改成只走模块内语义方法,进一步减少 execution repo 的公开 API 面。 +- 这一轮继续把同样的收口扩到 `src/module/{container,dataset,system}`:三处 repo 的 `Transaction(...) / withDB(...)` 空包装都已删除,service 组合点统一改成 `repo.db.Transaction(...) + NewRepository(tx)`;同时 `module/container` / `module/dataset` 中大批仅供模块内部使用的 CRUD、label、version、Helm/Datapack 关系方法已收成包内私有实现,`module/system` 中的 `getAuditLogByID(...)`、`getConfigByID(...)`、`getConfigHistory(...)`、`updateConfig(...)`、`createConfigHistory(...)`、`listConfigHistoriesByConfigID(...)` 也已一并私有化,进一步压缩 repo API 面。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/container ./module/dataset ./module/system ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./service/initialization -run '^$'`。 +- 紧接着又继续削了一轮 repo 暴露面和 compat 壳:`src/module/container/repository.go` 的 `ListContainers / ListContainerVersions`、`src/module/dataset/repository.go` 的 `ListDatasets / SearchDatasets / ListDatasetVersions`、`src/module/system/repository.go` 的 `ListAuditLogs / ListConfigs / ListConfigHistories` 已全部收成包内私有实现,当前这三块对外只剩真正有跨模块边界价值的方法(例如 dataset 的 `ListInjectionsByDatasetVersionID(...)`);同时 `src/app/compat_options.go` 里的 `ConsumerRuntimeOptions()` 空转发已删除,`src/app/consumer.go` 直接走 `CompatibilityRuntimeOptions()`,compat 启动壳再薄一层。对应编译检查已再次通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/container ./module/dataset ./module/system ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./service/initialization -run '^$'`。 +- 这一轮继续把 `app/*/options.go` 这批启动壳里的纯组合 helper 删了一层:`src/app/{iam,resource,system,orchestrator}/options.go` 中仅被各自 `Options(...)` 调用一次的 `Modules()` 已全部内联删除,`src/app/gateway/options.go` 里无调用价值的 `Modules()` 也已直接删除;同时 `src/app/compat_options.go` 内部仅被单点使用的 `ProducerInitializationOptions()`、`HTTPServerOptions()`、`CompatibilityOwnerFallbackOptions()` 也已折回主入口。当前启动链保留的 helper 主要只剩确实复用的 `CommonOptions(...)`、`ProducerHTTPModules()`、`RuntimeWorkerStackOptions()`、`ExecutionInjectionOwnerModules()` 这类有明确边界价值的组合。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./module/container ./module/dataset ./module/system ./service/initialization -run '^$'`。 +- 这一轮继续把“remote + local fallback” 双态适配器再压一层:`src/service/consumer/owner_adapter.go` 已把 execution/injection 的 local fallback 与 remote-only 两套 adapter 合并成统一结构,通过 `requireRemote` 控制 dedicated runtime-worker 是否允许回落本地 owner;`src/module/team/project_reader.go` 同样把 local / remote fallback / remote-only 三套 reader 合并成单一 `projectReaderAdapter`;并顺手把同类模式的 `src/module/project/project_statistics.go`、`src/module/evaluation/execution_query.go`、`src/module/system/runtime_query.go` 也统一成单 adapter + `requireRemote` 形态,减少重复实现与过渡态暴露面。仓库级补扫结果显示,当前生产代码里这类双 adapter 模式已基本清空;残余 `Transaction/withDB` 包装主要集中在 `module/auth` / `module/label` 这类还未进入本轮主线的模块。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./service/consumer ./module/team ./module/project ./module/evaluation ./module/system ./app ./app/runtime ./app/iam ./app/resource ./app/system ./app/gateway -run '^$'`。 +- 这一轮把前面补扫里最后两块明显残余也收掉了:`src/module/auth/repository.go` 的 `UserRepository/RoleRepository withDB(...) + Transaction(...)` 空包装已删除,`src/module/auth/service.go` 改成直接使用 `userRepo.db.Transaction(...)` + `NewUserRepository(tx)` / `NewRoleRepository(tx)`;`src/module/label/repository.go` 的 `Transaction(...)` 包装也已删除,`src/module/label/service.go` 同步切成 `repo.db.Transaction(...)`。复扫结果显示,当前 `src/app` / `src/module` / `src/service/consumer` 主线里已不再存在这类 repo `withDB(...)` / `Transaction(...)` compat 壳;剩余 `withDB(...)` 命中主要只在 consumer task-state builder 这种内部 fluent helper 上,不再是 repository 兼容层。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/auth ./module/label ./app ./app/gateway ./app/iam ./service/consumer ./module/team ./module/project ./module/evaluation ./module/system -run '^$'`。 diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 6c6d3be4..a04b9965 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -6,7 +6,7 @@ data: config.prod.toml: | name = "{{ .Values.configmap.name }}" version = "{{ .Values.configmap.version }}" - port = {{ .Values.configmap.port }} + port = {{ .Values.microservices.apiGateway.httpPort }} workspace = "{{ .Values.configmap.workspace }}" [system] @@ -36,7 +36,7 @@ data: [k8s] namespace = "{{ .Values.configmap.k8s.namespace }}" [k8s.service] - internal_url = "http://{{ .Release.Name }}-exp:8080" + internal_url = "http://{{ .Release.Name }}-api-gateway:{{ .Values.microservices.apiGateway.httpPort }}" [k8s.init_container] {{- range $key, $value := .Values.configmap.k8s.init_container }} {{ $key }} = {{ $value | quote }} @@ -66,7 +66,7 @@ data: claim_name = {{ $value.claim_name | quote }} {{- end }} {{- end }} - + [jfs] container_path = "{{ .Values.configmap.jfs.container_path }}" dataset_path = "{{ .Values.configmap.jfs.dataset_path }}" @@ -77,16 +77,46 @@ data: address = "{{ .Release.Name }}-buildkit:1234" {{- end }} + [clients.iam] + target = "{{ .Release.Name }}-iam-service:{{ .Values.microservices.iamService.grpcPort }}" + + [clients.orchestrator] + target = "{{ .Release.Name }}-orchestrator-service:{{ .Values.microservices.orchestratorService.grpcPort }}" + + [clients.resource] + target = "{{ .Release.Name }}-resource-service:{{ .Values.microservices.resourceService.grpcPort }}" + + [clients.runtime] + target = "{{ .Release.Name }}-runtime-worker-service:{{ .Values.microservices.runtimeWorkerService.grpcPort }}" + + [clients.system] + target = "{{ .Release.Name }}-system-service:{{ .Values.microservices.systemService.grpcPort }}" + + [iam.grpc] + addr = ":{{ .Values.microservices.iamService.grpcPort }}" + + [orchestrator.grpc] + addr = ":{{ .Values.microservices.orchestratorService.grpcPort }}" + + [resource.grpc] + addr = ":{{ .Values.microservices.resourceService.grpcPort }}" + + [runtime_worker.grpc] + addr = ":{{ .Values.microservices.runtimeWorkerService.grpcPort }}" + + [system.grpc] + addr = ":{{ .Values.microservices.systemService.grpcPort }}" + {{- if .Values.loki.enabled }} [loki] address = "http://{{ .Release.Name }}-loki:3100" timeout = "{{ .Values.configmap.loki.timeout }}" max_entries = {{ .Values.configmap.loki.max_entries }} + {{- end }} - [otlp.receiver] + [otlp_receiver] port = {{ .Values.configmap.otlp.port }} max_request_size = {{ .Values.configmap.otlp.max_request_size }} - {{- end }} --- apiVersion: v1 kind: ConfigMap @@ -134,4 +164,4 @@ metadata: name: {{ .Release.Name }}-system-helm-configs data: system_helm_configs.json: | -{{ include "helm.systemHelmConfigs" . | indent 4 }} \ No newline at end of file +{{ include "helm.systemHelmConfigs" . | indent 4 }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index a281fd01..83f636a0 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -1,54 +1,22 @@ -# exp Application apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Release.Name }}-producer + name: {{ .Release.Name }}-api-gateway spec: - replicas: 1 + replicas: {{ .Values.microservices.apiGateway.replicaCount }} selector: matchLabels: - app: {{ .Release.Name }}-producer + app: {{ .Release.Name }}-api-gateway template: metadata: annotations: rollout-timestamp: {{ now | quote }} labels: - app: {{ .Release.Name }}-producer + app: {{ .Release.Name }}-api-gateway spec: serviceAccountName: {{ .Release.Name }}-sa initContainers: - - name: wait-for-dependencies - image: {{ include "helm.image" (dict "imageConfig" .Values.images.busybox "global" .Values.global) }} - imagePullPolicy: "{{ .Values.images.busybox.pullPolicy }}" - command: - - sh - - -c - - | - # Parallel dependency check with timeout - TIMEOUT=120 - START=$(date +%s) - - check_jaeger() { while ! nc -z {{ .Release.Name }}-jaeger 4318; do sleep 1; done; } - check_redis() { while ! nc -z {{ .Release.Name }}-redis 6379; do sleep 1; done; } - check_mysql() { while ! nc -z {{ .Release.Name }}-mysql 3306; do sleep 1; done; } - check_etcd() { while ! nc -z {{ .Release.Name }}-etcd-headless 2379; do sleep 1; done; } - - # Start background checks - check_jaeger & PID1=$! - check_redis & PID2=$! - check_mysql & PID3=$! - check_etcd & PID4=$! - - # Wait with timeout - while kill -0 $PID1 2>/dev/null || kill -0 $PID2 2>/dev/null || kill -0 $PID3 2>/dev/null || kill -0 $PID4 2>/dev/null; do - [ $(($(date +%s) - START)) -ge $TIMEOUT ] && echo "Timeout waiting for dependencies" && exit 1 - sleep 5 - done - - # Verify all succeeded - wait $PID1 && wait $PID2 && wait $PID3 && wait $PID4 || exit 1 - echo "Dependencies ready in $(($(date +%s) - START))s" - - name: init-etcd-data + - name: init-etcd-producer-config image: pair-diag-cn-guangzhou.cr.volces.com/pair/etcdctl:latest imagePullPolicy: IfNotPresent command: @@ -59,58 +27,53 @@ spec: CONFIG_PREFIX="/rcabench/config/producer" CONFIG_YAML_PATH="/initial-config/etcd.yaml" FORCE_INIT="{{ .Values.initialConfig.force }}" - - echo "=== Etcd Initialization Started: $(date) ===" - - # Check if already initialized + INIT_VALUE=$(etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 get "$INIT_KEY" --print-value-only 2>/dev/null || echo "") if [ "$INIT_VALUE" = "true" ] && [ "$FORCE_INIT" != "true" ]; then - echo "Already initialized (found $INIT_KEY=true). Set initialConfig.force=true to reinitialize." + echo "producer config already initialized" exit 0 fi - - if [ "$FORCE_INIT" = "true" ]; then - echo "Force initialization enabled, proceeding..." - else - echo "No initialization marker found, proceeding with initialization..." - fi - - # Load initial config from YAML - echo "Loading config from $CONFIG_YAML_PATH..." + if [ -f "$CONFIG_YAML_PATH" ]; then while IFS=': ' read -r key_name value || [ -n "$key_name" ]; do - # Skip empty lines and comments [ -z "$key_name" ] && continue echo "$key_name" | grep -q '^#' && continue - - # Remove leading/trailing whitespace and quotes key_name=$(echo "$key_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') value=$(echo "$value" | sed "s/^[[:space:]]*//; s/[[:space:]]*$//; s/^[\"']//; s/[\"']$//") - - if [ -n "$key_name" ]; then - key="$CONFIG_PREFIX/$key_name" - echo "Setting: $key = $value" - etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$key" "$value" || true - fi + [ -n "$key_name" ] || continue + etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$CONFIG_PREFIX/$key_name" "$value" || true done < "$CONFIG_YAML_PATH" etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$INIT_KEY" "true" || true - echo "Config loaded successfully" - else - echo "Warning: $CONFIG_YAML_PATH not found" fi - - echo "=== Etcd Initialization Done: $(date) ===" volumeMounts: - name: etcd-initial-config mountPath: /initial-config readOnly: true containers: - - name: exp + - name: api-gateway image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" - command: ["/app/entrypoint.sh", "producer", "8080"] + args: + - api-gateway + - --conf + - /etc/rcabench/config.prod.toml + - --port + - {{ .Values.microservices.apiGateway.httpPort | quote }} ports: - - containerPort: 8080 + - containerPort: {{ .Values.microservices.apiGateway.httpPort }} + name: http + readinessProbe: + httpGet: + path: /system/health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /system/health + port: http + initialDelaySeconds: 10 + periodSeconds: 15 env: - name: GOPRIVATE value: "github.com/OperationsPAI/chaos-experiment" @@ -146,57 +109,166 @@ spec: persistentVolumeClaim: claimName: {{ .Release.Name }}-juicefs-dataset --- -# exp Application apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Release.Name }}-consumer + name: {{ .Release.Name }}-iam-service spec: - replicas: 1 + replicas: {{ .Values.microservices.iamService.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-iam-service + template: + metadata: + annotations: + rollout-timestamp: {{ now | quote }} + labels: + app: {{ .Release.Name }}-iam-service + spec: + serviceAccountName: {{ .Release.Name }}-sa + containers: + - name: iam-service + image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} + imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" + args: + - iam-service + - --conf + - /etc/rcabench/config.prod.toml + ports: + - containerPort: {{ .Values.microservices.iamService.grpcPort }} + name: grpc + readinessProbe: + grpc: + port: {{ .Values.microservices.iamService.grpcPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: {{ .Values.microservices.iamService.grpcPort }} + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.prod.toml + subPath: config.prod.toml + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-rcabench-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-orchestrator-service +spec: + replicas: {{ .Values.microservices.orchestratorService.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-orchestrator-service + template: + metadata: + annotations: + rollout-timestamp: {{ now | quote }} + labels: + app: {{ .Release.Name }}-orchestrator-service + spec: + serviceAccountName: {{ .Release.Name }}-sa + containers: + - name: orchestrator-service + image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} + imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" + args: + - orchestrator-service + - --conf + - /etc/rcabench/config.prod.toml + ports: + - containerPort: {{ .Values.microservices.orchestratorService.grpcPort }} + name: grpc + readinessProbe: + grpc: + port: {{ .Values.microservices.orchestratorService.grpcPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: {{ .Values.microservices.orchestratorService.grpcPort }} + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.prod.toml + subPath: config.prod.toml + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-rcabench-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-resource-service +spec: + replicas: {{ .Values.microservices.resourceService.replicaCount }} selector: matchLabels: - app: {{ .Release.Name }}-consumer + app: {{ .Release.Name }}-resource-service template: metadata: annotations: rollout-timestamp: {{ now | quote }} labels: - app: {{ .Release.Name }}-consumer + app: {{ .Release.Name }}-resource-service + spec: + serviceAccountName: {{ .Release.Name }}-sa + containers: + - name: resource-service + image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} + imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" + args: + - resource-service + - --conf + - /etc/rcabench/config.prod.toml + ports: + - containerPort: {{ .Values.microservices.resourceService.grpcPort }} + name: grpc + readinessProbe: + grpc: + port: {{ .Values.microservices.resourceService.grpcPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: {{ .Values.microservices.resourceService.grpcPort }} + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.prod.toml + subPath: config.prod.toml + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-rcabench-config +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-runtime-worker-service +spec: + replicas: {{ .Values.microservices.runtimeWorkerService.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-runtime-worker-service + template: + metadata: + annotations: + rollout-timestamp: {{ now | quote }} + labels: + app: {{ .Release.Name }}-runtime-worker-service spec: serviceAccountName: {{ .Release.Name }}-sa initContainers: - - name: wait-for-dependencies - image: {{ include "helm.image" (dict "imageConfig" .Values.images.busybox "global" .Values.global) }} - imagePullPolicy: "{{ .Values.images.busybox.pullPolicy }}" - command: - - sh - - -c - - | - # Parallel dependency check with timeout - TIMEOUT=120 - START=$(date +%s) - - check_jaeger() { while ! nc -z {{ .Release.Name }}-jaeger 4318; do sleep 1; done; } - check_redis() { while ! nc -z {{ .Release.Name }}-redis 6379; do sleep 1; done; } - check_mysql() { while ! nc -z {{ .Release.Name }}-mysql 3306; do sleep 1; done; } - check_etcd() { while ! nc -z {{ .Release.Name }}-etcd-headless 2379; do sleep 1; done; } - - # Start background checks - check_jaeger & PID1=$! - check_redis & PID2=$! - check_mysql & PID3=$! - check_etcd & PID4=$! - - # Wait with timeout - while kill -0 $PID1 2>/dev/null || kill -0 $PID2 2>/dev/null || kill -0 $PID3 2>/dev/null || kill -0 $PID4 2>/dev/null; do - [ $(($(date +%s) - START)) -ge $TIMEOUT ] && echo "Timeout waiting for dependencies" && exit 1 - sleep 5 - done - - # Verify all succeeded - wait $PID1 && wait $PID2 && wait $PID3 && wait $PID4 || exit 1 - echo "Dependencies ready in $(($(date +%s) - START))s" - - name: init-etcd-data + - name: init-etcd-consumer-config image: pair-diag-cn-guangzhou.cr.volces.com/pair/etcdctl:latest imagePullPolicy: IfNotPresent command: @@ -207,56 +279,51 @@ spec: CONFIG_PREFIX="/rcabench/config/consumer" CONFIG_YAML_PATH="/initial-config/etcd.yaml" FORCE_INIT="{{ .Values.initialConfig.force }}" - - echo "=== Etcd Initialization Started: $(date) ===" - - # Check if already initialized + INIT_VALUE=$(etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 get "$INIT_KEY" --print-value-only 2>/dev/null || echo "") if [ "$INIT_VALUE" = "true" ] && [ "$FORCE_INIT" != "true" ]; then - echo "Already initialized (found $INIT_KEY=true). Set initialConfig.force=true to reinitialize." + echo "consumer config already initialized" exit 0 fi - - if [ "$FORCE_INIT" = "true" ]; then - echo "Force initialization enabled, proceeding..." - else - echo "No initialization marker found, proceeding with initialization..." - fi - - # Load initial config from YAML - echo "Loading config from $CONFIG_YAML_PATH..." + if [ -f "$CONFIG_YAML_PATH" ]; then while IFS=': ' read -r key_name value || [ -n "$key_name" ]; do - # Skip empty lines and comments [ -z "$key_name" ] && continue echo "$key_name" | grep -q '^#' && continue - - # Remove leading/trailing whitespace and quotes key_name=$(echo "$key_name" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') value=$(echo "$value" | sed "s/^[[:space:]]*//; s/[[:space:]]*$//; s/^[\"']//; s/[\"']$//") - - if [ -n "$key_name" ]; then - key="$CONFIG_PREFIX/$key_name" - echo "Setting: $key = $value" - etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$key" "$value" || true - fi + [ -n "$key_name" ] || continue + etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$CONFIG_PREFIX/$key_name" "$value" || true done < "$CONFIG_YAML_PATH" etcdctl --endpoints=http://{{ .Release.Name }}-etcd-headless:2379 put "$INIT_KEY" "true" || true - echo "Config loaded successfully" - else - echo "Warning: $CONFIG_YAML_PATH not found" fi - - echo "=== Etcd Initialization Done: $(date) ===" volumeMounts: - name: etcd-initial-config mountPath: /initial-config readOnly: true containers: - - name: exp + - name: runtime-worker-service image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" - command: ["/app/entrypoint.sh", "consumer"] + args: + - runtime-worker-service + - --conf + - /etc/rcabench/config.prod.toml + ports: + - containerPort: {{ .Values.microservices.runtimeWorkerService.grpcPort }} + name: grpc + - containerPort: {{ .Values.configmap.otlp.port }} + name: otlp + readinessProbe: + grpc: + port: {{ .Values.microservices.runtimeWorkerService.grpcPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: {{ .Values.microservices.runtimeWorkerService.grpcPort }} + initialDelaySeconds: 10 + periodSeconds: 15 env: - name: GOPRIVATE value: "github.com/OperationsPAI/chaos-experiment" @@ -315,6 +382,53 @@ spec: - name: experiment-storage persistentVolumeClaim: claimName: {{ .Release.Name }}-juicefs-experiment-storage +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }}-system-service +spec: + replicas: {{ .Values.microservices.systemService.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }}-system-service + template: + metadata: + annotations: + rollout-timestamp: {{ now | quote }} + labels: + app: {{ .Release.Name }}-system-service + spec: + serviceAccountName: {{ .Release.Name }}-sa + containers: + - name: system-service + image: {{ include "helm.image" (dict "imageConfig" .Values.images.rcabench "global" .Values.global) }} + imagePullPolicy: "{{ .Values.images.rcabench.pullPolicy }}" + args: + - system-service + - --conf + - /etc/rcabench/config.prod.toml + ports: + - containerPort: {{ .Values.microservices.systemService.grpcPort }} + name: grpc + readinessProbe: + grpc: + port: {{ .Values.microservices.systemService.grpcPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: {{ .Values.microservices.systemService.grpcPort }} + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.prod.toml + subPath: config.prod.toml + volumes: + - name: config + configMap: + name: {{ .Release.Name }}-rcabench-config {{- if .Values.buildkit.enabled }} --- apiVersion: v1 @@ -375,7 +489,7 @@ spec: mountPath: /etc/rcabench/config.prod.toml subPath: config.prod.toml - name: containers-data - mountPath: "{{ .Values.configmap.container.storage_path }}" + mountPath: "{{ .Values.configmap.jfs.container_path }}" - name: buildkit-socket mountPath: /run/buildkit - name: buildkit-config @@ -406,4 +520,4 @@ spec: - key: harbor.lab.pj.crt path: harbor.lab.pj.crt {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index c393dc87..7444ede3 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -26,7 +26,7 @@ spec: targetPort: 14268 nodePort: 31468 - name: query - port: 16686 + port: 16686 targetPort: 16686 nodePort: 31686 {{- else }} @@ -52,7 +52,7 @@ spec: port: 14268 targetPort: 14268 - name: query - port: 16686 + port: 16686 targetPort: 16686 {{- end }} @@ -68,8 +68,8 @@ spec: selector: app: {{ .Release.Name }}-redis ports: - - port: 6379 - nodePort: 32279 + - port: 6379 + nodePort: 32279 {{- else }} apiVersion: v1 kind: Service @@ -80,7 +80,7 @@ spec: selector: app: {{ .Release.Name }}-redis ports: - - port: 6379 + - port: 6379 {{- end }} --- @@ -95,9 +95,9 @@ spec: selector: app: {{ .Release.Name }}-mysql ports: - - port: 3306 - targetPort: 3306 - nodePort: 32206 + - port: 3306 + targetPort: 3306 + nodePort: 32206 {{- else }} apiVersion: v1 kind: Service @@ -108,8 +108,8 @@ spec: selector: app: {{ .Release.Name }}-mysql ports: - - port: 3306 - targetPort: 3306 + - port: 3306 + targetPort: 3306 {{- end }} --- @@ -123,12 +123,12 @@ spec: selector: app: {{ .Release.Name }}-etcd ports: - - name: client - port: 2379 - targetPort: 2379 - - name: peer - port: 2380 - targetPort: 2380 + - name: client + port: 2379 + targetPort: 2379 + - name: peer + port: 2380 + targetPort: 2380 --- # etcd External Service @@ -142,9 +142,9 @@ spec: selector: app: {{ .Release.Name }}-etcd ports: - - port: 2379 - targetPort: 2379 - nodePort: 31379 + - port: 2379 + targetPort: 2379 + nodePort: 31379 {{- else }} apiVersion: v1 kind: Service @@ -155,47 +155,104 @@ spec: selector: app: {{ .Release.Name }}-etcd ports: - - port: 2379 - targetPort: 2379 + - port: 2379 + targetPort: 2379 {{- end }} --- -# exp Service -{{- if eq .Values.configmap.system.env_mode "staging" }} +# API Gateway Service apiVersion: v1 kind: Service metadata: - name: {{ .Release.Name }}-exp + name: {{ .Release.Name }}-api-gateway spec: + {{- if eq .Values.configmap.system.env_mode "staging" }} type: NodePort + {{- else }} + type: {{ .Values.microservices.apiGateway.service.type }} + {{- end }} selector: - app: {{ .Release.Name }}-producer + app: {{ .Release.Name }}-api-gateway ports: - - name: grpc - port: {{ .Values.configmap.otlp.port }} - targetPort: {{ .Values.configmap.otlp.port }} - nodePort: 32319 - - name: http - port: {{ .Values.configmap.port}} - targetPort: {{ .Values.configmap.port}} - nodePort: 32080 -{{- else }} + - name: http + port: {{ .Values.microservices.apiGateway.httpPort }} + targetPort: {{ .Values.microservices.apiGateway.httpPort }} + {{- if eq .Values.configmap.system.env_mode "staging" }} + nodePort: {{ .Values.microservices.apiGateway.service.nodePort }} + {{- end }} + +--- apiVersion: v1 kind: Service metadata: - name: {{ .Release.Name }}-exp + name: {{ .Release.Name }}-iam-service spec: type: ClusterIP selector: - app: {{ .Release.Name }}-producer + app: {{ .Release.Name }}-iam-service ports: - - name: grpc - port: {{ .Values.configmap.otlp.port }} - targetPort: {{ .Values.configmap.otlp.port }} - - name: http - port: {{ .Values.configmap.port}} - targetPort: {{ .Values.configmap.port}} -{{- end }} + - name: grpc + port: {{ .Values.microservices.iamService.grpcPort }} + targetPort: {{ .Values.microservices.iamService.grpcPort }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-orchestrator-service +spec: + type: ClusterIP + selector: + app: {{ .Release.Name }}-orchestrator-service + ports: + - name: grpc + port: {{ .Values.microservices.orchestratorService.grpcPort }} + targetPort: {{ .Values.microservices.orchestratorService.grpcPort }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-resource-service +spec: + type: ClusterIP + selector: + app: {{ .Release.Name }}-resource-service + ports: + - name: grpc + port: {{ .Values.microservices.resourceService.grpcPort }} + targetPort: {{ .Values.microservices.resourceService.grpcPort }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-runtime-worker-service +spec: + type: ClusterIP + selector: + app: {{ .Release.Name }}-runtime-worker-service + ports: + - name: grpc + port: {{ .Values.microservices.runtimeWorkerService.grpcPort }} + targetPort: {{ .Values.microservices.runtimeWorkerService.grpcPort }} + - name: otlp + port: {{ .Values.configmap.otlp.port }} + targetPort: {{ .Values.configmap.otlp.port }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-system-service +spec: + type: ClusterIP + selector: + app: {{ .Release.Name }}-system-service + ports: + - name: grpc + port: {{ .Values.microservices.systemService.grpcPort }} + targetPort: {{ .Values.microservices.systemService.grpcPort }} --- apiVersion: v1 @@ -259,36 +316,3 @@ spec: targetPort: 9090 {{- end }} {{- end }} - -{{- if .Values.grafana.enabled }} ---- -# Grafana Service -{{- if eq .Values.configmap.system.env_mode "staging" }} -apiVersion: v1 -kind: Service -metadata: - name: {{ .Release.Name }}-grafana -spec: - type: NodePort - selector: - app: {{ .Release.Name }}-grafana - ports: - - name: http - port: 3000 - targetPort: 3000 - nodePort: 32300 -{{- else }} -apiVersion: v1 -kind: Service -metadata: - name: {{ .Release.Name }}-grafana -spec: - type: ClusterIP - selector: - app: {{ .Release.Name }}-grafana - ports: - - name: http - port: 3000 - targetPort: 3000 -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml index c0485555..b6355f4e 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -147,7 +147,7 @@ initialization: configmap: name: "rcabench" version: "1.0.0" - port: 8080 + port: 8082 workspace: "/app" system: env_mode: "prod" @@ -193,6 +193,29 @@ configmap: port: 4319 max_request_size: 5242880 +microservices: + apiGateway: + replicaCount: 1 + service: + type: ClusterIP + nodePort: 32082 + httpPort: 8082 + iamService: + replicaCount: 1 + grpcPort: 9091 + orchestratorService: + replicaCount: 1 + grpcPort: 9092 + resourceService: + replicaCount: 1 + grpcPort: 9093 + runtimeWorkerService: + replicaCount: 1 + grpcPort: 9094 + systemService: + replicaCount: 1 + grpcPort: 9095 + persistence: # Global storage type setting - applies to containers, logs, and juicefs sections # Options: "juicefs" | "volcengine" | "external" diff --git a/manifests/microservices/README.md b/manifests/microservices/README.md new file mode 100644 index 00000000..c2004721 --- /dev/null +++ b/manifests/microservices/README.md @@ -0,0 +1,20 @@ +# Microservice Kubernetes Skeleton + +这目录承接当前六服务拆分后的第一版 Kubernetes skeleton。 + +当前文件: + +- `aegislab-microservices.yaml` + +用途: + +- 给 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` 提供第一版 Deployment/Service 骨架 +- 明确端口、启动命令、probe 约定、配置挂载方式 + +注意: + +- 这是一份 skeleton,不是最终生产部署方案 +- 当前仍假设: + - MySQL / Redis / Etcd / Jaeger / BuildKit 等基础依赖已由其他清单或平台层提供 + - `ConfigMap/aegislab-config` 已准备好并包含 `config.toml` +- 这份清单优先编码“边界和启动方式”,而不是覆盖全部生产级资源策略 diff --git a/manifests/microservices/aegislab-microservices.yaml b/manifests/microservices/aegislab-microservices.yaml new file mode 100644 index 00000000..29e9666c --- /dev/null +++ b/manifests/microservices/aegislab-microservices.yaml @@ -0,0 +1,330 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: aegislab +--- +apiVersion: v1 +kind: Service +metadata: + name: api-gateway + namespace: aegislab +spec: + selector: + app: api-gateway + ports: + - name: http + port: 8082 + targetPort: 8082 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: api-gateway + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: api-gateway + template: + metadata: + labels: + app: api-gateway + spec: + containers: + - name: api-gateway + image: opspai/rcabench:latest + args: ["api-gateway", "--conf", "/etc/rcabench/config.toml", "--port", "8082"] + ports: + - containerPort: 8082 + name: http + readinessProbe: + httpGet: + path: /system/health + port: 8082 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /system/health + port: 8082 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config +--- +apiVersion: v1 +kind: Service +metadata: + name: iam-service + namespace: aegislab +spec: + selector: + app: iam-service + ports: + - name: grpc + port: 9091 + targetPort: 9091 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: iam-service + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: iam-service + template: + metadata: + labels: + app: iam-service + spec: + containers: + - name: iam-service + image: opspai/rcabench:latest + args: ["iam-service", "--conf", "/etc/rcabench/config.toml"] + ports: + - containerPort: 9091 + name: grpc + readinessProbe: + grpc: + port: 9091 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: 9091 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config +--- +apiVersion: v1 +kind: Service +metadata: + name: orchestrator-service + namespace: aegislab +spec: + selector: + app: orchestrator-service + ports: + - name: grpc + port: 9092 + targetPort: 9092 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: orchestrator-service + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: orchestrator-service + template: + metadata: + labels: + app: orchestrator-service + spec: + containers: + - name: orchestrator-service + image: opspai/rcabench:latest + args: ["orchestrator-service", "--conf", "/etc/rcabench/config.toml"] + ports: + - containerPort: 9092 + name: grpc + readinessProbe: + grpc: + port: 9092 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: 9092 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config +--- +apiVersion: v1 +kind: Service +metadata: + name: resource-service + namespace: aegislab +spec: + selector: + app: resource-service + ports: + - name: grpc + port: 9093 + targetPort: 9093 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: resource-service + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: resource-service + template: + metadata: + labels: + app: resource-service + spec: + containers: + - name: resource-service + image: opspai/rcabench:latest + args: ["resource-service", "--conf", "/etc/rcabench/config.toml"] + ports: + - containerPort: 9093 + name: grpc + readinessProbe: + grpc: + port: 9093 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: 9093 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config +--- +apiVersion: v1 +kind: Service +metadata: + name: runtime-worker-service + namespace: aegislab +spec: + selector: + app: runtime-worker-service + ports: + - name: grpc + port: 9094 + targetPort: 9094 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: runtime-worker-service + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: runtime-worker-service + template: + metadata: + labels: + app: runtime-worker-service + spec: + containers: + - name: runtime-worker-service + image: opspai/rcabench:latest + args: ["runtime-worker-service", "--conf", "/etc/rcabench/config.toml"] + ports: + - containerPort: 9094 + name: grpc + readinessProbe: + grpc: + port: 9094 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: 9094 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config +--- +apiVersion: v1 +kind: Service +metadata: + name: system-service + namespace: aegislab +spec: + selector: + app: system-service + ports: + - name: grpc + port: 9095 + targetPort: 9095 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: system-service + namespace: aegislab +spec: + replicas: 1 + selector: + matchLabels: + app: system-service + template: + metadata: + labels: + app: system-service + spec: + containers: + - name: system-service + image: opspai/rcabench:latest + args: ["system-service", "--conf", "/etc/rcabench/config.toml"] + ports: + - containerPort: 9095 + name: grpc + readinessProbe: + grpc: + port: 9095 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + grpc: + port: 9095 + initialDelaySeconds: 10 + periodSeconds: 15 + volumeMounts: + - name: config + mountPath: /etc/rcabench/config.toml + subPath: config.toml + volumes: + - name: config + configMap: + name: aegislab-config diff --git a/src/app/app.go b/src/app/app.go index d733bb9d..5a51e094 100644 --- a/src/app/app.go +++ b/src/app/app.go @@ -15,18 +15,48 @@ import ( "go.uber.org/fx" ) -func CommonOptions(confPath string) fx.Option { +func BaseOptions(confPath string) fx.Option { return fx.Options( fx.Supply(configinfra.Params{Path: confPath}), - loggerinfra.Module, configinfra.Module, + loggerinfra.Module, + ) +} + +func ObserveOptions() fx.Option { + return fx.Options( + lokiinfra.Module, + tracinginfra.Module, + ) +} + +func DataOptions() fx.Option { + return fx.Options( dbinfra.Module, redisinfra.Module, + ) +} + +func CoordinationOptions() fx.Option { + return fx.Options( etcdinfra.Module, + ) +} + +func BuildInfraOptions() fx.Option { + return fx.Options( harborinfra.Module, helminfra.Module, buildkitinfra.Module, - lokiinfra.Module, - tracinginfra.Module, + ) +} + +func CommonOptions(confPath string) fx.Option { + return fx.Options( + BaseOptions(confPath), + ObserveOptions(), + DataOptions(), + CoordinationOptions(), + BuildInfraOptions(), ) } diff --git a/src/app/both.go b/src/app/both.go index 42a78537..5ac8b907 100644 --- a/src/app/both.go +++ b/src/app/both.go @@ -1,38 +1,10 @@ package app -import ( - chaosinfra "aegis/infra/chaos" - k8sinfra "aegis/infra/k8s" - runtimeinfra "aegis/infra/runtime" - controllerinterface "aegis/interface/controller" - httpinterface "aegis/interface/http" - receiverinterface "aegis/interface/receiver" - workerinterface "aegis/interface/worker" - "aegis/service/consumer" - - "go.uber.org/fx" -) +import "go.uber.org/fx" func BothOptions(confPath string, port string) fx.Option { return fx.Options( CommonOptions(confPath), - runtimeinfra.Module, - chaosinfra.Module, - k8sinfra.Module, - fx.Provide( - consumer.NewMonitor, - fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), - fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), - fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), - consumer.NewFaultBatchManager, - newProducerInitializer, - ), - ProducerHTTPModules(), - fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), - httpinterface.Module, - workerinterface.Module, - controllerinterface.Module, - receiverinterface.Module, - fx.Invoke(registerProducerInitialization), + BothCompatibilityOptions(port), ) } diff --git a/src/app/compat_options.go b/src/app/compat_options.go new file mode 100644 index 00000000..75ab360a --- /dev/null +++ b/src/app/compat_options.go @@ -0,0 +1,49 @@ +package app + +import ( + chaosinfra "aegis/infra/chaos" + k8sinfra "aegis/infra/k8s" + httpinterface "aegis/interface/http" + + "go.uber.org/fx" +) + +// ProducerCompatibilityOptions captures the standalone producer/api-gateway +// HTTP stack, including the HTTP-side K8s/chaos infra. +func ProducerCompatibilityOptions(port string) fx.Option { + return fx.Options( + chaosinfra.Module, + k8sinfra.Module, + ProducerHTTPEntryOptions(port), + ) +} + +// ProducerHTTPEntryOptions captures the compatibility producer HTTP surface +// shared by producer, both, and api-gateway entrypoints. +func ProducerHTTPEntryOptions(port string) fx.Option { + return fx.Options( + fx.Provide(newProducerInitializer), + fx.Invoke(registerProducerInitialization), + ProducerHTTPModules(), + fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), + httpinterface.Module, + ) +} + +// CompatibilityRuntimeOptions centralizes the legacy runtime stack that still +// needs local execution/injection owners for producer/consumer/both entrypoints. +func CompatibilityRuntimeOptions() fx.Option { + return fx.Options( + RuntimeWorkerStackOptions(), + ExecutionInjectionOwnerModules(), + ) +} + +// BothCompatibilityOptions captures the legacy combined producer+consumer +// runtime surface in one place. +func BothCompatibilityOptions(port string) fx.Option { + return fx.Options( + CompatibilityRuntimeOptions(), + ProducerHTTPEntryOptions(port), + ) +} diff --git a/src/app/consumer.go b/src/app/consumer.go index 733c51c5..15177ea6 100644 --- a/src/app/consumer.go +++ b/src/app/consumer.go @@ -1,32 +1,10 @@ package app -import ( - chaosinfra "aegis/infra/chaos" - k8sinfra "aegis/infra/k8s" - runtimeinfra "aegis/infra/runtime" - controllerinterface "aegis/interface/controller" - receiverinterface "aegis/interface/receiver" - workerinterface "aegis/interface/worker" - "aegis/service/consumer" - - "go.uber.org/fx" -) +import "go.uber.org/fx" func ConsumerOptions(confPath string) fx.Option { return fx.Options( CommonOptions(confPath), - runtimeinfra.Module, - chaosinfra.Module, - k8sinfra.Module, - fx.Provide( - consumer.NewMonitor, - fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), - fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), - fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), - consumer.NewFaultBatchManager, - ), - workerinterface.Module, - controllerinterface.Module, - receiverinterface.Module, + CompatibilityRuntimeOptions(), ) } diff --git a/src/app/gateway/auth_services.go b/src/app/gateway/auth_services.go new file mode 100644 index 00000000..5dd82c7e --- /dev/null +++ b/src/app/gateway/auth_services.go @@ -0,0 +1,129 @@ +package gatewayapp + +import ( + "context" + + authmodule "aegis/module/auth" + "aegis/utils" +) + +type authIAMClient interface { + Enabled() bool + Login(context.Context, *authmodule.LoginReq) (*authmodule.LoginResp, error) + Register(context.Context, *authmodule.RegisterReq) (*authmodule.UserInfo, error) + RefreshToken(context.Context, *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) + Logout(context.Context, *utils.Claims) error + ChangePassword(context.Context, *authmodule.ChangePasswordReq, int) error + GetProfile(context.Context, int) (*authmodule.UserProfileResp, error) + CreateAccessKey(context.Context, int, *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) + ListAccessKeys(context.Context, int, *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) + GetAccessKey(context.Context, int, int) (*authmodule.AccessKeyInfo, error) + DeleteAccessKey(context.Context, int, int) error + DisableAccessKey(context.Context, int, int) error + EnableAccessKey(context.Context, int, int) error + RotateAccessKey(context.Context, int, int) (*authmodule.AccessKeyWithSecretResp, error) + ExchangeAccessKeyToken(context.Context, *authmodule.AccessKeyTokenReq, string, string) (*authmodule.AccessKeyTokenResp, error) +} + +type remoteAwareAuthService struct { + authmodule.HandlerService + iam authIAMClient +} + +func (s remoteAwareAuthService) Login(ctx context.Context, req *authmodule.LoginReq) (*authmodule.LoginResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.Login(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) Register(ctx context.Context, req *authmodule.RegisterReq) (*authmodule.UserInfo, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.Register(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) RefreshToken(ctx context.Context, req *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RefreshToken(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) Logout(ctx context.Context, claims *utils.Claims) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.Logout(ctx, claims) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) ChangePassword(ctx context.Context, req *authmodule.ChangePasswordReq, userID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ChangePassword(ctx, req, userID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) GetProfile(ctx context.Context, userID int) (*authmodule.UserProfileResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetProfile(ctx, userID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) CreateAccessKey(ctx context.Context, userID int, req *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.CreateAccessKey(ctx, userID, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) ListAccessKeys(ctx context.Context, userID int, req *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListAccessKeys(ctx, userID, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyInfo, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetAccessKey(ctx, userID, accessKeyID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.DeleteAccessKey(ctx, userID, accessKeyID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.DisableAccessKey(ctx, userID, accessKeyID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.EnableAccessKey(ctx, userID, accessKeyID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyWithSecretResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RotateAccessKey(ctx, userID, accessKeyID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) ExchangeAccessKeyToken(ctx context.Context, req *authmodule.AccessKeyTokenReq, method, path string) (*authmodule.AccessKeyTokenResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ExchangeAccessKeyToken(ctx, req, method, path) + } + return nil, missingRemoteDependency("iam-service") +} diff --git a/src/app/gateway/metric_services.go b/src/app/gateway/metric_services.go new file mode 100644 index 00000000..83df9d7b --- /dev/null +++ b/src/app/gateway/metric_services.go @@ -0,0 +1,118 @@ +package gatewayapp + +import ( + "context" + "slices" + + "aegis/consts" + "aegis/dto" + containermodule "aegis/module/container" + metricmodule "aegis/module/metric" +) + +type metricOrchestratorClient interface { + Enabled() bool + GetInjectionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) + GetExecutionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) +} + +type metricResourceClient interface { + Enabled() bool + ListContainers(context.Context, *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) +} + +type remoteAwareMetricService struct { + metricmodule.HandlerService + orchestrator metricOrchestratorClient + resource metricResourceClient +} + +func (s remoteAwareMetricService) GetInjectionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.GetInjectionMetrics(ctx, req) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareMetricService) GetExecutionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.GetExecutionMetrics(ctx, req) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.AlgorithmMetrics, error) { + if s.orchestrator == nil || !s.orchestrator.Enabled() { + return nil, missingRemoteDependency("orchestrator-service") + } + if s.resource == nil || !s.resource.Enabled() { + return nil, missingRemoteDependency("resource-service") + } + + algorithms, err := s.listAlgorithmContainers(ctx, req) + if err != nil { + return nil, err + } + + metrics := &metricmodule.AlgorithmMetrics{ + Algorithms: make([]metricmodule.AlgorithmMetricItem, 0, len(algorithms)), + } + for _, algorithm := range algorithms { + algorithmID := algorithm.ID + executionMetrics, err := s.orchestrator.GetExecutionMetrics(ctx, &metricmodule.GetMetricsReq{ + StartTime: req.StartTime, + EndTime: req.EndTime, + AlgorithmID: &algorithmID, + }) + if err != nil || executionMetrics == nil || executionMetrics.TotalCount == 0 { + continue + } + metrics.Algorithms = append(metrics.Algorithms, metricmodule.AlgorithmMetricItem{ + AlgorithmID: algorithm.ID, + AlgorithmName: algorithm.Name, + ExecutionCount: executionMetrics.TotalCount, + SuccessCount: executionMetrics.SuccessCount, + FailedCount: executionMetrics.FailedCount, + SuccessRate: executionMetrics.SuccessRate, + AvgDuration: executionMetrics.AvgDuration, + }) + } + return metrics, nil +} + +func (s remoteAwareMetricService) listAlgorithmContainers(ctx context.Context, req *metricmodule.GetMetricsReq) ([]containermodule.ContainerResp, error) { + containerType := consts.ContainerTypeAlgorithm + status := consts.CommonEnabled + page := 1 + items := make([]containermodule.ContainerResp, 0) + + for { + resp, err := s.resource.ListContainers(ctx, &containermodule.ListContainerReq{ + PaginationReq: dto.PaginationReq{ + Page: page, + Size: consts.PageSizeXLarge, + }, + Type: &containerType, + Status: &status, + }) + if err != nil { + return nil, err + } + items = append(items, resp.Items...) + if resp.Pagination == nil || page >= resp.Pagination.TotalPages || len(resp.Items) == 0 { + break + } + page++ + } + + if req.AlgorithmID == nil { + return items, nil + } + index := slices.IndexFunc(items, func(item containermodule.ContainerResp) bool { + return item.ID == *req.AlgorithmID + }) + if index < 0 { + return []containermodule.ContainerResp{}, nil + } + return []containermodule.ContainerResp{items[index]}, nil +} diff --git a/src/app/gateway/metric_services_test.go b/src/app/gateway/metric_services_test.go new file mode 100644 index 00000000..db1ea854 --- /dev/null +++ b/src/app/gateway/metric_services_test.go @@ -0,0 +1,120 @@ +package gatewayapp + +import ( + "context" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + containermodule "aegis/module/container" + metricmodule "aegis/module/metric" +) + +type orchestratorMetricClientStub struct { + injectionReqs []*metricmodule.GetMetricsReq + executionReqs []*metricmodule.GetMetricsReq + injection *metricmodule.InjectionMetrics + execution map[int]metricmodule.ExecutionMetrics + enabled bool +} + +func (s *orchestratorMetricClientStub) Enabled() bool { + return s.enabled +} + +func (s *orchestratorMetricClientStub) GetInjectionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { + s.injectionReqs = append(s.injectionReqs, req) + return s.injection, nil +} + +func (s *orchestratorMetricClientStub) GetExecutionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { + s.executionReqs = append(s.executionReqs, req) + if req != nil && req.AlgorithmID != nil { + if metric, ok := s.execution[*req.AlgorithmID]; ok { + result := metric + return &result, nil + } + } + return &metricmodule.ExecutionMetrics{}, nil +} + +type resourceMetricClientStub struct { + responses []*dto.ListResp[containermodule.ContainerResp] + enabled bool + calls int +} + +func (s *resourceMetricClientStub) Enabled() bool { + return s.enabled +} + +func (s *resourceMetricClientStub) ListContainers(_ context.Context, _ *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { + idx := s.calls + s.calls++ + if idx >= len(s.responses) { + return &dto.ListResp[containermodule.ContainerResp]{}, nil + } + return s.responses[idx], nil +} + +func TestRemoteAwareMetricServiceGetInjectionMetricsRemoteOnly(t *testing.T) { + service := remoteAwareMetricService{} + _, err := service.GetInjectionMetrics(context.Background(), &metricmodule.GetMetricsReq{}) + if err == nil { + t.Fatal("GetInjectionMetrics() error = nil, want missing dependency") + } +} + +func TestRemoteAwareMetricServiceGetAlgorithmMetricsBuildsFromRemoteSources(t *testing.T) { + start := time.Now().Add(-time.Hour) + end := time.Now() + orchestrator := &orchestratorMetricClientStub{ + enabled: true, + execution: map[int]metricmodule.ExecutionMetrics{ + 1: {TotalCount: 3, SuccessCount: 2, FailedCount: 1, SuccessRate: 66.7, AvgDuration: 12.5}, + 2: {TotalCount: 0}, + 3: {TotalCount: 5, SuccessCount: 5, FailedCount: 0, SuccessRate: 100, AvgDuration: 8}, + }, + } + resource := &resourceMetricClientStub{ + enabled: true, + responses: []*dto.ListResp[containermodule.ContainerResp]{ + { + Items: []containermodule.ContainerResp{ + {ID: 1, Name: "algo-a", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, + {ID: 2, Name: "algo-b", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, + }, + Pagination: &dto.PaginationInfo{Page: 1, Size: 100, Total: 3, TotalPages: 2}, + }, + { + Items: []containermodule.ContainerResp{ + {ID: 3, Name: "algo-c", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, + }, + Pagination: &dto.PaginationInfo{Page: 2, Size: 100, Total: 3, TotalPages: 2}, + }, + }, + } + + service := remoteAwareMetricService{ + orchestrator: orchestrator, + resource: resource, + } + + resp, err := service.GetAlgorithmMetrics(context.Background(), &metricmodule.GetMetricsReq{ + StartTime: &start, + EndTime: &end, + }) + if err != nil { + t.Fatalf("GetAlgorithmMetrics() error = %v", err) + } + if len(resp.Algorithms) != 2 { + t.Fatalf("GetAlgorithmMetrics() algorithm count = %d, want 2", len(resp.Algorithms)) + } + if resp.Algorithms[0].AlgorithmName != "algo-a" || resp.Algorithms[1].AlgorithmName != "algo-c" { + t.Fatalf("GetAlgorithmMetrics() unexpected algorithms: %+v", resp.Algorithms) + } + if len(orchestrator.executionReqs) != 3 { + t.Fatalf("GetAlgorithmMetrics() execution calls = %d, want 3", len(orchestrator.executionReqs)) + } +} diff --git a/src/app/gateway/middleware_service.go b/src/app/gateway/middleware_service.go new file mode 100644 index 00000000..ddef6d26 --- /dev/null +++ b/src/app/gateway/middleware_service.go @@ -0,0 +1,80 @@ +package gatewayapp + +import ( + "context" + + "aegis/consts" + "aegis/dto" + "aegis/internalclient/iamclient" + "aegis/middleware" + "aegis/utils" +) + +type remoteAwareMiddlewareService struct { + base middleware.Service + iam *iamclient.Client +} + +func (s remoteAwareMiddlewareService) VerifyToken(ctx context.Context, token string) (*utils.Claims, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.VerifyToken(ctx, token) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) VerifyServiceToken(ctx context.Context, token string) (*utils.ServiceClaims, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.VerifyServiceToken(ctx, token) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) CheckUserPermission(ctx context.Context, params *dto.CheckPermissionParams) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.CheckUserPermission(ctx, params) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) IsUserTeamAdmin(ctx context.Context, userID, teamID int) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.IsUserTeamAdmin(ctx, userID, teamID) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) IsUserInTeam(ctx context.Context, userID, teamID int) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.IsUserInTeam(ctx, userID, teamID) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) IsTeamPublic(ctx context.Context, teamID int) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.IsTeamPublic(ctx, teamID) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) IsUserProjectAdmin(ctx context.Context, userID, projectID int) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.IsUserProjectAdmin(ctx, userID, projectID) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) IsUserInProject(ctx context.Context, userID, projectID int) (bool, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.IsUserInProject(ctx, userID, projectID) + } + return false, missingRemoteDependency("iam-service") +} + +func (s remoteAwareMiddlewareService) LogFailedAction(ipAddress, userAgent, action, errorMsg string, duration, userID int, resourceName consts.ResourceName) error { + return s.base.LogFailedAction(ipAddress, userAgent, action, errorMsg, duration, userID, resourceName) +} + +func (s remoteAwareMiddlewareService) LogUserAction(ipAddress, userAgent, action, details string, duration, userID int, resourceName consts.ResourceName) error { + return s.base.LogUserAction(ipAddress, userAgent, action, details, duration, userID, resourceName) +} diff --git a/src/app/gateway/options.go b/src/app/gateway/options.go new file mode 100644 index 00000000..717db2e3 --- /dev/null +++ b/src/app/gateway/options.go @@ -0,0 +1,175 @@ +package gatewayapp + +import ( + "aegis/app" + "aegis/internalclient/iamclient" + "aegis/internalclient/orchestratorclient" + "aegis/internalclient/resourceclient" + "aegis/internalclient/systemclient" + "aegis/middleware" + authmodule "aegis/module/auth" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + labelmodule "aegis/module/label" + metricmodule "aegis/module/metric" + notificationmodule "aegis/module/notification" + projectmodule "aegis/module/project" + rbacmodule "aegis/module/rbac" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" + teammodule "aegis/module/team" + tracemodule "aegis/module/trace" + usermodule "aegis/module/user" + + "go.uber.org/fx" +) + +// Options builds the dedicated api-gateway runtime. +func Options(confPath, port string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.CoordinationOptions(), + app.BuildInfraOptions(), + app.ProducerCompatibilityOptions(port), + app.RequireConfiguredTargets( + "api-gateway", + app.RequiredConfigTarget{Name: "iam-service", PrimaryKey: "clients.iam.target", LegacyKey: "iam.grpc.target"}, + app.RequiredConfigTarget{Name: "orchestrator-service", PrimaryKey: "clients.orchestrator.target", LegacyKey: "orchestrator.grpc.target"}, + app.RequiredConfigTarget{Name: "resource-service", PrimaryKey: "clients.resource.target", LegacyKey: "resource.grpc.target"}, + app.RequiredConfigTarget{Name: "system-service", PrimaryKey: "clients.system.target", LegacyKey: "system.grpc.target"}, + ), + iamclient.Module, + orchestratorclient.Module, + resourceclient.Module, + systemclient.Module, + fx.Decorate(func(local authmodule.HandlerService, remote *iamclient.Client) authmodule.HandlerService { + return remoteAwareAuthService{ + HandlerService: local, + iam: remote, + } + }), + fx.Decorate(func(local middleware.Service, remote *iamclient.Client) middleware.Service { + return remoteAwareMiddlewareService{ + base: local, + iam: remote, + } + }), + fx.Decorate(func(local usermodule.HandlerService, remote *iamclient.Client) usermodule.HandlerService { + return remoteAwareUserService{ + HandlerService: local, + iam: remote, + } + }), + fx.Decorate(func(local rbacmodule.HandlerService, remote *iamclient.Client) rbacmodule.HandlerService { + return remoteAwareRBACService{ + HandlerService: local, + iam: remote, + } + }), + fx.Decorate(func(local teammodule.HandlerService, remote *iamclient.Client) teammodule.HandlerService { + return remoteAwareTeamService{ + HandlerService: local, + iam: remote, + } + }), + fx.Decorate(func(local executionmodule.HandlerService, remote *orchestratorclient.Client) executionmodule.HandlerService { + return remoteAwareExecutionService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local injectionmodule.HandlerService, remote *orchestratorclient.Client) injectionmodule.HandlerService { + return remoteAwareInjectionService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local taskmodule.HandlerService, remote *orchestratorclient.Client) taskmodule.HandlerService { + return remoteAwareTaskService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local tracemodule.HandlerService, remote *orchestratorclient.Client) tracemodule.HandlerService { + return remoteAwareTraceService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local groupmodule.HandlerService, remote *orchestratorclient.Client) groupmodule.HandlerService { + return remoteAwareGroupService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local notificationmodule.HandlerService, remote *orchestratorclient.Client) notificationmodule.HandlerService { + return remoteAwareNotificationService{ + HandlerService: local, + orchestrator: remote, + } + }), + fx.Decorate(func(local projectmodule.HandlerService, remote *resourceclient.Client) projectmodule.HandlerService { + return remoteAwareProjectService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local containermodule.HandlerService, remote *resourceclient.Client) containermodule.HandlerService { + return remoteAwareContainerService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local datasetmodule.HandlerService, remote *resourceclient.Client) datasetmodule.HandlerService { + return remoteAwareDatasetService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local evaluationmodule.HandlerService, remote *resourceclient.Client) evaluationmodule.HandlerService { + return remoteAwareEvaluationService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local labelmodule.HandlerService, remote *resourceclient.Client) labelmodule.HandlerService { + return remoteAwareLabelService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local chaossystemmodule.HandlerService, remote *resourceclient.Client) chaossystemmodule.HandlerService { + return remoteAwareChaosSystemService{ + HandlerService: local, + resource: remote, + } + }), + fx.Decorate(func(local metricmodule.HandlerService, orchestrator *orchestratorclient.Client, resource *resourceclient.Client) metricmodule.HandlerService { + return remoteAwareMetricService{ + HandlerService: local, + orchestrator: orchestrator, + resource: resource, + } + }), + fx.Decorate(func(local systemmodule.HandlerService, remote *systemclient.Client) systemmodule.HandlerService { + return remoteAwareSystemService{ + HandlerService: local, + system: remote, + } + }), + fx.Decorate(func(local systemmetricmodule.HandlerService, remote *systemclient.Client) systemmetricmodule.HandlerService { + return remoteAwareSystemMetricService{ + HandlerService: local, + system: remote, + } + }), + ) +} diff --git a/src/app/gateway/orchestrator_services.go b/src/app/gateway/orchestrator_services.go new file mode 100644 index 00000000..91890341 --- /dev/null +++ b/src/app/gateway/orchestrator_services.go @@ -0,0 +1,339 @@ +package gatewayapp + +import ( + "context" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/internalclient/orchestratorclient" + "aegis/model" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + notificationmodule "aegis/module/notification" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + + "github.com/gorilla/websocket" + "github.com/redis/go-redis/v9" +) + +type remoteAwareExecutionService struct { + executionmodule.HandlerService + orchestrator *orchestratorclient.Client +} + +func (s remoteAwareExecutionService) SubmitAlgorithmExecution(ctx context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.SubmitExecution(ctx, req, groupID, userID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +type remoteAwareInjectionService struct { + injectionmodule.HandlerService + orchestrator *orchestratorclient.Client +} + +func (s remoteAwareInjectionService) SubmitFaultInjection(ctx context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.SubmitFaultInjection(ctx, req, groupID, userID, projectID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareInjectionService) SubmitDatapackBuilding(ctx context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.SubmitDatapackBuilding(ctx, req, groupID, userID, projectID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +type taskOrchestratorClient interface { + Enabled() bool + GetTask(context.Context, string) (*taskmodule.TaskDetailResp, error) + PollTaskLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) + ListTasks(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) +} + +type traceOrchestratorClient interface { + Enabled() bool + GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) + ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) + GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) + ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +type remoteAwareTaskService struct { + taskmodule.HandlerService + orchestrator taskOrchestratorClient +} + +func (s remoteAwareTaskService) GetDetail(ctx context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.GetTask(ctx, taskID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTaskService) List(ctx context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.ListTasks(ctx, req) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTaskService) GetForLogStream(ctx context.Context, taskID string) (*model.Task, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + if _, err := s.orchestrator.GetTask(ctx, taskID); err != nil { + return nil, err + } + return &model.Task{ID: taskID}, nil + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTaskService) StreamLogs(ctx context.Context, conn *websocket.Conn, task *model.Task) { + if s.orchestrator == nil || !s.orchestrator.Enabled() { + writeTaskWSMessage(conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeError, + Message: missingRemoteDependency("orchestrator-service").Error(), + }) + _ = conn.Close() + return + } + + streamer := remoteTaskLogStreamer{ + conn: conn, + orchestrator: s.orchestrator, + taskID: task.ID, + } + streamer.stream(ctx) +} + +const ( + remoteTaskLogWriteWait = 10 * time.Second + remoteTaskLogPongWait = 60 * time.Second + remoteTaskLogPingPeriod = 54 * time.Second + remoteTaskLogMaxMsgSize = 512 + remoteTaskPollInterval = time.Second + remoteTaskFlushWindow = 5 * time.Second +) + +type remoteTaskLogStreamer struct { + conn *websocket.Conn + orchestrator taskOrchestratorClient + taskID string +} + +func (s remoteTaskLogStreamer) stream(ctx context.Context) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + s.conn.SetReadLimit(remoteTaskLogMaxMsgSize) + _ = s.conn.SetReadDeadline(time.Now().Add(remoteTaskLogPongWait)) + s.conn.SetPongHandler(func(string) error { + _ = s.conn.SetReadDeadline(time.Now().Add(remoteTaskLogPongWait)) + return nil + }) + + go s.readLoop(cancel) + go s.pingLoop(ctx, cancel) + + initial, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, time.Time{}) + if err != nil { + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeError, + Message: err.Error(), + }) + _ = s.conn.Close() + return + } + lastTimestamp := initial.CreatedAt + if len(initial.Logs) > 0 { + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeHistory, + Logs: initial.Logs, + Total: len(initial.Logs), + }) + lastTimestamp = initial.Logs[len(initial.Logs)-1].Timestamp + } + if initial.Terminal { + s.flushTerminalLogs(ctx, lastTimestamp) + return + } + + ticker := time.NewTicker(remoteTaskPollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + resp, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, lastTimestamp) + if err != nil { + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeError, + Message: err.Error(), + }) + return + } + if len(resp.Logs) > 0 { + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeRealtime, + Logs: resp.Logs, + }) + lastTimestamp = resp.Logs[len(resp.Logs)-1].Timestamp + } + if resp.Terminal { + s.flushTerminalLogs(ctx, lastTimestamp) + return + } + } + } +} + +func (s remoteTaskLogStreamer) flushTerminalLogs(ctx context.Context, lastTimestamp time.Time) { + deadline := time.Now().Add(remoteTaskFlushWindow) + for time.Now().Before(deadline) { + resp, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, lastTimestamp) + if err == nil && len(resp.Logs) > 0 { + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeRealtime, + Logs: resp.Logs, + }) + lastTimestamp = resp.Logs[len(resp.Logs)-1].Timestamp + } + time.Sleep(remoteTaskPollInterval) + } + writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + Type: consts.WSLogTypeEnd, + Message: "task completed", + }) + _ = s.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "task completed"), time.Now().Add(remoteTaskLogWriteWait)) +} + +func (s remoteTaskLogStreamer) readLoop(cancel context.CancelFunc) { + defer cancel() + for { + if _, _, err := s.conn.ReadMessage(); err != nil { + return + } + } +} + +func (s remoteTaskLogStreamer) pingLoop(ctx context.Context, cancel context.CancelFunc) { + ticker := time.NewTicker(remoteTaskLogPingPeriod) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if err := s.conn.WriteControl(websocket.PingMessage, nil, time.Now().Add(remoteTaskLogWriteWait)); err != nil { + cancel() + return + } + } + } +} + +func writeTaskWSMessage(conn *websocket.Conn, msg taskmodule.WSLogMessage) { + _ = conn.SetWriteDeadline(time.Now().Add(remoteTaskLogWriteWait)) + _ = conn.WriteJSON(msg) +} + +type remoteAwareTraceService struct { + tracemodule.HandlerService + orchestrator traceOrchestratorClient +} + +func (s remoteAwareTraceService) GetTrace(ctx context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.GetTrace(ctx, traceID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTraceService) ListTraces(ctx context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.ListTraces(ctx, req) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTraceService) GetTraceStreamProcessor(ctx context.Context, traceID string) (*tracemodule.StreamProcessor, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + algorithms, err := s.orchestrator.GetTraceStreamAlgorithms(ctx, traceID) + if err != nil { + return nil, err + } + return tracemodule.NewStreamProcessor(algorithms), nil + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareTraceService) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.ReadTraceStreamMessages(ctx, streamKey, lastID, count, block) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +type groupOrchestratorClient interface { + Enabled() bool + GetGroupStats(context.Context, string) (*groupmodule.GroupStats, error) + GetGroupTraceCount(context.Context, string) (int, error) + ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +type remoteAwareGroupService struct { + groupmodule.HandlerService + orchestrator groupOrchestratorClient +} + +func (s remoteAwareGroupService) GetGroupStats(ctx context.Context, req *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.GetGroupStats(ctx, req.GroupID) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareGroupService) NewGroupStreamProcessor(ctx context.Context, groupID string) (*groupmodule.GroupStreamProcessor, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + totalTraces, err := s.orchestrator.GetGroupTraceCount(ctx, groupID) + if err != nil { + return nil, err + } + return groupmodule.NewGroupStreamProcessor(totalTraces), nil + } + return nil, missingRemoteDependency("orchestrator-service") +} + +func (s remoteAwareGroupService) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.ReadGroupStreamMessages(ctx, streamKey, lastID, count, block) + } + return nil, missingRemoteDependency("orchestrator-service") +} + +type notificationOrchestratorClient interface { + Enabled() bool + ReadNotificationStreamMessages(context.Context, string, int64, time.Duration) ([]redis.XStream, error) +} + +type remoteAwareNotificationService struct { + notificationmodule.HandlerService + orchestrator notificationOrchestratorClient +} + +func (s remoteAwareNotificationService) ReadStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + _ = streamKey + if s.orchestrator != nil && s.orchestrator.Enabled() { + return s.orchestrator.ReadNotificationStreamMessages(ctx, lastID, count, block) + } + return nil, missingRemoteDependency("orchestrator-service") +} diff --git a/src/app/gateway/orchestrator_services_test.go b/src/app/gateway/orchestrator_services_test.go new file mode 100644 index 00000000..6bb55448 --- /dev/null +++ b/src/app/gateway/orchestrator_services_test.go @@ -0,0 +1,187 @@ +package gatewayapp + +import ( + "context" + "testing" + "time" + + "aegis/dto" + groupmodule "aegis/module/group" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + + "github.com/redis/go-redis/v9" +) + +type orchestratorTaskClientStub struct { + enabled bool +} + +func (s *orchestratorTaskClientStub) Enabled() bool { return s.enabled } + +func (s *orchestratorTaskClientStub) GetTask(context.Context, string) (*taskmodule.TaskDetailResp, error) { + return &taskmodule.TaskDetailResp{TaskResp: taskmodule.TaskResp{ID: "task-1"}}, nil +} + +func (s *orchestratorTaskClientStub) PollTaskLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) { + return &taskmodule.TaskLogPollResp{ + Logs: []dto.LogEntry{{TaskID: "task-1", Line: "hello"}}, + Terminal: true, + State: "completed", + CreatedAt: time.Unix(1710000000, 0), + }, nil +} + +func (s *orchestratorTaskClientStub) ListTasks(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { + return &dto.ListResp[taskmodule.TaskResp]{Items: []taskmodule.TaskResp{{ID: "task-1"}}}, nil +} + +type orchestratorTraceClientStub struct { + enabled bool +} + +func (s *orchestratorTraceClientStub) Enabled() bool { return s.enabled } + +func (s *orchestratorTraceClientStub) GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) { + return &tracemodule.TraceDetailResp{TraceResp: tracemodule.TraceResp{ID: "trace-1"}}, nil +} + +func (s *orchestratorTraceClientStub) ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { + return &dto.ListResp[tracemodule.TraceResp]{Items: []tracemodule.TraceResp{{ID: "trace-1"}}}, nil +} + +func (s *orchestratorTraceClientStub) GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) { + return []dto.ContainerVersionItem{{ContainerName: "algo-a"}}, nil +} + +func (s *orchestratorTraceClientStub) ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) { + return []redis.XStream{{Stream: "trace:trace-1:log"}}, nil +} + +type orchestratorGroupClientStub struct { + enabled bool +} + +func (s *orchestratorGroupClientStub) Enabled() bool { return s.enabled } + +func (s *orchestratorGroupClientStub) GetGroupStats(context.Context, string) (*groupmodule.GroupStats, error) { + return &groupmodule.GroupStats{TotalTraces: 2}, nil +} + +func (s *orchestratorGroupClientStub) GetGroupTraceCount(context.Context, string) (int, error) { + return 2, nil +} + +func (s *orchestratorGroupClientStub) ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) { + return []redis.XStream{{Stream: "group:group-1:log"}}, nil +} + +type orchestratorNotificationClientStub struct { + enabled bool +} + +func (s *orchestratorNotificationClientStub) Enabled() bool { return s.enabled } + +func (s *orchestratorNotificationClientStub) ReadNotificationStreamMessages(context.Context, string, int64, time.Duration) ([]redis.XStream, error) { + return []redis.XStream{{Stream: "notifications:global"}}, nil +} + +func TestRemoteAwareTaskServiceRequiresOrchestrator(t *testing.T) { + service := remoteAwareTaskService{} + if _, err := service.List(context.Background(), &taskmodule.ListTaskReq{}); err == nil { + t.Fatal("List() error = nil, want missing dependency") + } +} + +func TestRemoteAwareTaskServiceUsesOrchestratorClient(t *testing.T) { + service := remoteAwareTaskService{orchestrator: &orchestratorTaskClientStub{enabled: true}} + resp, err := service.GetDetail(context.Background(), "task-1") + if err != nil { + t.Fatalf("GetDetail() error = %v", err) + } + if resp.ID != "task-1" { + t.Fatalf("GetDetail() unexpected response: %+v", resp) + } + + task, err := service.GetForLogStream(context.Background(), "task-1") + if err != nil { + t.Fatalf("GetForLogStream() error = %v", err) + } + if task.ID != "task-1" { + t.Fatalf("GetForLogStream() unexpected response: %+v", task) + } +} + +func TestRemoteAwareTraceServiceRequiresOrchestrator(t *testing.T) { + service := remoteAwareTraceService{} + if _, err := service.ListTraces(context.Background(), &tracemodule.ListTraceReq{}); err == nil { + t.Fatal("ListTraces() error = nil, want missing dependency") + } +} + +func TestRemoteAwareTraceServiceUsesOrchestratorClient(t *testing.T) { + service := remoteAwareTraceService{orchestrator: &orchestratorTraceClientStub{enabled: true}} + resp, err := service.GetTrace(context.Background(), "trace-1") + if err != nil { + t.Fatalf("GetTrace() error = %v", err) + } + if resp.ID != "trace-1" { + t.Fatalf("GetTrace() unexpected response: %+v", resp) + } + + processor, err := service.GetTraceStreamProcessor(context.Background(), "trace-1") + if err != nil { + t.Fatalf("GetTraceStreamProcessor() error = %v", err) + } + if processor == nil { + t.Fatal("GetTraceStreamProcessor() = nil") + } +} + +func TestRemoteAwareGroupServiceRequiresOrchestrator(t *testing.T) { + service := remoteAwareGroupService{} + if _, err := service.GetGroupStats(context.Background(), &groupmodule.GetGroupStatsReq{ + GroupID: "d7a4ed4b-1c91-4cdb-8af8-5520fa8d0ce0", + }); err == nil { + t.Fatal("GetGroupStats() error = nil, want missing dependency") + } +} + +func TestRemoteAwareGroupServiceUsesOrchestratorClient(t *testing.T) { + service := remoteAwareGroupService{orchestrator: &orchestratorGroupClientStub{enabled: true}} + resp, err := service.GetGroupStats(context.Background(), &groupmodule.GetGroupStatsReq{ + GroupID: "d7a4ed4b-1c91-4cdb-8af8-5520fa8d0ce0", + }) + if err != nil { + t.Fatalf("GetGroupStats() error = %v", err) + } + if resp.TotalTraces != 2 { + t.Fatalf("GetGroupStats() unexpected response: %+v", resp) + } + + processor, err := service.NewGroupStreamProcessor(context.Background(), "group-1") + if err != nil { + t.Fatalf("NewGroupStreamProcessor() error = %v", err) + } + if processor == nil { + t.Fatal("NewGroupStreamProcessor() = nil") + } +} + +func TestRemoteAwareNotificationServiceRequiresOrchestrator(t *testing.T) { + service := remoteAwareNotificationService{} + if _, err := service.ReadStreamMessages(context.Background(), "notifications:global", "0", 10, time.Second); err == nil { + t.Fatal("ReadStreamMessages() error = nil, want missing dependency") + } +} + +func TestRemoteAwareNotificationServiceUsesOrchestratorClient(t *testing.T) { + service := remoteAwareNotificationService{orchestrator: &orchestratorNotificationClientStub{enabled: true}} + resp, err := service.ReadStreamMessages(context.Background(), "notifications:global", "0", 10, time.Second) + if err != nil { + t.Fatalf("ReadStreamMessages() error = %v", err) + } + if len(resp) != 1 || resp[0].Stream != "notifications:global" { + t.Fatalf("ReadStreamMessages() unexpected response: %+v", resp) + } +} diff --git a/src/app/gateway/rbac_services.go b/src/app/gateway/rbac_services.go new file mode 100644 index 00000000..765603e5 --- /dev/null +++ b/src/app/gateway/rbac_services.go @@ -0,0 +1,129 @@ +package gatewayapp + +import ( + "context" + + "aegis/dto" + rbacmodule "aegis/module/rbac" +) + +type rbacIAMClient interface { + Enabled() bool + CreateRole(context.Context, *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) + DeleteRole(context.Context, int) error + GetRole(context.Context, int) (*rbacmodule.RoleDetailResp, error) + ListRoles(context.Context, *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) + UpdateRole(context.Context, *rbacmodule.UpdateRoleReq, int) (*rbacmodule.RoleResp, error) + AssignRolePermissions(context.Context, int, []int) error + RemoveRolePermissions(context.Context, int, []int) error + ListUsersFromRole(context.Context, int) ([]rbacmodule.UserListItem, error) + GetPermission(context.Context, int) (*rbacmodule.PermissionDetailResp, error) + ListPermissions(context.Context, *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) + ListRolesFromPermission(context.Context, int) ([]rbacmodule.RoleResp, error) + GetResource(context.Context, int) (*rbacmodule.ResourceResp, error) + ListResources(context.Context, *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) + ListResourcePermissions(context.Context, int) ([]rbacmodule.PermissionResp, error) +} + +type remoteAwareRBACService struct { + rbacmodule.HandlerService + iam rbacIAMClient +} + +func (s remoteAwareRBACService) CreateRole(ctx context.Context, req *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.CreateRole(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) DeleteRole(ctx context.Context, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.DeleteRole(ctx, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) GetRole(ctx context.Context, roleID int) (*rbacmodule.RoleDetailResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetRole(ctx, roleID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListRoles(ctx context.Context, req *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListRoles(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) UpdateRole(ctx context.Context, req *rbacmodule.UpdateRoleReq, roleID int) (*rbacmodule.RoleResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.UpdateRole(ctx, req, roleID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) AssignRolePermissions(ctx context.Context, permissionIDs []int, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignRolePermissions(ctx, roleID, permissionIDs) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) RemoveRolePermissions(ctx context.Context, permissionIDs []int, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveRolePermissions(ctx, roleID, permissionIDs) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListUsersFromRole(ctx context.Context, roleID int) ([]rbacmodule.UserListItem, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListUsersFromRole(ctx, roleID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) GetPermission(ctx context.Context, permissionID int) (*rbacmodule.PermissionDetailResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetPermission(ctx, permissionID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListPermissions(ctx context.Context, req *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListPermissions(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbacmodule.RoleResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListRolesFromPermission(ctx, permissionID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) GetResource(ctx context.Context, resourceID int) (*rbacmodule.ResourceResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetResource(ctx, resourceID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListResources(ctx context.Context, req *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListResources(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareRBACService) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbacmodule.PermissionResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListResourcePermissions(ctx, resourceID) + } + return nil, missingRemoteDependency("iam-service") +} diff --git a/src/app/gateway/remote_required.go b/src/app/gateway/remote_required.go new file mode 100644 index 00000000..5d3c8fc4 --- /dev/null +++ b/src/app/gateway/remote_required.go @@ -0,0 +1,7 @@ +package gatewayapp + +import "fmt" + +func missingRemoteDependency(name string) error { + return fmt.Errorf("%s remote client is not configured for api-gateway", name) +} diff --git a/src/app/gateway/resource_services.go b/src/app/gateway/resource_services.go new file mode 100644 index 00000000..7d6d13e8 --- /dev/null +++ b/src/app/gateway/resource_services.go @@ -0,0 +1,233 @@ +package gatewayapp + +import ( + "context" + + "aegis/dto" + "aegis/internalclient/resourceclient" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + labelmodule "aegis/module/label" + projectmodule "aegis/module/project" +) + +type remoteAwareProjectService struct { + projectmodule.HandlerService + resource *resourceclient.Client +} + +func (s remoteAwareProjectService) GetProjectDetail(ctx context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetProject(ctx, projectID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareProjectService) ListProjects(ctx context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListProjects(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +type remoteAwareContainerService struct { + containermodule.HandlerService + resource *resourceclient.Client +} + +func (s remoteAwareContainerService) GetContainer(ctx context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetContainer(ctx, containerID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareContainerService) ListContainers(ctx context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListContainers(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +type remoteAwareDatasetService struct { + datasetmodule.HandlerService + resource *resourceclient.Client +} + +func (s remoteAwareDatasetService) GetDataset(ctx context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetDataset(ctx, datasetID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareDatasetService) ListDatasets(ctx context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListDatasets(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +type remoteAwareEvaluationService struct { + evaluationmodule.HandlerService + resource *resourceclient.Client +} + +func (s remoteAwareEvaluationService) ListDatapackEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListDatapackEvaluationResults(ctx, req, userID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareEvaluationService) ListDatasetEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListDatasetEvaluationResults(ctx, req, userID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareEvaluationService) ListEvaluations(ctx context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListEvaluations(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareEvaluationService) GetEvaluation(ctx context.Context, evaluationID int) (*evaluationmodule.EvaluationResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetEvaluation(ctx, evaluationID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareEvaluationService) DeleteEvaluation(ctx context.Context, evaluationID int) error { + if s.resource != nil && s.resource.Enabled() { + return s.resource.DeleteEvaluation(ctx, evaluationID) + } + return missingRemoteDependency("resource-service") +} + +type remoteAwareLabelService struct { + labelmodule.HandlerService + resource labelResourceClient +} + +type labelResourceClient interface { + Enabled() bool + BatchDeleteLabels(context.Context, []int) error + CreateLabel(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) + DeleteLabel(context.Context, int) error + GetLabel(context.Context, int) (*labelmodule.LabelDetailResp, error) + ListLabels(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) + UpdateLabel(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) +} + +func (s remoteAwareLabelService) BatchDelete(ctx context.Context, ids []int) error { + if s.resource != nil && s.resource.Enabled() { + return s.resource.BatchDeleteLabels(ctx, ids) + } + return missingRemoteDependency("resource-service") +} + +func (s remoteAwareLabelService) Create(ctx context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.CreateLabel(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareLabelService) Delete(ctx context.Context, labelID int) error { + if s.resource != nil && s.resource.Enabled() { + return s.resource.DeleteLabel(ctx, labelID) + } + return missingRemoteDependency("resource-service") +} + +func (s remoteAwareLabelService) GetDetail(ctx context.Context, labelID int) (*labelmodule.LabelDetailResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetLabel(ctx, labelID) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareLabelService) List(ctx context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListLabels(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareLabelService) Update(ctx context.Context, req *labelmodule.UpdateLabelReq, labelID int) (*labelmodule.LabelResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.UpdateLabel(ctx, req, labelID) + } + return nil, missingRemoteDependency("resource-service") +} + +type remoteAwareChaosSystemService struct { + chaossystemmodule.HandlerService + resource chaosSystemResourceClient +} + +type chaosSystemResourceClient interface { + Enabled() bool + ListChaosSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) + GetChaosSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) + CreateChaosSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) + UpdateChaosSystem(context.Context, *chaossystemmodule.UpdateChaosSystemReq, int) (*chaossystemmodule.ChaosSystemResp, error) + DeleteChaosSystem(context.Context, int) error + UpsertChaosSystemMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error + ListChaosSystemMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) +} + +func (s remoteAwareChaosSystemService) ListSystems(ctx context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListChaosSystems(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) GetSystem(ctx context.Context, id int) (*chaossystemmodule.ChaosSystemResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.GetChaosSystem(ctx, id) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) CreateSystem(ctx context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.CreateChaosSystem(ctx, req) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) UpdateSystem(ctx context.Context, id int, req *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.UpdateChaosSystem(ctx, req, id) + } + return nil, missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) DeleteSystem(ctx context.Context, id int) error { + if s.resource != nil && s.resource.Enabled() { + return s.resource.DeleteChaosSystem(ctx, id) + } + return missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) UpsertMetadata(ctx context.Context, id int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { + if s.resource != nil && s.resource.Enabled() { + return s.resource.UpsertChaosSystemMetadata(ctx, id, req) + } + return missingRemoteDependency("resource-service") +} + +func (s remoteAwareChaosSystemService) ListMetadata(ctx context.Context, id int, metadataType string) ([]chaossystemmodule.SystemMetadataResp, error) { + if s.resource != nil && s.resource.Enabled() { + return s.resource.ListChaosSystemMetadata(ctx, id, metadataType) + } + return nil, missingRemoteDependency("resource-service") +} diff --git a/src/app/gateway/resource_services_test.go b/src/app/gateway/resource_services_test.go new file mode 100644 index 00000000..b0c49fa8 --- /dev/null +++ b/src/app/gateway/resource_services_test.go @@ -0,0 +1,104 @@ +package gatewayapp + +import ( + "context" + "testing" + + "aegis/dto" + chaossystemmodule "aegis/module/chaossystem" + labelmodule "aegis/module/label" +) + +type resourceLabelClientStub struct { + enabled bool +} + +func (s *resourceLabelClientStub) Enabled() bool { return s.enabled } + +func (s *resourceLabelClientStub) CreateLabel(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { + return &labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil +} + +func (s *resourceLabelClientStub) GetLabel(context.Context, int) (*labelmodule.LabelDetailResp, error) { + return &labelmodule.LabelDetailResp{LabelResp: labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}}, nil +} + +func (s *resourceLabelClientStub) ListLabels(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { + return &dto.ListResp[labelmodule.LabelResp]{Items: []labelmodule.LabelResp{{ID: 3, Key: "env", Value: "prod"}}}, nil +} + +func (s *resourceLabelClientStub) UpdateLabel(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) { + return &labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil +} + +func (s *resourceLabelClientStub) DeleteLabel(context.Context, int) error { return nil } + +func (s *resourceLabelClientStub) BatchDeleteLabels(context.Context, []int) error { return nil } + +func TestRemoteAwareLabelServiceRequiresResource(t *testing.T) { + service := remoteAwareLabelService{} + if _, err := service.List(context.Background(), &labelmodule.ListLabelReq{}); err == nil { + t.Fatal("List() error = nil, want missing dependency") + } +} + +func TestRemoteAwareLabelServiceUsesResourceClient(t *testing.T) { + service := remoteAwareLabelService{resource: &resourceLabelClientStub{enabled: true}} + resp, err := service.GetDetail(context.Background(), 3) + if err != nil { + t.Fatalf("GetDetail() error = %v", err) + } + if resp.ID != 3 || resp.Key != "env" { + t.Fatalf("GetDetail() unexpected response: %+v", resp) + } +} + +type resourceChaosSystemClientStub struct { + enabled bool +} + +func (s *resourceChaosSystemClientStub) Enabled() bool { return s.enabled } + +func (s *resourceChaosSystemClientStub) ListChaosSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { + return &dto.ListResp[chaossystemmodule.ChaosSystemResp]{Items: []chaossystemmodule.ChaosSystemResp{{ID: 8, Name: "k8s"}}}, nil +} + +func (s *resourceChaosSystemClientStub) GetChaosSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) { + return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +} + +func (s *resourceChaosSystemClientStub) CreateChaosSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +} + +func (s *resourceChaosSystemClientStub) UpdateChaosSystem(context.Context, *chaossystemmodule.UpdateChaosSystemReq, int) (*chaossystemmodule.ChaosSystemResp, error) { + return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +} + +func (s *resourceChaosSystemClientStub) DeleteChaosSystem(context.Context, int) error { return nil } + +func (s *resourceChaosSystemClientStub) UpsertChaosSystemMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error { + return nil +} + +func (s *resourceChaosSystemClientStub) ListChaosSystemMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) { + return []chaossystemmodule.SystemMetadataResp{{ID: 1, SystemName: "k8s"}}, nil +} + +func TestRemoteAwareChaosSystemServiceRequiresResource(t *testing.T) { + service := remoteAwareChaosSystemService{} + if _, err := service.ListSystems(context.Background(), &chaossystemmodule.ListChaosSystemReq{}); err == nil { + t.Fatal("ListSystems() error = nil, want missing dependency") + } +} + +func TestRemoteAwareChaosSystemServiceUsesResourceClient(t *testing.T) { + service := remoteAwareChaosSystemService{resource: &resourceChaosSystemClientStub{enabled: true}} + resp, err := service.GetSystem(context.Background(), 8) + if err != nil { + t.Fatalf("GetSystem() error = %v", err) + } + if resp.ID != 8 || resp.Name != "k8s" { + t.Fatalf("GetSystem() unexpected response: %+v", resp) + } +} diff --git a/src/app/gateway/system_services.go b/src/app/gateway/system_services.go new file mode 100644 index 00000000..0d5ebc45 --- /dev/null +++ b/src/app/gateway/system_services.go @@ -0,0 +1,97 @@ +package gatewayapp + +import ( + "context" + + "aegis/dto" + "aegis/internalclient/systemclient" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" +) + +type remoteAwareSystemService struct { + systemmodule.HandlerService + system *systemclient.Client +} + +func (s remoteAwareSystemService) GetHealth(ctx context.Context) (*systemmodule.HealthCheckResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetHealth(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) GetMetrics(ctx context.Context) (*systemmodule.MonitoringMetricsResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetMetrics(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) GetSystemInfo(ctx context.Context) (*systemmodule.SystemInfo, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetSystemInfo(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) ListNamespaceLocks(ctx context.Context) (*systemmodule.ListNamespaceLockResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.ListNamespaceLocks(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) ListQueuedTasks(ctx context.Context) (*systemmodule.QueuedTasksResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.ListQueuedTasks(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) GetAuditLog(ctx context.Context, id int) (*systemmodule.AuditLogDetailResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetAuditLog(ctx, id) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) ListAuditLogs(ctx context.Context, req *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { + if s.system != nil && s.system.Enabled() { + return s.system.ListAuditLogs(ctx, req) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) GetConfig(ctx context.Context, configID int) (*systemmodule.ConfigDetailResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetConfig(ctx, configID) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemService) ListConfigs(ctx context.Context, req *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { + if s.system != nil && s.system.Enabled() { + return s.system.ListConfigs(ctx, req) + } + return nil, missingRemoteDependency("system-service") +} + +type remoteAwareSystemMetricService struct { + systemmetricmodule.HandlerService + system *systemclient.Client +} + +func (s remoteAwareSystemMetricService) GetSystemMetrics(ctx context.Context) (*systemmetricmodule.SystemMetricsResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetSystemMetrics(ctx) + } + return nil, missingRemoteDependency("system-service") +} + +func (s remoteAwareSystemMetricService) GetSystemMetricsHistory(ctx context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { + if s.system != nil && s.system.Enabled() { + return s.system.GetSystemMetricsHistory(ctx) + } + return nil, missingRemoteDependency("system-service") +} diff --git a/src/app/gateway/team_services.go b/src/app/gateway/team_services.go new file mode 100644 index 00000000..e586df54 --- /dev/null +++ b/src/app/gateway/team_services.go @@ -0,0 +1,97 @@ +package gatewayapp + +import ( + "context" + + "aegis/dto" + teammodule "aegis/module/team" +) + +type teamIAMClient interface { + Enabled() bool + CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) + DeleteTeam(context.Context, int) error + GetTeam(context.Context, int) (*teammodule.TeamDetailResp, error) + ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) + UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) + ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) + AddTeamMember(context.Context, *teammodule.AddTeamMemberReq, int) error + RemoveTeamMember(context.Context, int, int, int) error + UpdateTeamMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error + ListTeamMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) +} + +type remoteAwareTeamService struct { + teammodule.HandlerService + iam teamIAMClient +} + +func (s remoteAwareTeamService) CreateTeam(ctx context.Context, req *teammodule.CreateTeamReq, userID int) (*teammodule.TeamResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.CreateTeam(ctx, req, userID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) DeleteTeam(ctx context.Context, teamID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.DeleteTeam(ctx, teamID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) GetTeamDetail(ctx context.Context, teamID int) (*teammodule.TeamDetailResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetTeam(ctx, teamID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) ListTeams(ctx context.Context, req *teammodule.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[teammodule.TeamResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListTeams(ctx, req, userID, isAdmin) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) UpdateTeam(ctx context.Context, req *teammodule.UpdateTeamReq, teamID int) (*teammodule.TeamResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.UpdateTeam(ctx, req, teamID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) ListTeamProjects(ctx context.Context, req *teammodule.TeamProjectListReq, teamID int) (*dto.ListResp[teammodule.TeamProjectItem], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListTeamProjects(ctx, req, teamID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) AddMember(ctx context.Context, req *teammodule.AddTeamMemberReq, teamID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AddTeamMember(ctx, req, teamID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) RemoveMember(ctx context.Context, teamID, currentUserID, targetUserID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveTeamMember(ctx, teamID, currentUserID, targetUserID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) UpdateMemberRole(ctx context.Context, req *teammodule.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.UpdateTeamMemberRole(ctx, req, teamID, targetUserID, currentUserID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareTeamService) ListMembers(ctx context.Context, req *teammodule.ListTeamMemberReq, teamID int) (*dto.ListResp[teammodule.TeamMemberResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListTeamMembers(ctx, req, teamID) + } + return nil, missingRemoteDependency("iam-service") +} diff --git a/src/app/gateway/team_services_test.go b/src/app/gateway/team_services_test.go new file mode 100644 index 00000000..2a09a1c6 --- /dev/null +++ b/src/app/gateway/team_services_test.go @@ -0,0 +1,60 @@ +package gatewayapp + +import ( + "context" + "testing" + + "aegis/dto" + teammodule "aegis/module/team" +) + +type iamTeamClientStub struct { + enabled bool +} + +func (s *iamTeamClientStub) Enabled() bool { return s.enabled } + +func (s *iamTeamClientStub) CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) { + return &teammodule.TeamResp{ID: 1, Name: "core"}, nil +} +func (s *iamTeamClientStub) DeleteTeam(context.Context, int) error { return nil } +func (s *iamTeamClientStub) GetTeam(context.Context, int) (*teammodule.TeamDetailResp, error) { + return &teammodule.TeamDetailResp{TeamResp: teammodule.TeamResp{ID: 1, Name: "core"}}, nil +} +func (s *iamTeamClientStub) ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) { + return &dto.ListResp[teammodule.TeamResp]{Items: []teammodule.TeamResp{{ID: 1, Name: "core"}}}, nil +} +func (s *iamTeamClientStub) UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) { + return &teammodule.TeamResp{ID: 1, Name: "core"}, nil +} +func (s *iamTeamClientStub) ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) { + return &dto.ListResp[teammodule.TeamProjectItem]{}, nil +} +func (s *iamTeamClientStub) AddTeamMember(context.Context, *teammodule.AddTeamMemberReq, int) error { + return nil +} +func (s *iamTeamClientStub) RemoveTeamMember(context.Context, int, int, int) error { return nil } +func (s *iamTeamClientStub) UpdateTeamMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error { + return nil +} +func (s *iamTeamClientStub) ListTeamMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) { + return &dto.ListResp[teammodule.TeamMemberResp]{}, nil +} + +func TestRemoteAwareTeamServiceRequiresIAM(t *testing.T) { + service := remoteAwareTeamService{} + if _, err := service.ListTeams(context.Background(), &teammodule.ListTeamReq{}, 7, true); err == nil { + t.Fatal("ListTeams() error = nil, want missing dependency") + } +} + +func TestRemoteAwareTeamServiceUsesIAMClient(t *testing.T) { + service := remoteAwareTeamService{iam: &iamTeamClientStub{enabled: true}} + resp, err := service.GetTeamDetail(context.Background(), 1) + if err != nil { + t.Fatalf("GetTeamDetail() error = %v", err) + } + if resp.ID != 1 || resp.Name != "core" { + t.Fatalf("GetTeamDetail() unexpected response: %+v", resp) + } +} diff --git a/src/app/gateway/user_services.go b/src/app/gateway/user_services.go new file mode 100644 index 00000000..3610f4c4 --- /dev/null +++ b/src/app/gateway/user_services.go @@ -0,0 +1,137 @@ +package gatewayapp + +import ( + "context" + + "aegis/dto" + usermodule "aegis/module/user" +) + +type userIAMClient interface { + Enabled() bool + CreateUser(context.Context, *usermodule.CreateUserReq) (*usermodule.UserResp, error) + DeleteUser(context.Context, int) error + GetUser(context.Context, int) (*usermodule.UserDetailResp, error) + ListUsers(context.Context, *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) + UpdateUser(context.Context, *usermodule.UpdateUserReq, int) (*usermodule.UserResp, error) + AssignUserRole(context.Context, int, int) error + RemoveUserRole(context.Context, int, int) error + AssignUserPermissions(context.Context, int, *usermodule.AssignUserPermissionReq) error + RemoveUserPermissions(context.Context, int, *usermodule.RemoveUserPermissionReq) error + AssignUserContainer(context.Context, int, int, int) error + RemoveUserContainer(context.Context, int, int) error + AssignUserDataset(context.Context, int, int, int) error + RemoveUserDataset(context.Context, int, int) error + AssignUserProject(context.Context, int, int, int) error + RemoveUserProject(context.Context, int, int) error +} + +type remoteAwareUserService struct { + usermodule.HandlerService + iam userIAMClient +} + +func (s remoteAwareUserService) CreateUser(ctx context.Context, req *usermodule.CreateUserReq) (*usermodule.UserResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.CreateUser(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) DeleteUser(ctx context.Context, userID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.DeleteUser(ctx, userID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) GetUserDetail(ctx context.Context, userID int) (*usermodule.UserDetailResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.GetUser(ctx, userID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) ListUsers(ctx context.Context, req *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.ListUsers(ctx, req) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) UpdateUser(ctx context.Context, req *usermodule.UpdateUserReq, userID int) (*usermodule.UserResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.UpdateUser(ctx, req, userID) + } + return nil, missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) AssignRole(ctx context.Context, userID, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignUserRole(ctx, userID, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) RemoveRole(ctx context.Context, userID, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveUserRole(ctx, userID, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) AssignPermissions(ctx context.Context, req *usermodule.AssignUserPermissionReq, userID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignUserPermissions(ctx, userID, req) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) RemovePermissions(ctx context.Context, req *usermodule.RemoveUserPermissionReq, userID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveUserPermissions(ctx, userID, req) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) AssignContainer(ctx context.Context, userID, containerID, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignUserContainer(ctx, userID, containerID, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) RemoveContainer(ctx context.Context, userID, containerID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveUserContainer(ctx, userID, containerID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) AssignDataset(ctx context.Context, userID, datasetID, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignUserDataset(ctx, userID, datasetID, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) RemoveDataset(ctx context.Context, userID, datasetID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveUserDataset(ctx, userID, datasetID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) AssignProject(ctx context.Context, userID, projectID, roleID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.AssignUserProject(ctx, userID, projectID, roleID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareUserService) RemoveProject(ctx context.Context, userID, projectID int) error { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RemoveUserProject(ctx, userID, projectID) + } + return missingRemoteDependency("iam-service") +} diff --git a/src/app/http_modules.go b/src/app/http_modules.go index 3a542431..91984304 100644 --- a/src/app/http_modules.go +++ b/src/app/http_modules.go @@ -26,6 +26,13 @@ import ( "go.uber.org/fx" ) +func ExecutionInjectionOwnerModules() fx.Option { + return fx.Options( + executionmodule.Module, + injectionmodule.Module, + ) +} + func ProducerHTTPModules() fx.Option { return fx.Options( authmodule.Module, @@ -33,9 +40,8 @@ func ProducerHTTPModules() fx.Option { containermodule.Module, datasetmodule.Module, evaluationmodule.Module, - executionmodule.Module, + ExecutionInjectionOwnerModules(), groupmodule.Module, - injectionmodule.Module, labelmodule.Module, metricmodule.Module, notificationmodule.Module, diff --git a/src/app/iam/options.go b/src/app/iam/options.go new file mode 100644 index 00000000..48919e64 --- /dev/null +++ b/src/app/iam/options.go @@ -0,0 +1,35 @@ +package iamapp + +import ( + "aegis/app" + grpciaminterface "aegis/interface/grpciam" + "aegis/internalclient/resourceclient" + "aegis/middleware" + authmodule "aegis/module/auth" + rbacmodule "aegis/module/rbac" + teammodule "aegis/module/team" + usermodule "aegis/module/user" + + "go.uber.org/fx" +) + +// Options builds the dedicated IAM service runtime. +func Options(confPath string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.RequireConfiguredTargets( + "iam-service", + app.RequiredConfigTarget{Name: "resource-service", PrimaryKey: "clients.resource.target", LegacyKey: "resource.grpc.target"}, + ), + resourceclient.Module, + teammodule.RemoteProjectReaderOption(), + authmodule.Module, + rbacmodule.Module, + teammodule.Module, + usermodule.Module, + fx.Provide(middleware.NewService), + grpciaminterface.Module, + ) +} diff --git a/src/app/orchestrator/options.go b/src/app/orchestrator/options.go new file mode 100644 index 00000000..54a0487e --- /dev/null +++ b/src/app/orchestrator/options.go @@ -0,0 +1,29 @@ +package orchestratorapp + +import ( + "aegis/app" + grpcorchestratorinterface "aegis/interface/grpcorchestrator" + groupmodule "aegis/module/group" + metricmodule "aegis/module/metric" + notificationmodule "aegis/module/notification" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + + "go.uber.org/fx" +) + +// Options builds the dedicated orchestrator service runtime. +func Options(confPath string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.ExecutionInjectionOwnerModules(), + groupmodule.Module, + metricmodule.Module, + notificationmodule.Module, + taskmodule.Module, + tracemodule.Module, + grpcorchestratorinterface.Module, + ) +} diff --git a/src/app/producer.go b/src/app/producer.go index a504a336..40c17939 100644 --- a/src/app/producer.go +++ b/src/app/producer.go @@ -1,22 +1,10 @@ package app -import ( - chaosinfra "aegis/infra/chaos" - k8sinfra "aegis/infra/k8s" - httpinterface "aegis/interface/http" - - "go.uber.org/fx" -) +import "go.uber.org/fx" func ProducerOptions(confPath string, port string) fx.Option { return fx.Options( CommonOptions(confPath), - chaosinfra.Module, - k8sinfra.Module, - fx.Provide(newProducerInitializer), - ProducerHTTPModules(), - fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), - httpinterface.Module, - fx.Invoke(registerProducerInitialization), + ProducerCompatibilityOptions(port), ) } diff --git a/src/app/remote_require.go b/src/app/remote_require.go new file mode 100644 index 00000000..3937eb31 --- /dev/null +++ b/src/app/remote_require.go @@ -0,0 +1,58 @@ +package app + +import ( + "context" + "fmt" + "strings" + + "aegis/config" + + "go.uber.org/fx" +) + +type RequiredConfigTarget struct { + Name string + PrimaryKey string + LegacyKey string +} + +func RequireConfiguredTargets(component string, targets ...RequiredConfigTarget) fx.Option { + return fx.Invoke(func(lc fx.Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(context.Context) error { + missing := missingRequiredTargets(targets...) + if len(missing) == 0 { + return nil + } + return fmt.Errorf("%s requires configured internal client targets: %s", component, strings.Join(missing, ", ")) + }, + }) + }) +} + +func missingRequiredTargets(targets ...RequiredConfigTarget) []string { + missing := make([]string, 0) + for _, target := range targets { + if target.PrimaryKey == "" { + continue + } + + primaryValue := strings.TrimSpace(config.GetString(target.PrimaryKey)) + legacyValue := strings.TrimSpace(config.GetString(target.LegacyKey)) + if primaryValue != "" || legacyValue != "" { + continue + } + + label := target.Name + if label == "" { + label = target.PrimaryKey + } + if target.LegacyKey != "" { + label = fmt.Sprintf("%s (%s or %s)", label, target.PrimaryKey, target.LegacyKey) + } else { + label = fmt.Sprintf("%s (%s)", label, target.PrimaryKey) + } + missing = append(missing, label) + } + return missing +} diff --git a/src/app/remote_require_test.go b/src/app/remote_require_test.go new file mode 100644 index 00000000..7f8ca783 --- /dev/null +++ b/src/app/remote_require_test.go @@ -0,0 +1,52 @@ +package app + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestMissingRequiredTargets(t *testing.T) { + primaryKey := "clients.runtime.target" + legacyKey := "runtime_worker.grpc.target" + + originalPrimary := viper.Get(primaryKey) + originalLegacy := viper.Get(legacyKey) + t.Cleanup(func() { + viper.Set(primaryKey, originalPrimary) + viper.Set(legacyKey, originalLegacy) + }) + + viper.Set(primaryKey, "") + viper.Set(legacyKey, "") + + missing := missingRequiredTargets(RequiredConfigTarget{ + Name: "runtime-worker-service", + PrimaryKey: primaryKey, + LegacyKey: legacyKey, + }) + if len(missing) != 1 { + t.Fatalf("expected 1 missing target, got %d: %v", len(missing), missing) + } + + viper.Set(primaryKey, "127.0.0.1:9094") + missing = missingRequiredTargets(RequiredConfigTarget{ + Name: "runtime-worker-service", + PrimaryKey: primaryKey, + LegacyKey: legacyKey, + }) + if len(missing) != 0 { + t.Fatalf("expected no missing target when primary key is set, got %v", missing) + } + + viper.Set(primaryKey, "") + viper.Set(legacyKey, "127.0.0.1:9094") + missing = missingRequiredTargets(RequiredConfigTarget{ + Name: "runtime-worker-service", + PrimaryKey: primaryKey, + LegacyKey: legacyKey, + }) + if len(missing) != 0 { + t.Fatalf("expected no missing target when legacy key is set, got %v", missing) + } +} diff --git a/src/app/resource/options.go b/src/app/resource/options.go new file mode 100644 index 00000000..e58bfbbd --- /dev/null +++ b/src/app/resource/options.go @@ -0,0 +1,38 @@ +package resourceapp + +import ( + "aegis/app" + grpcresourceinterface "aegis/interface/grpcresource" + "aegis/internalclient/orchestratorclient" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + labelmodule "aegis/module/label" + projectmodule "aegis/module/project" + + "go.uber.org/fx" +) + +// Options builds the dedicated resource service runtime. +func Options(confPath string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.RequireConfiguredTargets( + "resource-service", + app.RequiredConfigTarget{Name: "orchestrator-service", PrimaryKey: "clients.orchestrator.target", LegacyKey: "orchestrator.grpc.target"}, + ), + orchestratorclient.Module, + evaluationmodule.RemoteQueryOption(), + projectmodule.RemoteStatisticsOption(), + chaossystemmodule.Module, + containermodule.Module, + datasetmodule.Module, + evaluationmodule.Module, + labelmodule.Module, + projectmodule.Module, + grpcresourceinterface.Module, + ) +} diff --git a/src/app/runtime/options.go b/src/app/runtime/options.go new file mode 100644 index 00000000..b2e4965a --- /dev/null +++ b/src/app/runtime/options.go @@ -0,0 +1,25 @@ +package runtimeapp + +import ( + "aegis/app" + "aegis/service/consumer" + + "go.uber.org/fx" +) + +// Options builds the dedicated runtime-worker-service runtime. +func Options(confPath string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.CoordinationOptions(), + app.BuildInfraOptions(), + app.RuntimeWorkerStackOptions(), + consumer.RemoteOwnerOptions(), + app.RequireConfiguredTargets( + "runtime-worker-service", + app.RequiredConfigTarget{Name: "orchestrator-service", PrimaryKey: "clients.orchestrator.target", LegacyKey: "orchestrator.grpc.target"}, + ), + ) +} diff --git a/src/app/runtime_stack.go b/src/app/runtime_stack.go new file mode 100644 index 00000000..2b0c85c2 --- /dev/null +++ b/src/app/runtime_stack.go @@ -0,0 +1,47 @@ +package app + +import ( + chaosinfra "aegis/infra/chaos" + k8sinfra "aegis/infra/k8s" + runtimeinfra "aegis/infra/runtime" + controllerinterface "aegis/interface/controller" + grpcruntimeinterface "aegis/interface/grpcruntime" + receiverinterface "aegis/interface/receiver" + workerinterface "aegis/interface/worker" + "aegis/internalclient/orchestratorclient" + "aegis/service/consumer" + + "go.uber.org/fx" +) + +func RuntimeWorkerStackOptions() fx.Option { + return fx.Options( + runtimeinfra.Module, + chaosinfra.Module, + k8sinfra.Module, + orchestratorclient.Module, + RuntimeWorkerProviderOptions(), + RuntimeWorkerInterfaceOptions(), + ) +} + +func RuntimeWorkerProviderOptions() fx.Option { + return fx.Provide( + consumer.NewMonitor, + fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), + fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), + fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), + consumer.NewFaultBatchManager, + consumer.NewExecutionOwner, + consumer.NewInjectionOwner, + ) +} + +func RuntimeWorkerInterfaceOptions() fx.Option { + return fx.Options( + workerinterface.Module, + controllerinterface.Module, + grpcruntimeinterface.Module, + receiverinterface.Module, + ) +} diff --git a/src/app/service_entrypoints_test.go b/src/app/service_entrypoints_test.go new file mode 100644 index 00000000..1ea4e38f --- /dev/null +++ b/src/app/service_entrypoints_test.go @@ -0,0 +1,390 @@ +package app_test + +import ( + "context" + "fmt" + "net" + "net/http" + "testing" + "time" + + "aegis/app" + gatewayapp "aegis/app/gateway" + iamapp "aegis/app/iam" + orchestratorapp "aegis/app/orchestrator" + resourceapp "aegis/app/resource" + runtimeapp "aegis/app/runtime" + systemapp "aegis/app/system" + buildkitinfra "aegis/infra/buildkit" + etcdinfra "aegis/infra/etcd" + harborinfra "aegis/infra/harbor" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + lokiinfra "aegis/infra/loki" + redisinfra "aegis/infra/redis" + controllerinterface "aegis/interface/controller" + httpinterface "aegis/interface/http" + receiverinterface "aegis/interface/receiver" + workerinterface "aegis/interface/worker" + resourcev1 "aegis/proto/resource/v1" + runtimev1 "aegis/proto/runtime/v1" + systemv1 "aegis/proto/system/v1" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/redis/go-redis/v9" + "github.com/spf13/viper" + clientv3 "go.etcd.io/etcd/client/v3" + "go.opentelemetry.io/otel/sdk/trace" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "k8s.io/client-go/rest" +) + +func newSmokeDB(t *testing.T) (*gorm.DB, func()) { + t.Helper() + + sqlDB, _, err := sqlmock.New() + if err != nil { + t.Fatalf("create sqlmock: %v", err) + } + + db, err := gorm.Open(mysql.New(mysql.Config{ + Conn: sqlDB, + SkipInitializeWithVersion: true, + }), &gorm.Config{}) + if err != nil { + _ = sqlDB.Close() + t.Fatalf("open gorm db: %v", err) + } + + return db, func() { + _ = sqlDB.Close() + } +} + +func newDedicatedServiceReplacements(t *testing.T) (fx.Option, func()) { + t.Helper() + + db, cleanupDB := newSmokeDB(t) + redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}) + redisGateway := redisinfra.NewGateway(redisClient) + etcdClient := &clientv3.Client{} + etcdGateway := etcdinfra.NewGateway(etcdClient) + traceProvider := trace.NewTracerProvider() + controller := &k8sinfra.Controller{} + k8sGateway := k8sinfra.NewGateway(controller) + + return fx.Replace( + db, + redisGateway, + redisClient, + etcdGateway, + etcdClient, + &lokiinfra.Client{}, + traceProvider, + &rest.Config{}, + controller, + k8sGateway, + harborinfra.NewGateway(), + helminfra.NewGateway(), + buildkitinfra.NewGateway(), + &app.ProducerInitializer{StartFunc: func(context.Context) error { return nil }}, + &workerinterface.Lifecycle{StartFunc: func(context.Context) error { return nil }}, + &controllerinterface.Lifecycle{RunFunc: func(context.Context, context.CancelFunc) error { return nil }}, + &receiverinterface.Lifecycle{StartFunc: func(context.Context) error { return nil }}, + ), func() { + _ = redisClient.Close() + _ = traceProvider.Shutdown(context.Background()) + cleanupDB() + } +} + +func reserveLoopbackAddr(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen on loopback: %v", err) + } + addr := listener.Addr().String() + if err := listener.Close(); err != nil { + t.Fatalf("close reserved listener: %v", err) + } + return addr +} + +func setConfigValue(t *testing.T, key string, value any) { + t.Helper() + + original := viper.Get(key) + viper.Set(key, value) + t.Cleanup(func() { + viper.Set(key, original) + }) +} + +func waitForHTTPStatus(t *testing.T, client *http.Client, method, url string, want int) { + t.Helper() + + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + req, err := http.NewRequest(method, url, nil) + if err != nil { + t.Fatalf("create request %s %s: %v", method, url, err) + } + + resp, err := client.Do(req) + if err == nil { + _ = resp.Body.Close() + if resp.StatusCode == want { + return + } + } + time.Sleep(50 * time.Millisecond) + } + + req, _ := http.NewRequest(method, url, nil) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request %s %s failed: %v", method, url, err) + } + defer func() { + _ = resp.Body.Close() + }() + t.Fatalf("expected %d from %s %s, got %d", want, method, url, resp.StatusCode) +} + +func waitForRuntimePing(t *testing.T, addr string) { + t.Helper() + + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("create runtime grpc client: %v", err) + } + defer func() { + _ = conn.Close() + }() + + client := runtimev1.NewRuntimeServiceClient(conn) + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := client.Ping(context.Background(), &runtimev1.PingRequest{}) + if err == nil && resp.GetService() != "" { + return + } + time.Sleep(50 * time.Millisecond) + } + + resp, err := client.GetRuntimeStatus(context.Background(), &runtimev1.RuntimeStatusRequest{}) + if err != nil { + t.Fatalf("runtime grpc request failed: %v", err) + } + if resp.GetService() == "" { + t.Fatalf("runtime status missing service name: %+v", resp) + } +} + +func waitForResourcePing(t *testing.T, addr string) { + t.Helper() + + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("create resource grpc client: %v", err) + } + defer func() { + _ = conn.Close() + }() + + client := resourcev1.NewResourceServiceClient(conn) + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := client.Ping(context.Background(), &resourcev1.PingRequest{}) + if err == nil && resp.GetService() != "" { + return + } + time.Sleep(50 * time.Millisecond) + } + + resp, err := client.Ping(context.Background(), &resourcev1.PingRequest{}) + if err != nil { + t.Fatalf("resource grpc request failed: %v", err) + } + if resp.GetService() == "" { + t.Fatalf("resource ping missing service name: %+v", resp) + } +} + +func waitForSystemPing(t *testing.T, addr string) { + t.Helper() + + conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("create system grpc client: %v", err) + } + defer func() { + _ = conn.Close() + }() + + client := systemv1.NewSystemServiceClient(conn) + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + resp, err := client.Ping(context.Background(), &systemv1.PingRequest{}) + if err == nil && resp.GetService() != "" { + return + } + time.Sleep(50 * time.Millisecond) + } + + resp, err := client.Ping(context.Background(), &systemv1.PingRequest{}) + if err != nil { + t.Fatalf("system grpc request failed: %v", err) + } + if resp.GetService() == "" { + t.Fatalf("system ping missing service name: %+v", resp) + } +} + +func TestDedicatedServiceOptionsValidate(t *testing.T) { + for _, tc := range []struct { + name string + option fx.Option + }{ + {name: "gateway", option: gatewayapp.Options("..", "0")}, + {name: "runtime", option: runtimeapp.Options("..")}, + {name: "resource", option: resourceapp.Options("..")}, + {name: "system", option: systemapp.Options("..")}, + {name: "iam", option: iamapp.Options("..")}, + {name: "orchestrator", option: orchestratorapp.Options("..")}, + } { + t.Run(tc.name, func(t *testing.T) { + if err := fx.ValidateApp(tc.option); err != nil { + t.Fatalf("validate %s app: %v", tc.name, err) + } + }) + } +} + +func TestAPIGatewayStandaloneHTTPIntegrationSmoke(t *testing.T) { + replacements, cleanup := newDedicatedServiceReplacements(t) + defer cleanup() + + setConfigValue(t, "clients.iam.target", reserveLoopbackAddr(t)) + setConfigValue(t, "clients.orchestrator.target", reserveLoopbackAddr(t)) + setConfigValue(t, "clients.resource.target", reserveLoopbackAddr(t)) + setConfigValue(t, "clients.system.target", reserveLoopbackAddr(t)) + + addr := reserveLoopbackAddr(t) + appInstance := fx.New( + gatewayapp.Options("..", "0"), + replacements, + fx.Replace(httpinterface.ServerConfig{Addr: addr}), + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := appInstance.Start(startCtx); err != nil { + t.Fatalf("gateway app start failed: %v", err) + } + defer func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := appInstance.Stop(stopCtx); err != nil { + t.Fatalf("gateway app stop failed: %v", err) + } + }() + + client := &http.Client{Timeout: time.Second} + baseURL := fmt.Sprintf("http://%s", addr) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) +} + +func TestRuntimeWorkerStandaloneGRPCIntegrationSmoke(t *testing.T) { + replacements, cleanup := newDedicatedServiceReplacements(t) + defer cleanup() + + setConfigValue(t, "clients.orchestrator.target", reserveLoopbackAddr(t)) + addr := reserveLoopbackAddr(t) + setConfigValue(t, "runtime_worker.grpc.addr", addr) + + appInstance := fx.New( + runtimeapp.Options(".."), + replacements, + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := appInstance.Start(startCtx); err != nil { + t.Fatalf("runtime app start failed: %v", err) + } + defer func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := appInstance.Stop(stopCtx); err != nil { + t.Fatalf("runtime app stop failed: %v", err) + } + }() + + waitForRuntimePing(t, addr) +} + +func TestResourceServiceStandaloneGRPCIntegrationSmoke(t *testing.T) { + replacements, cleanup := newDedicatedServiceReplacements(t) + defer cleanup() + + setConfigValue(t, "clients.orchestrator.target", reserveLoopbackAddr(t)) + addr := reserveLoopbackAddr(t) + setConfigValue(t, "resource.grpc.addr", addr) + + appInstance := fx.New( + resourceapp.Options(".."), + replacements, + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := appInstance.Start(startCtx); err != nil { + t.Fatalf("resource app start failed: %v", err) + } + defer func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := appInstance.Stop(stopCtx); err != nil { + t.Fatalf("resource app stop failed: %v", err) + } + }() + + waitForResourcePing(t, addr) +} + +func TestSystemServiceStandaloneGRPCIntegrationSmoke(t *testing.T) { + replacements, cleanup := newDedicatedServiceReplacements(t) + defer cleanup() + + setConfigValue(t, "clients.runtime.target", reserveLoopbackAddr(t)) + addr := reserveLoopbackAddr(t) + setConfigValue(t, "system.grpc.addr", addr) + + appInstance := fx.New( + systemapp.Options(".."), + replacements, + ) + + startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer startCancel() + if err := appInstance.Start(startCtx); err != nil { + t.Fatalf("system app start failed: %v", err) + } + defer func() { + stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer stopCancel() + if err := appInstance.Stop(stopCtx); err != nil { + t.Fatalf("system app stop failed: %v", err) + } + }() + + waitForSystemPing(t, addr) +} diff --git a/src/app/startup_smoke_test.go b/src/app/startup_smoke_test.go index 9c594984..997ccc7c 100644 --- a/src/app/startup_smoke_test.go +++ b/src/app/startup_smoke_test.go @@ -140,8 +140,8 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, controllerLifecycle, receiverLifecycle, ), func() { - redisClient.Close() - traceProvider.Shutdown(context.Background()) + _ = redisClient.Close() + _ = traceProvider.Shutdown(context.Background()) cleanupDB() } } @@ -202,7 +202,9 @@ func waitForHTTPStatus(t *testing.T, client *http.Client, method, url string, wa if err != nil { t.Fatalf("request %s %s failed: %v", method, url, err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() t.Fatalf("expected %d from %s %s, got %d", want, method, url, resp.StatusCode) } diff --git a/src/app/system/options.go b/src/app/system/options.go new file mode 100644 index 00000000..da9ccf20 --- /dev/null +++ b/src/app/system/options.go @@ -0,0 +1,33 @@ +package systemapp + +import ( + "aegis/app" + k8sinfra "aegis/infra/k8s" + grpcsysteminterface "aegis/interface/grpcsystem" + "aegis/internalclient/runtimeclient" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + + "go.uber.org/fx" +) + +// Options builds the dedicated system service runtime. +func Options(confPath string) fx.Option { + return fx.Options( + app.BaseOptions(confPath), + app.ObserveOptions(), + app.DataOptions(), + app.CoordinationOptions(), + app.BuildInfraOptions(), + app.RequireConfiguredTargets( + "system-service", + app.RequiredConfigTarget{Name: "runtime-worker-service", PrimaryKey: "clients.runtime.target", LegacyKey: "runtime_worker.grpc.target"}, + ), + systemmodule.RemoteRuntimeQueryOption(), + k8sinfra.Module, + runtimeclient.Module, + systemmodule.Module, + systemmetricmodule.Module, + grpcsysteminterface.Module, + ) +} diff --git a/src/cmd/aegisctl/client/client.go b/src/cmd/aegisctl/client/client.go index 27faafac..e265ca57 100644 --- a/src/cmd/aegisctl/client/client.go +++ b/src/cmd/aegisctl/client/client.go @@ -94,7 +94,9 @@ func (c *Client) doRequest(method, path string, body any, headers map[string]str if err != nil { return fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() respBody, err := io.ReadAll(resp.Body) if err != nil { diff --git a/src/cmd/aegisctl/client/sse.go b/src/cmd/aegisctl/client/sse.go index b16d4bee..1a83961c 100644 --- a/src/cmd/aegisctl/client/sse.go +++ b/src/cmd/aegisctl/client/sse.go @@ -92,7 +92,9 @@ func (r *SSEReader) readStream(ctx context.Context, events chan<- SSEEvent) erro if err != nil { return fmt.Errorf("SSE connect: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("SSE server returned status %d", resp.StatusCode) diff --git a/src/cmd/aegisctl/client/ws.go b/src/cmd/aegisctl/client/ws.go index a4fb5e94..1dcfdec2 100644 --- a/src/cmd/aegisctl/client/ws.go +++ b/src/cmd/aegisctl/client/ws.go @@ -55,14 +55,16 @@ func (r *WSReader) Stream(ctx context.Context) (<-chan string, <-chan error) { errs <- fmt.Errorf("websocket connect: %w", err) return } - defer conn.Close() + defer func() { + _ = conn.Close() + }() // Close the connection when context is cancelled. go func() { <-ctx.Done() - conn.WriteMessage(websocket.CloseMessage, + _ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - conn.Close() + _ = conn.Close() }() for { diff --git a/src/cmd/aegisctl/cmd/inject.go b/src/cmd/aegisctl/cmd/inject.go index c48c73d3..98f2bd83 100644 --- a/src/cmd/aegisctl/cmd/inject.go +++ b/src/cmd/aegisctl/cmd/inject.go @@ -399,7 +399,9 @@ var injectDownloadCmd = &cobra.Command{ if err != nil { return fmt.Errorf("download request failed: %w", err) } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body) @@ -410,7 +412,9 @@ var injectDownloadCmd = &cobra.Command{ if err != nil { return fmt.Errorf("create output file: %w", err) } - defer f.Close() + defer func() { + _ = f.Close() + }() n, err := io.Copy(f, resp.Body) if err != nil { diff --git a/src/cmd/aegisctl/cmd/wait.go b/src/cmd/aegisctl/cmd/wait.go index faf7b818..317bdf32 100644 --- a/src/cmd/aegisctl/cmd/wait.go +++ b/src/cmd/aegisctl/cmd/wait.go @@ -121,11 +121,6 @@ func detectResourceType(c *client.Client, id string) (string, error) { return "", fmt.Errorf("lookup trace %s: %w", id, err) } -// stateResponse is a minimal struct to extract the state field from API responses. -type stateResponse struct { - State string `json:"state"` -} - // pollState fetches the current state and full data for the given resource. func pollState(c *client.Client, resourceType, id string) (string, any, error) { var path string diff --git a/src/cmd/api-gateway/main.go b/src/cmd/api-gateway/main.go new file mode 100644 index 00000000..2575528b --- /dev/null +++ b/src/cmd/api-gateway/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "flag" + + gatewayapp "aegis/app/gateway" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + port := flag.String("port", "8080", "port to run the API gateway on") + flag.Parse() + + fx.New(gatewayapp.Options(*conf, *port)).Run() +} diff --git a/src/cmd/iam-service/main.go b/src/cmd/iam-service/main.go new file mode 100644 index 00000000..fa6713bd --- /dev/null +++ b/src/cmd/iam-service/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + + iamapp "aegis/app/iam" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + flag.Parse() + + fx.New(iamapp.Options(*conf)).Run() +} diff --git a/src/cmd/orchestrator-service/main.go b/src/cmd/orchestrator-service/main.go new file mode 100644 index 00000000..c7345957 --- /dev/null +++ b/src/cmd/orchestrator-service/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + + orchestratorapp "aegis/app/orchestrator" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + flag.Parse() + + fx.New(orchestratorapp.Options(*conf)).Run() +} diff --git a/src/cmd/resource-service/main.go b/src/cmd/resource-service/main.go new file mode 100644 index 00000000..bdab26c7 --- /dev/null +++ b/src/cmd/resource-service/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + + resourceapp "aegis/app/resource" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + flag.Parse() + + fx.New(resourceapp.Options(*conf)).Run() +} diff --git a/src/cmd/runtime-worker-service/main.go b/src/cmd/runtime-worker-service/main.go new file mode 100644 index 00000000..8a122a6d --- /dev/null +++ b/src/cmd/runtime-worker-service/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + + runtimeapp "aegis/app/runtime" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + flag.Parse() + + fx.New(runtimeapp.Options(*conf)).Run() +} diff --git a/src/cmd/system-service/main.go b/src/cmd/system-service/main.go new file mode 100644 index 00000000..b23eeabe --- /dev/null +++ b/src/cmd/system-service/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "flag" + + systemapp "aegis/app/system" + + "go.uber.org/fx" +) + +func main() { + conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") + flag.Parse() + + fx.New(systemapp.Options(*conf)).Run() +} diff --git a/src/config.dev.toml b/src/config.dev.toml index e13e2805..56bf931a 100644 --- a/src/config.dev.toml +++ b/src/config.dev.toml @@ -60,6 +60,36 @@ experiment_storage_path = "/mnt/jfs/experiment_storage" [buildkit] address = "localhost:1234" +[clients.iam] +target = "localhost:9091" + +[clients.orchestrator] +target = "localhost:9092" + +[clients.resource] +target = "localhost:9093" + +[clients.runtime] +target = "localhost:9094" + +[clients.system] +target = "localhost:9095" + +[iam.grpc] +addr = ":9091" + +[orchestrator.grpc] +addr = ":9092" + +[resource.grpc] +addr = ":9093" + +[runtime_worker.grpc] +addr = ":9094" + +[system.grpc] +addr = ":9095" + [loki] address = "http://10.10.10.161:3100" timeout = "10s" diff --git a/src/consts/consts.go b/src/consts/consts.go index d1e5ebe0..f74f403e 100644 --- a/src/consts/consts.go +++ b/src/consts/consts.go @@ -187,6 +187,15 @@ func (ds DatapackState) MarshalJSON() ([]byte, error) { return json.Marshal(GetDatapackStateName(ds)) } +func (ds *DatapackState) UnmarshalJSON(data []byte) error { + var stateName string + if err := json.Unmarshal(data, &stateName); err != nil { + return err + } + *ds = *GetDatapackStateByName(stateName) + return nil +} + type ExecutionState int const ( diff --git a/src/dto/common.go b/src/dto/common.go index a5ecfa5e..3eb5541f 100644 --- a/src/dto/common.go +++ b/src/dto/common.go @@ -3,8 +3,6 @@ package dto import ( "aegis/consts" "fmt" - "strings" - "time" ) // PaginationInfo represents pagination information in responses @@ -69,74 +67,3 @@ type SortField struct { Field string `json:"field" binding:"required" example:"created_at"` Order string `json:"order" binding:"required,oneof=asc desc" example:"desc"` } - -// validateLabelItemsFiled validates a list of LabelItem structs -func validateLabelItemsFiled(labelItems []LabelItem) error { - for i, label := range labelItems { - if strings.TrimSpace(label.Key) == "" { - return fmt.Errorf("empty label key at index %d", i) - } - if strings.TrimSpace(label.Value) == "" { - return fmt.Errorf("empty label value at index %d", i) - } - } - return nil -} - -// validateLabelField validates a list of label strings in "key:value" format -func validateLabelsField(labelStrs []string) error { - for _, labelStr := range labelStrs { - if strings.TrimSpace(labelStr) == "" { - return fmt.Errorf("labels must not contain empty strings") - } - - parts := strings.SplitN(labelStr, ":", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid label format '%s'. Must be in 'key:value' format", labelStr) - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - if key == "" { - return fmt.Errorf("label key in '%s' cannot be empty", labelStr) - } - - if value == "" { - return fmt.Errorf("label value for key '%s' cannot be empty", key) - } - } - - return nil -} - -// validateStatusField validates a status field pointer -func validateStatusField(statusPtr *consts.StatusType, isMutation bool) error { - if statusPtr == nil { - return nil - } - - status := *statusPtr - - if _, exists := consts.ValidStatuses[status]; !exists { - return fmt.Errorf("invalid status value: %d", status) - } - - if isMutation && status == consts.CommonDeleted { - return fmt.Errorf("status value cannot be set to deleted (%d) directly through this update/create operation", consts.CommonDeleted) - } - - return nil -} - -// validateTimeField checks if the provided time string is in the specific format -func validateTimeField(timeStr, timeFormat string) error { - if timeStr == "" { - return nil - } - _, err := time.Parse(timeFormat, timeStr) - if err != nil { - return fmt.Errorf("invalid time format: %s", timeStr) - } - return nil -} diff --git a/src/httpx/request_id.go b/src/httpx/request_id.go new file mode 100644 index 00000000..eba9aee5 --- /dev/null +++ b/src/httpx/request_id.go @@ -0,0 +1,119 @@ +package httpx + +import ( + "context" + "strings" + + "github.com/google/uuid" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const ( + RequestIDHeader = "X-Request-Id" + requestIDMetadataKey = "x-request-id" +) + +type requestIDContextKey struct{} + +func NewRequestID() string { + return uuid.NewString() +} + +func WithRequestID(ctx context.Context, requestID string) context.Context { + if ctx == nil { + ctx = context.Background() + } + + requestID = strings.TrimSpace(requestID) + if requestID == "" { + return ctx + } + + return context.WithValue(ctx, requestIDContextKey{}, requestID) +} + +func RequestIDFromContext(ctx context.Context) string { + if ctx == nil { + return "" + } + + if requestID, ok := ctx.Value(requestIDContextKey{}).(string); ok && strings.TrimSpace(requestID) != "" { + return strings.TrimSpace(requestID) + } + + if md, ok := metadata.FromIncomingContext(ctx); ok { + if requestID := firstMetadataValue(md, requestIDMetadataKey); requestID != "" { + return requestID + } + } + + if md, ok := metadata.FromOutgoingContext(ctx); ok { + if requestID := firstMetadataValue(md, requestIDMetadataKey); requestID != "" { + return requestID + } + } + + return "" +} + +func WithOutgoingRequestID(ctx context.Context) context.Context { + requestID := RequestIDFromContext(ctx) + if requestID == "" { + return ctx + } + + md, _ := metadata.FromOutgoingContext(ctx) + md = md.Copy() + md.Set(requestIDMetadataKey, requestID) + return metadata.NewOutgoingContext(WithRequestID(ctx, requestID), md) +} + +func UnaryClientRequestIDInterceptor() grpc.UnaryClientInterceptor { + return func( + ctx context.Context, + method string, + req any, + reply any, + cc *grpc.ClientConn, + invoker grpc.UnaryInvoker, + opts ...grpc.CallOption, + ) error { + return invoker(WithOutgoingRequestID(ctx), method, req, reply, cc, opts...) + } +} + +func UnaryServerRequestIDInterceptor() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req any, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (any, error) { + requestID := RequestIDFromContext(ctx) + if requestID == "" { + requestID = NewRequestID() + } + + ctx = WithRequestID(ctx, requestID) + if err := grpc.SetHeader(ctx, metadata.Pairs(requestIDMetadataKey, requestID)); err != nil { + return nil, err + } + + return handler(ctx, req) + } +} + +func firstMetadataValue(md metadata.MD, key string) string { + if md == nil { + return "" + } + + values := md.Get(key) + for _, value := range values { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + return "" +} diff --git a/src/httpx/request_id_test.go b/src/httpx/request_id_test.go new file mode 100644 index 00000000..5a7050e8 --- /dev/null +++ b/src/httpx/request_id_test.go @@ -0,0 +1,48 @@ +package httpx + +import ( + "context" + "testing" + + "google.golang.org/grpc/metadata" +) + +func TestRequestIDFromContextPrefersLocalValue(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-request-id", "from-md")) + ctx = WithRequestID(ctx, "from-context") + + if got := RequestIDFromContext(ctx); got != "from-context" { + t.Fatalf("RequestIDFromContext() = %q, want %q", got, "from-context") + } +} + +func TestRequestIDFromContextFallsBackToIncomingMetadata(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), metadata.Pairs("x-request-id", "from-md")) + + if got := RequestIDFromContext(ctx); got != "from-md" { + t.Fatalf("RequestIDFromContext() = %q, want %q", got, "from-md") + } +} + +func TestWithOutgoingRequestIDCopiesValueToMetadata(t *testing.T) { + ctx := WithRequestID(context.Background(), "req-123") + + outgoing := WithOutgoingRequestID(ctx) + md, ok := metadata.FromOutgoingContext(outgoing) + if !ok { + t.Fatal("expected outgoing metadata to exist") + } + + values := md.Get("x-request-id") + if len(values) != 1 || values[0] != "req-123" { + t.Fatalf("outgoing request id = %v, want [req-123]", values) + } +} + +func TestRequestIDFromContextFallsBackToOutgoingMetadata(t *testing.T) { + ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("x-request-id", "outgoing-md")) + + if got := RequestIDFromContext(ctx); got != "outgoing-md" { + t.Fatalf("RequestIDFromContext() = %q, want %q", got, "outgoing-md") + } +} diff --git a/src/infra/redis/gateway.go b/src/infra/redis/gateway.go index 3f817240..9d5846c1 100644 --- a/src/infra/redis/gateway.go +++ b/src/infra/redis/gateway.go @@ -98,6 +98,14 @@ func (g *Gateway) ListRange(ctx context.Context, key string) ([]string, error) { return result, nil } +func (g *Gateway) ListLength(ctx context.Context, key string) (int64, error) { + result, err := g.clientOrInit().LLen(ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to get list length for key '%s': %w", key, err) + } + return result, nil +} + func (g *Gateway) SetMembers(ctx context.Context, key string) ([]string, error) { result, err := g.clientOrInit().SMembers(ctx, key).Result() if err != nil { @@ -181,6 +189,14 @@ func (g *Gateway) ZRangeByScore(ctx context.Context, key, min, max string) ([]st return result, nil } +func (g *Gateway) SortedSetCard(ctx context.Context, key string) (int64, error) { + result, err := g.clientOrInit().ZCard(ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to get sorted set size for key '%s': %w", key, err) + } + return result, nil +} + func (g *Gateway) ZAdd(ctx context.Context, key string, member redis.Z) error { if err := g.clientOrInit().ZAdd(ctx, key, member).Err(); err != nil { return fmt.Errorf("failed to add sorted set member for key '%s': %w", key, err) @@ -203,6 +219,14 @@ func (g *Gateway) SetRemove(ctx context.Context, key string, members ...any) (in return result, nil } +func (g *Gateway) SetCard(ctx context.Context, key string) (int64, error) { + result, err := g.clientOrInit().SCard(ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to get set size for key '%s': %w", key, err) + } + return result, nil +} + func (g *Gateway) XAdd(ctx context.Context, stream string, values map[string]any) error { _, err := g.clientOrInit().XAdd(ctx, &redis.XAddArgs{ Stream: stream, @@ -232,6 +256,25 @@ func (g *Gateway) Ping(ctx context.Context) error { return nil } +func (g *Gateway) HashLength(ctx context.Context, key string) (int64, error) { + result, err := g.clientOrInit().HLen(ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to get hash length for key '%s': %w", key, err) + } + return result, nil +} + +func (g *Gateway) GetInt64(ctx context.Context, key string) (int64, error) { + result, err := g.clientOrInit().Get(ctx, key).Int64() + if err == redis.Nil { + return 0, nil + } + if err != nil { + return 0, fmt.Errorf("failed to get int64 value for key '%s': %w", key, err) + } + return result, nil +} + func (g *Gateway) Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error { return g.clientOrInit().Watch(ctx, fn, keys...) } diff --git a/src/infra/redis/task_queue.go b/src/infra/redis/task_queue.go index fef6e487..2fc4d08a 100644 --- a/src/infra/redis/task_queue.go +++ b/src/infra/redis/task_queue.go @@ -22,6 +22,14 @@ const ( MaxConcurrency = 20 ) +type TaskQueueStats struct { + ReadyCount int64 + DelayedCount int64 + DeadCount int64 + IndexedCount int64 + ConcurrencyCount int64 +} + func (g *Gateway) SubmitImmediateTask(ctx context.Context, taskData []byte, taskID string) error { redisCli := g.clientOrInit() if err := redisCli.LPush(ctx, ReadyQueueKey, taskData).Err(); err != nil { @@ -42,10 +50,18 @@ func (g *Gateway) GetTask(ctx context.Context, timeout time.Duration) (string, e func (g *Gateway) HandleFailedTask(ctx context.Context, taskData []byte, backoffSec int) error { deadLetterTime := time.Now().Add(time.Duration(backoffSec) * time.Second).Unix() redisCli := g.clientOrInit() - return redisCli.ZAdd(ctx, DeadLetterKey, redis.Z{ + if err := redisCli.ZAdd(ctx, DeadLetterKey, redis.Z{ Score: float64(deadLetterTime), Member: taskData, - }).Err() + }).Err(); err != nil { + return err + } + + var task dto.UnifiedTask + if err := json.Unmarshal(taskData, &task); err == nil && task.TaskID != "" { + return redisCli.HSet(ctx, TaskIndexKey, task.TaskID, DeadLetterKey).Err() + } + return nil } func (g *Gateway) SubmitDelayedTask(ctx context.Context, taskData []byte, taskID string, executeTime int64) error { @@ -87,10 +103,19 @@ func (g *Gateway) ProcessDelayedTasks(ctx context.Context) ([]string, error) { } func (g *Gateway) HandleCronRescheduleFailure(ctx context.Context, taskData []byte) error { - return g.clientOrInit().ZAdd(ctx, DeadLetterKey, redis.Z{ + redisCli := g.clientOrInit() + if err := redisCli.ZAdd(ctx, DeadLetterKey, redis.Z{ Score: float64(time.Now().Unix()), Member: taskData, - }).Err() + }).Err(); err != nil { + return err + } + + var task dto.UnifiedTask + if err := json.Unmarshal(taskData, &task); err == nil && task.TaskID != "" { + return redisCli.HSet(ctx, TaskIndexKey, task.TaskID, DeadLetterKey).Err() + } + return nil } func (g *Gateway) AcquireConcurrencyLock(ctx context.Context) bool { @@ -130,6 +155,24 @@ func (g *Gateway) ListDelayedTasks(ctx context.Context, limit int64) ([]string, return taskDatas, nil } +func (g *Gateway) ListDeadLetterTasks(ctx context.Context, limit int64) ([]string, error) { + deadTasksWithScore, err := g.ZRangeByScoreWithScores(ctx, DeadLetterKey, limit) + if err != nil { + return nil, err + } + + taskDatas := make([]string, 0, len(deadTasksWithScore)) + for _, z := range deadTasksWithScore { + taskData, ok := z.Member.(string) + if !ok { + return nil, fmt.Errorf("invalid dead letter task data") + } + taskDatas = append(taskDatas, taskData) + } + + return taskDatas, nil +} + func (g *Gateway) ListReadyTasks(ctx context.Context) ([]string, error) { return g.ListRange(ctx, ReadyQueueKey) } @@ -192,3 +235,38 @@ func (g *Gateway) RemoveFromZSet(ctx context.Context, key, taskID string) bool { func (g *Gateway) DeleteTaskIndex(ctx context.Context, taskID string) error { return g.clientOrInit().HDel(ctx, TaskIndexKey, taskID).Err() } + +func (g *Gateway) GetTaskQueueStats(ctx context.Context) (TaskQueueStats, error) { + readyCount, err := g.ListLength(ctx, ReadyQueueKey) + if err != nil { + return TaskQueueStats{}, err + } + + delayedCount, err := g.SortedSetCard(ctx, DelayedQueueKey) + if err != nil { + return TaskQueueStats{}, err + } + + deadCount, err := g.SortedSetCard(ctx, DeadLetterKey) + if err != nil { + return TaskQueueStats{}, err + } + + indexedCount, err := g.HashLength(ctx, TaskIndexKey) + if err != nil { + return TaskQueueStats{}, err + } + + concurrencyCount, err := g.GetInt64(ctx, ConcurrencyLockKey) + if err != nil { + return TaskQueueStats{}, err + } + + return TaskQueueStats{ + ReadyCount: readyCount, + DelayedCount: delayedCount, + DeadCount: deadCount, + IndexedCount: indexedCount, + ConcurrencyCount: concurrencyCount, + }, nil +} diff --git a/src/interface/controller/module.go b/src/interface/controller/module.go index 7366a921..c27751a3 100644 --- a/src/interface/controller/module.go +++ b/src/interface/controller/module.go @@ -23,13 +23,15 @@ var Module = fx.Module("controller", type Params struct { fx.In - Controller *k8sinfra.Controller - K8sGateway *k8sinfra.Gateway - RedisGateway *redisinfra.Gateway - DB *gorm.DB - Monitor consumer.NamespaceMonitor - AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` - BatchManager *consumer.FaultBatchManager + Controller *k8sinfra.Controller + K8sGateway *k8sinfra.Gateway + RedisGateway *redisinfra.Gateway + DB *gorm.DB + Monitor consumer.NamespaceMonitor + AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` + BatchManager *consumer.FaultBatchManager + ExecutionOwner consumer.ExecutionOwner + InjectionOwner consumer.InjectionOwner } type Lifecycle struct { @@ -47,7 +49,20 @@ func (r *Lifecycle) start(ctx context.Context, cancel context.CancelFunc) error return r.RunFunc(ctx, cancel) } k8slogger.SetLogger(stdr.New(log.New(os.Stdout, "", log.LstdFlags))) - go r.params.Controller.Initialize(ctx, cancel, consumer.NewHandler(r.params.DB, r.params.Monitor, r.params.AlgoLimiter, r.params.K8sGateway, r.params.RedisGateway, r.params.BatchManager)) + go r.params.Controller.Initialize( + ctx, + cancel, + consumer.NewHandler( + r.params.DB, + r.params.Monitor, + r.params.AlgoLimiter, + r.params.K8sGateway, + r.params.RedisGateway, + r.params.BatchManager, + r.params.ExecutionOwner, + r.params.InjectionOwner, + ), + ) return nil } diff --git a/src/interface/grpciam/lifecycle.go b/src/interface/grpciam/lifecycle.go new file mode 100644 index 00000000..da8ff852 --- /dev/null +++ b/src/interface/grpciam/lifecycle.go @@ -0,0 +1,94 @@ +package grpciaminterface + +import ( + "context" + "fmt" + "net" + + "aegis/config" + "aegis/httpx" + iamv1 "aegis/proto/iam/v1" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const defaultIAMGRPCAddr = ":9091" + +type Lifecycle struct { + server *grpc.Server + addr string + listener net.Listener + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(iamServer *iamServer) (*Lifecycle, error) { + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(httpx.UnaryServerRequestIDInterceptor())) + iamv1.RegisterIAMServiceServer(grpcServer, iamServer) + + healthServer := health.NewServer() + healthServer.SetServingStatus(iamv1.IAMService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + + if config.GetBool("iam.grpc.reflection") { + reflection.Register(grpcServer) + } + + addr := config.GetString("iam.grpc.addr") + if addr == "" { + addr = defaultIAMGRPCAddr + } + + return &Lifecycle{ + server: grpcServer, + addr: addr, + }, nil +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + + listener, err := net.Listen("tcp", r.addr) + if err != nil { + return fmt.Errorf("listen iam grpc on %s: %w", r.addr, err) + } + r.listener = listener + + go func() { + logrus.Infof("Starting IAM gRPC server on %s", r.addr) + if err := r.server.Serve(listener); err != nil { + logrus.Errorf("iam gRPC server error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.server != nil { + r.server.GracefulStop() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return runner.start(ctx) + }, + OnStop: func(ctx context.Context) error { + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/grpciam/module.go b/src/interface/grpciam/module.go new file mode 100644 index 00000000..b52e02a1 --- /dev/null +++ b/src/interface/grpciam/module.go @@ -0,0 +1,11 @@ +package grpciaminterface + +import "go.uber.org/fx" + +var Module = fx.Module("grpc_iam", + fx.Provide( + newIAMServer, + newLifecycle, + ), + fx.Invoke(registerLifecycle), +) diff --git a/src/interface/grpciam/service.go b/src/interface/grpciam/service.go new file mode 100644 index 00000000..4795ef58 --- /dev/null +++ b/src/interface/grpciam/service.go @@ -0,0 +1,979 @@ +package grpciaminterface + +import ( + "context" + "encoding/json" + "errors" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/middleware" + authmodule "aegis/module/auth" + rbacmodule "aegis/module/rbac" + teammodule "aegis/module/team" + usermodule "aegis/module/user" + iamv1 "aegis/proto/iam/v1" + "aegis/utils" + + "github.com/golang-jwt/jwt/v5" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/structpb" +) + +type iamServer struct { + iamv1.UnimplementedIAMServiceServer + auth *authmodule.Service + authAPI authmodule.HandlerService + team teammodule.HandlerService + user usermodule.HandlerService + rbac rbacmodule.HandlerService + middleware middleware.Service +} + +func newIAMServer( + auth *authmodule.Service, + authAPI authmodule.HandlerService, + team teammodule.HandlerService, + user usermodule.HandlerService, + rbac rbacmodule.HandlerService, + middlewareService middleware.Service, +) *iamServer { + return &iamServer{ + auth: auth, + authAPI: authAPI, + team: team, + user: user, + rbac: rbac, + middleware: middlewareService, + } +} + +func (s *iamServer) VerifyToken(ctx context.Context, req *iamv1.VerifyTokenRequest) (*iamv1.VerifyTokenResponse, error) { + if req.GetToken() == "" { + return nil, status.Error(codes.InvalidArgument, "token is required") + } + + claims, err := s.auth.VerifyToken(ctx, req.GetToken()) + if err == nil { + return &iamv1.VerifyTokenResponse{ + Valid: true, + TokenType: "user", + UserId: int64(claims.UserID), + Username: claims.Username, + Email: claims.Email, + IsActive: claims.IsActive, + IsAdmin: claims.IsAdmin, + Roles: claims.Roles, + ExpiresAtUnix: claims.ExpiresAt.Unix(), + AuthType: claims.AuthType, + AccessKeyId: int64(claims.AccessKeyID), + }, nil + } + + serviceClaims, serviceErr := s.auth.VerifyServiceToken(ctx, req.GetToken()) + if serviceErr == nil { + return &iamv1.VerifyTokenResponse{ + Valid: true, + TokenType: "service", + TaskId: serviceClaims.TaskID, + ExpiresAtUnix: serviceClaims.ExpiresAt.Unix(), + }, nil + } + + return nil, status.Error(codes.Unauthenticated, err.Error()) +} + +func (s *iamServer) CheckPermission(ctx context.Context, req *iamv1.CheckPermissionRequest) (*iamv1.CheckPermissionResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + params := &dto.CheckPermissionParams{ + UserID: int(req.GetUserId()), + Action: consts.ActionName(req.GetAction()), + Scope: consts.ResourceScope(req.GetScope()), + ResourceName: consts.ResourceName(req.GetResourceName()), + TeamID: optionalID(req.GetTeamId()), + ProjectID: optionalID(req.GetProjectId()), + ContainerID: optionalID(req.GetContainerId()), + DatasetID: optionalID(req.GetDatasetId()), + } + + allowed, err := s.middleware.CheckUserPermission(ctx, params) + if err != nil { + if errors.Is(err, consts.ErrNotFound) { + return nil, status.Error(codes.NotFound, err.Error()) + } + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.CheckPermissionResponse{Allowed: allowed}, nil +} + +func (s *iamServer) Login(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { + body, err := decodeBody[authmodule.LoginReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.authAPI.Login(ctx, body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) Register(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { + body, err := decodeBody[authmodule.RegisterReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.authAPI.Register(ctx, body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) RefreshToken(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { + body, err := decodeBody[authmodule.TokenRefreshReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.authAPI.RefreshToken(ctx, body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) Logout(ctx context.Context, req *iamv1.LogoutRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetTokenId() == "" || req.GetExpiresAtUnix() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id, token_id, and expires_at_unix are required") + } + claims := &utils.Claims{ + UserID: int(req.GetUserId()), + RegisteredClaims: jwt.RegisteredClaims{ + ID: req.GetTokenId(), + ExpiresAt: jwt.NewNumericDate(time.Unix(req.GetExpiresAtUnix(), 0)), + }, + } + if err := s.authAPI.Logout(ctx, claims); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) ChangePassword(ctx context.Context, req *iamv1.UserBodyRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + body, err := decodeBody[authmodule.ChangePasswordReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.authAPI.ChangePassword(ctx, body, int(req.GetUserId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) GetProfile(ctx context.Context, req *iamv1.UserIDRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + resp, err := s.authAPI.GetProfile(ctx, int(req.GetUserId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) CreateAccessKey(ctx context.Context, req *iamv1.UserBodyRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + body, err := decodeBody[authmodule.CreateAccessKeyReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.authAPI.CreateAccessKey(ctx, int(req.GetUserId()), body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListAccessKeys(ctx context.Context, req *iamv1.UserQueryRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + query, err := decodeQuery[authmodule.ListAccessKeyReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.authAPI.ListAccessKeys(ctx, int(req.GetUserId()), query) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) GetAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + resp, err := s.authAPI.GetAccessKey(ctx, int(req.GetUserId()), int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) DeleteAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.authAPI.DeleteAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) DisableAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.authAPI.DisableAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) EnableAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.authAPI.EnableAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RotateAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + resp, err := s.authAPI.RotateAccessKey(ctx, int(req.GetUserId()), int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) CreateUser(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { + body, err := decodeBody[usermodule.CreateUserReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.user.CreateUser(ctx, body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) DeleteUser(ctx context.Context, req *iamv1.IDRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + if err := s.user.DeleteUser(ctx, int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) GetUser(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.user.GetUserDetail(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListUsers(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { + query, err := decodeQuery[usermodule.ListUserReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.user.ListUsers(ctx, query) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) UpdateUser(ctx context.Context, req *iamv1.UpdateByIDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + body, err := decodeBody[usermodule.UpdateUserReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.user.UpdateUser(ctx, body, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) AssignUserRole(ctx context.Context, req *iamv1.UserRoleBindingRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and role_id are required") + } + if err := s.user.AssignRole(ctx, int(req.GetUserId()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveUserRole(ctx context.Context, req *iamv1.UserRoleBindingRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and role_id are required") + } + if err := s.user.RemoveRole(ctx, int(req.GetUserId()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) AssignUserPermissions(ctx context.Context, req *iamv1.UserBodyRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + body, err := decodeBody[usermodule.AssignUserPermissionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.user.AssignPermissions(ctx, body, int(req.GetUserId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveUserPermissions(ctx context.Context, req *iamv1.UserBodyRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + body, err := decodeBody[usermodule.RemoveUserPermissionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.user.RemovePermissions(ctx, body, int(req.GetUserId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) AssignUserContainer(ctx context.Context, req *iamv1.UserResourceBindingRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetResourceId() <= 0 || req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id, resource_id, and role_id are required") + } + if err := s.user.AssignContainer(ctx, int(req.GetUserId()), int(req.GetResourceId()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveUserContainer(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.user.RemoveContainer(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) AssignUserDataset(ctx context.Context, req *iamv1.UserResourceBindingRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetResourceId() <= 0 || req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id, resource_id, and role_id are required") + } + if err := s.user.AssignDataset(ctx, int(req.GetUserId()), int(req.GetResourceId()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveUserDataset(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.user.RemoveDataset(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) AssignUserProject(ctx context.Context, req *iamv1.UserResourceBindingRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetResourceId() <= 0 || req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id, resource_id, and role_id are required") + } + if err := s.user.AssignProject(ctx, int(req.GetUserId()), int(req.GetResourceId()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveUserProject(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + if err := s.user.RemoveProject(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) CreateRole(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { + body, err := decodeBody[rbacmodule.CreateRoleReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.rbac.CreateRole(ctx, body) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) DeleteRole(ctx context.Context, req *iamv1.IDRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + if err := s.rbac.DeleteRole(ctx, int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) GetRole(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.GetRole(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListRoles(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { + query, err := decodeQuery[rbacmodule.ListRoleReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.rbac.ListRoles(ctx, query) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) UpdateRole(ctx context.Context, req *iamv1.UpdateByIDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + body, err := decodeBody[rbacmodule.UpdateRoleReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.rbac.UpdateRole(ctx, body, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) AssignRolePermissions(ctx context.Context, req *iamv1.RolePermissionsRequest) (*emptypb.Empty, error) { + if req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "role_id is required") + } + if err := validatePositiveInt64s(req.GetPermissionIds(), "permission_ids"); err != nil { + return nil, err + } + if err := s.rbac.AssignRolePermissions(ctx, int64sToInts(req.GetPermissionIds()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveRolePermissions(ctx context.Context, req *iamv1.RolePermissionsRequest) (*emptypb.Empty, error) { + if req.GetRoleId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "role_id is required") + } + if err := validatePositiveInt64s(req.GetPermissionIds(), "permission_ids"); err != nil { + return nil, err + } + if err := s.rbac.RemoveRolePermissions(ctx, int64sToInts(req.GetPermissionIds()), int(req.GetRoleId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) ListUsersFromRole(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.ListUsersFromRole(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) GetPermission(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.GetPermission(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListPermissions(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { + query, err := decodeQuery[rbacmodule.ListPermissionReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.rbac.ListPermissions(ctx, query) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListRolesFromPermission(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.ListRolesFromPermission(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) GetResource(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.GetResource(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListResources(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { + query, err := decodeQuery[rbacmodule.ListResourceReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.rbac.ListResources(ctx, query) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListResourcePermissions(ctx context.Context, req *iamv1.IDRequest) (*iamv1.StructResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.rbac.ListResourcePermissions(ctx, int(req.GetId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) IsUserTeamAdmin(ctx context.Context, req *iamv1.UserTeamRequest) (*iamv1.BoolResponse, error) { + if req.GetUserId() <= 0 || req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and team_id are required") + } + + allowed, err := s.middleware.IsUserTeamAdmin(ctx, int(req.GetUserId()), int(req.GetTeamId())) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.BoolResponse{Value: allowed}, nil +} + +func (s *iamServer) IsUserInTeam(ctx context.Context, req *iamv1.UserTeamRequest) (*iamv1.BoolResponse, error) { + if req.GetUserId() <= 0 || req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and team_id are required") + } + + allowed, err := s.middleware.IsUserInTeam(ctx, int(req.GetUserId()), int(req.GetTeamId())) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.BoolResponse{Value: allowed}, nil +} + +func (s *iamServer) IsTeamPublic(ctx context.Context, req *iamv1.TeamRequest) (*iamv1.BoolResponse, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + + allowed, err := s.middleware.IsTeamPublic(ctx, int(req.GetTeamId())) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.BoolResponse{Value: allowed}, nil +} + +func (s *iamServer) IsUserProjectAdmin(ctx context.Context, req *iamv1.UserProjectRequest) (*iamv1.BoolResponse, error) { + if req.GetUserId() <= 0 || req.GetProjectId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and project_id are required") + } + + allowed, err := s.middleware.IsUserProjectAdmin(ctx, int(req.GetUserId()), int(req.GetProjectId())) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.BoolResponse{Value: allowed}, nil +} + +func (s *iamServer) IsUserInProject(ctx context.Context, req *iamv1.UserProjectRequest) (*iamv1.BoolResponse, error) { + if req.GetUserId() <= 0 || req.GetProjectId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and project_id are required") + } + + allowed, err := s.middleware.IsUserInProject(ctx, int(req.GetUserId()), int(req.GetProjectId())) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &iamv1.BoolResponse{Value: allowed}, nil +} + +func (s *iamServer) ExchangeAccessKeyToken(ctx context.Context, req *iamv1.ExchangeAccessKeyTokenRequest) (*iamv1.ExchangeAccessKeyTokenResponse, error) { + authReq := &authmodule.AccessKeyTokenReq{ + AccessKey: req.GetAccessKey(), + Timestamp: req.GetTimestamp(), + Nonce: req.GetNonce(), + Signature: req.GetSignature(), + } + if err := authReq.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if req.GetMethod() == "" || req.GetPath() == "" { + return nil, status.Error(codes.InvalidArgument, "method and path are required") + } + + resp, err := s.auth.ExchangeAccessKeyToken(ctx, authReq, req.GetMethod(), req.GetPath()) + if err != nil { + return nil, mapIAMError(err) + } + return &iamv1.ExchangeAccessKeyTokenResponse{ + Token: resp.Token, + TokenType: resp.TokenType, + ExpiresAtUnix: resp.ExpiresAt.Unix(), + AuthType: resp.AuthType, + AccessKey: resp.AccessKey, + }, nil +} + +func (s *iamServer) CreateTeam(ctx context.Context, req *iamv1.CreateTeamRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + body, err := decodeBody[teammodule.CreateTeamReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.team.CreateTeam(ctx, body, int(req.GetUserId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) DeleteTeam(ctx context.Context, req *iamv1.TeamRequest) (*emptypb.Empty, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + if err := s.team.DeleteTeam(ctx, int(req.GetTeamId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) GetTeam(ctx context.Context, req *iamv1.TeamRequest) (*iamv1.StructResponse, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + resp, err := s.team.GetTeamDetail(ctx, int(req.GetTeamId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListTeams(ctx context.Context, req *iamv1.ListTeamsRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + query, err := decodeQuery[teammodule.ListTeamReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.team.ListTeams(ctx, query, int(req.GetUserId()), req.GetIsAdmin()) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) UpdateTeam(ctx context.Context, req *iamv1.UpdateTeamRequest) (*iamv1.StructResponse, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + body, err := decodeBody[teammodule.UpdateTeamReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.team.UpdateTeam(ctx, body, int(req.GetTeamId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) ListTeamProjects(ctx context.Context, req *iamv1.ListTeamProjectsRequest) (*iamv1.StructResponse, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + query, err := decodeQuery[teammodule.TeamProjectListReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.team.ListTeamProjects(ctx, query, int(req.GetTeamId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func (s *iamServer) AddTeamMember(ctx context.Context, req *iamv1.AddTeamMemberRequest) (*emptypb.Empty, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + body, err := decodeBody[teammodule.AddTeamMemberReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.team.AddMember(ctx, body, int(req.GetTeamId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RemoveTeamMember(ctx context.Context, req *iamv1.RemoveTeamMemberRequest) (*emptypb.Empty, error) { + if req.GetTeamId() <= 0 || req.GetCurrentUserId() <= 0 || req.GetTargetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id, current_user_id, and target_user_id are required") + } + if err := s.team.RemoveMember(ctx, int(req.GetTeamId()), int(req.GetCurrentUserId()), int(req.GetTargetUserId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) UpdateTeamMemberRole(ctx context.Context, req *iamv1.UpdateTeamMemberRoleRequest) (*emptypb.Empty, error) { + if req.GetTeamId() <= 0 || req.GetTargetUserId() <= 0 || req.GetCurrentUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id, target_user_id, and current_user_id are required") + } + body, err := decodeBody[teammodule.UpdateTeamMemberRoleReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.team.UpdateMemberRole(ctx, body, int(req.GetTeamId()), int(req.GetTargetUserId()), int(req.GetCurrentUserId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) ListTeamMembers(ctx context.Context, req *iamv1.ListTeamMembersRequest) (*iamv1.StructResponse, error) { + if req.GetTeamId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "team_id is required") + } + query, err := decodeQuery[teammodule.ListTeamMemberReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.team.ListMembers(ctx, query, int(req.GetTeamId())) + if err != nil { + return nil, mapIAMError(err) + } + return encodeStruct(resp) +} + +func optionalID(value int64) *int { + if value <= 0 { + return nil + } + id := int(value) + return &id +} + +func mapIAMError(err error) error { + switch { + case errors.Is(err, consts.ErrBadRequest): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, consts.ErrAuthenticationFailed): + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Is(err, consts.ErrPermissionDenied): + return status.Error(codes.PermissionDenied, err.Error()) + case errors.Is(err, consts.ErrNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, consts.ErrAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + case err != nil: + return status.Error(codes.Internal, err.Error()) + default: + return nil + } +} + +func encodeStruct(value any) (*iamv1.StructResponse, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + body, err := structpb.NewStruct(payload) + if err != nil { + return nil, err + } + return &iamv1.StructResponse{Data: body}, nil +} + +func decodeBody[T any](payload *structpb.Struct) (*T, error) { + return decodeQuery[T](payload) +} + +func decodeQuery[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + var zero T + return &zero, nil + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func validatePositiveInt64s(items []int64, field string) error { + if len(items) == 0 { + return status.Errorf(codes.InvalidArgument, "%s is required", field) + } + for _, item := range items { + if item <= 0 { + return status.Errorf(codes.InvalidArgument, "%s must contain positive integers", field) + } + } + return nil +} + +func int64sToInts(items []int64) []int { + if len(items) == 0 { + return nil + } + result := make([]int, 0, len(items)) + for _, item := range items { + result = append(result, int(item)) + } + return result +} diff --git a/src/interface/grpciam/service_test.go b/src/interface/grpciam/service_test.go new file mode 100644 index 00000000..a7a8d496 --- /dev/null +++ b/src/interface/grpciam/service_test.go @@ -0,0 +1,288 @@ +package grpciaminterface + +import ( + "context" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + "aegis/middleware" + authmodule "aegis/module/auth" + teammodule "aegis/module/team" + iamv1 "aegis/proto/iam/v1" + "aegis/utils" + + "google.golang.org/protobuf/types/known/structpb" +) + +type middlewareStub struct { + allowed bool + teamAdmin bool + teamMember bool + teamPublic bool + projectAdmin bool + projectMember bool +} + +func (middlewareStub) VerifyToken(context.Context, string) (*utils.Claims, error) { + return &utils.Claims{UserID: 1}, nil +} +func (middlewareStub) VerifyServiceToken(context.Context, string) (*utils.ServiceClaims, error) { + return &utils.ServiceClaims{ + TaskID: "task-1", + RegisteredClaims: utils.ServiceClaims{}.RegisteredClaims, + }, nil +} +func (m middlewareStub) CheckUserPermission(context.Context, *dto.CheckPermissionParams) (bool, error) { + return m.allowed, nil +} +func (m middlewareStub) IsUserTeamAdmin(context.Context, int, int) (bool, error) { + return m.teamAdmin, nil +} +func (m middlewareStub) IsUserInTeam(context.Context, int, int) (bool, error) { + return m.teamMember, nil +} +func (m middlewareStub) IsTeamPublic(context.Context, int) (bool, error) { return m.teamPublic, nil } +func (m middlewareStub) IsUserProjectAdmin(context.Context, int, int) (bool, error) { + return m.projectAdmin, nil +} +func (m middlewareStub) IsUserInProject(context.Context, int, int) (bool, error) { + return m.projectMember, nil +} +func (middlewareStub) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { + return nil +} +func (middlewareStub) LogUserAction(string, string, string, string, int, int, consts.ResourceName) error { + return nil +} + +var _ middleware.Service = middlewareStub{} + +type teamHandlerStub struct { + createResp *teammodule.TeamResp + detailResp *teammodule.TeamDetailResp + listResp *dto.ListResp[teammodule.TeamResp] + projectsResp *dto.ListResp[teammodule.TeamProjectItem] + membersResp *dto.ListResp[teammodule.TeamMemberResp] + updateResp *teammodule.TeamResp + createCalled bool + listCalled bool + listProjectsCalled bool +} + +func (s *teamHandlerStub) CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) { + s.createCalled = true + return s.createResp, nil +} +func (*teamHandlerStub) DeleteTeam(context.Context, int) error { return nil } +func (s *teamHandlerStub) GetTeamDetail(context.Context, int) (*teammodule.TeamDetailResp, error) { + return s.detailResp, nil +} +func (s *teamHandlerStub) ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) { + s.listCalled = true + return s.listResp, nil +} +func (s *teamHandlerStub) UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) { + return s.updateResp, nil +} +func (s *teamHandlerStub) ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) { + s.listProjectsCalled = true + return s.projectsResp, nil +} +func (*teamHandlerStub) AddMember(context.Context, *teammodule.AddTeamMemberReq, int) error { + return nil +} +func (*teamHandlerStub) RemoveMember(context.Context, int, int, int) error { return nil } +func (*teamHandlerStub) UpdateMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error { + return nil +} +func (s *teamHandlerStub) ListMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) { + return s.membersResp, nil +} + +func TestIAMServerVerifyTokenUser(t *testing.T) { + token, expiresAt, err := utils.GenerateToken(7, "demo", "demo@example.com", true, false, []string{"user"}) + if err != nil { + t.Fatalf("GenerateToken() error = %v", err) + } + + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) + resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) + if err != nil { + t.Fatalf("VerifyToken() error = %v", err) + } + + if !resp.Valid || resp.TokenType != "user" || resp.UserId != 7 { + t.Fatalf("VerifyToken() unexpected response: %+v", resp) + } + if resp.ExpiresAtUnix != expiresAt.Unix() { + t.Fatalf("VerifyToken() expires_at_unix = %d, want %d", resp.ExpiresAtUnix, expiresAt.Unix()) + } +} + +func TestIAMServerCheckPermission(t *testing.T) { + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) + resp, err := server.CheckPermission(context.Background(), &iamv1.CheckPermissionRequest{ + UserId: 7, + Action: string(consts.ActionRead), + Scope: string(consts.ScopeAll), + ResourceName: string(consts.ResourceProject), + }) + if err != nil { + t.Fatalf("CheckPermission() error = %v", err) + } + if !resp.Allowed { + t.Fatalf("CheckPermission() allowed = false, want true") + } +} + +func TestIAMServerVerifyTokenService(t *testing.T) { + token, _, err := utils.GenerateServiceToken("task-123") + if err != nil { + t.Fatalf("GenerateServiceToken() error = %v", err) + } + + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) + resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) + if err != nil { + t.Fatalf("VerifyToken() error = %v", err) + } + if !resp.Valid || resp.TokenType != "service" || resp.TaskId != "task-123" { + t.Fatalf("VerifyToken() unexpected service response: %+v", resp) + } + if resp.ExpiresAtUnix <= time.Now().Unix() { + t.Fatalf("VerifyToken() service expiry = %d, want future timestamp", resp.ExpiresAtUnix) + } +} + +func TestIAMServerMembershipChecks(t *testing.T) { + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{ + teamAdmin: true, + teamMember: true, + teamPublic: true, + projectAdmin: true, + projectMember: true, + }) + + t.Run("team admin", func(t *testing.T) { + resp, err := server.IsUserTeamAdmin(context.Background(), &iamv1.UserTeamRequest{UserId: 7, TeamId: 9}) + if err != nil { + t.Fatalf("IsUserTeamAdmin() error = %v", err) + } + if !resp.GetValue() { + t.Fatalf("IsUserTeamAdmin() value = false, want true") + } + }) + + t.Run("team member", func(t *testing.T) { + resp, err := server.IsUserInTeam(context.Background(), &iamv1.UserTeamRequest{UserId: 7, TeamId: 9}) + if err != nil { + t.Fatalf("IsUserInTeam() error = %v", err) + } + if !resp.GetValue() { + t.Fatalf("IsUserInTeam() value = false, want true") + } + }) + + t.Run("team public", func(t *testing.T) { + resp, err := server.IsTeamPublic(context.Background(), &iamv1.TeamRequest{TeamId: 9}) + if err != nil { + t.Fatalf("IsTeamPublic() error = %v", err) + } + if !resp.GetValue() { + t.Fatalf("IsTeamPublic() value = false, want true") + } + }) + + t.Run("project admin", func(t *testing.T) { + resp, err := server.IsUserProjectAdmin(context.Background(), &iamv1.UserProjectRequest{UserId: 7, ProjectId: 11}) + if err != nil { + t.Fatalf("IsUserProjectAdmin() error = %v", err) + } + if !resp.GetValue() { + t.Fatalf("IsUserProjectAdmin() value = false, want true") + } + }) + + t.Run("project member", func(t *testing.T) { + resp, err := server.IsUserInProject(context.Background(), &iamv1.UserProjectRequest{UserId: 7, ProjectId: 11}) + if err != nil { + t.Fatalf("IsUserInProject() error = %v", err) + } + if !resp.GetValue() { + t.Fatalf("IsUserInProject() value = false, want true") + } + }) +} + +func TestIAMServerTeamRPCs(t *testing.T) { + teamStub := &teamHandlerStub{ + createResp: &teammodule.TeamResp{ID: 9, Name: "core"}, + detailResp: &teammodule.TeamDetailResp{ + TeamResp: teammodule.TeamResp{ID: 9, Name: "core"}, + UserCount: 2, + ProjectCount: 3, + }, + listResp: &dto.ListResp[teammodule.TeamResp]{ + Items: []teammodule.TeamResp{{ID: 9, Name: "core"}}, + Pagination: &dto.PaginationInfo{Page: 1, Size: 20, Total: 1, TotalPages: 1}, + }, + projectsResp: &dto.ListResp[teammodule.TeamProjectItem]{ + Items: []teammodule.TeamProjectItem{{ID: 11, Name: "proj-a"}}, + Pagination: &dto.PaginationInfo{Page: 1, Size: 20, Total: 1, TotalPages: 1}, + }, + } + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, teamStub, nil, nil, middlewareStub{}) + + createBody, _ := structpb.NewStruct(map[string]any{"name": "core"}) + createResp, err := server.CreateTeam(context.Background(), &iamv1.CreateTeamRequest{ + UserId: 7, + Body: createBody, + }) + if err != nil { + t.Fatalf("CreateTeam() error = %v", err) + } + if createResp.GetData().AsMap()["id"] != float64(9) || !teamStub.createCalled { + t.Fatalf("CreateTeam() unexpected response: %+v", createResp.GetData().AsMap()) + } + + listQuery, _ := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + listResp, err := server.ListTeams(context.Background(), &iamv1.ListTeamsRequest{ + UserId: 7, + IsAdmin: true, + Query: listQuery, + }) + if err != nil { + t.Fatalf("ListTeams() error = %v", err) + } + items, ok := listResp.GetData().AsMap()["items"].([]any) + if !ok || len(items) != 1 || !teamStub.listCalled { + t.Fatalf("ListTeams() unexpected response: %+v", listResp.GetData().AsMap()) + } + + getResp, err := server.GetTeam(context.Background(), &iamv1.TeamRequest{TeamId: 9}) + if err != nil { + t.Fatalf("GetTeam() error = %v", err) + } + if getResp.GetData().AsMap()["project_count"] != float64(3) { + t.Fatalf("GetTeam() unexpected response: %+v", getResp.GetData().AsMap()) + } + + projectResp, err := server.ListTeamProjects(context.Background(), &iamv1.ListTeamProjectsRequest{ + TeamId: 9, + Query: listQuery, + }) + if err != nil { + t.Fatalf("ListTeamProjects() error = %v", err) + } + projectItems, ok := projectResp.GetData().AsMap()["items"].([]any) + if !ok || len(projectItems) != 1 || !teamStub.listProjectsCalled { + t.Fatalf("ListTeamProjects() unexpected response: %+v", projectResp.GetData().AsMap()) + } +} diff --git a/src/interface/grpcorchestrator/lifecycle.go b/src/interface/grpcorchestrator/lifecycle.go new file mode 100644 index 00000000..977f2bd2 --- /dev/null +++ b/src/interface/grpcorchestrator/lifecycle.go @@ -0,0 +1,94 @@ +package grpcorchestratorinterface + +import ( + "context" + "fmt" + "net" + + "aegis/config" + "aegis/httpx" + orchestratorv1 "aegis/proto/orchestrator/v1" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const defaultOrchestratorGRPCAddr = ":9092" + +type Lifecycle struct { + server *grpc.Server + addr string + listener net.Listener + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(orchestratorServer *orchestratorServer) (*Lifecycle, error) { + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(httpx.UnaryServerRequestIDInterceptor())) + orchestratorv1.RegisterOrchestratorServiceServer(grpcServer, orchestratorServer) + + healthServer := health.NewServer() + healthServer.SetServingStatus(orchestratorv1.OrchestratorService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + + if config.GetBool("orchestrator.grpc.reflection") { + reflection.Register(grpcServer) + } + + addr := config.GetString("orchestrator.grpc.addr") + if addr == "" { + addr = defaultOrchestratorGRPCAddr + } + + return &Lifecycle{ + server: grpcServer, + addr: addr, + }, nil +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + + listener, err := net.Listen("tcp", r.addr) + if err != nil { + return fmt.Errorf("listen orchestrator grpc on %s: %w", r.addr, err) + } + r.listener = listener + + go func() { + logrus.Infof("Starting orchestrator gRPC server on %s", r.addr) + if err := r.server.Serve(listener); err != nil { + logrus.Errorf("orchestrator gRPC server error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.server != nil { + r.server.GracefulStop() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return runner.start(ctx) + }, + OnStop: func(ctx context.Context) error { + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/grpcorchestrator/module.go b/src/interface/grpcorchestrator/module.go new file mode 100644 index 00000000..d5dca709 --- /dev/null +++ b/src/interface/grpcorchestrator/module.go @@ -0,0 +1,18 @@ +package grpcorchestratorinterface + +import ( + projectmodule "aegis/module/project" + + "go.uber.org/fx" +) + +var Module = fx.Module("grpc_orchestrator", + fx.Provide( + projectmodule.NewRepository, + newProjectStatisticsReader, + newTaskQueueController, + newOrchestratorServer, + newLifecycle, + ), + fx.Invoke(registerLifecycle), +) diff --git a/src/interface/grpcorchestrator/project_statistics.go b/src/interface/grpcorchestrator/project_statistics.go new file mode 100644 index 00000000..fb4beeb3 --- /dev/null +++ b/src/interface/grpcorchestrator/project_statistics.go @@ -0,0 +1,22 @@ +package grpcorchestratorinterface + +import ( + "aegis/dto" + projectmodule "aegis/module/project" +) + +type projectStatisticsReader interface { + ListProjectStatistics([]int) (map[int]*dto.ProjectStatistics, error) +} + +type projectRepositoryStatisticsReader struct { + repo *projectmodule.Repository +} + +func newProjectStatisticsReader(repo *projectmodule.Repository) projectStatisticsReader { + return &projectRepositoryStatisticsReader{repo: repo} +} + +func (r *projectRepositoryStatisticsReader) ListProjectStatistics(projectIDs []int) (map[int]*dto.ProjectStatistics, error) { + return r.repo.ListProjectStatistics(projectIDs) +} diff --git a/src/interface/grpcorchestrator/service.go b/src/interface/grpcorchestrator/service.go new file mode 100644 index 00000000..20aa2662 --- /dev/null +++ b/src/interface/grpcorchestrator/service.go @@ -0,0 +1,843 @@ +package grpcorchestratorinterface + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + redisinfra "aegis/infra/redis" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + metricmodule "aegis/module/metric" + notificationmodule "aegis/module/notification" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + orchestratorv1 "aegis/proto/orchestrator/v1" + "aegis/service/consumer" + + "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +const orchestratorServiceName = "orchestrator-service" + +type executionSubmitter interface { + SubmitAlgorithmExecution(context.Context, *executionmodule.SubmitExecutionReq, string, int) (*executionmodule.SubmitExecutionResp, error) + CreateExecutionRecord(context.Context, *executionmodule.RuntimeCreateExecutionReq) (int, error) + UpdateExecutionState(context.Context, *executionmodule.RuntimeUpdateExecutionStateReq) error + GetExecution(context.Context, int) (*executionmodule.ExecutionDetailResp, error) + ListEvaluationExecutionsByDatapack(context.Context, *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) + ListEvaluationExecutionsByDataset(context.Context, *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) +} + +type injectionSubmitter interface { + SubmitFaultInjection(context.Context, *injectionmodule.SubmitInjectionReq, string, int, *int) (*injectionmodule.SubmitInjectionResp, error) + SubmitDatapackBuilding(context.Context, *injectionmodule.SubmitDatapackBuildingReq, string, int, *int) (*injectionmodule.SubmitDatapackBuildingResp, error) + CreateInjectionRecord(context.Context, *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) + UpdateInjectionState(context.Context, *injectionmodule.RuntimeUpdateInjectionStateReq) error + UpdateInjectionTimestamps(context.Context, *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) +} + +type metricsReader interface { + GetInjectionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) + GetExecutionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) +} + +type taskReader interface { + GetDetail(context.Context, string) (*taskmodule.TaskDetailResp, error) + PollLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) + List(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) +} + +type traceReader interface { + GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) + ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) + GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) + ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +type groupReader interface { + GetGroupStats(context.Context, *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) + GetGroupTraceCount(string) (int64, error) + ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +type notificationReader interface { + ReadStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +type taskController interface { + CancelTask(context.Context, string) error + RetryTask(context.Context, string) (string, error) + ListDeadLetterTasks(context.Context, int64) ([]QueuedTaskResp, error) +} + +type taskQueueController struct { + redis *redisinfra.Gateway +} + +type QueuedTaskResp struct { + TaskID string `json:"task_id"` + Type string `json:"type"` + Queue string `json:"queue"` + TraceID string `json:"trace_id"` + GroupID string `json:"group_id"` + ProjectID int `json:"project_id"` + UserID int `json:"user_id"` + Immediate bool `json:"immediate"` + ExecuteTime int64 `json:"execute_time"` + RestartNum int `json:"restart_num"` + State string `json:"state"` +} + +func newTaskQueueController(redis *redisinfra.Gateway) taskController { + return &taskQueueController{redis: redis} +} + +func (c *taskQueueController) CancelTask(_ context.Context, taskID string) error { + return consumer.CancelTask(c.redis, taskID) +} + +func (c *taskQueueController) RetryTask(ctx context.Context, taskID string) (string, error) { + queue, _, task, err := c.findTask(ctx, taskID) + if err != nil { + return "", err + } + + switch queue { + case redisinfra.ReadyQueueKey: + return queue, nil + case redisinfra.DelayedQueueKey, redisinfra.DeadLetterKey: + if ok := c.redis.RemoveFromZSet(ctx, queue, taskID); !ok { + return "", fmt.Errorf("%w: task %s not found in %s", consts.ErrNotFound, taskID, queue) + } + default: + return "", fmt.Errorf("%w: unsupported queue %s", consts.ErrBadRequest, queue) + } + + if err := c.redis.DeleteTaskIndex(ctx, taskID); err != nil { + return "", fmt.Errorf("delete task index: %w", err) + } + + task.State = consts.TaskPending + data, err := json.Marshal(task) + if err != nil { + return "", err + } + if task.ExecuteTime > time.Now().Unix() && !task.Immediate { + if err := c.redis.SubmitDelayedTask(ctx, data, task.TaskID, task.ExecuteTime); err != nil { + return "", err + } + return redisinfra.DelayedQueueKey, nil + } + + task.Immediate = true + task.ExecuteTime = time.Now().Unix() + data, err = json.Marshal(task) + if err != nil { + return "", err + } + if err := c.redis.SubmitImmediateTask(ctx, data, task.TaskID); err != nil { + return "", err + } + return redisinfra.ReadyQueueKey, nil +} + +func (c *taskQueueController) ListDeadLetterTasks(ctx context.Context, limit int64) ([]QueuedTaskResp, error) { + if limit <= 0 { + limit = 100 + } + items, err := c.redis.ListDeadLetterTasks(ctx, limit) + if err != nil { + return nil, err + } + return decodeQueuedTasks(items, redisinfra.DeadLetterKey) +} + +func (c *taskQueueController) findTask(ctx context.Context, taskID string) (string, string, *dto.UnifiedTask, error) { + if taskID == "" { + return "", "", nil, fmt.Errorf("%w: task_id is required", consts.ErrBadRequest) + } + + if queue, err := c.redis.GetTaskQueue(ctx, taskID); err == nil && queue != "" { + if taskData, task, ok := c.findTaskInQueue(ctx, queue, taskID); ok { + return queue, taskData, task, nil + } + } + + for _, queue := range []string{redisinfra.ReadyQueueKey, redisinfra.DelayedQueueKey, redisinfra.DeadLetterKey} { + if taskData, task, ok := c.findTaskInQueue(ctx, queue, taskID); ok { + return queue, taskData, task, nil + } + } + + return "", "", nil, fmt.Errorf("%w: task %s not found", consts.ErrNotFound, taskID) +} + +func (c *taskQueueController) findTaskInQueue(ctx context.Context, queue, taskID string) (string, *dto.UnifiedTask, bool) { + var items []string + var err error + + switch queue { + case redisinfra.ReadyQueueKey: + items, err = c.redis.ListReadyTasks(ctx) + case redisinfra.DelayedQueueKey: + items, err = c.redis.ListDelayedTasks(ctx, 1000) + case redisinfra.DeadLetterKey: + items, err = c.redis.ListDeadLetterTasks(ctx, 1000) + default: + return "", nil, false + } + if err != nil { + return "", nil, false + } + + for _, item := range items { + var task dto.UnifiedTask + if json.Unmarshal([]byte(item), &task) == nil && task.TaskID == taskID { + return item, &task, true + } + } + return "", nil, false +} + +func decodeQueuedTasks(items []string, queue string) ([]QueuedTaskResp, error) { + result := make([]QueuedTaskResp, 0, len(items)) + for _, item := range items { + var task dto.UnifiedTask + if err := json.Unmarshal([]byte(item), &task); err != nil { + return nil, err + } + result = append(result, QueuedTaskResp{ + TaskID: task.TaskID, + Type: consts.GetTaskTypeName(task.Type), + Queue: queue, + TraceID: task.TraceID, + GroupID: task.GroupID, + ProjectID: task.ProjectID, + UserID: task.UserID, + Immediate: task.Immediate, + ExecuteTime: task.ExecuteTime, + RestartNum: task.ReStartNum, + State: consts.GetTaskStateName(task.State), + }) + } + return result, nil +} + +type orchestratorServer struct { + orchestratorv1.UnimplementedOrchestratorServiceServer + execution executionSubmitter + injection injectionSubmitter + metrics metricsReader + projects projectStatisticsReader + tasks taskController + taskRead taskReader + traceRead traceReader + groupRead groupReader + notify notificationReader +} + +func newOrchestratorServer( + execution *executionmodule.Service, + injection *injectionmodule.Service, + metrics *metricmodule.Service, + projects projectStatisticsReader, + tasks taskController, + taskRead *taskmodule.Service, + traceRead *tracemodule.Service, + groupRead *groupmodule.Service, + notify *notificationmodule.Service, +) *orchestratorServer { + return &orchestratorServer{ + execution: execution, + injection: injection, + metrics: metrics, + projects: projects, + tasks: tasks, + taskRead: taskRead, + traceRead: traceRead, + groupRead: groupRead, + notify: notify, + } +} + +func (s *orchestratorServer) Ping(context.Context, *orchestratorv1.PingRequest) (*orchestratorv1.PingResponse, error) { + return &orchestratorv1.PingResponse{ + Service: orchestratorServiceName, + AppId: consts.AppID, + Status: "ok", + TimestampUnix: time.Now().Unix(), + }, nil +} + +func (s *orchestratorServer) SubmitExecution(ctx context.Context, req *orchestratorv1.SubmitExecutionRequest) (*orchestratorv1.SubmitExecutionResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + body, err := decodeBody[executionmodule.SubmitExecutionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.execution.SubmitAlgorithmExecution(ctx, body, resolveGroupID(req.GetGroupId()), int(req.GetUserId())) + if err != nil { + return nil, mapOrchestratorError(err) + } + + items := make([]*orchestratorv1.SubmittedExecutionItem, 0, len(resp.Items)) + for _, item := range resp.Items { + pbItem := &orchestratorv1.SubmittedExecutionItem{ + Index: int64(item.Index), + TraceId: item.TraceID, + TaskId: item.TaskID, + AlgorithmId: int64(item.AlgorithmID), + AlgorithmVersionId: int64(item.AlgorithmVersionID), + } + if item.DatapackID != nil { + pbItem.HasDatapackId = true + pbItem.DatapackId = int64(*item.DatapackID) + } + if item.DatasetID != nil { + pbItem.HasDatasetId = true + pbItem.DatasetId = int64(*item.DatasetID) + } + items = append(items, pbItem) + } + + return &orchestratorv1.SubmitExecutionResponse{ + GroupId: resp.GroupID, + Items: items, + }, nil +} + +func (s *orchestratorServer) SubmitFaultInjection(ctx context.Context, req *orchestratorv1.SubmitFaultInjectionRequest) (*orchestratorv1.SubmitFaultInjectionResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + body, err := decodeBody[injectionmodule.SubmitInjectionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.injection.SubmitFaultInjection(ctx, body, resolveGroupID(req.GetGroupId()), int(req.GetUserId()), optionalID(req.GetProjectId())) + if err != nil { + return nil, mapOrchestratorError(err) + } + + items := make([]*orchestratorv1.SubmittedInjectionItem, 0, len(resp.Items)) + for _, item := range resp.Items { + items = append(items, &orchestratorv1.SubmittedInjectionItem{ + Index: int64(item.Index), + TraceId: item.TraceID, + TaskId: item.TaskID, + }) + } + + result := &orchestratorv1.SubmitFaultInjectionResponse{ + GroupId: resp.GroupID, + Items: items, + OriginalCount: int64(resp.OriginalCount), + } + if resp.Warnings != nil { + result.Warnings = &orchestratorv1.InjectionWarnings{ + DuplicateServicesInBatch: resp.Warnings.DuplicateServicesInBatch, + DuplicateBatchesInRequest: intsToInt64s( + resp.Warnings.DuplicateBatchesInRequest, + ), + BatchesExistInDatabase: intsToInt64s(resp.Warnings.BatchesExistInDatabase), + } + } + return result, nil +} + +func (s *orchestratorServer) SubmitDatapackBuilding(ctx context.Context, req *orchestratorv1.SubmitDatapackBuildingRequest) (*orchestratorv1.SubmitDatapackBuildingResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + body, err := decodeBody[injectionmodule.SubmitDatapackBuildingReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.injection.SubmitDatapackBuilding(ctx, body, resolveGroupID(req.GetGroupId()), int(req.GetUserId()), optionalID(req.GetProjectId())) + if err != nil { + return nil, mapOrchestratorError(err) + } + + items := make([]*orchestratorv1.SubmittedBuildingItem, 0, len(resp.Items)) + for _, item := range resp.Items { + items = append(items, &orchestratorv1.SubmittedBuildingItem{ + Index: int64(item.Index), + TraceId: item.TraceID, + TaskId: item.TaskID, + }) + } + + return &orchestratorv1.SubmitDatapackBuildingResponse{ + GroupId: resp.GroupID, + Items: items, + }, nil +} + +func (s *orchestratorServer) CreateExecution(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[executionmodule.RuntimeCreateExecutionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + executionID, err := s.execution.CreateExecutionRecord(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"execution_id": executionID}) +} + +func (s *orchestratorServer) CreateInjection(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[injectionmodule.RuntimeCreateInjectionReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.injection.CreateInjectionRecord(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) UpdateExecutionState(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[executionmodule.RuntimeUpdateExecutionStateReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.execution.UpdateExecutionState(ctx, body); err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"updated": true}) +} + +func (s *orchestratorServer) UpdateInjectionState(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[injectionmodule.RuntimeUpdateInjectionStateReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.injection.UpdateInjectionState(ctx, body); err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"updated": true}) +} + +func (s *orchestratorServer) UpdateInjectionTimestamps(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[injectionmodule.RuntimeUpdateInjectionTimestampReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.injection.UpdateInjectionTimestamps(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) CancelTask(ctx context.Context, req *orchestratorv1.CancelTaskRequest) (*orchestratorv1.CancelTaskResponse, error) { + if req.GetTaskId() == "" { + return nil, status.Error(codes.InvalidArgument, "task_id is required") + } + + if err := s.tasks.CancelTask(ctx, req.GetTaskId()); err != nil { + return nil, mapOrchestratorError(err) + } + return &orchestratorv1.CancelTaskResponse{Cancelled: true}, nil +} + +func (s *orchestratorServer) GetExecution(ctx context.Context, req *orchestratorv1.GetExecutionRequest) (*orchestratorv1.StructResponse, error) { + if req.GetExecutionId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "execution_id is required") + } + resp, err := s.execution.GetExecution(ctx, int(req.GetExecutionId())) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) GetInjectionMetrics(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[metricmodule.GetMetricsReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if s.metrics == nil { + return nil, status.Error(codes.FailedPrecondition, "metrics service is not configured") + } + resp, err := s.metrics.GetInjectionMetrics(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) GetExecutionMetrics(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[metricmodule.GetMetricsReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if s.metrics == nil { + return nil, status.Error(codes.FailedPrecondition, "metrics service is not configured") + } + resp, err := s.metrics.GetExecutionMetrics(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) ListProjectStatistics(ctx context.Context, req *orchestratorv1.ListProjectStatisticsRequest) (*orchestratorv1.StructResponse, error) { + projectIDs := int64sToInts(req.GetProjectIds()) + for _, projectID := range projectIDs { + if projectID <= 0 { + return nil, status.Error(codes.InvalidArgument, "project_ids must be greater than 0") + } + } + resp, err := s.projects.ListProjectStatistics(projectIDs) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) ListEvaluationExecutionsByDatapack(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[executionmodule.EvaluationExecutionsByDatapackReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.execution.ListEvaluationExecutionsByDatapack(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"items": resp}) +} + +func (s *orchestratorServer) ListEvaluationExecutionsByDataset(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { + body, err := decodeBody[executionmodule.EvaluationExecutionsByDatasetReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.execution.ListEvaluationExecutionsByDataset(ctx, body) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"items": resp}) +} + +func (s *orchestratorServer) GetTask(ctx context.Context, req *orchestratorv1.GetTaskRequest) (*orchestratorv1.StructResponse, error) { + if req.GetTaskId() == "" { + return nil, status.Error(codes.InvalidArgument, "task_id is required") + } + resp, err := s.taskRead.GetDetail(ctx, req.GetTaskId()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) PollTaskLogs(ctx context.Context, req *orchestratorv1.PollTaskLogsRequest) (*orchestratorv1.StructResponse, error) { + if req.GetTaskId() == "" { + return nil, status.Error(codes.InvalidArgument, "task_id is required") + } + after := time.Time{} + if req.GetAfterUnixNano() > 0 { + after = time.Unix(0, req.GetAfterUnixNano()) + } + resp, err := s.taskRead.PollLogs(ctx, req.GetTaskId(), after) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) ListTasks(ctx context.Context, req *orchestratorv1.ListTasksRequest) (*orchestratorv1.StructResponse, error) { + query, err := decodeQuery[taskmodule.ListTaskReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.taskRead.List(ctx, query) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) GetTrace(ctx context.Context, req *orchestratorv1.GetTraceRequest) (*orchestratorv1.StructResponse, error) { + if req.GetTraceId() == "" { + return nil, status.Error(codes.InvalidArgument, "trace_id is required") + } + resp, err := s.traceRead.GetTrace(ctx, req.GetTraceId()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) ListTraces(ctx context.Context, req *orchestratorv1.ListTracesRequest) (*orchestratorv1.StructResponse, error) { + query, err := decodeQuery[tracemodule.ListTraceReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + resp, err := s.traceRead.ListTraces(ctx, query) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) GetGroupStats(ctx context.Context, req *orchestratorv1.GetGroupStatsRequest) (*orchestratorv1.StructResponse, error) { + if req.GetGroupId() == "" { + return nil, status.Error(codes.InvalidArgument, "group_id is required") + } + resp, err := s.groupRead.GetGroupStats(ctx, &groupmodule.GetGroupStatsReq{GroupID: req.GetGroupId()}) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(resp) +} + +func (s *orchestratorServer) GetTraceStreamState(ctx context.Context, req *orchestratorv1.GetTraceStreamStateRequest) (*orchestratorv1.StructResponse, error) { + if req.GetTraceId() == "" { + return nil, status.Error(codes.InvalidArgument, "trace_id is required") + } + algorithms, err := s.traceRead.GetTraceStreamAlgorithms(ctx, req.GetTraceId()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(traceStreamStateResp{Algorithms: algorithms}) +} + +func (s *orchestratorServer) ReadTraceStreamMessages(ctx context.Context, req *orchestratorv1.ReadStreamMessagesRequest) (*orchestratorv1.StructResponse, error) { + if req.GetStreamKey() == "" { + return nil, status.Error(codes.InvalidArgument, "stream_key is required") + } + resp, err := s.traceRead.ReadTraceStreamMessages(ctx, req.GetStreamKey(), req.GetLastId(), req.GetCount(), time.Duration(req.GetBlockMillis())*time.Millisecond) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStreamMessages(resp) +} + +func (s *orchestratorServer) GetGroupStreamState(ctx context.Context, req *orchestratorv1.GetGroupStreamStateRequest) (*orchestratorv1.StructResponse, error) { + if req.GetGroupId() == "" { + return nil, status.Error(codes.InvalidArgument, "group_id is required") + } + totalTraces, err := s.groupRead.GetGroupTraceCount(req.GetGroupId()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(groupStreamStateResp{TotalTraces: int(totalTraces)}) +} + +func (s *orchestratorServer) ReadGroupStreamMessages(ctx context.Context, req *orchestratorv1.ReadStreamMessagesRequest) (*orchestratorv1.StructResponse, error) { + if req.GetStreamKey() == "" { + return nil, status.Error(codes.InvalidArgument, "stream_key is required") + } + resp, err := s.groupRead.ReadGroupStreamMessages(ctx, req.GetStreamKey(), req.GetLastId(), req.GetCount(), time.Duration(req.GetBlockMillis())*time.Millisecond) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStreamMessages(resp) +} + +func (s *orchestratorServer) ReadNotificationStreamMessages(ctx context.Context, req *orchestratorv1.ReadStreamMessagesRequest) (*orchestratorv1.StructResponse, error) { + if req.GetStreamKey() == "" { + return nil, status.Error(codes.InvalidArgument, "stream_key is required") + } + if s.notify == nil { + return nil, status.Error(codes.FailedPrecondition, "notification service is not configured") + } + resp, err := s.notify.ReadStreamMessages(ctx, req.GetStreamKey(), req.GetLastId(), req.GetCount(), time.Duration(req.GetBlockMillis())*time.Millisecond) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStreamMessages(resp) +} + +func (s *orchestratorServer) ListDeadLetterTasks(ctx context.Context, req *orchestratorv1.ListDeadLetterTasksRequest) (*orchestratorv1.StructResponse, error) { + resp, err := s.tasks.ListDeadLetterTasks(ctx, req.GetLimit()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return encodeStruct(map[string]any{"items": resp}) +} + +func (s *orchestratorServer) RetryTask(ctx context.Context, req *orchestratorv1.RetryTaskRequest) (*orchestratorv1.RetryTaskResponse, error) { + if req.GetTaskId() == "" { + return nil, status.Error(codes.InvalidArgument, "task_id is required") + } + queue, err := s.tasks.RetryTask(ctx, req.GetTaskId()) + if err != nil { + return nil, mapOrchestratorError(err) + } + return &orchestratorv1.RetryTaskResponse{Accepted: true, Queue: queue}, nil +} + +func resolveGroupID(groupID string) string { + if groupID != "" { + return groupID + } + return uuid.NewString() +} + +func optionalID(value int64) *int { + if value <= 0 { + return nil + } + id := int(value) + return &id +} + +func intsToInt64s(items []int) []int64 { + if len(items) == 0 { + return nil + } + result := make([]int64, 0, len(items)) + for _, item := range items { + result = append(result, int64(item)) + } + return result +} + +func int64sToInts(items []int64) []int { + if len(items) == 0 { + return nil + } + result := make([]int, 0, len(items)) + for _, item := range items { + result = append(result, int(item)) + } + return result +} + +func decodeBody[T any](body *structpb.Struct) (*T, error) { + if body == nil { + return nil, errors.New("body is required") + } + + data, err := json.Marshal(body.AsMap()) + if err != nil { + return nil, err + } + + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func decodeQuery[T any](query *structpb.Struct) (*T, error) { + var result T + if query == nil { + return &result, nil + } + + data, err := json.Marshal(query.AsMap()) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func encodeStruct(value any) (*orchestratorv1.StructResponse, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + item, err := structpb.NewStruct(payload) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &orchestratorv1.StructResponse{Data: item}, nil +} + +type traceStreamStateResp struct { + Algorithms []dto.ContainerVersionItem `json:"algorithms"` +} + +type groupStreamStateResp struct { + TotalTraces int `json:"total_traces"` +} + +type streamBatchResp struct { + Messages []streamMessageResp `json:"messages"` +} + +type streamMessageResp struct { + ID string `json:"id"` + Values map[string]any `json:"values"` +} + +func encodeStreamMessages(streams []redis.XStream) (*orchestratorv1.StructResponse, error) { + messages := []streamMessageResp{} + if len(streams) > 0 { + messages = make([]streamMessageResp, 0, len(streams[0].Messages)) + for _, item := range streams[0].Messages { + messages = append(messages, streamMessageResp{ + ID: item.ID, + Values: item.Values, + }) + } + } + return encodeStruct(streamBatchResp{Messages: messages}) +} + +func mapOrchestratorError(err error) error { + switch { + case errors.Is(err, consts.ErrAuthenticationFailed): + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Is(err, consts.ErrPermissionDenied): + return status.Error(codes.PermissionDenied, err.Error()) + case errors.Is(err, consts.ErrBadRequest): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, consts.ErrNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, consts.ErrAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + case err != nil: + return status.Error(codes.Internal, err.Error()) + default: + return nil + } +} diff --git a/src/interface/grpcorchestrator/service_test.go b/src/interface/grpcorchestrator/service_test.go new file mode 100644 index 00000000..507cbb8c --- /dev/null +++ b/src/interface/grpcorchestrator/service_test.go @@ -0,0 +1,888 @@ +package grpcorchestratorinterface + +import ( + "context" + "errors" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + metricmodule "aegis/module/metric" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + orchestratorv1 "aegis/proto/orchestrator/v1" + + "github.com/redis/go-redis/v9" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type executionSubmitterStub struct { + resp *executionmodule.SubmitExecutionResp + id int + item *executionmodule.ExecutionDetailResp + evaluationItems []executionmodule.EvaluationExecutionItem + err error +} + +func (s executionSubmitterStub) SubmitAlgorithmExecution(_ context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { + if req.ProjectName == "" || groupID == "" || userID <= 0 { + return nil, errors.New("unexpected request") + } + return s.resp, s.err +} + +func (s executionSubmitterStub) CreateExecutionRecord(_ context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { + if req.TaskID == "" || req.AlgorithmVersionID <= 0 || req.DatapackID <= 0 { + return 0, errors.New("unexpected runtime execution request") + } + return s.id, s.err +} + +func (s executionSubmitterStub) UpdateExecutionState(_ context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { + if req.ExecutionID <= 0 { + return errors.New("unexpected execution state request") + } + return s.err +} + +func (s executionSubmitterStub) GetExecution(_ context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { + if executionID <= 0 { + return nil, errors.New("missing execution id") + } + return s.item, s.err +} + +func (s executionSubmitterStub) ListEvaluationExecutionsByDatapack(_ context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { + if req.AlgorithmVersionID <= 0 || req.DatapackName == "" { + return nil, errors.New("unexpected datapack evaluation query") + } + return s.evaluationItems, s.err +} + +func (s executionSubmitterStub) ListEvaluationExecutionsByDataset(_ context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { + if req.AlgorithmVersionID <= 0 || req.DatasetVersionID <= 0 { + return nil, errors.New("unexpected dataset evaluation query") + } + return s.evaluationItems, s.err +} + +type injectionSubmitterStub struct { + injectionResp *injectionmodule.SubmitInjectionResp + buildResp *injectionmodule.SubmitDatapackBuildingResp + item *dto.InjectionItem + err error +} + +func (s injectionSubmitterStub) SubmitFaultInjection(_ context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { + if req.Pedestal == nil || req.Benchmark == nil || groupID == "" || userID <= 0 { + return nil, errors.New("unexpected injection request") + } + if projectID == nil || *projectID != 9 { + return nil, errors.New("unexpected project id") + } + return s.injectionResp, s.err +} + +func (s injectionSubmitterStub) SubmitDatapackBuilding(_ context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { + if len(req.Specs) == 0 || groupID == "" || userID <= 0 { + return nil, errors.New("unexpected datapack request") + } + if projectID == nil || *projectID != 5 { + return nil, errors.New("unexpected project id") + } + return s.buildResp, s.err +} + +func (s injectionSubmitterStub) CreateInjectionRecord(_ context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { + if req.Name == "" || req.TaskID == "" { + return nil, errors.New("unexpected runtime injection request") + } + return s.item, s.err +} + +type metricsReaderStub struct { + injection *metricmodule.InjectionMetrics + execution *metricmodule.ExecutionMetrics + err error +} + +func (s metricsReaderStub) GetInjectionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.injection, s.err +} + +func (s metricsReaderStub) GetExecutionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.execution, s.err +} + +func (s injectionSubmitterStub) UpdateInjectionState(_ context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { + if req.Name == "" { + return errors.New("unexpected injection state request") + } + return s.err +} + +func (s injectionSubmitterStub) UpdateInjectionTimestamps(_ context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { + if req.Name == "" { + return nil, errors.New("unexpected injection timestamp request") + } + return s.item, s.err +} + +type taskControllerStub struct { + taskID string + queue string + listResp []QueuedTaskResp + err error +} + +func (s taskControllerStub) CancelTask(_ context.Context, taskID string) error { + if taskID == "" { + return errors.New("missing task id") + } + if s.taskID != "" && s.taskID != taskID { + return errors.New("unexpected task id") + } + return s.err +} + +func (s taskControllerStub) RetryTask(_ context.Context, taskID string) (string, error) { + if taskID == "" { + return "", errors.New("missing task id") + } + if s.taskID != "" && s.taskID != taskID { + return "", errors.New("unexpected task id") + } + return s.queue, s.err +} + +func (s taskControllerStub) ListDeadLetterTasks(_ context.Context, limit int64) ([]QueuedTaskResp, error) { + if limit == 0 { + return s.listResp, s.err + } + return s.listResp, s.err +} + +type taskReaderStub struct { + detail *taskmodule.TaskDetailResp + list *dto.ListResp[taskmodule.TaskResp] + err error +} + +func (s taskReaderStub) GetDetail(_ context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { + if taskID == "" { + return nil, errors.New("missing task id") + } + return s.detail, s.err +} + +func (s taskReaderStub) PollLogs(_ context.Context, taskID string, _ time.Time) (*taskmodule.TaskLogPollResp, error) { + if taskID == "" { + return nil, errors.New("missing task id") + } + return &taskmodule.TaskLogPollResp{ + Logs: []dto.LogEntry{{TaskID: taskID, Line: "hello"}}, + Terminal: false, + State: consts.GetTaskStateName(consts.TaskPending), + CreatedAt: time.Unix(1710000000, 0), + }, s.err +} + +func (s taskReaderStub) List(_ context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.list, s.err +} + +type traceReaderStub struct { + detail *tracemodule.TraceDetailResp + list *dto.ListResp[tracemodule.TraceResp] + algorithms []dto.ContainerVersionItem + messages []redis.XStream + err error +} + +func (s traceReaderStub) GetTrace(_ context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { + if traceID == "" { + return nil, errors.New("missing trace id") + } + return s.detail, s.err +} + +func (s traceReaderStub) ListTraces(_ context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.list, s.err +} + +func (s traceReaderStub) GetTraceStreamAlgorithms(_ context.Context, traceID string) ([]dto.ContainerVersionItem, error) { + if traceID == "" { + return nil, errors.New("missing trace id") + } + return s.algorithms, s.err +} + +func (s traceReaderStub) ReadTraceStreamMessages(_ context.Context, streamKey, _ string, _ int64, _ time.Duration) ([]redis.XStream, error) { + if streamKey == "" { + return nil, errors.New("missing stream key") + } + return s.messages, s.err +} + +type groupReaderStub struct { + stats *groupmodule.GroupStats + count int64 + messages []redis.XStream + err error +} + +func (s groupReaderStub) GetGroupStats(_ context.Context, req *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) { + if req == nil || req.GroupID == "" { + return nil, errors.New("missing group id") + } + return s.stats, s.err +} + +func (s groupReaderStub) GetGroupTraceCount(groupID string) (int64, error) { + if groupID == "" { + return 0, errors.New("missing group id") + } + if s.count == 0 { + return 1, s.err + } + return s.count, s.err +} + +func (s groupReaderStub) ReadGroupStreamMessages(_ context.Context, streamKey, _ string, _ int64, _ time.Duration) ([]redis.XStream, error) { + if streamKey == "" { + return nil, errors.New("missing stream key") + } + return s.messages, s.err +} + +type notificationReaderStub struct { + messages []redis.XStream + err error +} + +func (s notificationReaderStub) ReadStreamMessages(_ context.Context, streamKey, _ string, _ int64, _ time.Duration) ([]redis.XStream, error) { + if streamKey == "" { + return nil, errors.New("missing stream key") + } + return s.messages, s.err +} + +func TestOrchestratorServerSubmitExecution(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{resp: &executionmodule.SubmitExecutionResp{ + GroupID: "group-1", + Items: []executionmodule.SubmitExecutionItem{{ + Index: 0, + TraceID: "trace-1", + TaskID: "task-1", + AlgorithmID: 11, + AlgorithmVersionID: 12, + }}, + }}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + body, err := structpb.NewStruct(map[string]any{ + "project_name": "demo", + "specs": []any{ + map[string]any{ + "algorithm": map[string]any{ + "name": "algo", + "version": "1.0.0", + }, + "datapack": "dp-1", + }, + }, + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.SubmitExecution(context.Background(), &orchestratorv1.SubmitExecutionRequest{ + GroupId: "group-1", + UserId: 7, + Body: body, + }) + if err != nil { + t.Fatalf("SubmitExecution() error = %v", err) + } + if resp.GroupId != "group-1" || len(resp.Items) != 1 || resp.Items[0].TaskId != "task-1" { + t.Fatalf("SubmitExecution() unexpected response: %+v", resp) + } +} + +func TestOrchestratorServerSubmitFaultInjection(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{injectionResp: &injectionmodule.SubmitInjectionResp{ + GroupID: "group-2", + OriginalCount: 1, + Items: []injectionmodule.SubmitInjectionItem{{ + Index: 0, + TraceID: "trace-2", + TaskID: "task-2", + }}, + Warnings: &injectionmodule.InjectionWarnings{ + DuplicateServicesInBatch: []string{"svc-a"}, + }, + }}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + body, err := structpb.NewStruct(map[string]any{ + "project_name": "demo", + "pedestal": map[string]any{ + "name": "pedestal", + "version": "1.0.0", + }, + "benchmark": map[string]any{ + "name": "bench", + "version": "1.0.0", + }, + "interval": 10, + "pre_duration": 5, + "specs": []any{ + []any{ + map[string]any{ + "type": "PodChaos", + "name": "fault-a", + }, + }, + }, + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.SubmitFaultInjection(context.Background(), &orchestratorv1.SubmitFaultInjectionRequest{ + GroupId: "group-2", + UserId: 8, + ProjectId: 9, + Body: body, + }) + if err != nil { + t.Fatalf("SubmitFaultInjection() error = %v", err) + } + if resp.GroupId != "group-2" || len(resp.Items) != 1 || resp.Warnings == nil { + t.Fatalf("SubmitFaultInjection() unexpected response: %+v", resp) + } +} + +func TestOrchestratorServerRuntimeMutations(t *testing.T) { + injectionItem := &dto.InjectionItem{ID: 33, Name: "dp-1"} + server := &orchestratorServer{ + execution: executionSubmitterStub{ + id: 22, + item: &executionmodule.ExecutionDetailResp{ + ExecutionResp: executionmodule.ExecutionResp{ID: 22}, + }, + evaluationItems: []executionmodule.EvaluationExecutionItem{{ + Datapack: "dp-1", + ExecutionRef: executionmodule.ExecutionRef{ + ExecutionID: 22, + }, + }}, + }, + injection: injectionSubmitterStub{item: injectionItem}, + metrics: metricsReaderStub{ + injection: &metricmodule.InjectionMetrics{TotalCount: 3}, + execution: &metricmodule.ExecutionMetrics{TotalCount: 4}, + }, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + body, err := structpb.NewStruct(map[string]any{ + "task_id": "task-1", + "algorithm_version_id": 10, + "datapack_id": 11, + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + resp, err := server.CreateExecution(context.Background(), &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + t.Fatalf("CreateExecution() error = %v", err) + } + if resp.GetData().AsMap()["execution_id"] != float64(22) { + t.Fatalf("CreateExecution() unexpected response: %+v", resp.GetData().AsMap()) + } + + injectionBody, err := structpb.NewStruct(map[string]any{ + "name": "dp-1", + "task_id": "task-2", + "display_config": "{}", + "engine_config": "[]", + "groundtruth_source": "auto", + "pre_duration": 5, + "state": consts.GetDatapackStateName(consts.DatapackInitial), + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + created, err := server.CreateInjection(context.Background(), &orchestratorv1.MutationRequest{Body: injectionBody}) + if err != nil { + t.Fatalf("CreateInjection() error = %v", err) + } + if created.GetData().AsMap()["name"] != "dp-1" { + t.Fatalf("CreateInjection() unexpected response: %+v", created.GetData().AsMap()) + } + + updateExecBody, _ := structpb.NewStruct(map[string]any{"execution_id": 22, "state": consts.GetExecutionStateName(consts.ExecutionSuccess)}) + if _, err := server.UpdateExecutionState(context.Background(), &orchestratorv1.MutationRequest{Body: updateExecBody}); err != nil { + t.Fatalf("UpdateExecutionState() error = %v", err) + } + + updateInjectionBody, _ := structpb.NewStruct(map[string]any{"name": "dp-1", "state": consts.GetDatapackStateName(consts.DatapackInjectSuccess)}) + if _, err := server.UpdateInjectionState(context.Background(), &orchestratorv1.MutationRequest{Body: updateInjectionBody}); err != nil { + t.Fatalf("UpdateInjectionState() error = %v", err) + } + + updateTimestampBody, _ := structpb.NewStruct(map[string]any{ + "name": "dp-1", + "start_time": time.Now().Format(time.RFC3339Nano), + "end_time": time.Now().Add(time.Minute).Format(time.RFC3339Nano), + }) + if _, err := server.UpdateInjectionTimestamps(context.Background(), &orchestratorv1.MutationRequest{Body: updateTimestampBody}); err != nil { + t.Fatalf("UpdateInjectionTimestamps() error = %v", err) + } + + got, err := server.GetExecution(context.Background(), &orchestratorv1.GetExecutionRequest{ExecutionId: 22}) + if err != nil { + t.Fatalf("GetExecution() error = %v", err) + } + if got.GetData().AsMap()["id"] != float64(22) { + t.Fatalf("GetExecution() unexpected response: %+v", got.GetData().AsMap()) + } + + metricQuery, _ := structpb.NewStruct(map[string]any{}) + injectionMetricsResp, err := server.GetInjectionMetrics(context.Background(), &orchestratorv1.MutationRequest{Body: metricQuery}) + if err != nil { + t.Fatalf("GetInjectionMetrics() error = %v", err) + } + if injectionMetricsResp.GetData().AsMap()["total_count"] != float64(3) { + t.Fatalf("GetInjectionMetrics() unexpected response: %+v", injectionMetricsResp.GetData().AsMap()) + } + + executionMetricsResp, err := server.GetExecutionMetrics(context.Background(), &orchestratorv1.MutationRequest{Body: metricQuery}) + if err != nil { + t.Fatalf("GetExecutionMetrics() error = %v", err) + } + if executionMetricsResp.GetData().AsMap()["total_count"] != float64(4) { + t.Fatalf("GetExecutionMetrics() unexpected response: %+v", executionMetricsResp.GetData().AsMap()) + } + + datapackQuery, _ := structpb.NewStruct(map[string]any{ + "algorithm_version_id": 11, + "datapack_name": "dp-1", + }) + datapackResp, err := server.ListEvaluationExecutionsByDatapack(context.Background(), &orchestratorv1.MutationRequest{Body: datapackQuery}) + if err != nil { + t.Fatalf("ListEvaluationExecutionsByDatapack() error = %v", err) + } + datapackItems, ok := datapackResp.GetData().AsMap()["items"].([]any) + if !ok || len(datapackItems) != 1 { + t.Fatalf("ListEvaluationExecutionsByDatapack() unexpected response: %+v", datapackResp.GetData().AsMap()) + } + + datasetQuery, _ := structpb.NewStruct(map[string]any{ + "algorithm_version_id": 11, + "dataset_version_id": 7, + }) + datasetResp, err := server.ListEvaluationExecutionsByDataset(context.Background(), &orchestratorv1.MutationRequest{Body: datasetQuery}) + if err != nil { + t.Fatalf("ListEvaluationExecutionsByDataset() error = %v", err) + } + datasetItems, ok := datasetResp.GetData().AsMap()["items"].([]any) + if !ok || len(datasetItems) != 1 { + t.Fatalf("ListEvaluationExecutionsByDataset() unexpected response: %+v", datasetResp.GetData().AsMap()) + } +} + +func TestOrchestratorServerCancelTaskNotFound(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{taskID: "task-404", err: consts.ErrNotFound}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + _, err := server.CancelTask(context.Background(), &orchestratorv1.CancelTaskRequest{TaskId: "task-404"}) + if err == nil { + t.Fatal("CancelTask() error = nil, want error") + } + if status.Code(err) != codes.NotFound { + t.Fatalf("CancelTask() code = %s, want %s", status.Code(err), codes.NotFound) + } +} + +func TestOrchestratorServerListDeadLetterTasks(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{listResp: []QueuedTaskResp{{ + TaskID: "task-dead", + Queue: "task:dead", + Type: consts.GetTaskTypeName(consts.TaskTypeRunAlgorithm), + }}}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + resp, err := server.ListDeadLetterTasks(context.Background(), &orchestratorv1.ListDeadLetterTasksRequest{Limit: 10}) + if err != nil { + t.Fatalf("ListDeadLetterTasks() error = %v", err) + } + items, ok := resp.GetData().AsMap()["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("ListDeadLetterTasks() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerRetryTask(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{taskID: "task-dead", queue: "task:ready"}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + resp, err := server.RetryTask(context.Background(), &orchestratorv1.RetryTaskRequest{TaskId: "task-dead"}) + if err != nil { + t.Fatalf("RetryTask() error = %v", err) + } + if !resp.GetAccepted() || resp.GetQueue() != "task:ready" { + t.Fatalf("RetryTask() unexpected response: %+v", resp) + } +} + +func TestOrchestratorServerGetTask(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{detail: &taskmodule.TaskDetailResp{ + TaskResp: taskmodule.TaskResp{ + ID: "task-1", + Type: consts.GetTaskTypeName(consts.TaskTypeRunAlgorithm), + State: consts.GetTaskStateName(consts.TaskPending), + }, + }}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + resp, err := server.GetTask(context.Background(), &orchestratorv1.GetTaskRequest{TaskId: "task-1"}) + if err != nil { + t.Fatalf("GetTask() error = %v", err) + } + if resp.GetData().AsMap()["id"] != "task-1" { + t.Fatalf("GetTask() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerPollTaskLogs(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + resp, err := server.PollTaskLogs(context.Background(), &orchestratorv1.PollTaskLogsRequest{TaskId: "task-1"}) + if err != nil { + t.Fatalf("PollTaskLogs() error = %v", err) + } + items, ok := resp.GetData().AsMap()["logs"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("PollTaskLogs() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerListTasks(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{list: &dto.ListResp[taskmodule.TaskResp]{ + Items: []taskmodule.TaskResp{{ID: "task-1"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListTasks(context.Background(), &orchestratorv1.ListTasksRequest{Query: query}) + if err != nil { + t.Fatalf("ListTasks() error = %v", err) + } + items, ok := resp.GetData().AsMap()["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("ListTasks() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerGetTrace(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{detail: &tracemodule.TraceDetailResp{ + TraceResp: tracemodule.TraceResp{ + ID: "trace-1", + Type: "full_pipeline", + GroupID: "group-1", + State: consts.GetTraceStateName(consts.TracePending), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + StartTime: time.Now(), + }, + }}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + resp, err := server.GetTrace(context.Background(), &orchestratorv1.GetTraceRequest{TraceId: "trace-1"}) + if err != nil { + t.Fatalf("GetTrace() error = %v", err) + } + if resp.GetData().AsMap()["id"] != "trace-1" { + t.Fatalf("GetTrace() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerListTraces(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{list: &dto.ListResp[tracemodule.TraceResp]{ + Items: []tracemodule.TraceResp{{ID: "trace-1"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListTraces(context.Background(), &orchestratorv1.ListTracesRequest{Query: query}) + if err != nil { + t.Fatalf("ListTraces() error = %v", err) + } + items, ok := resp.GetData().AsMap()["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("ListTraces() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerGetGroupStats(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{stats: &groupmodule.GroupStats{ + TotalTraces: 3, + AvgDuration: 4.5, + }}, + notify: notificationReaderStub{}, + } + + resp, err := server.GetGroupStats(context.Background(), &orchestratorv1.GetGroupStatsRequest{ + GroupId: "d7a4ed4b-1c91-4cdb-8af8-5520fa8d0ce0", + }) + if err != nil { + t.Fatalf("GetGroupStats() error = %v", err) + } + if resp.GetData().AsMap()["total_traces"] != float64(3) { + t.Fatalf("GetGroupStats() unexpected response: %+v", resp.GetData().AsMap()) + } + if resp.GetData().AsMap()["avg_duration"] != 4.5 { + t.Fatalf("GetGroupStats() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestOrchestratorServerTraceAndGroupStreamRPCs(t *testing.T) { + traceMessages := []redis.XStream{{ + Stream: "trace:trace-1:log", + Messages: []redis.XMessage{{ + ID: "1-0", + Values: map[string]any{ + "type": "info", + }, + }}, + }} + groupMessages := []redis.XStream{{ + Stream: "group:group-1:log", + Messages: []redis.XMessage{{ + ID: "2-0", + Values: map[string]any{ + "trace_id": "trace-1", + }, + }}, + }} + + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{ + algorithms: []dto.ContainerVersionItem{{ContainerName: "algo-a"}}, + messages: traceMessages, + }, + groupRead: groupReaderStub{ + count: 3, + messages: groupMessages, + }, + notify: notificationReaderStub{}, + } + + traceState, err := server.GetTraceStreamState(context.Background(), &orchestratorv1.GetTraceStreamStateRequest{TraceId: "trace-1"}) + if err != nil { + t.Fatalf("GetTraceStreamState() error = %v", err) + } + algorithms, ok := traceState.GetData().AsMap()["algorithms"].([]any) + if !ok || len(algorithms) != 1 { + t.Fatalf("GetTraceStreamState() unexpected response: %+v", traceState.GetData().AsMap()) + } + + traceResp, err := server.ReadTraceStreamMessages(context.Background(), &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: "trace:trace-1:log", + LastId: "0", + Count: 10, + }) + if err != nil { + t.Fatalf("ReadTraceStreamMessages() error = %v", err) + } + traceItems, ok := traceResp.GetData().AsMap()["messages"].([]any) + if !ok || len(traceItems) != 1 { + t.Fatalf("ReadTraceStreamMessages() unexpected response: %+v", traceResp.GetData().AsMap()) + } + + groupState, err := server.GetGroupStreamState(context.Background(), &orchestratorv1.GetGroupStreamStateRequest{GroupId: "group-1"}) + if err != nil { + t.Fatalf("GetGroupStreamState() error = %v", err) + } + if groupState.GetData().AsMap()["total_traces"] != float64(3) { + t.Fatalf("GetGroupStreamState() unexpected response: %+v", groupState.GetData().AsMap()) + } + + groupResp, err := server.ReadGroupStreamMessages(context.Background(), &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: "group:group-1:log", + LastId: "0", + Count: 10, + }) + if err != nil { + t.Fatalf("ReadGroupStreamMessages() error = %v", err) + } + groupItems, ok := groupResp.GetData().AsMap()["messages"].([]any) + if !ok || len(groupItems) != 1 { + t.Fatalf("ReadGroupStreamMessages() unexpected response: %+v", groupResp.GetData().AsMap()) + } +} + +func TestOrchestratorServerReadNotificationStreamMessages(t *testing.T) { + server := &orchestratorServer{ + execution: executionSubmitterStub{}, + injection: injectionSubmitterStub{}, + metrics: metricsReaderStub{}, + tasks: taskControllerStub{}, + taskRead: taskReaderStub{}, + traceRead: traceReaderStub{}, + groupRead: groupReaderStub{}, + notify: notificationReaderStub{messages: []redis.XStream{{ + Stream: consts.NotificationStreamKey, + Messages: []redis.XMessage{{ + ID: "3-0", + Values: map[string]any{ + "type": "execution", + }, + }}, + }}}, + } + + resp, err := server.ReadNotificationStreamMessages(context.Background(), &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: consts.NotificationStreamKey, + LastId: "0", + Count: 10, + }) + if err != nil { + t.Fatalf("ReadNotificationStreamMessages() error = %v", err) + } + items, ok := resp.GetData().AsMap()["messages"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("ReadNotificationStreamMessages() unexpected response: %+v", resp.GetData().AsMap()) + } +} diff --git a/src/interface/grpcresource/lifecycle.go b/src/interface/grpcresource/lifecycle.go new file mode 100644 index 00000000..d18b02a3 --- /dev/null +++ b/src/interface/grpcresource/lifecycle.go @@ -0,0 +1,94 @@ +package grpcresourceinterface + +import ( + "context" + "fmt" + "net" + + "aegis/config" + "aegis/httpx" + resourcev1 "aegis/proto/resource/v1" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const defaultResourceGRPCAddr = ":9093" + +type Lifecycle struct { + server *grpc.Server + addr string + listener net.Listener + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(resourceServer *resourceServer) (*Lifecycle, error) { + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(httpx.UnaryServerRequestIDInterceptor())) + resourcev1.RegisterResourceServiceServer(grpcServer, resourceServer) + + healthServer := health.NewServer() + healthServer.SetServingStatus(resourcev1.ResourceService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + + if config.GetBool("resource.grpc.reflection") { + reflection.Register(grpcServer) + } + + addr := config.GetString("resource.grpc.addr") + if addr == "" { + addr = defaultResourceGRPCAddr + } + + return &Lifecycle{ + server: grpcServer, + addr: addr, + }, nil +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + + listener, err := net.Listen("tcp", r.addr) + if err != nil { + return fmt.Errorf("listen resource grpc on %s: %w", r.addr, err) + } + r.listener = listener + + go func() { + logrus.Infof("Starting resource gRPC server on %s", r.addr) + if err := r.server.Serve(listener); err != nil { + logrus.Errorf("resource gRPC server error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.server != nil { + r.server.GracefulStop() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return runner.start(ctx) + }, + OnStop: func(ctx context.Context) error { + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/grpcresource/module.go b/src/interface/grpcresource/module.go new file mode 100644 index 00000000..ffa1859e --- /dev/null +++ b/src/interface/grpcresource/module.go @@ -0,0 +1,11 @@ +package grpcresourceinterface + +import "go.uber.org/fx" + +var Module = fx.Module("grpc_resource", + fx.Provide( + newResourceServer, + newLifecycle, + ), + fx.Invoke(registerLifecycle), +) diff --git a/src/interface/grpcresource/service.go b/src/interface/grpcresource/service.go new file mode 100644 index 00000000..422911a8 --- /dev/null +++ b/src/interface/grpcresource/service.go @@ -0,0 +1,543 @@ +package grpcresourceinterface + +import ( + "context" + "encoding/json" + "errors" + "time" + + "aegis/consts" + "aegis/dto" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + labelmodule "aegis/module/label" + projectmodule "aegis/module/project" + resourcev1 "aegis/proto/resource/v1" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/structpb" +) + +const resourceServiceName = "resource-service" + +type projectReader interface { + GetProjectDetail(context.Context, int) (*projectmodule.ProjectDetailResp, error) + ListProjects(context.Context, *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) +} + +type containerReader interface { + GetContainer(context.Context, int) (*containermodule.ContainerDetailResp, error) + ListContainers(context.Context, *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) +} + +type datasetReader interface { + GetDataset(context.Context, int) (*datasetmodule.DatasetDetailResp, error) + ListDatasets(context.Context, *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) +} + +type evaluationReader interface { + ListDatapackEvaluationResults(context.Context, *evaluationmodule.BatchEvaluateDatapackReq, int) (*evaluationmodule.BatchEvaluateDatapackResp, error) + ListDatasetEvaluationResults(context.Context, *evaluationmodule.BatchEvaluateDatasetReq, int) (*evaluationmodule.BatchEvaluateDatasetResp, error) + ListEvaluations(context.Context, *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) + GetEvaluation(context.Context, int) (*evaluationmodule.EvaluationResp, error) + DeleteEvaluation(context.Context, int) error +} + +type labelReader interface { + BatchDelete(context.Context, []int) error + Create(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) + Delete(context.Context, int) error + GetDetail(context.Context, int) (*labelmodule.LabelDetailResp, error) + List(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) + Update(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) +} + +type chaosSystemReader interface { + ListSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) + GetSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) + CreateSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) + UpdateSystem(context.Context, int, *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) + DeleteSystem(context.Context, int) error + UpsertMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error + ListMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) +} + +type chaosSystemMetadataListResponse struct { + Items []chaossystemmodule.SystemMetadataResp `json:"items"` +} + +type resourceServer struct { + resourcev1.UnimplementedResourceServiceServer + projects projectReader + containers containerReader + datasets datasetReader + labels labelReader + chaosSystems chaosSystemReader + evaluations evaluationReader +} + +func newResourceServer( + projects *projectmodule.Service, + containers *containermodule.Service, + datasets *datasetmodule.Service, + labels labelmodule.HandlerService, + chaosSystems chaossystemmodule.HandlerService, + evaluations *evaluationmodule.Service, +) *resourceServer { + return &resourceServer{ + projects: projects, + containers: containers, + datasets: datasets, + labels: labels, + chaosSystems: chaosSystems, + evaluations: evaluations, + } +} + +func (s *resourceServer) Ping(context.Context, *resourcev1.PingRequest) (*resourcev1.PingResponse, error) { + return &resourcev1.PingResponse{ + Service: resourceServiceName, + AppId: consts.AppID, + Status: "ok", + TimestampUnix: time.Now().Unix(), + }, nil +} + +func (s *resourceServer) ListProjects(ctx context.Context, req *resourcev1.ListProjectsRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[projectmodule.ListProjectReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.projects.ListProjects(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) GetProject(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.projects.GetProjectDetail(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) ListContainers(ctx context.Context, req *resourcev1.ListContainersRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[containermodule.ListContainerReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.containers.ListContainers(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) GetContainer(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.containers.GetContainer(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) ListDatasets(ctx context.Context, req *resourcev1.ListDatasetsRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[datasetmodule.ListDatasetReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.datasets.ListDatasets(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) GetDataset(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.datasets.GetDataset(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) CreateLabel(ctx context.Context, req *resourcev1.MutationRequest) (*resourcev1.ResourceItemResponse, error) { + body, err := decodeQuery[labelmodule.CreateLabelReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.labels.Create(ctx, body) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) GetLabel(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.labels.GetDetail(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) ListLabels(ctx context.Context, req *resourcev1.QueryRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[labelmodule.ListLabelReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.labels.List(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) UpdateLabel(ctx context.Context, req *resourcev1.UpdateByIDRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + body, err := decodeQuery[labelmodule.UpdateLabelReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := body.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.labels.Update(ctx, body, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) DeleteLabel(ctx context.Context, req *resourcev1.GetResourceRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + if err := s.labels.Delete(ctx, int(req.GetId())); err != nil { + return nil, mapResourceError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *resourceServer) BatchDeleteLabels(ctx context.Context, req *resourcev1.BatchDeleteRequest) (*emptypb.Empty, error) { + if err := validatePositiveInt64s(req.GetIds(), "ids"); err != nil { + return nil, err + } + if err := s.labels.BatchDelete(ctx, int64sToInts(req.GetIds())); err != nil { + return nil, mapResourceError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *resourceServer) ListChaosSystems(ctx context.Context, req *resourcev1.QueryRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[chaossystemmodule.ListChaosSystemReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.chaosSystems.ListSystems(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) GetChaosSystem(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.chaosSystems.GetSystem(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) CreateChaosSystem(ctx context.Context, req *resourcev1.MutationRequest) (*resourcev1.ResourceItemResponse, error) { + body, err := decodeQuery[chaossystemmodule.CreateChaosSystemReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.chaosSystems.CreateSystem(ctx, body) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) UpdateChaosSystem(ctx context.Context, req *resourcev1.UpdateByIDRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + body, err := decodeQuery[chaossystemmodule.UpdateChaosSystemReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.chaosSystems.UpdateSystem(ctx, int(req.GetId()), body) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) DeleteChaosSystem(ctx context.Context, req *resourcev1.GetResourceRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + if err := s.chaosSystems.DeleteSystem(ctx, int(req.GetId())); err != nil { + return nil, mapResourceError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *resourceServer) UpsertChaosSystemMetadata(ctx context.Context, req *resourcev1.UpdateByIDRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + body, err := decodeQuery[chaossystemmodule.BulkUpsertSystemMetadataReq](req.GetBody()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := s.chaosSystems.UpsertMetadata(ctx, int(req.GetId()), body); err != nil { + return nil, mapResourceError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *resourceServer) ListChaosSystemMetadata(ctx context.Context, req *resourcev1.IDQueryRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + query, err := decodeQuery[struct { + Type string `json:"type"` + }](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.chaosSystems.ListMetadata(ctx, int(req.GetId()), query.Type) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(chaosSystemMetadataListResponse{Items: resp}) +} + +func (s *resourceServer) ListDatapackEvaluationResults(ctx context.Context, req *resourcev1.ListDatapackEvaluationsRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + query, err := decodeQuery[evaluationmodule.BatchEvaluateDatapackReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.evaluations.ListDatapackEvaluationResults(ctx, query, int(req.GetUserId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) ListDatasetEvaluationResults(ctx context.Context, req *resourcev1.ListDatasetEvaluationsRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetUserId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id is required") + } + + query, err := decodeQuery[evaluationmodule.BatchEvaluateDatasetReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.evaluations.ListDatasetEvaluationResults(ctx, query, int(req.GetUserId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) ListEvaluations(ctx context.Context, req *resourcev1.ListEvaluationsRequest) (*resourcev1.ResourceListResponse, error) { + query, err := decodeQuery[evaluationmodule.ListEvaluationReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.evaluations.ListEvaluations(ctx, query) + if err != nil { + return nil, mapResourceError(err) + } + return encodeListResponse(resp) +} + +func (s *resourceServer) GetEvaluation(ctx context.Context, req *resourcev1.GetResourceRequest) (*resourcev1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + resp, err := s.evaluations.GetEvaluation(ctx, int(req.GetId())) + if err != nil { + return nil, mapResourceError(err) + } + return encodeItemResponse(resp) +} + +func (s *resourceServer) DeleteEvaluation(ctx context.Context, req *resourcev1.GetResourceRequest) (*emptypb.Empty, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + if err := s.evaluations.DeleteEvaluation(ctx, int(req.GetId())); err != nil { + return nil, mapResourceError(err) + } + return &emptypb.Empty{}, nil +} + +func decodeQuery[T any](query *structpb.Struct) (*T, error) { + var result T + if query == nil { + return &result, nil + } + + data, err := json.Marshal(query.AsMap()) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func encodeItemResponse(value any) (*resourcev1.ResourceItemResponse, error) { + item, err := toStruct(value) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &resourcev1.ResourceItemResponse{Data: item}, nil +} + +func encodeListResponse(value any) (*resourcev1.ResourceListResponse, error) { + item, err := toStruct(value) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &resourcev1.ResourceListResponse{Data: item}, nil +} + +func toStruct(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + return structpb.NewStruct(payload) +} + +func mapResourceError(err error) error { + switch { + case errors.Is(err, consts.ErrAuthenticationFailed): + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Is(err, consts.ErrPermissionDenied): + return status.Error(codes.PermissionDenied, err.Error()) + case errors.Is(err, consts.ErrBadRequest): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, consts.ErrNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, consts.ErrAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + case err != nil: + return status.Error(codes.Internal, err.Error()) + default: + return nil + } +} + +func validatePositiveInt64s(items []int64, field string) error { + if len(items) == 0 { + return status.Errorf(codes.InvalidArgument, "%s is required", field) + } + for _, item := range items { + if item <= 0 { + return status.Errorf(codes.InvalidArgument, "%s must contain positive integers", field) + } + } + return nil +} + +func int64sToInts(items []int64) []int { + if len(items) == 0 { + return nil + } + result := make([]int, 0, len(items)) + for _, item := range items { + result = append(result, int(item)) + } + return result +} diff --git a/src/interface/grpcresource/service_test.go b/src/interface/grpcresource/service_test.go new file mode 100644 index 00000000..c3353083 --- /dev/null +++ b/src/interface/grpcresource/service_test.go @@ -0,0 +1,465 @@ +package grpcresourceinterface + +import ( + "context" + "errors" + "testing" + + "aegis/consts" + "aegis/dto" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + labelmodule "aegis/module/label" + projectmodule "aegis/module/project" + resourcev1 "aegis/proto/resource/v1" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type projectReaderStub struct { + listResp *dto.ListResp[projectmodule.ProjectResp] + getResp *projectmodule.ProjectDetailResp + err error +} + +func (s projectReaderStub) GetProjectDetail(_ context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { + if projectID <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s projectReaderStub) ListProjects(_ context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +type containerReaderStub struct { + listResp *dto.ListResp[containermodule.ContainerResp] + getResp *containermodule.ContainerDetailResp + err error +} + +func (s containerReaderStub) GetContainer(_ context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { + if containerID <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s containerReaderStub) ListContainers(_ context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +type datasetReaderStub struct { + listResp *dto.ListResp[datasetmodule.DatasetResp] + getResp *datasetmodule.DatasetDetailResp + err error +} + +func (s datasetReaderStub) GetDataset(_ context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { + if datasetID <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s datasetReaderStub) ListDatasets(_ context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +type evaluationReaderStub struct { + datapackResp *evaluationmodule.BatchEvaluateDatapackResp + datasetResp *evaluationmodule.BatchEvaluateDatasetResp + listResp *dto.ListResp[evaluationmodule.EvaluationResp] + getResp *evaluationmodule.EvaluationResp + err error +} + +func (s evaluationReaderStub) ListDatapackEvaluationResults(_ context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { + if req == nil || userID <= 0 { + return nil, errors.New("invalid request") + } + return s.datapackResp, s.err +} + +func (s evaluationReaderStub) ListDatasetEvaluationResults(_ context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { + if req == nil || userID <= 0 { + return nil, errors.New("invalid request") + } + return s.datasetResp, s.err +} + +func (s evaluationReaderStub) ListEvaluations(_ context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +func (s evaluationReaderStub) GetEvaluation(_ context.Context, id int) (*evaluationmodule.EvaluationResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s evaluationReaderStub) DeleteEvaluation(_ context.Context, id int) error { + if id <= 0 { + return errors.New("invalid id") + } + return s.err +} + +type labelReaderStub struct { + listResp *dto.ListResp[labelmodule.LabelResp] + getResp *labelmodule.LabelDetailResp + itemResp *labelmodule.LabelResp + err error +} + +func (s labelReaderStub) BatchDelete(_ context.Context, ids []int) error { + if len(ids) == 0 { + return errors.New("ids required") + } + return s.err +} + +func (s labelReaderStub) Create(_ context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.itemResp, s.err +} + +func (s labelReaderStub) Delete(_ context.Context, id int) error { + if id <= 0 { + return errors.New("invalid id") + } + return s.err +} + +func (s labelReaderStub) GetDetail(_ context.Context, id int) (*labelmodule.LabelDetailResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s labelReaderStub) List(_ context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +func (s labelReaderStub) Update(_ context.Context, req *labelmodule.UpdateLabelReq, id int) (*labelmodule.LabelResp, error) { + if req == nil || id <= 0 { + return nil, errors.New("invalid request") + } + return s.itemResp, s.err +} + +type chaosSystemReaderStub struct { + listResp *dto.ListResp[chaossystemmodule.ChaosSystemResp] + getResp *chaossystemmodule.ChaosSystemResp + metadataResp []chaossystemmodule.SystemMetadataResp + err error +} + +func (s chaosSystemReaderStub) ListSystems(_ context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.listResp, s.err +} + +func (s chaosSystemReaderStub) GetSystem(_ context.Context, id int) (*chaossystemmodule.ChaosSystemResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.getResp, s.err +} + +func (s chaosSystemReaderStub) CreateSystem(_ context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + if req == nil { + return nil, errors.New("nil request") + } + return s.getResp, s.err +} + +func (s chaosSystemReaderStub) UpdateSystem(_ context.Context, id int, req *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + if id <= 0 || req == nil { + return nil, errors.New("invalid request") + } + return s.getResp, s.err +} + +func (s chaosSystemReaderStub) DeleteSystem(_ context.Context, id int) error { + if id <= 0 { + return errors.New("invalid id") + } + return s.err +} + +func (s chaosSystemReaderStub) UpsertMetadata(_ context.Context, id int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { + if id <= 0 || req == nil { + return errors.New("invalid request") + } + return s.err +} + +func (s chaosSystemReaderStub) ListMetadata(_ context.Context, id int, _ string) ([]chaossystemmodule.SystemMetadataResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.metadataResp, s.err +} + +func TestResourceServerListProjects(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{listResp: &dto.ListResp[projectmodule.ProjectResp]{ + Items: []projectmodule.ProjectResp{{ID: 1, Name: "demo"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListProjects(context.Background(), &resourcev1.ListProjectsRequest{Query: query}) + if err != nil { + t.Fatalf("ListProjects() error = %v", err) + } + if got := resp.GetData().AsMap()["items"]; got == nil { + t.Fatalf("ListProjects() missing items in response: %+v", resp.GetData().AsMap()) + } +} + +func TestResourceServerGetDatasetNotFound(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{err: consts.ErrNotFound}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + _, err := server.GetDataset(context.Background(), &resourcev1.GetResourceRequest{Id: 8}) + if err == nil { + t.Fatal("GetDataset() error = nil, want error") + } + if status.Code(err) != codes.NotFound { + t.Fatalf("GetDataset() code = %s, want %s", status.Code(err), codes.NotFound) + } +} + +func TestResourceServerListContainersInvalidQuery(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": -1}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + _, err = server.ListContainers(context.Background(), &resourcev1.ListContainersRequest{Query: query}) + if err == nil { + t.Fatal("ListContainers() error = nil, want error") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("ListContainers() code = %s, want %s", status.Code(err), codes.InvalidArgument) + } +} + +func TestResourceServerListDatapackEvaluationsRequiresUserID(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{ + "specs": []any{ + map[string]any{ + "algorithm": map[string]any{"name": "algo", "version": "v1.0.0"}, + "datapack": "pack-a", + }, + }, + }) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + _, err = server.ListDatapackEvaluationResults(context.Background(), &resourcev1.ListDatapackEvaluationsRequest{Query: query}) + if err == nil { + t.Fatal("ListDatapackEvaluationResults() error = nil, want error") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("ListDatapackEvaluationResults() code = %s, want %s", status.Code(err), codes.InvalidArgument) + } +} + +func TestResourceServerListEvaluations(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{listResp: &dto.ListResp[evaluationmodule.EvaluationResp]{ + Items: []evaluationmodule.EvaluationResp{{ID: 3, EvalType: consts.EvalTypeDataset}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListEvaluations(context.Background(), &resourcev1.ListEvaluationsRequest{Query: query}) + if err != nil { + t.Fatalf("ListEvaluations() error = %v", err) + } + if got := resp.GetData().AsMap()["items"]; got == nil { + t.Fatalf("ListEvaluations() missing items in response: %+v", resp.GetData().AsMap()) + } +} + +func TestResourceServerListLabels(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{listResp: &dto.ListResp[labelmodule.LabelResp]{ + Items: []labelmodule.LabelResp{{ID: 9, Key: "env", Value: "prod"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListLabels(context.Background(), &resourcev1.QueryRequest{Query: query}) + if err != nil { + t.Fatalf("ListLabels() error = %v", err) + } + if got := resp.GetData().AsMap()["items"]; got == nil { + t.Fatalf("ListLabels() missing items in response: %+v", resp.GetData().AsMap()) + } +} + +func TestResourceServerBatchDeleteLabelsRequiresIDs(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{}, + evaluations: evaluationReaderStub{}, + } + + _, err := server.BatchDeleteLabels(context.Background(), &resourcev1.BatchDeleteRequest{}) + if err == nil { + t.Fatal("BatchDeleteLabels() error = nil, want error") + } + if status.Code(err) != codes.InvalidArgument { + t.Fatalf("BatchDeleteLabels() code = %s, want %s", status.Code(err), codes.InvalidArgument) + } +} + +func TestResourceServerListChaosSystems(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{listResp: &dto.ListResp[chaossystemmodule.ChaosSystemResp]{ + Items: []chaossystemmodule.ChaosSystemResp{{ID: 4, Name: "k8s"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListChaosSystems(context.Background(), &resourcev1.QueryRequest{Query: query}) + if err != nil { + t.Fatalf("ListChaosSystems() error = %v", err) + } + if got := resp.GetData().AsMap()["items"]; got == nil { + t.Fatalf("ListChaosSystems() missing items in response: %+v", resp.GetData().AsMap()) + } +} + +func TestResourceServerListChaosSystemMetadata(t *testing.T) { + server := &resourceServer{ + projects: projectReaderStub{}, + containers: containerReaderStub{}, + datasets: datasetReaderStub{}, + labels: labelReaderStub{}, + chaosSystems: chaosSystemReaderStub{metadataResp: []chaossystemmodule.SystemMetadataResp{ + {ID: 1, SystemName: "k8s", MetadataType: "service", ServiceName: "api"}, + }}, + evaluations: evaluationReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"type": "service"}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListChaosSystemMetadata(context.Background(), &resourcev1.IDQueryRequest{Id: 4, Query: query}) + if err != nil { + t.Fatalf("ListChaosSystemMetadata() error = %v", err) + } + items, ok := resp.GetData().AsMap()["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("ListChaosSystemMetadata() unexpected response: %+v", resp.GetData().AsMap()) + } +} diff --git a/src/interface/grpcruntime/lifecycle.go b/src/interface/grpcruntime/lifecycle.go new file mode 100644 index 00000000..331364d7 --- /dev/null +++ b/src/interface/grpcruntime/lifecycle.go @@ -0,0 +1,94 @@ +package grpcruntimeinterface + +import ( + "context" + "fmt" + "net" + + "aegis/config" + "aegis/httpx" + runtimev1 "aegis/proto/runtime/v1" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const defaultRuntimeGRPCAddr = ":9094" + +type Lifecycle struct { + server *grpc.Server + addr string + listener net.Listener + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(runtimeServer *runtimeServer) (*Lifecycle, error) { + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(httpx.UnaryServerRequestIDInterceptor())) + runtimev1.RegisterRuntimeServiceServer(grpcServer, runtimeServer) + + healthServer := health.NewServer() + healthServer.SetServingStatus(runtimev1.RuntimeService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + + if config.GetBool("runtime_worker.grpc.reflection") { + reflection.Register(grpcServer) + } + + addr := config.GetString("runtime_worker.grpc.addr") + if addr == "" { + addr = defaultRuntimeGRPCAddr + } + + return &Lifecycle{ + server: grpcServer, + addr: addr, + }, nil +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + + listener, err := net.Listen("tcp", r.addr) + if err != nil { + return fmt.Errorf("listen runtime grpc on %s: %w", r.addr, err) + } + r.listener = listener + + go func() { + logrus.Infof("Starting runtime gRPC server on %s", r.addr) + if err := r.server.Serve(listener); err != nil { + logrus.Errorf("runtime gRPC server error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.server != nil { + r.server.GracefulStop() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return runner.start(ctx) + }, + OnStop: func(ctx context.Context) error { + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/grpcruntime/module.go b/src/interface/grpcruntime/module.go new file mode 100644 index 00000000..4e22597e --- /dev/null +++ b/src/interface/grpcruntime/module.go @@ -0,0 +1,11 @@ +package grpcruntimeinterface + +import "go.uber.org/fx" + +var Module = fx.Module("grpc_runtime", + fx.Provide( + newRuntimeServer, + newLifecycle, + ), + fx.Invoke(registerLifecycle), +) diff --git a/src/interface/grpcruntime/service.go b/src/interface/grpcruntime/service.go new file mode 100644 index 00000000..93e52b0b --- /dev/null +++ b/src/interface/grpcruntime/service.go @@ -0,0 +1,231 @@ +package grpcruntimeinterface + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/consts" + "aegis/dto" + buildkitinfra "aegis/infra/buildkit" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + taskmodule "aegis/module/task" + runtimev1 "aegis/proto/runtime/v1" + "aegis/service/consumer" + + "go.uber.org/fx" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + "gorm.io/gorm" +) + +type runtimeServerParams struct { + fx.In + + DB *gorm.DB + RedisGateway *redisinfra.Gateway + K8sGateway *k8sinfra.Gateway + BuildKit *buildkitinfra.Gateway + Helm *helminfra.Gateway + RestartLimiter *consumer.TokenBucketRateLimiter `name:"restart_limiter"` + BuildLimiter *consumer.TokenBucketRateLimiter `name:"build_limiter"` + AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` +} + +type runtimeServer struct { + runtimev1.UnimplementedRuntimeServiceServer + snapshots *consumer.RuntimeSnapshotService + redis *redisinfra.Gateway +} + +func newRuntimeServer(params runtimeServerParams) *runtimeServer { + return &runtimeServer{ + snapshots: consumer.NewRuntimeSnapshotService( + params.DB, + params.RedisGateway, + params.K8sGateway, + params.BuildKit, + params.Helm, + params.RestartLimiter, + params.BuildLimiter, + params.AlgoLimiter, + ), + redis: params.RedisGateway, + } +} + +func (s *runtimeServer) Ping(ctx context.Context, _ *runtimev1.PingRequest) (*runtimev1.PingResponse, error) { + status := s.snapshots.RuntimeStatus(ctx) + return &runtimev1.PingResponse{ + Service: status.ServiceName, + AppId: status.AppID, + Status: "ok", + TimestampUnix: time.Now().Unix(), + }, nil +} + +func (s *runtimeServer) GetRuntimeStatus(ctx context.Context, _ *runtimev1.RuntimeStatusRequest) (*runtimev1.RuntimeStatusResponse, error) { + status := s.snapshots.RuntimeStatus(ctx) + return &runtimev1.RuntimeStatusResponse{ + Service: status.ServiceName, + Mode: status.Mode, + AppId: status.AppID, + StartedAtUnix: status.StartedAt.Unix(), + UptimeSeconds: status.UptimeSeconds, + DbAvailable: status.DB.Available, + DbHealthy: status.DB.Healthy, + DbError: status.DB.Error, + RedisAvailable: status.Redis.Available, + RedisHealthy: status.Redis.Healthy, + RedisError: status.Redis.Error, + K8SAvailable: status.K8s.Available, + K8SHealthy: status.K8s.Healthy, + K8SError: status.K8s.Error, + BuildkitAvailable: status.BuildKit.Available, + BuildkitHealthy: status.BuildKit.Healthy, + BuildkitError: status.BuildKit.Error, + HelmAvailable: status.Helm.Available, + HelmHealthy: status.Helm.Healthy, + HelmError: status.Helm.Error, + }, nil +} + +func (s *runtimeServer) GetQueueStatus(ctx context.Context, _ *runtimev1.QueueStatusRequest) (*runtimev1.QueueStatusResponse, error) { + stats, err := s.snapshots.QueueStatus(ctx) + if err != nil { + return nil, err + } + return &runtimev1.QueueStatusResponse{ + ReadyCount: stats.ReadyCount, + DelayedCount: stats.DelayedCount, + DeadCount: stats.DeadCount, + IndexedCount: stats.IndexedCount, + ConcurrencyCount: stats.ConcurrencyCount, + }, nil +} + +func (s *runtimeServer) GetLimiterStatus(ctx context.Context, _ *runtimev1.LimiterStatusRequest) (*runtimev1.LimiterStatusResponse, error) { + snapshots := s.snapshots.LimiterStatus(ctx) + items := make([]*runtimev1.LimiterStatus, 0, len(snapshots)) + for _, snapshot := range snapshots { + item := &runtimev1.LimiterStatus{ + ServiceName: snapshot.ServiceName, + BucketKey: snapshot.BucketKey, + MaxTokens: int64(snapshot.MaxTokens), + WaitTimeoutSeconds: int64(snapshot.WaitTimeout.Seconds()), + InUseTokens: snapshot.InUseTokens, + } + if snapshot.InUseTokensLoadErr != nil { + item.Error = snapshot.InUseTokensLoadErr.Error() + } + items = append(items, item) + } + return &runtimev1.LimiterStatusResponse{Items: items}, nil +} + +func (s *runtimeServer) GetNamespaceLocks(ctx context.Context, _ *runtimev1.PingRequest) (*runtimev1.StructResponse, error) { + items, err := listNamespaceLocks(ctx, s.redis) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return encodeStruct(items) +} + +func (s *runtimeServer) GetQueuedTasks(ctx context.Context, _ *runtimev1.PingRequest) (*runtimev1.StructResponse, error) { + items, err := listQueuedTasks(ctx, s.redis) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return encodeStruct(items) +} + +func listNamespaceLocks(ctx context.Context, redis *redisinfra.Gateway) (map[string]map[string]any, error) { + namespaces, err := redis.SetMembers(ctx, consts.NamespacesKey) + if err != nil { + return nil, err + } + + items := make(map[string]map[string]any, len(namespaces)) + for _, namespace := range namespaces { + nsKey := fmt.Sprintf(consts.NamespaceKeyPattern, namespace) + values, err := redis.HashGetAll(ctx, nsKey) + if err != nil { + return nil, err + } + entry := make(map[string]any, len(values)) + for key, value := range values { + entry[key] = value + } + items[namespace] = entry + } + return items, nil +} + +func listQueuedTasks(ctx context.Context, redis *redisinfra.Gateway) (map[string]any, error) { + readyItems, err := redis.ListReadyTasks(ctx) + if err != nil { + return nil, err + } + delayedItems, err := redis.ListDelayedTasks(ctx, 1000) + if err != nil { + return nil, err + } + + readyTasks, err := decodeQueuedTasks(readyItems) + if err != nil { + return nil, err + } + delayedTasks, err := decodeQueuedTasks(delayedItems) + if err != nil { + return nil, err + } + + return map[string]any{ + "ready_tasks": readyTasks, + "delayed_tasks": delayedTasks, + }, nil +} + +func decodeQueuedTasks(items []string) ([]taskmodule.TaskResp, error) { + result := make([]taskmodule.TaskResp, 0, len(items)) + for _, item := range items { + var task dto.UnifiedTask + if err := json.Unmarshal([]byte(item), &task); err != nil { + return nil, err + } + result = append(result, taskmodule.TaskResp{ + ID: task.TaskID, + Type: consts.GetTaskTypeName(task.Type), + Immediate: task.Immediate, + ExecuteTime: task.ExecuteTime, + CronExpr: task.CronExpr, + TraceID: task.TraceID, + GroupID: task.GroupID, + State: consts.GetTaskStateName(task.State), + Status: consts.GetStatusTypeName(consts.CommonEnabled), + ProjectID: task.ProjectID, + }) + } + return result, nil +} + +func encodeStruct(value any) (*runtimev1.StructResponse, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + item, err := structpb.NewStruct(payload) + if err != nil { + return nil, err + } + return &runtimev1.StructResponse{Data: item}, nil +} diff --git a/src/interface/grpcruntime/service_test.go b/src/interface/grpcruntime/service_test.go new file mode 100644 index 00000000..9f1a8529 --- /dev/null +++ b/src/interface/grpcruntime/service_test.go @@ -0,0 +1,52 @@ +package grpcruntimeinterface + +import ( + "context" + "testing" + "time" + + "aegis/consts" + runtimev1 "aegis/proto/runtime/v1" + "aegis/service/consumer" +) + +func TestRuntimeServerStatusEndpoints(t *testing.T) { + originalStart := consts.InitialTime + originalAppID := consts.AppID + startedAt := time.Unix(1_700_000_000, 0) + consts.InitialTime = &startedAt + consts.AppID = "app-test" + t.Cleanup(func() { + consts.InitialTime = originalStart + consts.AppID = originalAppID + }) + + server := &runtimeServer{ + snapshots: consumer.NewRuntimeSnapshotService(nil, nil, nil, nil, nil, nil, nil, nil), + } + + pingResp, err := server.Ping(context.Background(), &runtimev1.PingRequest{}) + if err != nil { + t.Fatalf("Ping() error = %v", err) + } + if pingResp.Service != consumer.RuntimeServiceName { + t.Fatalf("Ping() service = %q, want %q", pingResp.Service, consumer.RuntimeServiceName) + } + if pingResp.AppId != "app-test" { + t.Fatalf("Ping() app id = %q, want %q", pingResp.AppId, "app-test") + } + + statusResp, err := server.GetRuntimeStatus(context.Background(), &runtimev1.RuntimeStatusRequest{}) + if err != nil { + t.Fatalf("GetRuntimeStatus() error = %v", err) + } + if statusResp.Service != consumer.RuntimeServiceName { + t.Fatalf("GetRuntimeStatus() service = %q, want %q", statusResp.Service, consumer.RuntimeServiceName) + } + if statusResp.Mode != "runtime-worker" { + t.Fatalf("GetRuntimeStatus() mode = %q, want %q", statusResp.Mode, "runtime-worker") + } + if statusResp.DbAvailable || statusResp.RedisAvailable || statusResp.K8SAvailable || statusResp.BuildkitAvailable || statusResp.HelmAvailable { + t.Fatalf("GetRuntimeStatus() unexpected dependency availability: %+v", statusResp) + } +} diff --git a/src/interface/grpcsystem/lifecycle.go b/src/interface/grpcsystem/lifecycle.go new file mode 100644 index 00000000..ec2afc42 --- /dev/null +++ b/src/interface/grpcsystem/lifecycle.go @@ -0,0 +1,94 @@ +package grpcsysteminterface + +import ( + "context" + "fmt" + "net" + + "aegis/config" + "aegis/httpx" + systemv1 "aegis/proto/system/v1" + + "github.com/sirupsen/logrus" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/health" + grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +const defaultSystemGRPCAddr = ":9095" + +type Lifecycle struct { + server *grpc.Server + addr string + listener net.Listener + StartFunc func(context.Context) error + StopFunc func() +} + +func newLifecycle(systemServer *systemServer) (*Lifecycle, error) { + grpcServer := grpc.NewServer(grpc.UnaryInterceptor(httpx.UnaryServerRequestIDInterceptor())) + systemv1.RegisterSystemServiceServer(grpcServer, systemServer) + + healthServer := health.NewServer() + healthServer.SetServingStatus(systemv1.SystemService_ServiceDesc.ServiceName, grpc_health_v1.HealthCheckResponse_SERVING) + healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) + grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) + + if config.GetBool("system.grpc.reflection") { + reflection.Register(grpcServer) + } + + addr := config.GetString("system.grpc.addr") + if addr == "" { + addr = defaultSystemGRPCAddr + } + + return &Lifecycle{ + server: grpcServer, + addr: addr, + }, nil +} + +func (r *Lifecycle) start(ctx context.Context) error { + if r.StartFunc != nil { + return r.StartFunc(ctx) + } + + listener, err := net.Listen("tcp", r.addr) + if err != nil { + return fmt.Errorf("listen system grpc on %s: %w", r.addr, err) + } + r.listener = listener + + go func() { + logrus.Infof("Starting system gRPC server on %s", r.addr) + if err := r.server.Serve(listener); err != nil { + logrus.Errorf("system gRPC server error: %v", err) + } + }() + return nil +} + +func (r *Lifecycle) stop() { + if r.StopFunc != nil { + r.StopFunc() + return + } + if r.server != nil { + r.server.GracefulStop() + } +} + +func registerLifecycle(lc fx.Lifecycle, runner *Lifecycle) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return runner.start(ctx) + }, + OnStop: func(ctx context.Context) error { + runner.stop() + return nil + }, + }) +} diff --git a/src/interface/grpcsystem/module.go b/src/interface/grpcsystem/module.go new file mode 100644 index 00000000..3a9316ea --- /dev/null +++ b/src/interface/grpcsystem/module.go @@ -0,0 +1,11 @@ +package grpcsysteminterface + +import "go.uber.org/fx" + +var Module = fx.Module("grpc_system", + fx.Provide( + newSystemServer, + newLifecycle, + ), + fx.Invoke(registerLifecycle), +) diff --git a/src/interface/grpcsystem/service.go b/src/interface/grpcsystem/service.go new file mode 100644 index 00000000..d30d86d4 --- /dev/null +++ b/src/interface/grpcsystem/service.go @@ -0,0 +1,233 @@ +package grpcsysteminterface + +import ( + "context" + "encoding/json" + "errors" + "time" + + "aegis/consts" + "aegis/dto" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + systemv1 "aegis/proto/system/v1" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +const systemServiceName = "system-service" + +type systemReader interface { + GetHealth(context.Context) (*systemmodule.HealthCheckResp, error) + GetMetrics(context.Context) (*systemmodule.MonitoringMetricsResp, error) + GetSystemInfo(context.Context) (*systemmodule.SystemInfo, error) + ListNamespaceLocks(context.Context) (*systemmodule.ListNamespaceLockResp, error) + ListQueuedTasks(context.Context) (*systemmodule.QueuedTasksResp, error) + GetAuditLog(context.Context, int) (*systemmodule.AuditLogDetailResp, error) + ListAuditLogs(context.Context, *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) + GetConfig(context.Context, int) (*systemmodule.ConfigDetailResp, error) + ListConfigs(context.Context, *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) +} + +type metricsReader interface { + GetSystemMetrics(context.Context) (*systemmetricmodule.SystemMetricsResp, error) + GetSystemMetricsHistory(context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) +} + +type systemServer struct { + systemv1.UnimplementedSystemServiceServer + system systemReader + metrics metricsReader +} + +func newSystemServer(system *systemmodule.Service, metrics *systemmetricmodule.Service) *systemServer { + return &systemServer{ + system: system, + metrics: metrics, + } +} + +func (s *systemServer) Ping(context.Context, *systemv1.PingRequest) (*systemv1.PingResponse, error) { + return &systemv1.PingResponse{ + Service: systemServiceName, + AppId: consts.AppID, + Status: "ok", + TimestampUnix: time.Now().Unix(), + }, nil +} + +func (s *systemServer) GetHealth(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.system.GetHealth(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) GetMetrics(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.system.GetMetrics(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) GetSystemInfo(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.system.GetSystemInfo(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) ListConfigs(ctx context.Context, req *systemv1.ListConfigsRequest) (*systemv1.ResourceListResponse, error) { + query, err := decodeQuery[systemmodule.ListConfigReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.system.ListConfigs(ctx, query) + if err != nil { + return nil, mapSystemError(err) + } + return encodeListResponse(resp) +} + +func (s *systemServer) GetConfig(ctx context.Context, req *systemv1.GetResourceRequest) (*systemv1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.system.GetConfig(ctx, int(req.GetId())) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) ListAuditLogs(ctx context.Context, req *systemv1.ListAuditLogsRequest) (*systemv1.ResourceListResponse, error) { + query, err := decodeQuery[systemmodule.ListAuditLogReq](req.GetQuery()) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + if err := query.Validate(); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + resp, err := s.system.ListAuditLogs(ctx, query) + if err != nil { + return nil, mapSystemError(err) + } + return encodeListResponse(resp) +} + +func (s *systemServer) GetAuditLog(ctx context.Context, req *systemv1.GetResourceRequest) (*systemv1.ResourceItemResponse, error) { + if req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + resp, err := s.system.GetAuditLog(ctx, int(req.GetId())) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) ListNamespaceLocks(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.system.ListNamespaceLocks(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) ListQueuedTasks(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.system.ListQueuedTasks(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) GetSystemMetrics(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.metrics.GetSystemMetrics(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func (s *systemServer) GetSystemMetricsHistory(ctx context.Context, _ *systemv1.PingRequest) (*systemv1.ResourceItemResponse, error) { + resp, err := s.metrics.GetSystemMetricsHistory(ctx) + if err != nil { + return nil, mapSystemError(err) + } + return encodeItemResponse(resp) +} + +func decodeQuery[T any](query *structpb.Struct) (*T, error) { + var result T + if query == nil { + return &result, nil + } + + data, err := json.Marshal(query.AsMap()) + if err != nil { + return nil, err + } + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func encodeItemResponse(value any) (*systemv1.ResourceItemResponse, error) { + item, err := toStruct(value) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &systemv1.ResourceItemResponse{Data: item}, nil +} + +func encodeListResponse(value any) (*systemv1.ResourceListResponse, error) { + item, err := toStruct(value) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &systemv1.ResourceListResponse{Data: item}, nil +} + +func toStruct(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + return structpb.NewStruct(payload) +} + +func mapSystemError(err error) error { + switch { + case errors.Is(err, consts.ErrAuthenticationFailed): + return status.Error(codes.Unauthenticated, err.Error()) + case errors.Is(err, consts.ErrPermissionDenied): + return status.Error(codes.PermissionDenied, err.Error()) + case errors.Is(err, consts.ErrBadRequest): + return status.Error(codes.InvalidArgument, err.Error()) + case errors.Is(err, consts.ErrNotFound): + return status.Error(codes.NotFound, err.Error()) + case errors.Is(err, consts.ErrAlreadyExists): + return status.Error(codes.AlreadyExists, err.Error()) + case err != nil: + return status.Error(codes.Internal, err.Error()) + default: + return nil + } +} diff --git a/src/interface/grpcsystem/service_test.go b/src/interface/grpcsystem/service_test.go new file mode 100644 index 00000000..82412ec9 --- /dev/null +++ b/src/interface/grpcsystem/service_test.go @@ -0,0 +1,171 @@ +package grpcsysteminterface + +import ( + "context" + "errors" + "testing" + "time" + + "aegis/consts" + "aegis/dto" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + systemv1 "aegis/proto/system/v1" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type systemReaderStub struct { + health *systemmodule.HealthCheckResp + metrics *systemmodule.MonitoringMetricsResp + info *systemmodule.SystemInfo + locks *systemmodule.ListNamespaceLockResp + queued *systemmodule.QueuedTasksResp + audit *systemmodule.AuditLogDetailResp + audits *dto.ListResp[systemmodule.AuditLogResp] + config *systemmodule.ConfigDetailResp + configs *dto.ListResp[systemmodule.ConfigResp] + err error +} + +func (s systemReaderStub) GetHealth(context.Context) (*systemmodule.HealthCheckResp, error) { + return s.health, s.err +} +func (s systemReaderStub) GetMetrics(context.Context) (*systemmodule.MonitoringMetricsResp, error) { + return s.metrics, s.err +} +func (s systemReaderStub) GetSystemInfo(context.Context) (*systemmodule.SystemInfo, error) { + return s.info, s.err +} +func (s systemReaderStub) ListNamespaceLocks(context.Context) (*systemmodule.ListNamespaceLockResp, error) { + return s.locks, s.err +} +func (s systemReaderStub) ListQueuedTasks(context.Context) (*systemmodule.QueuedTasksResp, error) { + return s.queued, s.err +} +func (s systemReaderStub) GetAuditLog(_ context.Context, id int) (*systemmodule.AuditLogDetailResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.audit, s.err +} +func (s systemReaderStub) ListAuditLogs(context.Context, *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { + return s.audits, s.err +} +func (s systemReaderStub) GetConfig(_ context.Context, id int) (*systemmodule.ConfigDetailResp, error) { + if id <= 0 { + return nil, errors.New("invalid id") + } + return s.config, s.err +} +func (s systemReaderStub) ListConfigs(context.Context, *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { + return s.configs, s.err +} + +type metricsReaderStub struct { + current *systemmetricmodule.SystemMetricsResp + history *systemmetricmodule.SystemMetricsHistoryResp + err error +} + +func (s metricsReaderStub) GetSystemMetrics(context.Context) (*systemmetricmodule.SystemMetricsResp, error) { + return s.current, s.err +} +func (s metricsReaderStub) GetSystemMetricsHistory(context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { + return s.history, s.err +} + +func TestSystemServerGetHealth(t *testing.T) { + server := &systemServer{ + system: systemReaderStub{ + health: &systemmodule.HealthCheckResp{ + Status: "healthy", + Timestamp: time.Now(), + Version: "v1", + Uptime: "1m", + Services: map[string]systemmodule.ServiceInfo{ + "redis": {Status: "healthy"}, + }, + }, + metrics: &systemmodule.MonitoringMetricsResp{}, + info: &systemmodule.SystemInfo{}, + }, + metrics: metricsReaderStub{}, + } + + resp, err := server.GetHealth(context.Background(), &systemv1.PingRequest{}) + if err != nil { + t.Fatalf("GetHealth() error = %v", err) + } + if resp.GetData().AsMap()["status"] != "healthy" { + t.Fatalf("GetHealth() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestSystemServerListConfigs(t *testing.T) { + server := &systemServer{ + system: systemReaderStub{ + configs: &dto.ListResp[systemmodule.ConfigResp]{ + Items: []systemmodule.ConfigResp{{ID: 1, Key: "demo.key"}}, + Pagination: &dto.PaginationInfo{ + Page: 1, Size: 20, Total: 1, TotalPages: 1, + }, + }, + metrics: &systemmodule.MonitoringMetricsResp{}, + info: &systemmodule.SystemInfo{}, + }, + metrics: metricsReaderStub{}, + } + + query, err := structpb.NewStruct(map[string]any{"page": 1, "size": 20}) + if err != nil { + t.Fatalf("NewStruct() error = %v", err) + } + + resp, err := server.ListConfigs(context.Background(), &systemv1.ListConfigsRequest{Query: query}) + if err != nil { + t.Fatalf("ListConfigs() error = %v", err) + } + if resp.GetData().AsMap()["items"] == nil { + t.Fatalf("ListConfigs() unexpected response: %+v", resp.GetData().AsMap()) + } +} + +func TestSystemServerGetAuditLogNotFound(t *testing.T) { + server := &systemServer{ + system: systemReaderStub{err: consts.ErrNotFound}, + metrics: metricsReaderStub{}, + } + + _, err := server.GetAuditLog(context.Background(), &systemv1.GetResourceRequest{Id: 1}) + if err == nil { + t.Fatal("GetAuditLog() error = nil, want error") + } + if status.Code(err) != codes.NotFound { + t.Fatalf("GetAuditLog() code = %s, want %s", status.Code(err), codes.NotFound) + } +} + +func TestSystemServerGetSystemMetricsHistory(t *testing.T) { + server := &systemServer{ + system: systemReaderStub{ + metrics: &systemmodule.MonitoringMetricsResp{}, + info: &systemmodule.SystemInfo{}, + }, + metrics: metricsReaderStub{ + history: &systemmetricmodule.SystemMetricsHistoryResp{ + CPU: []systemmetricmodule.MetricValue{{Value: 1}}, + }, + }, + } + + resp, err := server.GetSystemMetricsHistory(context.Background(), &systemv1.PingRequest{}) + if err != nil { + t.Fatalf("GetSystemMetricsHistory() error = %v", err) + } + if resp.GetData().AsMap()["cpu"] == nil { + t.Fatalf("GetSystemMetricsHistory() unexpected response: %+v", resp.GetData().AsMap()) + } +} diff --git a/src/interface/worker/module.go b/src/interface/worker/module.go index b3181674..1a28f66a 100644 --- a/src/interface/worker/module.go +++ b/src/interface/worker/module.go @@ -36,6 +36,8 @@ type Params struct { BuildLimiter *consumer.TokenBucketRateLimiter `name:"build_limiter"` AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` BatchManager *consumer.FaultBatchManager + ExecutionOwner consumer.ExecutionOwner + InjectionOwner consumer.InjectionOwner } type Lifecycle struct { @@ -82,6 +84,8 @@ func (r *Lifecycle) start(ctx context.Context) error { BuildKitGateway: params.BuildKit, HelmGateway: params.Helm, FaultBatchManager: params.BatchManager, + ExecutionOwner: params.ExecutionOwner, + InjectionOwner: params.InjectionOwner, }) return nil } diff --git a/src/internalclient/iamclient/client.go b/src/internalclient/iamclient/client.go new file mode 100644 index 00000000..28e4d423 --- /dev/null +++ b/src/internalclient/iamclient/client.go @@ -0,0 +1,1024 @@ +package iamclient + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/httpx" + "aegis/middleware" + authmodule "aegis/module/auth" + rbacmodule "aegis/module/rbac" + teammodule "aegis/module/team" + usermodule "aegis/module/user" + iamv1 "aegis/proto/iam/v1" + "aegis/utils" + + "github.com/golang-jwt/jwt/v5" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + target string + conn *grpc.ClientConn + rpc iamv1.IAMServiceClient +} + +func NewClient(lc fx.Lifecycle) (*Client, error) { + target := config.GetString("clients.iam.target") + if target == "" { + target = config.GetString("iam.grpc.target") + } + if target == "" { + return &Client{}, nil + } + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(httpx.UnaryClientRequestIDInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("create iam grpc client: %w", err) + } + + client := &Client{ + target: target, + conn: conn, + rpc: iamv1.NewIAMServiceClient(conn), + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return conn.Close() + }, + }) + + return client, nil +} + +func (c *Client) Enabled() bool { + return c != nil && c.rpc != nil +} + +func (c *Client) VerifyToken(ctx context.Context, token string) (*utils.Claims, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.VerifyToken(ctx, &iamv1.VerifyTokenRequest{Token: token}) + if err != nil { + return nil, mapRPCError(err) + } + if resp.GetTokenType() != "user" { + return nil, fmt.Errorf("token is not a user token") + } + return &utils.Claims{ + UserID: int(resp.GetUserId()), + Username: resp.GetUsername(), + Email: resp.GetEmail(), + IsActive: resp.GetIsActive(), + IsAdmin: resp.GetIsAdmin(), + Roles: resp.GetRoles(), + AuthType: resp.GetAuthType(), + AccessKeyID: int(resp.GetAccessKeyId()), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Unix(resp.GetExpiresAtUnix(), 0)), + }, + }, nil +} + +func (c *Client) VerifyServiceToken(ctx context.Context, token string) (*utils.ServiceClaims, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.VerifyToken(ctx, &iamv1.VerifyTokenRequest{Token: token}) + if err != nil { + return nil, mapRPCError(err) + } + if resp.GetTokenType() != "service" { + return nil, fmt.Errorf("token is not a service token") + } + return &utils.ServiceClaims{ + TaskID: resp.GetTaskId(), + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Unix(resp.GetExpiresAtUnix(), 0)), + }, + }, nil +} + +func (c *Client) CheckUserPermission(ctx context.Context, params *dto.CheckPermissionParams) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + if params == nil { + return false, fmt.Errorf("permission params are nil") + } + + req := &iamv1.CheckPermissionRequest{ + UserId: int64(params.UserID), + Action: string(params.Action), + Scope: string(params.Scope), + ResourceName: string(params.ResourceName), + } + if params.TeamID != nil { + req.TeamId = int64(*params.TeamID) + } + if params.ProjectID != nil { + req.ProjectId = int64(*params.ProjectID) + } + if params.ContainerID != nil { + req.ContainerId = int64(*params.ContainerID) + } + if params.DatasetID != nil { + req.DatasetId = int64(*params.DatasetID) + } + + resp, err := c.rpc.CheckPermission(ctx, req) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetAllowed(), nil +} + +func (c *Client) IsUserTeamAdmin(ctx context.Context, userID, teamID int) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.IsUserTeamAdmin(ctx, &iamv1.UserTeamRequest{ + UserId: int64(userID), + TeamId: int64(teamID), + }) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetValue(), nil +} + +func (c *Client) IsUserInTeam(ctx context.Context, userID, teamID int) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.IsUserInTeam(ctx, &iamv1.UserTeamRequest{ + UserId: int64(userID), + TeamId: int64(teamID), + }) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetValue(), nil +} + +func (c *Client) IsTeamPublic(ctx context.Context, teamID int) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.IsTeamPublic(ctx, &iamv1.TeamRequest{TeamId: int64(teamID)}) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetValue(), nil +} + +func (c *Client) IsUserProjectAdmin(ctx context.Context, userID, projectID int) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.IsUserProjectAdmin(ctx, &iamv1.UserProjectRequest{ + UserId: int64(userID), + ProjectId: int64(projectID), + }) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetValue(), nil +} + +func (c *Client) IsUserInProject(ctx context.Context, userID, projectID int) (bool, error) { + if !c.Enabled() { + return false, fmt.Errorf("iam grpc client is not configured") + } + + resp, err := c.rpc.IsUserInProject(ctx, &iamv1.UserProjectRequest{ + UserId: int64(userID), + ProjectId: int64(projectID), + }) + if err != nil { + return false, mapRPCError(err) + } + return resp.GetValue(), nil +} + +func (c *Client) Login(ctx context.Context, req *authmodule.LoginReq) (*authmodule.LoginResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode login request: %w", err) + } + resp, err := c.rpc.Login(ctx, &iamv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.LoginResp](resp.GetData()) +} + +func (c *Client) Register(ctx context.Context, req *authmodule.RegisterReq) (*authmodule.UserInfo, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode register request: %w", err) + } + resp, err := c.rpc.Register(ctx, &iamv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.UserInfo](resp.GetData()) +} + +func (c *Client) RefreshToken(ctx context.Context, req *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode refresh token request: %w", err) + } + resp, err := c.rpc.RefreshToken(ctx, &iamv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.TokenRefreshResp](resp.GetData()) +} + +func (c *Client) Logout(ctx context.Context, claims *utils.Claims) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + if claims == nil || claims.ExpiresAt == nil || claims.ID == "" { + return fmt.Errorf("logout claims are incomplete") + } + _, err := c.rpc.Logout(ctx, &iamv1.LogoutRequest{ + UserId: int64(claims.UserID), + TokenId: claims.ID, + ExpiresAtUnix: claims.ExpiresAt.Unix(), + }) + return mapRPCError(err) +} + +func (c *Client) ChangePassword(ctx context.Context, req *authmodule.ChangePasswordReq, userID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode change password request: %w", err) + } + _, err = c.rpc.ChangePassword(ctx, &iamv1.UserBodyRequest{ + UserId: int64(userID), + Body: body, + }) + return mapRPCError(err) +} + +func (c *Client) GetProfile(ctx context.Context, userID int) (*authmodule.UserProfileResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetProfile(ctx, &iamv1.UserIDRequest{UserId: int64(userID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.UserProfileResp](resp.GetData()) +} + +func (c *Client) CreateAccessKey(ctx context.Context, userID int, req *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode create access key request: %w", err) + } + resp, err := c.rpc.CreateAccessKey(ctx, &iamv1.UserBodyRequest{ + UserId: int64(userID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.AccessKeyWithSecretResp](resp.GetData()) +} + +func (c *Client) ListAccessKeys(ctx context.Context, userID int, req *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list access keys request: %w", err) + } + resp, err := c.rpc.ListAccessKeys(ctx, &iamv1.UserQueryRequest{ + UserId: int64(userID), + Query: query, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.ListAccessKeyResp](resp.GetData()) +} + +func (c *Client) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyInfo, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetAccessKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.AccessKeyInfo](resp.GetData()) +} + +func (c *Client) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.DeleteAccessKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + return mapRPCError(err) +} + +func (c *Client) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.DisableAccessKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + return mapRPCError(err) +} + +func (c *Client) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.EnableAccessKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + return mapRPCError(err) +} + +func (c *Client) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyWithSecretResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.RotateAccessKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[authmodule.AccessKeyWithSecretResp](resp.GetData()) +} + +func (c *Client) ExchangeAccessKeyToken(ctx context.Context, req *authmodule.AccessKeyTokenReq, method, path string) (*authmodule.AccessKeyTokenResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.ExchangeAccessKeyToken(ctx, &iamv1.ExchangeAccessKeyTokenRequest{ + AccessKey: req.AccessKey, + Timestamp: req.Timestamp, + Nonce: req.Nonce, + Signature: req.Signature, + Method: method, + Path: path, + }) + if err != nil { + return nil, mapRPCError(err) + } + return &authmodule.AccessKeyTokenResp{ + Token: resp.GetToken(), + TokenType: resp.GetTokenType(), + ExpiresAt: time.Unix(resp.GetExpiresAtUnix(), 0), + AuthType: resp.GetAuthType(), + AccessKey: resp.GetAccessKey(), + }, nil +} + +func (c *Client) CreateUser(ctx context.Context, req *usermodule.CreateUserReq) (*usermodule.UserResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode create user request: %w", err) + } + resp, err := c.rpc.CreateUser(ctx, &iamv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[usermodule.UserResp](resp.GetData()) +} + +func (c *Client) DeleteUser(ctx context.Context, userID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.DeleteUser(ctx, &iamv1.IDRequest{Id: int64(userID)}) + return mapRPCError(err) +} + +func (c *Client) GetUser(ctx context.Context, userID int) (*usermodule.UserDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetUser(ctx, &iamv1.IDRequest{Id: int64(userID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[usermodule.UserDetailResp](resp.GetData()) +} + +func (c *Client) ListUsers(ctx context.Context, req *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list users request: %w", err) + } + resp, err := c.rpc.ListUsers(ctx, &iamv1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[usermodule.UserResp]](resp.GetData()) +} + +func (c *Client) UpdateUser(ctx context.Context, req *usermodule.UpdateUserReq, userID int) (*usermodule.UserResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode update user request: %w", err) + } + resp, err := c.rpc.UpdateUser(ctx, &iamv1.UpdateByIDRequest{ + Id: int64(userID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[usermodule.UserResp](resp.GetData()) +} + +func (c *Client) AssignUserRole(ctx context.Context, userID, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.AssignUserRole(ctx, &iamv1.UserRoleBindingRequest{ + UserId: int64(userID), + RoleId: int64(roleID), + }) + return mapRPCError(err) +} + +func (c *Client) RemoveUserRole(ctx context.Context, userID, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveUserRole(ctx, &iamv1.UserRoleBindingRequest{ + UserId: int64(userID), + RoleId: int64(roleID), + }) + return mapRPCError(err) +} + +func (c *Client) AssignUserPermissions(ctx context.Context, userID int, req *usermodule.AssignUserPermissionReq) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode assign user permissions request: %w", err) + } + _, err = c.rpc.AssignUserPermissions(ctx, &iamv1.UserBodyRequest{ + UserId: int64(userID), + Body: body, + }) + return mapRPCError(err) +} + +func (c *Client) RemoveUserPermissions(ctx context.Context, userID int, req *usermodule.RemoveUserPermissionReq) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode remove user permissions request: %w", err) + } + _, err = c.rpc.RemoveUserPermissions(ctx, &iamv1.UserBodyRequest{ + UserId: int64(userID), + Body: body, + }) + return mapRPCError(err) +} + +func (c *Client) AssignUserContainer(ctx context.Context, userID, containerID, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.AssignUserContainer(ctx, &iamv1.UserResourceBindingRequest{ + UserId: int64(userID), + ResourceId: int64(containerID), + RoleId: int64(roleID), + }) + return mapRPCError(err) +} + +func (c *Client) RemoveUserContainer(ctx context.Context, userID, containerID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveUserContainer(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(containerID), + }) + return mapRPCError(err) +} + +func (c *Client) AssignUserDataset(ctx context.Context, userID, datasetID, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.AssignUserDataset(ctx, &iamv1.UserResourceBindingRequest{ + UserId: int64(userID), + ResourceId: int64(datasetID), + RoleId: int64(roleID), + }) + return mapRPCError(err) +} + +func (c *Client) RemoveUserDataset(ctx context.Context, userID, datasetID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveUserDataset(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(datasetID), + }) + return mapRPCError(err) +} + +func (c *Client) AssignUserProject(ctx context.Context, userID, projectID, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.AssignUserProject(ctx, &iamv1.UserResourceBindingRequest{ + UserId: int64(userID), + ResourceId: int64(projectID), + RoleId: int64(roleID), + }) + return mapRPCError(err) +} + +func (c *Client) RemoveUserProject(ctx context.Context, userID, projectID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveUserProject(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(projectID), + }) + return mapRPCError(err) +} + +func (c *Client) CreateRole(ctx context.Context, req *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode create role request: %w", err) + } + resp, err := c.rpc.CreateRole(ctx, &iamv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[rbacmodule.RoleResp](resp.GetData()) +} + +func (c *Client) DeleteRole(ctx context.Context, roleID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.DeleteRole(ctx, &iamv1.IDRequest{Id: int64(roleID)}) + return mapRPCError(err) +} + +func (c *Client) GetRole(ctx context.Context, roleID int) (*rbacmodule.RoleDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetRole(ctx, &iamv1.IDRequest{Id: int64(roleID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[rbacmodule.RoleDetailResp](resp.GetData()) +} + +func (c *Client) ListRoles(ctx context.Context, req *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list roles request: %w", err) + } + resp, err := c.rpc.ListRoles(ctx, &iamv1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[rbacmodule.RoleResp]](resp.GetData()) +} + +func (c *Client) UpdateRole(ctx context.Context, req *rbacmodule.UpdateRoleReq, roleID int) (*rbacmodule.RoleResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode update role request: %w", err) + } + resp, err := c.rpc.UpdateRole(ctx, &iamv1.UpdateByIDRequest{ + Id: int64(roleID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[rbacmodule.RoleResp](resp.GetData()) +} + +func (c *Client) AssignRolePermissions(ctx context.Context, roleID int, permissionIDs []int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.AssignRolePermissions(ctx, &iamv1.RolePermissionsRequest{ + RoleId: int64(roleID), + PermissionIds: intsToInt64s(permissionIDs), + }) + return mapRPCError(err) +} + +func (c *Client) RemoveRolePermissions(ctx context.Context, roleID int, permissionIDs []int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveRolePermissions(ctx, &iamv1.RolePermissionsRequest{ + RoleId: int64(roleID), + PermissionIds: intsToInt64s(permissionIDs), + }) + return mapRPCError(err) +} + +func (c *Client) ListUsersFromRole(ctx context.Context, roleID int) ([]rbacmodule.UserListItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.ListUsersFromRole(ctx, &iamv1.IDRequest{Id: int64(roleID)}) + if err != nil { + return nil, mapRPCError(err) + } + data, err := decodeStruct[[]rbacmodule.UserListItem](resp.GetData()) + if err != nil { + return nil, err + } + return *data, nil +} + +func (c *Client) GetPermission(ctx context.Context, permissionID int) (*rbacmodule.PermissionDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetPermission(ctx, &iamv1.IDRequest{Id: int64(permissionID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[rbacmodule.PermissionDetailResp](resp.GetData()) +} + +func (c *Client) ListPermissions(ctx context.Context, req *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list permissions request: %w", err) + } + resp, err := c.rpc.ListPermissions(ctx, &iamv1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[rbacmodule.PermissionResp]](resp.GetData()) +} + +func (c *Client) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbacmodule.RoleResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.ListRolesFromPermission(ctx, &iamv1.IDRequest{Id: int64(permissionID)}) + if err != nil { + return nil, mapRPCError(err) + } + data, err := decodeStruct[[]rbacmodule.RoleResp](resp.GetData()) + if err != nil { + return nil, err + } + return *data, nil +} + +func (c *Client) GetResource(ctx context.Context, resourceID int) (*rbacmodule.ResourceResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetResource(ctx, &iamv1.IDRequest{Id: int64(resourceID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[rbacmodule.ResourceResp](resp.GetData()) +} + +func (c *Client) ListResources(ctx context.Context, req *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list resources request: %w", err) + } + resp, err := c.rpc.ListResources(ctx, &iamv1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[rbacmodule.ResourceResp]](resp.GetData()) +} + +func (c *Client) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbacmodule.PermissionResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.ListResourcePermissions(ctx, &iamv1.IDRequest{Id: int64(resourceID)}) + if err != nil { + return nil, mapRPCError(err) + } + data, err := decodeStruct[[]rbacmodule.PermissionResp](resp.GetData()) + if err != nil { + return nil, err + } + return *data, nil +} + +func (c *Client) CreateTeam(ctx context.Context, req *teammodule.CreateTeamReq, userID int) (*teammodule.TeamResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode create team request: %w", err) + } + resp, err := c.rpc.CreateTeam(ctx, &iamv1.CreateTeamRequest{ + UserId: int64(userID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[teammodule.TeamResp](resp.GetData()) +} + +func (c *Client) DeleteTeam(ctx context.Context, teamID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.DeleteTeam(ctx, &iamv1.TeamRequest{TeamId: int64(teamID)}) + return mapRPCError(err) +} + +func (c *Client) GetTeam(ctx context.Context, teamID int) (*teammodule.TeamDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + resp, err := c.rpc.GetTeam(ctx, &iamv1.TeamRequest{TeamId: int64(teamID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[teammodule.TeamDetailResp](resp.GetData()) +} + +func (c *Client) ListTeams(ctx context.Context, req *teammodule.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[teammodule.TeamResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list teams request: %w", err) + } + resp, err := c.rpc.ListTeams(ctx, &iamv1.ListTeamsRequest{ + UserId: int64(userID), + IsAdmin: isAdmin, + Query: query, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[teammodule.TeamResp]](resp.GetData()) +} + +func (c *Client) UpdateTeam(ctx context.Context, req *teammodule.UpdateTeamReq, teamID int) (*teammodule.TeamResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode update team request: %w", err) + } + resp, err := c.rpc.UpdateTeam(ctx, &iamv1.UpdateTeamRequest{ + TeamId: int64(teamID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[teammodule.TeamResp](resp.GetData()) +} + +func (c *Client) ListTeamProjects(ctx context.Context, req *teammodule.TeamProjectListReq, teamID int) (*dto.ListResp[teammodule.TeamProjectItem], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list team projects request: %w", err) + } + resp, err := c.rpc.ListTeamProjects(ctx, &iamv1.ListTeamProjectsRequest{ + TeamId: int64(teamID), + Query: query, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[teammodule.TeamProjectItem]](resp.GetData()) +} + +func (c *Client) AddTeamMember(ctx context.Context, req *teammodule.AddTeamMemberReq, teamID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode add team member request: %w", err) + } + _, err = c.rpc.AddTeamMember(ctx, &iamv1.AddTeamMemberRequest{ + TeamId: int64(teamID), + Body: body, + }) + return mapRPCError(err) +} + +func (c *Client) RemoveTeamMember(ctx context.Context, teamID, currentUserID, targetUserID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RemoveTeamMember(ctx, &iamv1.RemoveTeamMemberRequest{ + TeamId: int64(teamID), + CurrentUserId: int64(currentUserID), + TargetUserId: int64(targetUserID), + }) + return mapRPCError(err) +} + +func (c *Client) UpdateTeamMemberRole(ctx context.Context, req *teammodule.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode update team member role request: %w", err) + } + _, err = c.rpc.UpdateTeamMemberRole(ctx, &iamv1.UpdateTeamMemberRoleRequest{ + TeamId: int64(teamID), + TargetUserId: int64(targetUserID), + CurrentUserId: int64(currentUserID), + Body: body, + }) + return mapRPCError(err) +} + +func (c *Client) ListTeamMembers(ctx context.Context, req *teammodule.ListTeamMemberReq, teamID int) (*dto.ListResp[teammodule.TeamMemberResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("iam grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode list team members request: %w", err) + } + resp, err := c.rpc.ListTeamMembers(ctx, &iamv1.ListTeamMembersRequest{ + TeamId: int64(teamID), + Query: query, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[teammodule.TeamMemberResp]](resp.GetData()) +} + +var _ middleware.TokenVerifier = (*Client)(nil) + +func toStructPB(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + return structpb.NewStruct(payload) +} + +func decodeStruct[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + return nil, fmt.Errorf("iam payload is nil") + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func intsToInt64s(items []int) []int64 { + if len(items) == 0 { + return nil + } + result := make([]int64, 0, len(items)) + for _, item := range items { + result = append(result, int64(item)) + } + return result +} + +func mapRPCError(err error) error { + if err == nil { + return nil + } + st, ok := status.FromError(err) + if !ok { + return err + } + + switch st.Code() { + case codes.Unauthenticated: + return fmt.Errorf("%w: %s", consts.ErrAuthenticationFailed, st.Message()) + case codes.PermissionDenied: + return fmt.Errorf("%w: %s", consts.ErrPermissionDenied, st.Message()) + case codes.InvalidArgument: + return fmt.Errorf("%w: %s", consts.ErrBadRequest, st.Message()) + case codes.NotFound: + return fmt.Errorf("%w: %s", consts.ErrNotFound, st.Message()) + case codes.AlreadyExists: + return fmt.Errorf("%w: %s", consts.ErrAlreadyExists, st.Message()) + default: + return fmt.Errorf("iam rpc failed: %w", err) + } +} diff --git a/src/internalclient/iamclient/module.go b/src/internalclient/iamclient/module.go new file mode 100644 index 00000000..c764af5f --- /dev/null +++ b/src/internalclient/iamclient/module.go @@ -0,0 +1,7 @@ +package iamclient + +import "go.uber.org/fx" + +var Module = fx.Module("iam_client", + fx.Provide(NewClient), +) diff --git a/src/internalclient/orchestratorclient/client.go b/src/internalclient/orchestratorclient/client.go new file mode 100644 index 00000000..60b04e8a --- /dev/null +++ b/src/internalclient/orchestratorclient/client.go @@ -0,0 +1,664 @@ +package orchestratorclient + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/httpx" + executionmodule "aegis/module/execution" + groupmodule "aegis/module/group" + injectionmodule "aegis/module/injection" + metricmodule "aegis/module/metric" + taskmodule "aegis/module/task" + tracemodule "aegis/module/trace" + orchestratorv1 "aegis/proto/orchestrator/v1" + + "github.com/redis/go-redis/v9" + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + target string + conn *grpc.ClientConn + rpc orchestratorv1.OrchestratorServiceClient +} + +func NewClient(lc fx.Lifecycle) (*Client, error) { + target := config.GetString("clients.orchestrator.target") + if target == "" { + target = config.GetString("orchestrator.grpc.target") + } + if target == "" { + return &Client{}, nil + } + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(httpx.UnaryClientRequestIDInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("create orchestrator grpc client: %w", err) + } + + client := &Client{ + target: target, + conn: conn, + rpc: orchestratorv1.NewOrchestratorServiceClient(conn), + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return conn.Close() + }, + }) + + return client, nil +} + +func (c *Client) Enabled() bool { + return c != nil && c.rpc != nil +} + +func (c *Client) SubmitExecution(ctx context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode submit execution request: %w", err) + } + + resp, err := c.rpc.SubmitExecution(ctx, &orchestratorv1.SubmitExecutionRequest{ + GroupId: groupID, + UserId: int64(userID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + + items := make([]executionmodule.SubmitExecutionItem, 0, len(resp.GetItems())) + for _, item := range resp.GetItems() { + mapped := executionmodule.SubmitExecutionItem{ + Index: int(item.GetIndex()), + TraceID: item.GetTraceId(), + TaskID: item.GetTaskId(), + AlgorithmID: int(item.GetAlgorithmId()), + AlgorithmVersionID: int(item.GetAlgorithmVersionId()), + } + if item.GetHasDatapackId() { + value := int(item.GetDatapackId()) + mapped.DatapackID = &value + } + if item.GetHasDatasetId() { + value := int(item.GetDatasetId()) + mapped.DatasetID = &value + } + items = append(items, mapped) + } + + return &executionmodule.SubmitExecutionResp{ + GroupID: resp.GetGroupId(), + Items: items, + }, nil +} + +func (c *Client) SubmitFaultInjection(ctx context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode submit fault injection request: %w", err) + } + + pbReq := &orchestratorv1.SubmitFaultInjectionRequest{ + GroupId: groupID, + UserId: int64(userID), + Body: body, + } + if projectID != nil { + pbReq.ProjectId = int64(*projectID) + } + + resp, err := c.rpc.SubmitFaultInjection(ctx, pbReq) + if err != nil { + return nil, mapRPCError(err) + } + + items := make([]injectionmodule.SubmitInjectionItem, 0, len(resp.GetItems())) + for _, item := range resp.GetItems() { + items = append(items, injectionmodule.SubmitInjectionItem{ + Index: int(item.GetIndex()), + TraceID: item.GetTraceId(), + TaskID: item.GetTaskId(), + }) + } + + result := &injectionmodule.SubmitInjectionResp{ + GroupID: resp.GetGroupId(), + Items: items, + OriginalCount: int(resp.GetOriginalCount()), + } + if warnings := resp.GetWarnings(); warnings != nil { + result.Warnings = &injectionmodule.InjectionWarnings{ + DuplicateServicesInBatch: warnings.GetDuplicateServicesInBatch(), + DuplicateBatchesInRequest: int64sToInts(warnings.GetDuplicateBatchesInRequest()), + BatchesExistInDatabase: int64sToInts(warnings.GetBatchesExistInDatabase()), + } + } + return result, nil +} + +func (c *Client) SubmitDatapackBuilding(ctx context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode submit datapack building request: %w", err) + } + + pbReq := &orchestratorv1.SubmitDatapackBuildingRequest{ + GroupId: groupID, + UserId: int64(userID), + Body: body, + } + if projectID != nil { + pbReq.ProjectId = int64(*projectID) + } + + resp, err := c.rpc.SubmitDatapackBuilding(ctx, pbReq) + if err != nil { + return nil, mapRPCError(err) + } + + items := make([]injectionmodule.SubmitBuildingItem, 0, len(resp.GetItems())) + for _, item := range resp.GetItems() { + items = append(items, injectionmodule.SubmitBuildingItem{ + Index: int(item.GetIndex()), + TraceID: item.GetTraceId(), + TaskID: item.GetTaskId(), + }) + } + + return &injectionmodule.SubmitDatapackBuildingResp{ + GroupID: resp.GetGroupId(), + Items: items, + }, nil +} + +func (c *Client) CreateExecution(ctx context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { + if !c.Enabled() { + return 0, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return 0, fmt.Errorf("encode create execution request: %w", err) + } + resp, err := c.rpc.CreateExecution(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return 0, mapRPCError(err) + } + data := resp.GetData().AsMap() + executionID, ok := data["execution_id"].(float64) + if !ok { + return 0, fmt.Errorf("orchestrator payload missing execution_id") + } + return int(executionID), nil +} + +func (c *Client) CreateInjection(ctx context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode create injection request: %w", err) + } + resp, err := c.rpc.CreateInjection(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.InjectionItem](resp.GetData()) +} + +func (c *Client) UpdateExecutionState(ctx context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { + if !c.Enabled() { + return fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode update execution state request: %w", err) + } + _, err = c.rpc.UpdateExecutionState(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) UpdateInjectionState(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { + if !c.Enabled() { + return fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode update injection state request: %w", err) + } + _, err = c.rpc.UpdateInjectionState(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) UpdateInjectionTimestamps(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode update injection timestamps request: %w", err) + } + resp, err := c.rpc.UpdateInjectionTimestamps(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.InjectionItem](resp.GetData()) +} + +func (c *Client) GetExecution(ctx context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetExecution(ctx, &orchestratorv1.GetExecutionRequest{ExecutionId: int64(executionID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[executionmodule.ExecutionDetailResp](resp.GetData()) +} + +func (c *Client) GetInjectionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode injection metrics request: %w", err) + } + resp, err := c.rpc.GetInjectionMetrics(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[metricmodule.InjectionMetrics](resp.GetData()) +} + +func (c *Client) GetExecutionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode execution metrics request: %w", err) + } + resp, err := c.rpc.GetExecutionMetrics(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[metricmodule.ExecutionMetrics](resp.GetData()) +} + +func (c *Client) ListProjectStatistics(ctx context.Context, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.ListProjectStatistics(ctx, &orchestratorv1.ListProjectStatisticsRequest{ + ProjectIds: intsToInt64s(projectIDs), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeProjectStatisticsMap(resp.GetData()) +} + +func (c *Client) ListEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode evaluation datapack query: %w", err) + } + resp, err := c.rpc.ListEvaluationExecutionsByDatapack(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStructItems[executionmodule.EvaluationExecutionItem](resp.GetData()) +} + +func (c *Client) ListEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode evaluation dataset query: %w", err) + } + resp, err := c.rpc.ListEvaluationExecutionsByDataset(ctx, &orchestratorv1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStructItems[executionmodule.EvaluationExecutionItem](resp.GetData()) +} + +func (c *Client) GetTask(ctx context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetTask(ctx, &orchestratorv1.GetTaskRequest{TaskId: taskID}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[taskmodule.TaskDetailResp](resp.GetData()) +} + +func (c *Client) PollTaskLogs(ctx context.Context, taskID string, after time.Time) (*taskmodule.TaskLogPollResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + req := &orchestratorv1.PollTaskLogsRequest{TaskId: taskID} + if !after.IsZero() { + req.AfterUnixNano = after.UnixNano() + } + resp, err := c.rpc.PollTaskLogs(ctx, req) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[taskmodule.TaskLogPollResp](resp.GetData()) +} + +func (c *Client) ListTasks(ctx context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode task list request: %w", err) + } + resp, err := c.rpc.ListTasks(ctx, &orchestratorv1.ListTasksRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[taskmodule.TaskResp]](resp.GetData()) +} + +func (c *Client) GetTrace(ctx context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetTrace(ctx, &orchestratorv1.GetTraceRequest{TraceId: traceID}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[tracemodule.TraceDetailResp](resp.GetData()) +} + +func (c *Client) ListTraces(ctx context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode trace list request: %w", err) + } + resp, err := c.rpc.ListTraces(ctx, &orchestratorv1.ListTracesRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[tracemodule.TraceResp]](resp.GetData()) +} + +func (c *Client) GetGroupStats(ctx context.Context, groupID string) (*groupmodule.GroupStats, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetGroupStats(ctx, &orchestratorv1.GetGroupStatsRequest{GroupId: groupID}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[groupmodule.GroupStats](resp.GetData()) +} + +func (c *Client) GetTraceStreamAlgorithms(ctx context.Context, traceID string) ([]dto.ContainerVersionItem, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetTraceStreamState(ctx, &orchestratorv1.GetTraceStreamStateRequest{TraceId: traceID}) + if err != nil { + return nil, mapRPCError(err) + } + state, err := decodeStruct[traceStreamStateResp](resp.GetData()) + if err != nil { + return nil, err + } + return state.Algorithms, nil +} + +func (c *Client) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.ReadTraceStreamMessages(ctx, &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: streamKey, + LastId: lastID, + Count: count, + BlockMillis: block.Milliseconds(), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStreamMessages(resp.GetData(), streamKey) +} + +func (c *Client) GetGroupTraceCount(ctx context.Context, groupID string) (int, error) { + if !c.Enabled() { + return 0, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.GetGroupStreamState(ctx, &orchestratorv1.GetGroupStreamStateRequest{GroupId: groupID}) + if err != nil { + return 0, mapRPCError(err) + } + state, err := decodeStruct[groupStreamStateResp](resp.GetData()) + if err != nil { + return 0, err + } + return state.TotalTraces, nil +} + +func (c *Client) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.ReadGroupStreamMessages(ctx, &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: streamKey, + LastId: lastID, + Count: count, + BlockMillis: block.Milliseconds(), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStreamMessages(resp.GetData(), streamKey) +} + +func (c *Client) ReadNotificationStreamMessages(ctx context.Context, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { + if !c.Enabled() { + return nil, fmt.Errorf("orchestrator grpc client is not configured") + } + resp, err := c.rpc.ReadNotificationStreamMessages(ctx, &orchestratorv1.ReadStreamMessagesRequest{ + StreamKey: consts.NotificationStreamKey, + LastId: lastID, + Count: count, + BlockMillis: block.Milliseconds(), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStreamMessages(resp.GetData(), consts.NotificationStreamKey) +} + +type traceStreamStateResp struct { + Algorithms []dto.ContainerVersionItem `json:"algorithms"` +} + +type groupStreamStateResp struct { + TotalTraces int `json:"total_traces"` +} + +type streamBatchResp struct { + Messages []streamMessageResp `json:"messages"` +} + +type streamMessageResp struct { + ID string `json:"id"` + Values map[string]any `json:"values"` +} + +func toStructPB(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + + return structpb.NewStruct(payload) +} + +func decodeStruct[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + return nil, fmt.Errorf("orchestrator payload is nil") + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func decodeStructItems[T any](payload *structpb.Struct) ([]T, error) { + type listEnvelope[T any] struct { + Items []T `json:"items"` + } + + result, err := decodeStruct[listEnvelope[T]](payload) + if err != nil { + return nil, err + } + return result.Items, nil +} + +func decodeStreamMessages(payload *structpb.Struct, streamKey string) ([]redis.XStream, error) { + result, err := decodeStruct[streamBatchResp](payload) + if err != nil { + return nil, err + } + messages := make([]redis.XMessage, 0, len(result.Messages)) + for _, item := range result.Messages { + messages = append(messages, redis.XMessage{ + ID: item.ID, + Values: item.Values, + }) + } + return []redis.XStream{{ + Stream: streamKey, + Messages: messages, + }}, nil +} + +func decodeProjectStatisticsMap(payload *structpb.Struct) (map[int]*dto.ProjectStatistics, error) { + if payload == nil { + return map[int]*dto.ProjectStatistics{}, nil + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + raw := map[string]dto.ProjectStatistics{} + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + result := make(map[int]*dto.ProjectStatistics, len(raw)) + for key, value := range raw { + var projectID int + if _, err := fmt.Sscanf(key, "%d", &projectID); err != nil { + return nil, fmt.Errorf("invalid project statistics key %q: %w", key, err) + } + stats := value + result[projectID] = &stats + } + return result, nil +} + +func intsToInt64s(items []int) []int64 { + if len(items) == 0 { + return nil + } + result := make([]int64, 0, len(items)) + for _, item := range items { + result = append(result, int64(item)) + } + return result +} + +func int64sToInts(items []int64) []int { + if len(items) == 0 { + return nil + } + result := make([]int, 0, len(items)) + for _, item := range items { + result = append(result, int(item)) + } + return result +} + +func mapRPCError(err error) error { + st, ok := status.FromError(err) + if !ok { + return err + } + + switch st.Code() { + case codes.Unauthenticated: + return fmt.Errorf("%w: %s", consts.ErrAuthenticationFailed, st.Message()) + case codes.PermissionDenied: + return fmt.Errorf("%w: %s", consts.ErrPermissionDenied, st.Message()) + case codes.InvalidArgument: + return fmt.Errorf("%w: %s", consts.ErrBadRequest, st.Message()) + case codes.NotFound: + return fmt.Errorf("%w: %s", consts.ErrNotFound, st.Message()) + case codes.AlreadyExists: + return fmt.Errorf("%w: %s", consts.ErrAlreadyExists, st.Message()) + default: + return fmt.Errorf("orchestrator rpc failed: %w", err) + } +} diff --git a/src/internalclient/orchestratorclient/module.go b/src/internalclient/orchestratorclient/module.go new file mode 100644 index 00000000..f244f323 --- /dev/null +++ b/src/internalclient/orchestratorclient/module.go @@ -0,0 +1,7 @@ +package orchestratorclient + +import "go.uber.org/fx" + +var Module = fx.Module("orchestrator_client", + fx.Provide(NewClient), +) diff --git a/src/internalclient/resourceclient/client.go b/src/internalclient/resourceclient/client.go new file mode 100644 index 00000000..2dd5f7de --- /dev/null +++ b/src/internalclient/resourceclient/client.go @@ -0,0 +1,472 @@ +package resourceclient + +import ( + "context" + "encoding/json" + "fmt" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/httpx" + chaossystemmodule "aegis/module/chaossystem" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" + evaluationmodule "aegis/module/evaluation" + labelmodule "aegis/module/label" + projectmodule "aegis/module/project" + resourcev1 "aegis/proto/resource/v1" + + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + target string + conn *grpc.ClientConn + rpc resourcev1.ResourceServiceClient +} + +func NewClient(lc fx.Lifecycle) (*Client, error) { + target := config.GetString("clients.resource.target") + if target == "" { + target = config.GetString("resource.grpc.target") + } + if target == "" { + return &Client{}, nil + } + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(httpx.UnaryClientRequestIDInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("create resource grpc client: %w", err) + } + + client := &Client{ + target: target, + conn: conn, + rpc: resourcev1.NewResourceServiceClient(conn), + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return conn.Close() + }, + }) + + return client, nil +} + +func (c *Client) Enabled() bool { + return c != nil && c.rpc != nil +} + +func (c *Client) ListProjects(ctx context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode project list request: %w", err) + } + resp, err := c.rpc.ListProjects(ctx, &resourcev1.ListProjectsRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[projectmodule.ProjectResp]](resp.GetData()) +} + +func (c *Client) GetProject(ctx context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetProject(ctx, &resourcev1.GetResourceRequest{Id: int64(projectID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[projectmodule.ProjectDetailResp](resp.GetData()) +} + +func (c *Client) ListContainers(ctx context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode container list request: %w", err) + } + resp, err := c.rpc.ListContainers(ctx, &resourcev1.ListContainersRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[containermodule.ContainerResp]](resp.GetData()) +} + +func (c *Client) GetContainer(ctx context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetContainer(ctx, &resourcev1.GetResourceRequest{Id: int64(containerID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[containermodule.ContainerDetailResp](resp.GetData()) +} + +func (c *Client) ListDatasets(ctx context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode dataset list request: %w", err) + } + resp, err := c.rpc.ListDatasets(ctx, &resourcev1.ListDatasetsRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[datasetmodule.DatasetResp]](resp.GetData()) +} + +func (c *Client) GetDataset(ctx context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetDataset(ctx, &resourcev1.GetResourceRequest{Id: int64(datasetID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[datasetmodule.DatasetDetailResp](resp.GetData()) +} + +func (c *Client) CreateLabel(ctx context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode label create request: %w", err) + } + resp, err := c.rpc.CreateLabel(ctx, &resourcev1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[labelmodule.LabelResp](resp.GetData()) +} + +func (c *Client) GetLabel(ctx context.Context, labelID int) (*labelmodule.LabelDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetLabel(ctx, &resourcev1.GetResourceRequest{Id: int64(labelID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[labelmodule.LabelDetailResp](resp.GetData()) +} + +func (c *Client) ListLabels(ctx context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode label list request: %w", err) + } + resp, err := c.rpc.ListLabels(ctx, &resourcev1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[labelmodule.LabelResp]](resp.GetData()) +} + +func (c *Client) UpdateLabel(ctx context.Context, req *labelmodule.UpdateLabelReq, labelID int) (*labelmodule.LabelResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode label update request: %w", err) + } + resp, err := c.rpc.UpdateLabel(ctx, &resourcev1.UpdateByIDRequest{ + Id: int64(labelID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[labelmodule.LabelResp](resp.GetData()) +} + +func (c *Client) DeleteLabel(ctx context.Context, labelID int) error { + if !c.Enabled() { + return fmt.Errorf("resource grpc client is not configured") + } + _, err := c.rpc.DeleteLabel(ctx, &resourcev1.GetResourceRequest{Id: int64(labelID)}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) BatchDeleteLabels(ctx context.Context, ids []int) error { + if !c.Enabled() { + return fmt.Errorf("resource grpc client is not configured") + } + _, err := c.rpc.BatchDeleteLabels(ctx, &resourcev1.BatchDeleteRequest{Ids: intsToInt64s(ids)}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) ListChaosSystems(ctx context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode chaos system list request: %w", err) + } + resp, err := c.rpc.ListChaosSystems(ctx, &resourcev1.QueryRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[chaossystemmodule.ChaosSystemResp]](resp.GetData()) +} + +func (c *Client) GetChaosSystem(ctx context.Context, systemID int) (*chaossystemmodule.ChaosSystemResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetChaosSystem(ctx, &resourcev1.GetResourceRequest{Id: int64(systemID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) +} + +func (c *Client) CreateChaosSystem(ctx context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode chaos system create request: %w", err) + } + resp, err := c.rpc.CreateChaosSystem(ctx, &resourcev1.MutationRequest{Body: body}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) +} + +func (c *Client) UpdateChaosSystem(ctx context.Context, req *chaossystemmodule.UpdateChaosSystemReq, systemID int) (*chaossystemmodule.ChaosSystemResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode chaos system update request: %w", err) + } + resp, err := c.rpc.UpdateChaosSystem(ctx, &resourcev1.UpdateByIDRequest{ + Id: int64(systemID), + Body: body, + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) +} + +func (c *Client) DeleteChaosSystem(ctx context.Context, systemID int) error { + if !c.Enabled() { + return fmt.Errorf("resource grpc client is not configured") + } + _, err := c.rpc.DeleteChaosSystem(ctx, &resourcev1.GetResourceRequest{Id: int64(systemID)}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) UpsertChaosSystemMetadata(ctx context.Context, systemID int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { + if !c.Enabled() { + return fmt.Errorf("resource grpc client is not configured") + } + body, err := toStructPB(req) + if err != nil { + return fmt.Errorf("encode chaos system metadata request: %w", err) + } + _, err = c.rpc.UpsertChaosSystemMetadata(ctx, &resourcev1.UpdateByIDRequest{ + Id: int64(systemID), + Body: body, + }) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func (c *Client) ListChaosSystemMetadata(ctx context.Context, systemID int, metadataType string) ([]chaossystemmodule.SystemMetadataResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(map[string]any{"type": metadataType}) + if err != nil { + return nil, fmt.Errorf("encode chaos system metadata query: %w", err) + } + resp, err := c.rpc.ListChaosSystemMetadata(ctx, &resourcev1.IDQueryRequest{ + Id: int64(systemID), + Query: query, + }) + if err != nil { + return nil, mapRPCError(err) + } + items, err := decodeStruct[struct { + Items []chaossystemmodule.SystemMetadataResp `json:"items"` + }](resp.GetData()) + if err != nil { + return nil, err + } + return items.Items, nil +} + +func (c *Client) ListDatapackEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode datapack evaluation request: %w", err) + } + resp, err := c.rpc.ListDatapackEvaluationResults(ctx, &resourcev1.ListDatapackEvaluationsRequest{ + Query: query, + UserId: int64(userID), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[evaluationmodule.BatchEvaluateDatapackResp](resp.GetData()) +} + +func (c *Client) ListDatasetEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode dataset evaluation request: %w", err) + } + resp, err := c.rpc.ListDatasetEvaluationResults(ctx, &resourcev1.ListDatasetEvaluationsRequest{ + Query: query, + UserId: int64(userID), + }) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[evaluationmodule.BatchEvaluateDatasetResp](resp.GetData()) +} + +func (c *Client) ListEvaluations(ctx context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode evaluation list request: %w", err) + } + resp, err := c.rpc.ListEvaluations(ctx, &resourcev1.ListEvaluationsRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[evaluationmodule.EvaluationResp]](resp.GetData()) +} + +func (c *Client) GetEvaluation(ctx context.Context, evaluationID int) (*evaluationmodule.EvaluationResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("resource grpc client is not configured") + } + resp, err := c.rpc.GetEvaluation(ctx, &resourcev1.GetResourceRequest{Id: int64(evaluationID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[evaluationmodule.EvaluationResp](resp.GetData()) +} + +func (c *Client) DeleteEvaluation(ctx context.Context, evaluationID int) error { + if !c.Enabled() { + return fmt.Errorf("resource grpc client is not configured") + } + _, err := c.rpc.DeleteEvaluation(ctx, &resourcev1.GetResourceRequest{Id: int64(evaluationID)}) + if err != nil { + return mapRPCError(err) + } + return nil +} + +func toStructPB(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + return structpb.NewStruct(payload) +} + +func decodeStruct[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + return nil, fmt.Errorf("resource payload is nil") + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func intsToInt64s(items []int) []int64 { + if len(items) == 0 { + return nil + } + result := make([]int64, 0, len(items)) + for _, item := range items { + result = append(result, int64(item)) + } + return result +} + +func mapRPCError(err error) error { + st, ok := status.FromError(err) + if !ok { + return err + } + switch st.Code() { + case codes.Unauthenticated: + return fmt.Errorf("%w: %s", consts.ErrAuthenticationFailed, st.Message()) + case codes.PermissionDenied: + return fmt.Errorf("%w: %s", consts.ErrPermissionDenied, st.Message()) + case codes.InvalidArgument: + return fmt.Errorf("%w: %s", consts.ErrBadRequest, st.Message()) + case codes.NotFound: + return fmt.Errorf("%w: %s", consts.ErrNotFound, st.Message()) + case codes.AlreadyExists: + return fmt.Errorf("%w: %s", consts.ErrAlreadyExists, st.Message()) + default: + return fmt.Errorf("resource rpc failed: %w", err) + } +} diff --git a/src/internalclient/resourceclient/module.go b/src/internalclient/resourceclient/module.go new file mode 100644 index 00000000..3478126b --- /dev/null +++ b/src/internalclient/resourceclient/module.go @@ -0,0 +1,7 @@ +package resourceclient + +import "go.uber.org/fx" + +var Module = fx.Module("resource_client", + fx.Provide(NewClient), +) diff --git a/src/internalclient/runtimeclient/client.go b/src/internalclient/runtimeclient/client.go new file mode 100644 index 00000000..40a79e0d --- /dev/null +++ b/src/internalclient/runtimeclient/client.go @@ -0,0 +1,122 @@ +package runtimeclient + +import ( + "context" + "encoding/json" + "fmt" + + "aegis/config" + "aegis/consts" + "aegis/httpx" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" + runtimev1 "aegis/proto/runtime/v1" + + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + target string + conn *grpc.ClientConn + rpc runtimev1.RuntimeServiceClient +} + +func NewClient(lc fx.Lifecycle) (*Client, error) { + target := config.GetString("clients.runtime.target") + if target == "" { + target = config.GetString("runtime_worker.grpc.target") + } + if target == "" { + return &Client{}, nil + } + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(httpx.UnaryClientRequestIDInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("create runtime grpc client: %w", err) + } + + client := &Client{ + target: target, + conn: conn, + rpc: runtimev1.NewRuntimeServiceClient(conn), + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return conn.Close() + }, + }) + + return client, nil +} + +func (c *Client) Enabled() bool { + return c != nil && c.rpc != nil +} + +func (c *Client) GetNamespaceLocks(ctx context.Context) (*systemmetricmodule.ListNamespaceLockResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("runtime grpc client is not configured") + } + resp, err := c.rpc.GetNamespaceLocks(ctx, &runtimev1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmetricmodule.ListNamespaceLockResp](resp.GetData()) +} + +func (c *Client) GetQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("runtime grpc client is not configured") + } + resp, err := c.rpc.GetQueuedTasks(ctx, &runtimev1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[taskmodule.QueuedTasksResp](resp.GetData()) +} + +func decodeStruct[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + return nil, fmt.Errorf("runtime payload is nil") + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func mapRPCError(err error) error { + st, ok := status.FromError(err) + if !ok { + return err + } + switch st.Code() { + case codes.Unauthenticated: + return fmt.Errorf("%w: %s", consts.ErrAuthenticationFailed, st.Message()) + case codes.PermissionDenied: + return fmt.Errorf("%w: %s", consts.ErrPermissionDenied, st.Message()) + case codes.InvalidArgument: + return fmt.Errorf("%w: %s", consts.ErrBadRequest, st.Message()) + case codes.NotFound: + return fmt.Errorf("%w: %s", consts.ErrNotFound, st.Message()) + case codes.AlreadyExists: + return fmt.Errorf("%w: %s", consts.ErrAlreadyExists, st.Message()) + default: + return fmt.Errorf("runtime rpc failed: %w", err) + } +} diff --git a/src/internalclient/runtimeclient/module.go b/src/internalclient/runtimeclient/module.go new file mode 100644 index 00000000..7bdd2b21 --- /dev/null +++ b/src/internalclient/runtimeclient/module.go @@ -0,0 +1,7 @@ +package runtimeclient + +import "go.uber.org/fx" + +var Module = fx.Module("runtime_client", + fx.Provide(NewClient), +) diff --git a/src/internalclient/systemclient/client.go b/src/internalclient/systemclient/client.go new file mode 100644 index 00000000..cee5f356 --- /dev/null +++ b/src/internalclient/systemclient/client.go @@ -0,0 +1,242 @@ +package systemclient + +import ( + "context" + "encoding/json" + "fmt" + + "aegis/config" + "aegis/consts" + "aegis/dto" + "aegis/httpx" + systemmodule "aegis/module/system" + systemmetricmodule "aegis/module/systemmetric" + systemv1 "aegis/proto/system/v1" + + "go.uber.org/fx" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" +) + +type Client struct { + target string + conn *grpc.ClientConn + rpc systemv1.SystemServiceClient +} + +func NewClient(lc fx.Lifecycle) (*Client, error) { + target := config.GetString("clients.system.target") + if target == "" { + target = config.GetString("system.grpc.target") + } + if target == "" { + return &Client{}, nil + } + + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithUnaryInterceptor(httpx.UnaryClientRequestIDInterceptor()), + ) + if err != nil { + return nil, fmt.Errorf("create system grpc client: %w", err) + } + + client := &Client{ + target: target, + conn: conn, + rpc: systemv1.NewSystemServiceClient(conn), + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return conn.Close() + }, + }) + + return client, nil +} + +func (c *Client) Enabled() bool { + return c != nil && c.rpc != nil +} + +func (c *Client) GetHealth(ctx context.Context) (*systemmodule.HealthCheckResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetHealth(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.HealthCheckResp](resp.GetData()) +} + +func (c *Client) GetMetrics(ctx context.Context) (*systemmodule.MonitoringMetricsResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetMetrics(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.MonitoringMetricsResp](resp.GetData()) +} + +func (c *Client) GetSystemInfo(ctx context.Context) (*systemmodule.SystemInfo, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetSystemInfo(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.SystemInfo](resp.GetData()) +} + +func (c *Client) ListConfigs(ctx context.Context, req *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode system config list request: %w", err) + } + resp, err := c.rpc.ListConfigs(ctx, &systemv1.ListConfigsRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[systemmodule.ConfigResp]](resp.GetData()) +} + +func (c *Client) GetConfig(ctx context.Context, configID int) (*systemmodule.ConfigDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetConfig(ctx, &systemv1.GetResourceRequest{Id: int64(configID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.ConfigDetailResp](resp.GetData()) +} + +func (c *Client) ListAuditLogs(ctx context.Context, req *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + query, err := toStructPB(req) + if err != nil { + return nil, fmt.Errorf("encode system audit list request: %w", err) + } + resp, err := c.rpc.ListAuditLogs(ctx, &systemv1.ListAuditLogsRequest{Query: query}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[dto.ListResp[systemmodule.AuditLogResp]](resp.GetData()) +} + +func (c *Client) GetAuditLog(ctx context.Context, auditLogID int) (*systemmodule.AuditLogDetailResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetAuditLog(ctx, &systemv1.GetResourceRequest{Id: int64(auditLogID)}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.AuditLogDetailResp](resp.GetData()) +} + +func (c *Client) ListNamespaceLocks(ctx context.Context) (*systemmodule.ListNamespaceLockResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.ListNamespaceLocks(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.ListNamespaceLockResp](resp.GetData()) +} + +func (c *Client) ListQueuedTasks(ctx context.Context) (*systemmodule.QueuedTasksResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.ListQueuedTasks(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmodule.QueuedTasksResp](resp.GetData()) +} + +func (c *Client) GetSystemMetrics(ctx context.Context) (*systemmetricmodule.SystemMetricsResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetSystemMetrics(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmetricmodule.SystemMetricsResp](resp.GetData()) +} + +func (c *Client) GetSystemMetricsHistory(ctx context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { + if !c.Enabled() { + return nil, fmt.Errorf("system grpc client is not configured") + } + resp, err := c.rpc.GetSystemMetricsHistory(ctx, &systemv1.PingRequest{}) + if err != nil { + return nil, mapRPCError(err) + } + return decodeStruct[systemmetricmodule.SystemMetricsHistoryResp](resp.GetData()) +} + +func toStructPB(value any) (*structpb.Struct, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + payload := map[string]any{} + if err := json.Unmarshal(data, &payload); err != nil { + return nil, err + } + return structpb.NewStruct(payload) +} + +func decodeStruct[T any](payload *structpb.Struct) (*T, error) { + if payload == nil { + return nil, fmt.Errorf("system payload is nil") + } + data, err := json.Marshal(payload.AsMap()) + if err != nil { + return nil, err + } + var result T + if err := json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} + +func mapRPCError(err error) error { + st, ok := status.FromError(err) + if !ok { + return err + } + switch st.Code() { + case codes.Unauthenticated: + return fmt.Errorf("%w: %s", consts.ErrAuthenticationFailed, st.Message()) + case codes.PermissionDenied: + return fmt.Errorf("%w: %s", consts.ErrPermissionDenied, st.Message()) + case codes.InvalidArgument: + return fmt.Errorf("%w: %s", consts.ErrBadRequest, st.Message()) + case codes.NotFound: + return fmt.Errorf("%w: %s", consts.ErrNotFound, st.Message()) + case codes.AlreadyExists: + return fmt.Errorf("%w: %s", consts.ErrAlreadyExists, st.Message()) + default: + return fmt.Errorf("system rpc failed: %w", err) + } +} diff --git a/src/internalclient/systemclient/module.go b/src/internalclient/systemclient/module.go new file mode 100644 index 00000000..ca202fbb --- /dev/null +++ b/src/internalclient/systemclient/module.go @@ -0,0 +1,7 @@ +package systemclient + +import "go.uber.org/fx" + +var Module = fx.Module("system_client", + fx.Provide(NewClient), +) diff --git a/src/main.go b/src/main.go index c6023790..81f5062e 100644 --- a/src/main.go +++ b/src/main.go @@ -21,6 +21,12 @@ import ( "os" "aegis/app" + gatewayapp "aegis/app/gateway" + iamapp "aegis/app/iam" + orchestratorapp "aegis/app/orchestrator" + resourceapp "aegis/app/resource" + runtimeapp "aegis/app/runtime" + systemapp "aegis/app/system" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -45,7 +51,7 @@ func main() { Use: "rcabench", Short: "RCA Bench is a benchmarking tool", Run: func(cmd *cobra.Command, args []string) { - logrus.Println("Please specify a mode: producer, consumer, or both") + logrus.Println("Please specify a mode: producer, consumer, both, api-gateway, iam-service, orchestrator-service, resource-service, runtime-worker-service, or system-service") }, } @@ -68,8 +74,36 @@ func main() { bothCmd := newModeCommand("both", "Run as both producer and consumer", func() { fx.New(app.BothOptions(viper.GetString("conf"), viper.GetString("port"))).Run() }) + apiGatewayCmd := newModeCommand("api-gateway", "Run as the API gateway", func() { + fx.New(gatewayapp.Options(viper.GetString("conf"), viper.GetString("port"))).Run() + }) + iamServiceCmd := newModeCommand("iam-service", "Run as the IAM service", func() { + fx.New(iamapp.Options(viper.GetString("conf"))).Run() + }) + orchestratorServiceCmd := newModeCommand("orchestrator-service", "Run as the orchestrator service", func() { + fx.New(orchestratorapp.Options(viper.GetString("conf"))).Run() + }) + resourceServiceCmd := newModeCommand("resource-service", "Run as the resource service", func() { + fx.New(resourceapp.Options(viper.GetString("conf"))).Run() + }) + runtimeWorkerServiceCmd := newModeCommand("runtime-worker-service", "Run as the runtime worker service", func() { + fx.New(runtimeapp.Options(viper.GetString("conf"))).Run() + }) + systemServiceCmd := newModeCommand("system-service", "Run as the system service", func() { + fx.New(systemapp.Options(viper.GetString("conf"))).Run() + }) - rootCmd.AddCommand(producerCmd, consumerCmd, bothCmd) + rootCmd.AddCommand( + producerCmd, + consumerCmd, + bothCmd, + apiGatewayCmd, + iamServiceCmd, + orchestratorServiceCmd, + resourceServiceCmd, + runtimeWorkerServiceCmd, + systemServiceCmd, + ) if err := rootCmd.Execute(); err != nil { logrus.Println(err.Error()) os.Exit(1) diff --git a/src/middleware/auth.go b/src/middleware/auth.go index dfab5300..d1752220 100644 --- a/src/middleware/auth.go +++ b/src/middleware/auth.go @@ -9,21 +9,27 @@ import ( "github.com/gin-gonic/gin" ) +func extractTokenFromHeader(header string) (string, error) { + return utils.ExtractTokenFromHeader(header) +} + // JWTAuth is the JWT authentication middleware // Supports both user tokens and service tokens (for K8s jobs) func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { // Extract token from Authorization header authHeader := c.GetHeader("Authorization") - token, err := utils.ExtractTokenFromHeader(authHeader) + token, err := extractTokenFromHeader(authHeader) if err != nil { dto.ErrorResponse(c, http.StatusUnauthorized, "Unauthorized: "+err.Error()) c.Abort() return } + service := serviceFromContext(c) + // Try to validate as user token first - claims, err := utils.ValidateToken(token) + claims, err := service.VerifyToken(c.Request.Context(), token) if err == nil { // Valid user token - store user information in context c.Set("user_id", claims.UserID) @@ -39,7 +45,7 @@ func JWTAuth() gin.HandlerFunc { } // Try to validate as service token (for K8s jobs) - serviceClaims, serviceErr := utils.ValidateServiceToken(token) + serviceClaims, serviceErr := service.VerifyServiceToken(c.Request.Context(), token) if serviceErr == nil { // Valid service token - store service information in context c.Set("task_id", serviceClaims.TaskID) @@ -69,15 +75,17 @@ func OptionalJWTAuth() gin.HandlerFunc { return } - token, err := utils.ExtractTokenFromHeader(authHeader) + token, err := extractTokenFromHeader(authHeader) if err != nil { // Invalid header format, continue without auth c.Next() return } + service := serviceFromContext(c) + // Try to validate as user token first - claims, err := utils.ValidateToken(token) + claims, err := service.VerifyToken(c.Request.Context(), token) if err == nil { // Valid user token, set user information c.Set("user_id", claims.UserID) @@ -93,7 +101,7 @@ func OptionalJWTAuth() gin.HandlerFunc { } // Try to validate as service token (for K8s jobs) - serviceClaims, serviceErr := utils.ValidateServiceToken(token) + serviceClaims, serviceErr := service.VerifyServiceToken(c.Request.Context(), token) if serviceErr == nil { // Valid service token, set service information c.Set("task_id", serviceClaims.TaskID) diff --git a/src/middleware/deps.go b/src/middleware/deps.go index cd2638a4..82d2ae43 100644 --- a/src/middleware/deps.go +++ b/src/middleware/deps.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "errors" "fmt" "time" @@ -8,18 +9,24 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" + "aegis/utils" "github.com/gin-gonic/gin" "gorm.io/gorm" ) +type TokenVerifier interface { + VerifyToken(ctx context.Context, token string) (*utils.Claims, error) + VerifyServiceToken(ctx context.Context, token string) (*utils.ServiceClaims, error) +} + type permissionChecker interface { - CheckUserPermission(params *dto.CheckPermissionParams) (bool, error) - IsUserTeamAdmin(userID, teamID int) (bool, error) - IsUserInTeam(userID, teamID int) (bool, error) - IsTeamPublic(teamID int) (bool, error) - IsUserProjectAdmin(userID, projectID int) (bool, error) - IsUserInProject(userID, projectID int) (bool, error) + CheckUserPermission(context.Context, *dto.CheckPermissionParams) (bool, error) + IsUserTeamAdmin(context.Context, int, int) (bool, error) + IsUserInTeam(context.Context, int, int) (bool, error) + IsTeamPublic(context.Context, int) (bool, error) + IsUserProjectAdmin(context.Context, int, int) (bool, error) + IsUserInProject(context.Context, int, int) (bool, error) } type auditLogger interface { @@ -28,13 +35,16 @@ type auditLogger interface { } type Service interface { + TokenVerifier permissionChecker auditLogger } const middlewareServiceContextKey = "middleware.service" -func NewService(db *gorm.DB) Service { return &dbBackedMiddlewareService{db: db} } +func NewService(db *gorm.DB, verifier TokenVerifier) Service { + return &dbBackedMiddlewareService{db: db, verifier: verifier} +} func InjectService(service Service) gin.HandlerFunc { if service == nil { @@ -67,10 +77,25 @@ func serviceFromContext(c *gin.Context) Service { } type dbBackedMiddlewareService struct { - db *gorm.DB + db *gorm.DB + verifier TokenVerifier +} + +func (s *dbBackedMiddlewareService) VerifyToken(ctx context.Context, token string) (*utils.Claims, error) { + if s.verifier == nil { + return nil, fmt.Errorf("token verifier not initialized") + } + return s.verifier.VerifyToken(ctx, token) +} + +func (s *dbBackedMiddlewareService) VerifyServiceToken(ctx context.Context, token string) (*utils.ServiceClaims, error) { + if s.verifier == nil { + return nil, fmt.Errorf("token verifier not initialized") + } + return s.verifier.VerifyServiceToken(ctx, token) } -func (s *dbBackedMiddlewareService) CheckUserPermission(params *dto.CheckPermissionParams) (bool, error) { +func (s *dbBackedMiddlewareService) CheckUserPermission(_ context.Context, params *dto.CheckPermissionParams) (bool, error) { if err := params.Validate(); err != nil { return false, fmt.Errorf("invalid request: %w", err) } @@ -86,7 +111,7 @@ func (s *dbBackedMiddlewareService) CheckUserPermission(params *dto.CheckPermiss return s.checkUserHasPermission(params, permission.ID) } -func (s *dbBackedMiddlewareService) IsUserInTeam(userID, teamID int) (bool, error) { +func (s *dbBackedMiddlewareService) IsUserInTeam(_ context.Context, userID, teamID int) (bool, error) { ut, err := s.getUserTeamRole(userID, teamID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -97,7 +122,7 @@ func (s *dbBackedMiddlewareService) IsUserInTeam(userID, teamID int) (bool, erro return ut != nil, nil } -func (s *dbBackedMiddlewareService) IsUserTeamAdmin(userID, teamID int) (bool, error) { +func (s *dbBackedMiddlewareService) IsUserTeamAdmin(_ context.Context, userID, teamID int) (bool, error) { ut, err := s.getUserTeamRole(userID, teamID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -108,7 +133,7 @@ func (s *dbBackedMiddlewareService) IsUserTeamAdmin(userID, teamID int) (bool, e return ut != nil && ut.Role != nil && ut.Role.Name == consts.RoleTeamAdmin.String(), nil } -func (s *dbBackedMiddlewareService) IsTeamPublic(teamID int) (bool, error) { +func (s *dbBackedMiddlewareService) IsTeamPublic(_ context.Context, teamID int) (bool, error) { team, err := s.getTeamByID(teamID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -119,7 +144,7 @@ func (s *dbBackedMiddlewareService) IsTeamPublic(teamID int) (bool, error) { return team.IsPublic, nil } -func (s *dbBackedMiddlewareService) IsUserInProject(userID, projectID int) (bool, error) { +func (s *dbBackedMiddlewareService) IsUserInProject(_ context.Context, userID, projectID int) (bool, error) { up, err := s.getUserProjectRole(userID, projectID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -130,7 +155,7 @@ func (s *dbBackedMiddlewareService) IsUserInProject(userID, projectID int) (bool return up != nil, nil } -func (s *dbBackedMiddlewareService) IsUserProjectAdmin(userID, projectID int) (bool, error) { +func (s *dbBackedMiddlewareService) IsUserProjectAdmin(_ context.Context, userID, projectID int) (bool, error) { up, err := s.getUserProjectRole(userID, projectID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -350,57 +375,34 @@ func (s *dbBackedMiddlewareService) createAuditLog(db *gorm.DB, log *model.Audit return db.Create(log).Error } -type noopPermissionChecker struct{} +type noopMiddlewareService struct{} -func (noopPermissionChecker) CheckUserPermission(*dto.CheckPermissionParams) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") -} -func (noopPermissionChecker) IsUserTeamAdmin(int, int) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") -} -func (noopPermissionChecker) IsUserInTeam(int, int) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") -} -func (noopPermissionChecker) IsTeamPublic(int) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") +func (noopMiddlewareService) VerifyToken(context.Context, string) (*utils.Claims, error) { + return nil, fmt.Errorf("token verifier not initialized") } -func (noopPermissionChecker) IsUserProjectAdmin(int, int) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") +func (noopMiddlewareService) VerifyServiceToken(context.Context, string) (*utils.ServiceClaims, error) { + return nil, fmt.Errorf("token verifier not initialized") } -func (noopPermissionChecker) IsUserInProject(int, int) (bool, error) { - return false, fmt.Errorf("permission checker not initialized") -} - -type noopMiddlewareService struct{} -func (noopMiddlewareService) CheckUserPermission(*dto.CheckPermissionParams) (bool, error) { +func (noopMiddlewareService) CheckUserPermission(context.Context, *dto.CheckPermissionParams) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -func (noopMiddlewareService) IsUserTeamAdmin(int, int) (bool, error) { +func (noopMiddlewareService) IsUserTeamAdmin(context.Context, int, int) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -func (noopMiddlewareService) IsUserInTeam(int, int) (bool, error) { +func (noopMiddlewareService) IsUserInTeam(context.Context, int, int) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -func (noopMiddlewareService) IsTeamPublic(int) (bool, error) { +func (noopMiddlewareService) IsTeamPublic(context.Context, int) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -func (noopMiddlewareService) IsUserProjectAdmin(int, int) (bool, error) { +func (noopMiddlewareService) IsUserProjectAdmin(context.Context, int, int) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -func (noopMiddlewareService) IsUserInProject(int, int) (bool, error) { +func (noopMiddlewareService) IsUserInProject(context.Context, int, int) (bool, error) { return false, fmt.Errorf("permission checker not initialized") } -type noopAuditLogger struct{} - -func (noopAuditLogger) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { - return fmt.Errorf("audit logger not initialized") -} -func (noopAuditLogger) LogUserAction(string, string, string, string, int, int, consts.ResourceName) error { - return fmt.Errorf("audit logger not initialized") -} - func (noopMiddlewareService) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { return fmt.Errorf("audit logger not initialized") } diff --git a/src/middleware/middleware.go b/src/middleware/middleware.go index 05253a21..02595339 100644 --- a/src/middleware/middleware.go +++ b/src/middleware/middleware.go @@ -1,6 +1,7 @@ package middleware import ( + "aegis/httpx" "regexp" "github.com/google/uuid" @@ -43,6 +44,19 @@ func GroupID() gin.HandlerFunc { } } +func RequestID() gin.HandlerFunc { + return func(c *gin.Context) { + requestID := c.GetHeader(httpx.RequestIDHeader) + if requestID == "" { + requestID = httpx.NewRequestID() + } + + c.Writer.Header().Set(httpx.RequestIDHeader, requestID) + c.Request = c.Request.WithContext(httpx.WithRequestID(c.Request.Context(), requestID)) + c.Next() + } +} + func TracerMiddleware() gin.HandlerFunc { return func(c *gin.Context) { groupID := c.GetString("groupID") diff --git a/src/middleware/permission.go b/src/middleware/permission.go index 5e282a65..e5a590d4 100644 --- a/src/middleware/permission.go +++ b/src/middleware/permission.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "fmt" "net/http" "strconv" @@ -17,6 +18,7 @@ type permissionContext struct { isAdmin bool roles []string checker permissionChecker + ctx context.Context teamID *int projectID *int containerID *int @@ -64,6 +66,7 @@ func extractPermissionContext(c *gin.Context) (*permissionContext, string) { isAdmin: isAdmin, roles: roles, checker: permissionCheckerFromContext(c), + ctx: c.Request.Context(), } // Extract optional IDs from URL parameters @@ -144,7 +147,7 @@ func withPermissionCheck(checkFunc permissionCheckFunc) gin.HandlerFunc { // singlePermission creates a check for a single permission func singlePermission(permission consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { - return ctx.checker.CheckUserPermission(&dto.CheckPermissionParams{ + return ctx.checker.CheckUserPermission(ctx.ctx, &dto.CheckPermissionParams{ UserID: ctx.userID, Action: permission.Action, Scope: permission.Scope, @@ -163,6 +166,7 @@ func anyPermission(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { hasPermission, err := ctx.checker.CheckUserPermission( + ctx.ctx, &dto.CheckPermissionParams{ UserID: ctx.userID, Action: perm.Action, @@ -191,6 +195,7 @@ func allPermissions(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { hasPermission, err := ctx.checker.CheckUserPermission( + ctx.ctx, &dto.CheckPermissionParams{ UserID: ctx.userID, Action: perm.Action, @@ -268,7 +273,7 @@ func teamAccessCheck(requireAdmin bool) permissionCheckFunc { // If admin access required, check team admin status if requireAdmin { - isTeamAdmin, err := ctx.checker.IsUserTeamAdmin(ctx.userID, *ctx.teamID) + isTeamAdmin, err := ctx.checker.IsUserTeamAdmin(ctx.ctx, ctx.userID, *ctx.teamID) if err != nil { return false, err } @@ -276,13 +281,13 @@ func teamAccessCheck(requireAdmin bool) permissionCheckFunc { } // For member access: check if member OR team is public - isMember, err := ctx.checker.IsUserInTeam(ctx.userID, *ctx.teamID) + isMember, err := ctx.checker.IsUserInTeam(ctx.ctx, ctx.userID, *ctx.teamID) if err == nil && isMember { return true, nil } // Check if team is public - isPublic, err := ctx.checker.IsTeamPublic(*ctx.teamID) + isPublic, err := ctx.checker.IsTeamPublic(ctx.ctx, *ctx.teamID) if err == nil && isPublic { return true, nil } @@ -305,7 +310,7 @@ func projectAccessCheck(requireAdmin bool) permissionCheckFunc { // Check project admin status if required if requireAdmin { - isProjectAdmin, err := ctx.checker.IsUserProjectAdmin(ctx.userID, *ctx.projectID) + isProjectAdmin, err := ctx.checker.IsUserProjectAdmin(ctx.ctx, ctx.userID, *ctx.projectID) if err != nil { return false, err } @@ -313,7 +318,7 @@ func projectAccessCheck(requireAdmin bool) permissionCheckFunc { } // Check if user is project member - isMember, err := ctx.checker.IsUserInProject(ctx.userID, *ctx.projectID) + isMember, err := ctx.checker.IsUserInProject(ctx.ctx, ctx.userID, *ctx.projectID) if err != nil { return false, err } diff --git a/src/module/auth/handler.go b/src/module/auth/handler.go index a80ff41e..fbbc124c 100644 --- a/src/module/auth/handler.go +++ b/src/module/auth/handler.go @@ -13,10 +13,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/auth/handler_service.go b/src/module/auth/handler_service.go new file mode 100644 index 00000000..5377ffb4 --- /dev/null +++ b/src/module/auth/handler_service.go @@ -0,0 +1,29 @@ +package authmodule + +import ( + "context" + + "aegis/utils" +) + +// HandlerService captures the auth operations consumed by the HTTP handler. +type HandlerService interface { + Login(context.Context, *LoginReq) (*LoginResp, error) + Register(context.Context, *RegisterReq) (*UserInfo, error) + RefreshToken(context.Context, *TokenRefreshReq) (*TokenRefreshResp, error) + Logout(context.Context, *utils.Claims) error + ChangePassword(context.Context, *ChangePasswordReq, int) error + GetProfile(context.Context, int) (*UserProfileResp, error) + CreateAccessKey(context.Context, int, *CreateAccessKeyReq) (*AccessKeyWithSecretResp, error) + ListAccessKeys(context.Context, int, *ListAccessKeyReq) (*ListAccessKeyResp, error) + GetAccessKey(context.Context, int, int) (*AccessKeyInfo, error) + DeleteAccessKey(context.Context, int, int) error + DisableAccessKey(context.Context, int, int) error + EnableAccessKey(context.Context, int, int) error + RotateAccessKey(context.Context, int, int) (*AccessKeyWithSecretResp, error) + ExchangeAccessKeyToken(context.Context, *AccessKeyTokenReq, string, string) (*AccessKeyTokenResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/auth/middleware_adapter.go b/src/module/auth/middleware_adapter.go new file mode 100644 index 00000000..9b5131be --- /dev/null +++ b/src/module/auth/middleware_adapter.go @@ -0,0 +1,7 @@ +package authmodule + +import "aegis/middleware" + +func NewTokenVerifier(service *Service) middleware.TokenVerifier { + return service +} diff --git a/src/module/auth/module.go b/src/module/auth/module.go index 6abcccb7..80009f3a 100644 --- a/src/module/auth/module.go +++ b/src/module/auth/module.go @@ -10,5 +10,7 @@ var Module = fx.Module("auth", fx.Provide(NewAccessKeyRepository), fx.Provide(NewTokenStore), fx.Provide(NewService), + fx.Provide(AsHandlerService), + fx.Provide(NewTokenVerifier), fx.Provide(NewHandler), ) diff --git a/src/module/auth/repository.go b/src/module/auth/repository.go index b965330d..6c1826b1 100644 --- a/src/module/auth/repository.go +++ b/src/module/auth/repository.go @@ -24,14 +24,6 @@ func NewUserRepository(db *gorm.DB) *UserRepository { return &UserRepository{db: db} } -func (r *UserRepository) withDB(db *gorm.DB) *UserRepository { - return &UserRepository{db: db} -} - -func (r *UserRepository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - func (r *UserRepository) Create(user *model.User) error { if err := r.db.Omit(userOmitFields).Create(user).Error; err != nil { return fmt.Errorf("failed to create user: %w", err) @@ -121,10 +113,6 @@ func NewRoleRepository(db *gorm.DB) *RoleRepository { return &RoleRepository{db: db} } -func (r *RoleRepository) withDB(db *gorm.DB) *RoleRepository { - return &RoleRepository{db: db} -} - func (r *RoleRepository) ListByUserID(userID int) ([]model.Role, error) { var roles []model.Role if err := r.db.Table("roles"). diff --git a/src/module/auth/service.go b/src/module/auth/service.go index 248e8d9a..6664fb09 100644 --- a/src/module/auth/service.go +++ b/src/module/auth/service.go @@ -41,8 +41,8 @@ func (s *Service) Register(ctx context.Context, req *RegisterReq) (*UserInfo, er } var createdUser *model.User - err := s.userRepo.Transaction(func(tx *gorm.DB) error { - userRepo := s.userRepo.withDB(tx) + err := s.userRepo.db.Transaction(func(tx *gorm.DB) error { + userRepo := NewUserRepository(tx) if _, err := userRepo.GetByUsername(req.Username); err == nil { return fmt.Errorf("%w: username is already taken", consts.ErrAlreadyExists) @@ -83,9 +83,9 @@ func (s *Service) Login(ctx context.Context, req *LoginReq) (*LoginResp, error) var token string var expiresAt time.Time - err := s.userRepo.Transaction(func(tx *gorm.DB) error { - userRepo := s.userRepo.withDB(tx) - roleRepo := s.roleRepo.withDB(tx) + err := s.userRepo.db.Transaction(func(tx *gorm.DB) error { + userRepo := NewUserRepository(tx) + roleRepo := NewRoleRepository(tx) user, err := userRepo.GetByUsername(req.Username) if err != nil { @@ -169,13 +169,36 @@ func (s *Service) Logout(ctx context.Context, claims *utils.Claims) error { return nil } +func (s *Service) VerifyToken(ctx context.Context, token string) (*utils.Claims, error) { + claims, err := utils.ValidateToken(token) + if err != nil { + return nil, err + } + + if s.tokenStore != nil { + blacklisted, err := s.tokenStore.IsTokenBlacklisted(ctx, claims.ID) + if err != nil { + return nil, err + } + if blacklisted { + return nil, fmt.Errorf("%w: token has been revoked", consts.ErrAuthenticationFailed) + } + } + + return claims, nil +} + +func (s *Service) VerifyServiceToken(ctx context.Context, token string) (*utils.ServiceClaims, error) { + return utils.ValidateServiceToken(token) +} + func (s *Service) ChangePassword(ctx context.Context, req *ChangePasswordReq, userID int) error { if req == nil { return fmt.Errorf("change password request is nil") } - return s.userRepo.Transaction(func(tx *gorm.DB) error { - userRepo := s.userRepo.withDB(tx) + return s.userRepo.db.Transaction(func(tx *gorm.DB) error { + userRepo := NewUserRepository(tx) user, err := userRepo.GetByID(userID) if err != nil { diff --git a/src/module/auth/token_store.go b/src/module/auth/token_store.go index 6f7e9b1a..f4c4c6b0 100644 --- a/src/module/auth/token_store.go +++ b/src/module/auth/token_store.go @@ -56,3 +56,16 @@ func (s *TokenStore) ReserveAccessKeyNonce(ctx context.Context, accessKey, nonce } return nil } + +func (s *TokenStore) IsTokenBlacklisted(ctx context.Context, tokenID string) (bool, error) { + if s == nil || s.redis == nil || tokenID == "" { + return false, nil + } + + key := fmt.Sprintf(tokenBlacklistPrefix, tokenID) + exists, err := s.redis.Exists(ctx, key) + if err != nil { + return false, fmt.Errorf("failed to check blacklisted token: %w", err) + } + return exists, nil +} diff --git a/src/module/chaossystem/handler.go b/src/module/chaossystem/handler.go index a6934303..7f9b3e88 100644 --- a/src/module/chaossystem/handler.go +++ b/src/module/chaossystem/handler.go @@ -11,10 +11,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/chaossystem/handler_service.go b/src/module/chaossystem/handler_service.go new file mode 100644 index 00000000..23b3c9a0 --- /dev/null +++ b/src/module/chaossystem/handler_service.go @@ -0,0 +1,22 @@ +package chaossystemmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures chaos system operations consumed by HTTP and resource gRPC handlers. +type HandlerService interface { + ListSystems(context.Context, *ListChaosSystemReq) (*dto.ListResp[ChaosSystemResp], error) + GetSystem(context.Context, int) (*ChaosSystemResp, error) + CreateSystem(context.Context, *CreateChaosSystemReq) (*ChaosSystemResp, error) + UpdateSystem(context.Context, int, *UpdateChaosSystemReq) (*ChaosSystemResp, error) + DeleteSystem(context.Context, int) error + UpsertMetadata(context.Context, int, *BulkUpsertSystemMetadataReq) error + ListMetadata(context.Context, int, string) ([]SystemMetadataResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/chaossystem/module.go b/src/module/chaossystem/module.go index ef1bdeca..2a75c3f9 100644 --- a/src/module/chaossystem/module.go +++ b/src/module/chaossystem/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("chaos_system", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/container/core.go b/src/module/container/core.go index 4b06500c..300c9e09 100644 --- a/src/module/container/core.go +++ b/src/module/container/core.go @@ -2,16 +2,14 @@ package containermodule import ( "aegis/model" - - "gorm.io/gorm" ) -func CreateContainerCore(tx *gorm.DB, container *model.Container, userID int) (*model.Container, error) { - service := NewService(NewRepository(tx), NewBuildGateway(), NewHelmFileStore(), nil) - return service.createContainerCore(service.repo, container, userID) +func (r *Repository) CreateContainerCore(container *model.Container, userID int) (*model.Container, error) { + service := NewService(r, NewBuildGateway(), NewHelmFileStore(), nil) + return service.createContainerCore(r, container, userID) } -func UploadHelmValueFileFromPath(tx *gorm.DB, containerName string, helmConfig *model.HelmConfig, srcFilePath string) error { +func (r *Repository) UploadHelmValueFileFromPath(containerName string, helmConfig *model.HelmConfig, srcFilePath string) error { store := NewHelmFileStore() targetPath, err := store.SaveValueFile(containerName, nil, srcFilePath) if err != nil { @@ -19,5 +17,5 @@ func UploadHelmValueFileFromPath(tx *gorm.DB, containerName string, helmConfig * } helmConfig.ValueFile = targetPath - return NewRepository(tx).UpdateHelmConfig(helmConfig) + return r.updateHelmConfig(helmConfig) } diff --git a/src/module/container/handler.go b/src/module/container/handler.go index 9c38dcf7..05c34e85 100644 --- a/src/module/container/handler.go +++ b/src/module/container/handler.go @@ -15,10 +15,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/container/handler_service.go b/src/module/container/handler_service.go new file mode 100644 index 00000000..ba892342 --- /dev/null +++ b/src/module/container/handler_service.go @@ -0,0 +1,31 @@ +package containermodule + +import ( + "context" + + "aegis/dto" + + "mime/multipart" +) + +// HandlerService captures the container operations consumed by the HTTP handler. +type HandlerService interface { + CreateContainer(context.Context, *CreateContainerReq, int) (*ContainerResp, error) + DeleteContainer(context.Context, int) error + GetContainer(context.Context, int) (*ContainerDetailResp, error) + ListContainers(context.Context, *ListContainerReq) (*dto.ListResp[ContainerResp], error) + UpdateContainer(context.Context, *UpdateContainerReq, int) (*ContainerResp, error) + ManageContainerLabels(context.Context, *ManageContainerLabelReq, int) (*ContainerResp, error) + CreateContainerVersion(context.Context, *CreateContainerVersionReq, int, int) (*ContainerVersionResp, error) + DeleteContainerVersion(context.Context, int) error + GetContainerVersion(context.Context, int, int) (*ContainerVersionDetailResp, error) + ListContainerVersions(context.Context, *ListContainerVersionReq, int) (*dto.ListResp[ContainerVersionResp], error) + UpdateContainerVersion(context.Context, *UpdateContainerVersionReq, int, int) (*ContainerVersionResp, error) + SubmitContainerBuilding(context.Context, *SubmitBuildContainerReq, string, int) (*SubmitContainerBuildResp, error) + UploadHelmChart(context.Context, *multipart.FileHeader, int, int, int) (*UploadHelmChartResp, error) + UploadHelmValueFile(context.Context, *multipart.FileHeader, int, int, int) (*UploadHelmValueFileResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/container/module.go b/src/module/container/module.go index e988a324..f7d19b5b 100644 --- a/src/module/container/module.go +++ b/src/module/container/module.go @@ -7,5 +7,6 @@ var Module = fx.Module("container", fx.Provide(NewBuildGateway), fx.Provide(NewHelmFileStore), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/container/repository.go b/src/module/container/repository.go index 264799c0..989eb2c2 100644 --- a/src/module/container/repository.go +++ b/src/module/container/repository.go @@ -24,15 +24,7 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) GetRoleByName(name string) (*model.Role, error) { +func (r *Repository) getRoleByName(name string) (*model.Role, error) { var role model.Role if err := r.db.Where("name = ? and status != ?", name, consts.CommonDeleted).First(&role).Error; err != nil { return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) @@ -40,21 +32,21 @@ func (r *Repository) GetRoleByName(name string) (*model.Role, error) { return &role, nil } -func (r *Repository) CreateContainer(container *model.Container) error { +func (r *Repository) createContainer(container *model.Container) error { if err := r.db.Omit(containerCommonOmitFields, containerModelOmitFields).Create(container).Error; err != nil { return fmt.Errorf("failed to create container: %w", err) } return nil } -func (r *Repository) CreateUserContainer(userContainer *model.UserContainer) error { +func (r *Repository) createUserContainer(userContainer *model.UserContainer) error { if err := r.db.Omit("active_user_container").Create(userContainer).Error; err != nil { return fmt.Errorf("failed to create user-container association: %w", err) } return nil } -func (r *Repository) BatchDeleteContainerVersions(containerID int) (int64, error) { +func (r *Repository) batchDeleteContainerVersions(containerID int) (int64, error) { result := r.db.Model(&model.ContainerVersion{}). Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -64,7 +56,7 @@ func (r *Repository) BatchDeleteContainerVersions(containerID int) (int64, error return result.RowsAffected, nil } -func (r *Repository) RemoveUsersFromContainer(containerID int) (int64, error) { +func (r *Repository) removeUsersFromContainer(containerID int) (int64, error) { result := r.db.Model(&model.UserContainer{}). Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -74,7 +66,7 @@ func (r *Repository) RemoveUsersFromContainer(containerID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) ClearContainerLabels(containerIDs []int, labelIDs []int) error { +func (r *Repository) clearContainerLabels(containerIDs []int, labelIDs []int) error { if len(containerIDs) == 0 { return nil } @@ -89,7 +81,7 @@ func (r *Repository) ClearContainerLabels(containerIDs []int, labelIDs []int) er return nil } -func (r *Repository) DeleteContainer(containerID int) (int64, error) { +func (r *Repository) deleteContainer(containerID int) (int64, error) { result := r.db.Model(&model.Container{}). Where("id = ? AND status != ?", containerID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -99,7 +91,7 @@ func (r *Repository) DeleteContainer(containerID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) GetContainerByID(containerID int) (*model.Container, error) { +func (r *Repository) getContainerByID(containerID int) (*model.Container, error) { var container model.Container if err := r.db.Where("id = ? AND status != ?", containerID, consts.CommonDeleted).First(&container).Error; err != nil { return nil, fmt.Errorf("failed to find container with id %d: %w", containerID, err) @@ -107,7 +99,7 @@ func (r *Repository) GetContainerByID(containerID int) (*model.Container, error) return &container, nil } -func (r *Repository) ListContainerVersionsByContainerID(containerID int) ([]model.ContainerVersion, error) { +func (r *Repository) listContainerVersionsByContainerID(containerID int) ([]model.ContainerVersion, error) { var versions []model.ContainerVersion if err := r.db. Preload("Container"). @@ -119,7 +111,59 @@ func (r *Repository) ListContainerVersionsByContainerID(containerID int) ([]mode return versions, nil } -func (r *Repository) ListContainers(limit, offset int, containerType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]model.Container, int64, error) { +func (r *Repository) batchGetContainerVersions(containerType consts.ContainerType, containerNames []string, userID int) ([]model.ContainerVersion, error) { + if len(containerNames) == 0 { + return []model.ContainerVersion{}, nil + } + + var versions []model.ContainerVersion + query := r.db.Table("container_versions cv"). + Preload("Container"). + Where("cv.status = ?", consts.CommonEnabled). + Order("cv.container_id DESC, cv.name_major DESC, cv.name_minor DESC, cv.name_patch DESC") + + query = query.Joins("INNER JOIN containers c ON c.id = cv.container_id"). + Where("c.type = ? AND c.name IN (?) AND c.status = ?", containerType, containerNames, consts.CommonEnabled) + + if userID > 0 { + query = query.Joins( + "LEFT JOIN user_containers uc ON uc.container_id = c.id AND uc.user_id = ? AND uc.status = ?", + userID, consts.CommonEnabled, + ).Where( + r.db.Where("c.is_public = ?", true).Or("uc.container_id IS NOT NULL"), + ) + } + + if err := query.Find(&versions).Error; err != nil { + return nil, fmt.Errorf("failed to query container versions: %w", err) + } + return versions, nil +} + +func (r *Repository) checkContainerExistsWithDifferentType(containerName string, requestedType consts.ContainerType, userID int) (bool, consts.ContainerType, error) { + var container model.Container + query := r.db.Table("containers"). + Where("name = ? AND type != ? AND status = ?", containerName, requestedType, consts.CommonEnabled) + + if userID > 0 { + query = query.Joins( + "LEFT JOIN user_containers uc ON uc.container_id = containers.id AND uc.user_id = ? AND uc.status = ?", + userID, consts.CommonEnabled, + ).Where( + r.db.Where("containers.is_public = ?", true).Or("uc.container_id IS NOT NULL"), + ) + } + + if err := query.First(&container).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return false, 0, nil + } + return false, 0, fmt.Errorf("failed to check container existence: %w", err) + } + return true, container.Type, nil +} + +func (r *Repository) listContainers(limit, offset int, containerType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]model.Container, int64, error) { var ( containers []model.Container total int64 @@ -145,7 +189,7 @@ func (r *Repository) ListContainers(limit, offset int, containerType *consts.Con return containers, total, nil } -func (r *Repository) ListContainerLabels(containerIDs []int) (map[int][]model.Label, error) { +func (r *Repository) listContainerLabels(containerIDs []int) (map[int][]model.Label, error) { if len(containerIDs) == 0 { return nil, nil } @@ -174,14 +218,14 @@ func (r *Repository) ListContainerLabels(containerIDs []int) (map[int][]model.La return labelsMap, nil } -func (r *Repository) UpdateContainer(container *model.Container) error { +func (r *Repository) updateContainer(container *model.Container) error { if err := r.db.Omit(containerCommonOmitFields).Save(container).Error; err != nil { return fmt.Errorf("failed to update container: %w", err) } return nil } -func (r *Repository) AddContainerLabels(containerLabels []model.ContainerLabel) error { +func (r *Repository) addContainerLabels(containerLabels []model.ContainerLabel) error { if len(containerLabels) == 0 { return nil } @@ -191,7 +235,7 @@ func (r *Repository) AddContainerLabels(containerLabels []model.ContainerLabel) return nil } -func (r *Repository) ListLabelIDsByKeyAndContainerID(containerID int, keys []string) ([]int, error) { +func (r *Repository) listLabelIDsByKeyAndContainerID(containerID int, keys []string) ([]int, error) { var labelIDs []int if err := r.db.Table("labels l"). Select("l.id"). @@ -203,7 +247,7 @@ func (r *Repository) ListLabelIDsByKeyAndContainerID(containerID int, keys []str return labelIDs, nil } -func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { +func (r *Repository) batchDecreaseLabelUsages(labelIDs []int, decrement int) error { if len(labelIDs) == 0 { return nil } @@ -218,7 +262,7 @@ func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) err return nil } -func (r *Repository) ListLabelsByContainerID(containerID int) ([]model.Label, error) { +func (r *Repository) listLabelsByContainerID(containerID int) ([]model.Label, error) { var labels []model.Label if err := r.db.Model(&model.Label{}). Joins("JOIN container_labels cl ON cl.label_id = labels.id"). @@ -229,7 +273,7 @@ func (r *Repository) ListLabelsByContainerID(containerID int) ([]model.Label, er return labels, nil } -func (r *Repository) BatchCreateContainerVersions(versions []model.ContainerVersion) error { +func (r *Repository) batchCreateContainerVersions(versions []model.ContainerVersion) error { if len(versions) == 0 { return fmt.Errorf("no container versions to create") } @@ -239,7 +283,7 @@ func (r *Repository) BatchCreateContainerVersions(versions []model.ContainerVers return nil } -func (r *Repository) BatchCreateOrFindParameterConfigs(params []model.ParameterConfig) error { +func (r *Repository) batchCreateOrFindParameterConfigs(params []model.ParameterConfig) error { if len(params) == 0 { return nil } @@ -249,7 +293,7 @@ func (r *Repository) BatchCreateOrFindParameterConfigs(params []model.ParameterC return nil } -func (r *Repository) ListParameterConfigsByKeys(configs []model.ParameterConfig) ([]model.ParameterConfig, error) { +func (r *Repository) listParameterConfigsByKeys(configs []model.ParameterConfig) ([]model.ParameterConfig, error) { if len(configs) == 0 { return []model.ParameterConfig{}, nil } @@ -266,7 +310,40 @@ func (r *Repository) ListParameterConfigsByKeys(configs []model.ParameterConfig) return results, nil } -func (r *Repository) AddContainerVersionEnvVars(envVars []model.ContainerVersionEnvVar) error { +func (r *Repository) listContainerVersionEnvVars(keys []string, containerVersionID int) ([]model.ParameterConfig, error) { + query := r.db.Model(&model.ParameterConfig{}). + Joins("JOIN container_version_env_vars cvev ON cvev.parameter_config_id = parameter_configs.id"). + Where("cvev.container_version_id = ?", containerVersionID). + Where("parameter_configs.category = ?", consts.ParameterCategoryEnvVars) + + if len(keys) > 0 { + query = query.Where("parameter_configs.config_key IN (?)", keys) + } + + var params []model.ParameterConfig + if err := query.Find(¶ms).Error; err != nil { + return nil, fmt.Errorf("failed to list container env vars: %w", err) + } + return params, nil +} + +func (r *Repository) listHelmConfigValues(keys []string, helmConfigID int) ([]model.ParameterConfig, error) { + query := r.db.Model(&model.ParameterConfig{}). + Joins("JOIN helm_config_values hcv ON hcv.parameter_config_id = parameter_configs.id"). + Where("hcv.helm_config_id = ?", helmConfigID) + + if len(keys) > 0 { + query = query.Where("parameter_configs.config_key IN (?)", keys) + } + + var params []model.ParameterConfig + if err := query.Find(¶ms).Error; err != nil { + return nil, fmt.Errorf("failed to list helm values: %w", err) + } + return params, nil +} + +func (r *Repository) addContainerVersionEnvVars(envVars []model.ContainerVersionEnvVar) error { if len(envVars) == 0 { return nil } @@ -276,7 +353,7 @@ func (r *Repository) AddContainerVersionEnvVars(envVars []model.ContainerVersion return nil } -func (r *Repository) BatchCreateHelmConfigs(helmConfigs []*model.HelmConfig) error { +func (r *Repository) batchCreateHelmConfigs(helmConfigs []*model.HelmConfig) error { if len(helmConfigs) == 0 { return fmt.Errorf("no helm configs to create") } @@ -286,7 +363,7 @@ func (r *Repository) BatchCreateHelmConfigs(helmConfigs []*model.HelmConfig) err return nil } -func (r *Repository) AddHelmConfigValues(helmValues []model.HelmConfigValue) error { +func (r *Repository) addHelmConfigValues(helmValues []model.HelmConfigValue) error { if len(helmValues) == 0 { return nil } @@ -296,7 +373,7 @@ func (r *Repository) AddHelmConfigValues(helmValues []model.HelmConfigValue) err return nil } -func (r *Repository) DeleteContainerVersion(versionID int) (int64, error) { +func (r *Repository) deleteContainerVersion(versionID int) (int64, error) { result := r.db.Model(&model.ContainerVersion{}). Where("id = ? AND status != ?", versionID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -306,7 +383,7 @@ func (r *Repository) DeleteContainerVersion(versionID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) GetContainerVersionByID(versionID int) (*model.ContainerVersion, error) { +func (r *Repository) getContainerVersionByID(versionID int) (*model.ContainerVersion, error) { var version model.ContainerVersion if err := r.db. Preload("Container"). @@ -318,7 +395,7 @@ func (r *Repository) GetContainerVersionByID(versionID int) (*model.ContainerVer return &version, nil } -func (r *Repository) ListContainerVersions(limit, offset int, containerID int, status *consts.StatusType) ([]model.ContainerVersion, int64, error) { +func (r *Repository) listContainerVersions(limit, offset int, containerID int, status *consts.StatusType) ([]model.ContainerVersion, int64, error) { var ( versions []model.ContainerVersion total int64 @@ -338,14 +415,14 @@ func (r *Repository) ListContainerVersions(limit, offset int, containerID int, s return versions, total, nil } -func (r *Repository) UpdateContainerVersion(version *model.ContainerVersion) error { +func (r *Repository) updateContainerVersion(version *model.ContainerVersion) error { if err := r.db.Omit(containerVersionModelOmitFields).Save(version).Error; err != nil { return fmt.Errorf("failed to update container version: %w", err) } return nil } -func (r *Repository) GetHelmConfigByContainerVersionID(versionID int) (*model.HelmConfig, error) { +func (r *Repository) getHelmConfigByContainerVersionID(versionID int) (*model.HelmConfig, error) { var helmConfig model.HelmConfig if err := r.db.Preload("ContainerVersion").Where("container_version_id = ?", versionID).First(&helmConfig).Error; err != nil { return nil, fmt.Errorf("failed to find helm config for version id %d: %w", versionID, err) @@ -353,7 +430,7 @@ func (r *Repository) GetHelmConfigByContainerVersionID(versionID int) (*model.He return &helmConfig, nil } -func (r *Repository) UpdateHelmConfig(helmConfig *model.HelmConfig) error { +func (r *Repository) updateHelmConfig(helmConfig *model.HelmConfig) error { if err := r.db.Save(helmConfig).Error; err != nil { return fmt.Errorf("failed to update helm config: %w", err) } diff --git a/src/service/common/container.go b/src/module/container/resolve.go similarity index 59% rename from src/service/common/container.go rename to src/module/container/resolve.go index c1981ccd..573a45ba 100644 --- a/src/service/common/container.go +++ b/src/module/container/resolve.go @@ -1,39 +1,39 @@ -package common +package containermodule import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/repository" "aegis/utils" "fmt" - - "gorm.io/gorm" + "reflect" + "regexp" + "strings" ) -func ListContainerVersionEnvVarsWithDB(db *gorm.DB, specs []dto.ParameterSpec, version *model.ContainerVersion) ([]dto.ParameterItem, error) { - return listParameterItemsWithDB(db, specs, repository.ListContainerVersionEnvVars, version.ID, version) +var templateVarRegex = regexp.MustCompile(`{{\s*\.([a-zA-Z0-9_]+)\s*}}`) + +func (r *Repository) ListContainerVersionEnvVars(specs []dto.ParameterSpec, version *model.ContainerVersion) ([]dto.ParameterItem, error) { + return listParameterItemsWithDB(r, specs, r.listContainerVersionEnvVars, version.ID, version) } -func ListHelmConfigValuesWithDB(db *gorm.DB, specs []dto.ParameterSpec, cfg *model.HelmConfig) ([]dto.ParameterItem, error) { - return listParameterItemsWithDB(db, specs, repository.ListHelmConfigValues, cfg.ID, cfg.ContainerVersion) +func (r *Repository) ListHelmConfigValues(specs []dto.ParameterSpec, cfg *model.HelmConfig) ([]dto.ParameterItem, error) { + return listParameterItemsWithDB(r, specs, r.listHelmConfigValues, cfg.ID, cfg.ContainerVersion) } -func MapRefsToContainerVersionsWithDB(db *gorm.DB, refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) (map[*dto.ContainerRef]model.ContainerVersion, error) { - versions, err := getUniqueVersionsForContainerRefsWithDB(db, refs, containerType, userID) +func (r *Repository) ResolveContainerVersions(refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) (map[*dto.ContainerRef]model.ContainerVersion, error) { + versions, err := getUniqueVersionsForContainerRefs(r, refs, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to batch get container versions: %w", err) } flatMap := make(map[string][]model.ContainerVersion) hierarchicalMap := make(map[string]map[string]model.ContainerVersion) - for _, version := range versions { containerName := version.Container.Name versionName := version.Name flatMap[containerName] = append(flatMap[containerName], version) - if _, exists := hierarchicalMap[containerName]; !exists { hierarchicalMap[containerName] = make(map[string]model.ContainerVersion) } @@ -48,8 +48,7 @@ func MapRefsToContainerVersionsWithDB(db *gorm.DB, refs []*dto.ContainerRef, con if _, exists := hierarchicalMap[ref.Name]; !exists { availableContainers := getAvailableContainerNames(hierarchicalMap) if len(availableContainers) == 0 { - // Check if container exists with different type - exists, actualType, err := repository.CheckContainerExistsWithDifferentType(db, ref.Name, containerType, userID) + exists, actualType, err := r.checkContainerExistsWithDifferentType(ref.Name, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to check container type: %w", err) } @@ -66,14 +65,12 @@ func MapRefsToContainerVersionsWithDB(db *gorm.DB, refs []*dto.ContainerRef, con if _, exists := hierarchicalMap[ref.Name][ref.Version]; !exists { return nil, fmt.Errorf("%s container version not found: %s:%s (available versions for %s: %v)", containerTypeName, ref.Name, ref.Version, ref.Name, getAvailableVersions(hierarchicalMap, ref.Name)) } - result = hierarchicalMap[ref.Name][ref.Version] } else { if _, exists := flatMap[ref.Name]; !exists { availableContainers := getAvailableContainerNames(hierarchicalMap) if len(availableContainers) == 0 { - // Check if container exists with different type - exists, actualType, err := repository.CheckContainerExistsWithDifferentType(db, ref.Name, containerType, userID) + exists, actualType, err := r.checkContainerExistsWithDifferentType(ref.Name, containerType, userID) if err != nil { return nil, fmt.Errorf("failed to check container type: %w", err) } @@ -88,21 +85,19 @@ func MapRefsToContainerVersionsWithDB(db *gorm.DB, refs []*dto.ContainerRef, con } result = flatMap[ref.Name][0] } - results[ref] = result } return results, nil } -func getUniqueVersionsForContainerRefsWithDB(db *gorm.DB, refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) ([]model.ContainerVersion, error) { +func getUniqueVersionsForContainerRefs(repo *Repository, refs []*dto.ContainerRef, containerType consts.ContainerType, userID int) ([]model.ContainerVersion, error) { containerNamesSet := make(map[string]struct{}, len(refs)) for _, ref := range refs { if ref.Name != "" { containerNamesSet[ref.Name] = struct{}{} } } - if len(containerNamesSet) == 0 { return []model.ContainerVersion{}, nil } @@ -111,26 +106,19 @@ func getUniqueVersionsForContainerRefsWithDB(db *gorm.DB, refs []*dto.ContainerR for name := range containerNamesSet { requiredNames = append(requiredNames, name) } - - versions, err := repository.BatchGetContainerVersions(db, containerType, requiredNames, userID) - if err != nil { - return nil, fmt.Errorf("failed to batch get container versions: %w", err) - } - - return versions, nil + return repo.batchGetContainerVersions(containerType, requiredNames, userID) } -func listParameterItemsWithDB(db *gorm.DB, specs []dto.ParameterSpec, fetcher repository.ParameterConfigFetcher, resourceID int, contextCfg any) ([]dto.ParameterItem, error) { +func listParameterItemsWithDB(repo *Repository, specs []dto.ParameterSpec, fetcher func([]string, int) ([]model.ParameterConfig, error), resourceID int, contextCfg any) ([]dto.ParameterItem, error) { keys := make([]string, 0, len(specs)) for _, item := range specs { keys = append(keys, item.Key) } - paramConfigs, err := fetcher(db, keys, resourceID) + paramConfigs, err := fetcher(keys, resourceID) if err != nil { return nil, fmt.Errorf("failed to list configurations: %w", err) } - if len(paramConfigs) == 0 && len(specs) > 0 { return nil, fmt.Errorf("no configurations found for the provided specs") } @@ -141,14 +129,12 @@ func listParameterItemsWithDB(db *gorm.DB, specs []dto.ParameterSpec, fetcher re } processedParamConfigs := make(map[string]struct{}) - items := make([]dto.ParameterItem, 0, len(specs)) for _, spec := range specs { config, exists := paramConfigMap[spec.Key] if !exists { return nil, fmt.Errorf("configuration not found for key: %s", spec.Key) } - processedParamConfigs[spec.Key] = struct{}{} item, err := processParameterConfig(config, spec.Value, contextCfg) @@ -160,22 +146,22 @@ func listParameterItemsWithDB(db *gorm.DB, specs []dto.ParameterSpec, fetcher re } } - for _, paramConfigMap := range paramConfigMap { - if _, processed := processedParamConfigs[paramConfigMap.Key]; !processed { - item, err := processParameterConfig(paramConfigMap, nil, contextCfg) - if err != nil { - return nil, fmt.Errorf("failed to process parameter config for key %s: %w", paramConfigMap.Key, err) - } - if item != nil { - items = append(items, *item) - } + for _, paramConfig := range paramConfigMap { + if _, processed := processedParamConfigs[paramConfig.Key]; processed { + continue + } + item, err := processParameterConfig(paramConfig, nil, contextCfg) + if err != nil { + return nil, fmt.Errorf("failed to process parameter config for key %s: %w", paramConfig.Key, err) + } + if item != nil { + items = append(items, *item) } } return items, nil } -// processParameterConfig processes a single parameter configuration and returns the corresponding parameter item func processParameterConfig(config model.ParameterConfig, userValue any, contextCfg any) (*dto.ParameterItem, error) { switch config.Type { case consts.ParameterTypeFixed: @@ -191,23 +177,14 @@ func processParameterConfig(config model.ParameterConfig, userValue any, context finalValue = convertedValue } } - - return &dto.ParameterItem{ - Key: config.Key, - Value: finalValue, - }, nil - + return &dto.ParameterItem{Key: config.Key, Value: finalValue}, nil case consts.ParameterTypeDynamic: if config.TemplateString == nil || *config.TemplateString == "" { return nil, fmt.Errorf("dynamic parameter %s is missing a template string", config.Key) } - templateVars := extractTemplateVars(*config.TemplateString) if len(templateVars) == 0 { - return &dto.ParameterItem{ - Key: config.Key, - TemplateString: *config.TemplateString, - }, nil + return &dto.ParameterItem{Key: config.Key, TemplateString: *config.TemplateString}, nil } renderedValue, err := renderTemplate(*config.TemplateString, templateVars, contextCfg) @@ -218,35 +195,72 @@ func processParameterConfig(config model.ParameterConfig, userValue any, context return nil, fmt.Errorf("required dynamic parameter %s rendered to an empty string", config.Key) } if renderedValue != "" { - return &dto.ParameterItem{ - Key: config.Key, - Value: renderedValue, - }, nil + return &dto.ParameterItem{Key: config.Key, Value: renderedValue}, nil } - return nil, nil default: - return nil, fmt.Errorf("unknown parameter type for key %s", config.Key) + return nil, fmt.Errorf("unsupported parameter type: %v", config.Type) + } +} + +func extractTemplateVars(templateString string) []string { + matches := templateVarRegex.FindAllStringSubmatch(templateString, -1) + if matches == nil { + return nil + } + + variables := make([]string, 0, len(matches)) + for _, match := range matches { + if len(match) > 1 { + variables = append(variables, match[1]) + } + } + return variables +} + +func renderTemplate(templateStr string, vars []string, context any) (string, error) { + contextValue := reflect.ValueOf(context) + if contextValue.Kind() == reflect.Ptr { + contextValue = contextValue.Elem() } + + renderedString := templateStr + contextType := contextValue.Type() + for _, varName := range vars { + fieldValue := contextValue.FieldByName(varName) + if !fieldValue.IsValid() { + return "", fmt.Errorf("variable '%s' not found in context structure", varName) + } + + fieldType, found := contextType.FieldByName(varName) + if !found || fieldType.PkgPath != "" { + return "", fmt.Errorf("variable '%s' is not an exported field in context", varName) + } + + strValue, err := utils.ConvertSimpleTypeToString(fieldValue.Interface()) + if err != nil { + return "", fmt.Errorf("failed to convert context value for %s: %w", varName, err) + } + + renderedString = strings.ReplaceAll(renderedString, fmt.Sprintf("{{ .%s }}", varName), strValue) + renderedString = strings.ReplaceAll(renderedString, fmt.Sprintf("{{.%s}}", varName), strValue) + } + return renderedString, nil } -// getAvailableContainerNames returns a list of available container names from the hierarchical map -func getAvailableContainerNames(hierarchicalMap map[string]map[string]model.ContainerVersion) []string { - names := make([]string, 0, len(hierarchicalMap)) - for name := range hierarchicalMap { +func getAvailableContainerNames(versions map[string]map[string]model.ContainerVersion) []string { + names := make([]string, 0, len(versions)) + for name := range versions { names = append(names, name) } return names } -// getAvailableVersions returns a list of available versions for a specific container -func getAvailableVersions(hierarchicalMap map[string]map[string]model.ContainerVersion, containerName string) []string { - if versions, exists := hierarchicalMap[containerName]; exists { - versionNames := make([]string, 0, len(versions)) - for versionName := range versions { - versionNames = append(versionNames, versionName) - } - return versionNames +func getAvailableVersions(versions map[string]map[string]model.ContainerVersion, containerName string) []string { + items := versions[containerName] + results := make([]string, 0, len(items)) + for version := range items { + results = append(results, version) } - return []string{} + return results } diff --git a/src/module/container/service.go b/src/module/container/service.go index 46de4a6c..fe1d31e1 100644 --- a/src/module/container/service.go +++ b/src/module/container/service.go @@ -10,6 +10,7 @@ import ( "aegis/dto" redisinfra "aegis/infra/redis" "aegis/model" + labelmodule "aegis/module/label" "aegis/service/common" "gorm.io/gorm" @@ -32,8 +33,8 @@ func (s *Service) CreateContainer(_ context.Context, req *CreateContainerReq, us } container := req.ConvertToContainer() - err := s.repo.Transaction(func(tx *gorm.DB) error { - createdContainer, err := s.createContainerCore(s.repo.withDB(tx), container, userID) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + createdContainer, err := s.createContainerCore(NewRepository(tx), container, userID) if err != nil { return fmt.Errorf("failed to create container: %w", err) } @@ -48,18 +49,18 @@ func (s *Service) CreateContainer(_ context.Context, req *CreateContainerReq, us } func (s *Service) DeleteContainer(_ context.Context, containerID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if _, err := repo.BatchDeleteContainerVersions(containerID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if _, err := repo.batchDeleteContainerVersions(containerID); err != nil { return fmt.Errorf("failed to delete container versions: %w", err) } - if _, err := repo.RemoveUsersFromContainer(containerID); err != nil { + if _, err := repo.removeUsersFromContainer(containerID); err != nil { return fmt.Errorf("failed to remove all users from container: %w", err) } - if err := repo.ClearContainerLabels([]int{containerID}, nil); err != nil { + if err := repo.clearContainerLabels([]int{containerID}, nil); err != nil { return fmt.Errorf("failed to clear container labels: %w", err) } - rows, err := repo.DeleteContainer(containerID) + rows, err := repo.deleteContainer(containerID) if err != nil { return fmt.Errorf("failed to delete container: %w", err) } @@ -71,7 +72,7 @@ func (s *Service) DeleteContainer(_ context.Context, containerID int) error { } func (s *Service) GetContainer(_ context.Context, containerID int) (*ContainerDetailResp, error) { - container, err := s.repo.GetContainerByID(containerID) + container, err := s.repo.getContainerByID(containerID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) @@ -79,7 +80,7 @@ func (s *Service) GetContainer(_ context.Context, containerID int) (*ContainerDe return nil, fmt.Errorf("failed to get container: %w", err) } - versions, err := s.repo.ListContainerVersionsByContainerID(container.ID) + versions, err := s.repo.listContainerVersionsByContainerID(container.ID) if err != nil { return nil, fmt.Errorf("failed to get container versions: %w", err) } @@ -95,7 +96,7 @@ func (s *Service) GetContainer(_ context.Context, containerID int) (*ContainerDe func (s *Service) ListContainers(_ context.Context, req *ListContainerReq) (*dto.ListResp[ContainerResp], error) { limit, offset := req.ToGormParams() - containers, total, err := s.repo.ListContainers(limit, offset, req.Type, req.IsPublic, req.Status) + containers, total, err := s.repo.listContainers(limit, offset, req.Type, req.IsPublic, req.Status) if err != nil { return nil, fmt.Errorf("failed to list containers: %w", err) } @@ -105,7 +106,7 @@ func (s *Service) ListContainers(_ context.Context, req *ListContainerReq) (*dto containerIDs = append(containerIDs, container.ID) } - labelsMap, err := s.repo.ListContainerLabels(containerIDs) + labelsMap, err := s.repo.listContainerLabels(containerIDs) if err != nil { return nil, fmt.Errorf("failed to list container labels: %w", err) } @@ -127,9 +128,9 @@ func (s *Service) ListContainers(_ context.Context, req *ListContainerReq) (*dto func (s *Service) UpdateContainer(_ context.Context, req *UpdateContainerReq, containerID int) (*ContainerResp, error) { var updatedContainer *model.Container - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - container, err := repo.GetContainerByID(containerID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + container, err := repo.getContainerByID(containerID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: container with id %d not found", consts.ErrNotFound, containerID) @@ -138,7 +139,7 @@ func (s *Service) UpdateContainer(_ context.Context, req *UpdateContainerReq, co } req.PatchContainerModel(container) - if err := repo.UpdateContainer(container); err != nil { + if err := repo.updateContainer(container); err != nil { return fmt.Errorf("failed to update container: %w", err) } @@ -157,9 +158,9 @@ func (s *Service) ManageContainerLabels(_ context.Context, req *ManageContainerL } var managedContainer *model.Container - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - container, err := repo.GetContainerByID(containerID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + container, err := repo.getContainerByID(containerID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: container not found", consts.ErrNotFound) @@ -168,7 +169,7 @@ func (s *Service) ManageContainerLabels(_ context.Context, req *ManageContainerL } if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -181,29 +182,29 @@ func (s *Service) ManageContainerLabels(_ context.Context, req *ManageContainerL }) } - if err := repo.AddContainerLabels(containerLabels); err != nil { + if err := repo.addContainerLabels(containerLabels); err != nil { return fmt.Errorf("failed to add container labels: %w", err) } } if len(req.RemoveLabels) > 0 { - labelIDs, err := repo.ListLabelIDsByKeyAndContainerID(containerID, req.RemoveLabels) + labelIDs, err := repo.listLabelIDsByKeyAndContainerID(containerID, req.RemoveLabels) if err != nil { return fmt.Errorf("failed to find label IDs: %w", err) } if len(labelIDs) > 0 { - if err := repo.ClearContainerLabels([]int{containerID}, labelIDs); err != nil { + if err := repo.clearContainerLabels([]int{containerID}, labelIDs); err != nil { return fmt.Errorf("failed to delete container-label associations: %w", err) } - if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + if err := repo.batchDecreaseLabelUsages(labelIDs, 1); err != nil { return fmt.Errorf("failed to decrease label usage counts: %w", err) } } } - labels, err := repo.ListLabelsByContainerID(container.ID) + labels, err := repo.listLabelsByContainerID(container.ID) if err != nil { return fmt.Errorf("failed to get container labels: %w", err) } @@ -228,8 +229,8 @@ func (s *Service) CreateContainerVersion(_ context.Context, req *CreateContainer version.UserID = userID var createdVersion *model.ContainerVersion - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) versions, err := s.createContainerVersionsCore(repo, []model.ContainerVersion{*version}) if err != nil { return fmt.Errorf("failed to create container version: %w", err) @@ -245,7 +246,7 @@ func (s *Service) CreateContainerVersion(_ context.Context, req *CreateContainer } func (s *Service) DeleteContainerVersion(_ context.Context, versionID int) error { - rows, err := s.repo.DeleteContainerVersion(versionID) + rows, err := s.repo.deleteContainerVersion(versionID) if err != nil { return fmt.Errorf("failed to delete container version: %w", err) } @@ -256,14 +257,14 @@ func (s *Service) DeleteContainerVersion(_ context.Context, versionID int) error } func (s *Service) GetContainerVersion(_ context.Context, containerID, versionID int) (*ContainerVersionDetailResp, error) { - if _, err := s.repo.GetContainerByID(containerID); err != nil { + if _, err := s.repo.getContainerByID(containerID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: container id: %d", consts.ErrNotFound, containerID) } return nil, fmt.Errorf("failed to get container: %w", err) } - version, err := s.repo.GetContainerVersionByID(versionID) + version, err := s.repo.getContainerVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) @@ -273,7 +274,7 @@ func (s *Service) GetContainerVersion(_ context.Context, containerID, versionID resp := NewContainerVersionDetailResp(version) - helmConfig, err := s.repo.GetHelmConfigByContainerVersionID(version.ID) + helmConfig, err := s.repo.getHelmConfigByContainerVersionID(version.ID) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("failed to get helm config: %w", err) } @@ -291,7 +292,7 @@ func (s *Service) GetContainerVersion(_ context.Context, containerID, versionID func (s *Service) ListContainerVersions(_ context.Context, req *ListContainerVersionReq, containerID int) (*dto.ListResp[ContainerVersionResp], error) { limit, offset := req.ToGormParams() - versions, total, err := s.repo.ListContainerVersions(limit, offset, containerID, req.Status) + versions, total, err := s.repo.listContainerVersions(limit, offset, containerID, req.Status) if err != nil { return nil, fmt.Errorf("failed to list container versions: %w", err) } @@ -311,9 +312,9 @@ func (s *Service) UpdateContainerVersion(_ context.Context, req *UpdateContainer _ = containerID var updatedVersion *model.ContainerVersion - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - version, err := repo.GetContainerVersionByID(versionID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + version, err := repo.getContainerVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) @@ -322,14 +323,14 @@ func (s *Service) UpdateContainerVersion(_ context.Context, req *UpdateContainer } req.PatchContainerVersionModel(version) - if err := repo.UpdateContainerVersion(version); err != nil { + if err := repo.updateContainerVersion(version); err != nil { return fmt.Errorf("failed to update container version: %w", err) } updatedVersion = version if req.HelmConfigRequest != nil { - helmConfig, err := repo.GetHelmConfigByContainerVersionID(version.ID) + helmConfig, err := repo.getHelmConfigByContainerVersionID(version.ID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("helm config not found for version id %d", versionID) @@ -340,7 +341,7 @@ func (s *Service) UpdateContainerVersion(_ context.Context, req *UpdateContainer if err := req.HelmConfigRequest.PatchHelmConfigModel(helmConfig); err != nil { return fmt.Errorf("failed to patch helm config model: %w", err) } - if err := repo.UpdateHelmConfig(helmConfig); err != nil { + if err := repo.updateHelmConfig(helmConfig); err != nil { return fmt.Errorf("failed to update helm config: %w", err) } } @@ -414,7 +415,7 @@ func (s *Service) UploadHelmChart(_ context.Context, file *multipart.FileHeader, filename := file.Filename containerVersion.HelmConfig.LocalPath = targetPath containerVersion.HelmConfig.Checksum = checksum - if err := s.repo.UpdateHelmConfig(containerVersion.HelmConfig); err != nil { + if err := s.repo.updateHelmConfig(containerVersion.HelmConfig); err != nil { return nil, fmt.Errorf("failed to update helm config: %w", err) } @@ -444,7 +445,7 @@ func (s *Service) UploadHelmValueFile(_ context.Context, file *multipart.FileHea } func (s *Service) createContainerCore(repo *Repository, container *model.Container, userID int) (*model.Container, error) { - role, err := repo.GetRoleByName(consts.RoleContainerAdmin.String()) + role, err := repo.getRoleByName(consts.RoleContainerAdmin.String()) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: role %v not found", consts.ErrNotFound, consts.RoleContainerAdmin) @@ -452,14 +453,14 @@ func (s *Service) createContainerCore(repo *Repository, container *model.Contain return nil, fmt.Errorf("failed to get project owner role: %w", err) } - if err := repo.CreateContainer(container); err != nil { + if err := repo.createContainer(container); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, consts.ErrAlreadyExists } return nil, err } - if err := repo.CreateUserContainer(&model.UserContainer{ + if err := repo.createUserContainer(&model.UserContainer{ UserID: userID, ContainerID: container.ID, RoleID: role.ID, @@ -487,7 +488,7 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model return nil, nil } - if err := repo.BatchCreateContainerVersions(versions); err != nil { + if err := repo.batchCreateContainerVersions(versions); err != nil { return nil, fmt.Errorf("failed to create container versions: %w", err) } @@ -512,11 +513,11 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model envVars[i] = item.envVar } - if err := repo.BatchCreateOrFindParameterConfigs(envVars); err != nil { + if err := repo.batchCreateOrFindParameterConfigs(envVars); err != nil { return nil, fmt.Errorf("failed to create parameter configs: %w", err) } - actualEnvVars, err := repo.ListParameterConfigsByKeys(envVars) + actualEnvVars, err := repo.listParameterConfigsByKeys(envVars) if err != nil { return nil, fmt.Errorf("failed to list parameter configs: %w", err) } @@ -541,7 +542,7 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model }) } - if err := repo.AddContainerVersionEnvVars(relations); err != nil { + if err := repo.addContainerVersionEnvVars(relations); err != nil { return nil, fmt.Errorf("failed to create container version env var relations: %w", err) } } @@ -558,7 +559,7 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model return versions, nil } - if err := repo.BatchCreateHelmConfigs(helmConfigs); err != nil { + if err := repo.batchCreateHelmConfigs(helmConfigs); err != nil { return nil, fmt.Errorf("failed to create helm configs: %w", err) } @@ -586,11 +587,11 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model helmValues[i] = item.value } - if err := repo.BatchCreateOrFindParameterConfigs(helmValues); err != nil { + if err := repo.batchCreateOrFindParameterConfigs(helmValues); err != nil { return nil, fmt.Errorf("failed to create helm parameter configs: %w", err) } - actualHelmValues, err := repo.ListParameterConfigsByKeys(helmValues) + actualHelmValues, err := repo.listParameterConfigsByKeys(helmValues) if err != nil { return nil, fmt.Errorf("failed to list helm parameter configs: %w", err) } @@ -615,7 +616,7 @@ func (s *Service) createContainerVersionsCore(repo *Repository, versions []model }) } - if err := repo.AddHelmConfigValues(relations); err != nil { + if err := repo.addHelmConfigValues(relations); err != nil { return nil, fmt.Errorf("failed to create helm config value relations: %w", err) } @@ -628,14 +629,14 @@ func (s *Service) uploadHelmValueFileCore(containerName string, helmConfig *mode return err } helmConfig.ValueFile = targetPath - if err := s.repo.UpdateHelmConfig(helmConfig); err != nil { + if err := s.repo.updateHelmConfig(helmConfig); err != nil { return fmt.Errorf("failed to update helm config: %w", err) } return nil } func (s *Service) validateHelmConfigVersion(containerID, versionID int) (*model.ContainerVersion, error) { - containerVersion, err := s.repo.GetContainerVersionByID(versionID) + containerVersion, err := s.repo.getContainerVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: container version %d not found", consts.ErrNotFound, versionID) diff --git a/src/module/dataset/api_types.go b/src/module/dataset/api_types.go index a3d6f2fd..928dc1f4 100644 --- a/src/module/dataset/api_types.go +++ b/src/module/dataset/api_types.go @@ -1,6 +1,7 @@ package datasetmodule import ( + "encoding/json" "fmt" "strings" "time" @@ -8,8 +9,9 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - injectionmodule "aegis/module/injection" "aegis/utils" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" ) // CreateDatasetReq represents dataset creation request. @@ -329,7 +331,7 @@ func NewDatasetVersionResp(version *model.DatasetVersion) *DatasetVersionResp { type DatasetVersionDetailResp struct { DatasetVersionResp - Datapacks []injectionmodule.InjectionResp `json:"datapacks,omitempty"` + Datapacks []DatasetDatapackResp `json:"datapacks,omitempty"` } func NewDatasetVersionDetailResp(version *model.DatasetVersion) *DatasetVersionDetailResp { @@ -338,11 +340,78 @@ func NewDatasetVersionDetailResp(version *model.DatasetVersion) *DatasetVersionD } if len(version.Datapacks) > 0 { - resp.Datapacks = make([]injectionmodule.InjectionResp, 0, len(version.Datapacks)) + resp.Datapacks = make([]DatasetDatapackResp, 0, len(version.Datapacks)) for _, datapack := range version.Datapacks { - resp.Datapacks = append(resp.Datapacks, *injectionmodule.NewInjectionResp(&datapack)) + resp.Datapacks = append(resp.Datapacks, *NewDatasetDatapackResp(&datapack)) } } return resp } + +type DatasetDatapackResp struct { + ID int `json:"id"` + Name string `json:"name"` + Source string `json:"source"` + FaultType string `json:"fault_type"` + Category string `json:"category"` + DisplayConfig map[string]any `json:"display_config,omitempty" swaggertype:"object"` + PreDuration int `json:"pre_duration"` + StartTime *time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + State consts.DatapackState `json:"state" swaggertype:"string"` + Status string `json:"status"` + GroundtruthSource string `json:"groundtruth_source"` + BenchmarkID *int `json:"benchmark_id"` + BenchmarkName string `json:"benchmark_name"` + PedestalID *int `json:"pedestal_id"` + PedestalName string `json:"pedestal_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +func NewDatasetDatapackResp(injection *model.FaultInjection) *DatasetDatapackResp { + resp := &DatasetDatapackResp{ + ID: injection.ID, + Name: injection.Name, + Source: string(injection.Source), + Category: injection.Category.String(), + PreDuration: injection.PreDuration, + StartTime: injection.StartTime, + EndTime: injection.EndTime, + State: injection.State, + Status: consts.GetStatusTypeName(injection.Status), + GroundtruthSource: injection.GroundtruthSource, + BenchmarkID: injection.BenchmarkID, + PedestalID: injection.PedestalID, + CreatedAt: injection.CreatedAt, + UpdatedAt: injection.UpdatedAt, + } + + if injection.FaultType == consts.Hybrid { + resp.FaultType = "hybrid" + } else { + resp.FaultType = chaos.ChaosTypeMap[injection.FaultType] + } + + if injection.DisplayConfig != nil { + var displayConfigData map[string]any + _ = json.Unmarshal([]byte(*injection.DisplayConfig), &displayConfigData) + resp.DisplayConfig = displayConfigData + } + + if injection.Benchmark != nil && injection.Benchmark.Container != nil { + resp.BenchmarkName = injection.Benchmark.Container.Name + } + if injection.Pedestal != nil && injection.Pedestal.Container != nil { + resp.PedestalName = injection.Pedestal.Container.Name + } + if len(injection.Labels) > 0 { + resp.Labels = make([]dto.LabelItem, 0, len(injection.Labels)) + for _, l := range injection.Labels { + resp.Labels = append(resp.Labels, dto.LabelItem{Key: l.Key, Value: l.Value, IsSystem: l.IsSystem}) + } + } + return resp +} diff --git a/src/module/dataset/core.go b/src/module/dataset/core.go index 1bdeb646..3aea12d9 100644 --- a/src/module/dataset/core.go +++ b/src/module/dataset/core.go @@ -2,11 +2,9 @@ package datasetmodule import ( "aegis/model" - - "gorm.io/gorm" ) -func CreateDatasetCore(tx *gorm.DB, dataset *model.Dataset, versions []model.DatasetVersion, userID int) (*model.Dataset, error) { - service := NewService(NewRepository(tx), NewDatapackFileStore()) - return service.createDatasetCore(service.repo, dataset, versions, userID) +func (r *Repository) CreateDatasetCore(dataset *model.Dataset, versions []model.DatasetVersion, userID int) (*model.Dataset, error) { + service := NewService(r, NewDatapackFileStore()) + return service.createDatasetCore(r, dataset, versions, userID) } diff --git a/src/module/dataset/handler.go b/src/module/dataset/handler.go index 1b118130..86821551 100644 --- a/src/module/dataset/handler.go +++ b/src/module/dataset/handler.go @@ -16,10 +16,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/dataset/handler_service.go b/src/module/dataset/handler_service.go new file mode 100644 index 00000000..5b6842e6 --- /dev/null +++ b/src/module/dataset/handler_service.go @@ -0,0 +1,32 @@ +package datasetmodule + +import ( + "archive/zip" + "context" + + "aegis/dto" + "aegis/utils" +) + +// HandlerService captures the dataset operations consumed by the HTTP handler. +type HandlerService interface { + CreateDataset(context.Context, *CreateDatasetReq, int) (*DatasetResp, error) + DeleteDataset(context.Context, int) error + GetDataset(context.Context, int) (*DatasetDetailResp, error) + ListDatasets(context.Context, *ListDatasetReq) (*dto.ListResp[DatasetResp], error) + SearchDatasets(context.Context, *SearchDatasetReq) (*dto.ListResp[DatasetDetailResp], error) + UpdateDataset(context.Context, *UpdateDatasetReq, int) (*DatasetResp, error) + ManageDatasetLabels(context.Context, *ManageDatasetLabelReq, int) (*DatasetResp, error) + CreateDatasetVersion(context.Context, *CreateDatasetVersionReq, int, int) (*DatasetVersionResp, error) + DeleteDatasetVersion(context.Context, int) error + GetDatasetVersion(context.Context, int, int) (*DatasetVersionDetailResp, error) + ListDatasetVersions(context.Context, *ListDatasetVersionReq, int) (*dto.ListResp[DatasetVersionResp], error) + UpdateDatasetVersion(context.Context, *UpdateDatasetVersionReq, int, int) (*DatasetVersionResp, error) + GetDatasetVersionFilename(context.Context, int, int) (string, error) + DownloadDatasetVersion(context.Context, *zip.Writer, []utils.ExculdeRule, int) error + ManageDatasetVersionInjections(context.Context, *ManageDatasetVersionInjectionReq, int) (*DatasetVersionDetailResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/dataset/module.go b/src/module/dataset/module.go index 41e13127..d4856ee8 100644 --- a/src/module/dataset/module.go +++ b/src/module/dataset/module.go @@ -6,5 +6,6 @@ var Module = fx.Module("dataset", fx.Provide(NewRepository), fx.Provide(NewDatapackFileStore), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/dataset/repository.go b/src/module/dataset/repository.go index 37e71677..656e2a29 100644 --- a/src/module/dataset/repository.go +++ b/src/module/dataset/repository.go @@ -4,7 +4,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/repository" + "aegis/searchx" "fmt" "gorm.io/gorm" @@ -24,15 +24,7 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) GetRoleByName(name string) (*model.Role, error) { +func (r *Repository) getRoleByName(name string) (*model.Role, error) { var role model.Role if err := r.db.Where("name = ? and status != ?", name, consts.CommonDeleted).First(&role).Error; err != nil { return nil, fmt.Errorf("failed to find role with name %s: %w", name, err) @@ -40,21 +32,21 @@ func (r *Repository) GetRoleByName(name string) (*model.Role, error) { return &role, nil } -func (r *Repository) CreateDataset(dataset *model.Dataset) error { +func (r *Repository) createDataset(dataset *model.Dataset) error { if err := r.db.Omit(datasetCommonOmitFields).Create(dataset).Error; err != nil { return fmt.Errorf("failed to create dataset: %v", err) } return nil } -func (r *Repository) CreateUserDataset(userDataset *model.UserDataset) error { +func (r *Repository) createUserDataset(userDataset *model.UserDataset) error { if err := r.db.Omit("active_user_dataset").Create(userDataset).Error; err != nil { return fmt.Errorf("failed to create user-dataset association: %w", err) } return nil } -func (r *Repository) BatchDeleteDatasetVersions(datasetID int) (int64, error) { +func (r *Repository) batchDeleteDatasetVersions(datasetID int) (int64, error) { result := r.db.Model(&model.DatasetVersion{}). Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -64,7 +56,7 @@ func (r *Repository) BatchDeleteDatasetVersions(datasetID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) RemoveUsersFromDataset(datasetID int) (int64, error) { +func (r *Repository) removeUsersFromDataset(datasetID int) (int64, error) { result := r.db.Model(&model.UserDataset{}). Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -74,7 +66,7 @@ func (r *Repository) RemoveUsersFromDataset(datasetID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) DeleteDataset(datasetID int) (int64, error) { +func (r *Repository) deleteDataset(datasetID int) (int64, error) { result := r.db.Model(&model.Dataset{}). Where("id = ? AND status != ?", datasetID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -84,7 +76,7 @@ func (r *Repository) DeleteDataset(datasetID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) GetDatasetByID(datasetID int) (*model.Dataset, error) { +func (r *Repository) getDatasetByID(datasetID int) (*model.Dataset, error) { var dataset model.Dataset if err := r.db.Where("id = ? AND status != ?", datasetID, consts.CommonDeleted).First(&dataset).Error; err != nil { return nil, fmt.Errorf("failed to get dataset: %v", err) @@ -92,7 +84,7 @@ func (r *Repository) GetDatasetByID(datasetID int) (*model.Dataset, error) { return &dataset, nil } -func (r *Repository) ListDatasetVersionsByDatasetID(datasetID int) ([]model.DatasetVersion, error) { +func (r *Repository) listDatasetVersionsByDatasetID(datasetID int) ([]model.DatasetVersion, error) { var versions []model.DatasetVersion if err := r.db.Where("dataset_id = ?", datasetID).Find(&versions).Error; err != nil { return nil, fmt.Errorf("failed to list dataset versions for dataset %d: %w", datasetID, err) @@ -100,7 +92,36 @@ func (r *Repository) ListDatasetVersionsByDatasetID(datasetID int) ([]model.Data return versions, nil } -func (r *Repository) ListDatasets(limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]model.Dataset, int64, error) { +func (r *Repository) batchGetDatasetVersions(datasetNames []string, userID int) ([]model.DatasetVersion, error) { + if len(datasetNames) == 0 { + return []model.DatasetVersion{}, nil + } + + var versions []model.DatasetVersion + query := r.db.Table("dataset_versions dv"). + Preload("Dataset"). + Where("dv.status = ?", consts.CommonEnabled). + Order("dv.dataset_id DESC, dv.name_major DESC, dv.name_minor DESC, dv.name_patch DESC") + + query = query.Joins("INNER JOIN datasets d ON d.id = dv.dataset_id"). + Where("d.name IN (?) AND d.status = ?", datasetNames, consts.CommonEnabled) + + if userID > 0 { + query = query.Joins( + "LEFT JOIN user_datasets ud ON ud.dataset_id = d.id AND ud.user_id = ? AND ud.status = ?", + userID, consts.CommonEnabled, + ).Where( + r.db.Where("d.is_public = ?", true).Or("ud.dataset_id IS NOT NULL"), + ) + } + + if err := query.Find(&versions).Error; err != nil { + return nil, fmt.Errorf("failed to query dataset versions: %w", err) + } + return versions, nil +} + +func (r *Repository) listDatasets(limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]model.Dataset, int64, error) { var ( datasets []model.Dataset total int64 @@ -126,8 +147,8 @@ func (r *Repository) ListDatasets(limit, offset int, datasetType string, isPubli return datasets, total, nil } -func (r *Repository) SearchDatasets(searchReq *dto.SearchReq[consts.DatasetField]) ([]model.Dataset, int64, error) { - qb := repository.NewSearchQueryBuilder(r.db, consts.DatasetAllowedFields) +func (r *Repository) searchDatasets(searchReq *dto.SearchReq[consts.DatasetField]) ([]model.Dataset, int64, error) { + qb := searchx.NewQueryBuilder(r.db, consts.DatasetAllowedFields) qb.ApplySearchReq(searchReq.Filters, searchReq.Keyword, searchReq.Sort, searchReq.GroupBy, model.Dataset{}) qb.ApplyIncludes(searchReq.Includes) qb.ApplyIncludeFields(searchReq.IncludeFields) @@ -150,7 +171,7 @@ func (r *Repository) SearchDatasets(searchReq *dto.SearchReq[consts.DatasetField return items, total, nil } -func (r *Repository) ListDatasetLabels(datasetIDs []int) (map[int][]model.Label, error) { +func (r *Repository) listDatasetLabels(datasetIDs []int) (map[int][]model.Label, error) { if len(datasetIDs) == 0 { return nil, nil } @@ -179,14 +200,14 @@ func (r *Repository) ListDatasetLabels(datasetIDs []int) (map[int][]model.Label, return labelsMap, nil } -func (r *Repository) UpdateDataset(dataset *model.Dataset) error { +func (r *Repository) updateDataset(dataset *model.Dataset) error { if err := r.db.Omit(datasetCommonOmitFields).Save(dataset).Error; err != nil { return fmt.Errorf("failed to update dataset: %v", err) } return nil } -func (r *Repository) AddDatasetLabels(datasetLabels []model.DatasetLabel) error { +func (r *Repository) addDatasetLabels(datasetLabels []model.DatasetLabel) error { if len(datasetLabels) == 0 { return nil } @@ -199,7 +220,7 @@ func (r *Repository) AddDatasetLabels(datasetLabels []model.DatasetLabel) error return nil } -func (r *Repository) ListLabelIDsByKeyAndDatasetID(datasetID int, keys []string) ([]int, error) { +func (r *Repository) listLabelIDsByKeyAndDatasetID(datasetID int, keys []string) ([]int, error) { var labelIDs []int if err := r.db.Table("labels l"). Select("l.id"). @@ -211,7 +232,7 @@ func (r *Repository) ListLabelIDsByKeyAndDatasetID(datasetID int, keys []string) return labelIDs, nil } -func (r *Repository) ClearDatasetLabels(datasetIDs []int, labelIDs []int) error { +func (r *Repository) clearDatasetLabels(datasetIDs []int, labelIDs []int) error { if len(datasetIDs) == 0 { return nil } @@ -226,7 +247,7 @@ func (r *Repository) ClearDatasetLabels(datasetIDs []int, labelIDs []int) error return nil } -func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { +func (r *Repository) batchDecreaseLabelUsages(labelIDs []int, decrement int) error { if len(labelIDs) == 0 { return nil } @@ -241,7 +262,7 @@ func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) err return nil } -func (r *Repository) ListLabelsByDatasetID(datasetID int) ([]model.Label, error) { +func (r *Repository) listLabelsByDatasetID(datasetID int) ([]model.Label, error) { var labels []model.Label if err := r.db.Model(&model.Label{}). Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). @@ -252,7 +273,7 @@ func (r *Repository) ListLabelsByDatasetID(datasetID int) ([]model.Label, error) return labels, nil } -func (r *Repository) BatchCreateDatasetVersions(versions []model.DatasetVersion) error { +func (r *Repository) batchCreateDatasetVersions(versions []model.DatasetVersion) error { if len(versions) == 0 { return fmt.Errorf("no dataset versions to create") } @@ -262,7 +283,7 @@ func (r *Repository) BatchCreateDatasetVersions(versions []model.DatasetVersion) return nil } -func (r *Repository) DeleteDatasetVersion(versionID int) (int64, error) { +func (r *Repository) deleteDatasetVersion(versionID int) (int64, error) { result := r.db.Model(&model.DatasetVersion{}). Where("id = ? AND status != ?", versionID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -272,7 +293,7 @@ func (r *Repository) DeleteDatasetVersion(versionID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) GetDatasetVersionByID(versionID int) (*model.DatasetVersion, error) { +func (r *Repository) getDatasetVersionByID(versionID int) (*model.DatasetVersion, error) { var version model.DatasetVersion if err := r.db.Preload("Datapacks").Where("id = ?", versionID).First(&version).Error; err != nil { return nil, fmt.Errorf("failed to get dataset version: %v", err) @@ -280,7 +301,7 @@ func (r *Repository) GetDatasetVersionByID(versionID int) (*model.DatasetVersion return &version, nil } -func (r *Repository) ListDatasetVersions(limit, offset int, datasetID int, status *consts.StatusType) ([]model.DatasetVersion, int64, error) { +func (r *Repository) listDatasetVersions(limit, offset int, datasetID int, status *consts.StatusType) ([]model.DatasetVersion, int64, error) { var ( versions []model.DatasetVersion total int64 @@ -300,14 +321,14 @@ func (r *Repository) ListDatasetVersions(limit, offset int, datasetID int, statu return versions, total, nil } -func (r *Repository) UpdateDatasetVersion(version *model.DatasetVersion) error { +func (r *Repository) updateDatasetVersion(version *model.DatasetVersion) error { if err := r.db.Omit(datasetVersionModelOmitFields).Save(version).Error; err != nil { return fmt.Errorf("failed to update dataset version: %w", err) } return nil } -func (r *Repository) ListInjectionIDsByNames(names []string) (map[string]int, error) { +func (r *Repository) listInjectionIDsByNames(names []string) (map[string]int, error) { if len(names) == 0 { return map[string]int{}, nil } @@ -331,7 +352,7 @@ func (r *Repository) ListInjectionIDsByNames(names []string) (map[string]int, er return result, nil } -func (r *Repository) AddDatasetVersionInjections(items []model.DatasetVersionInjection) error { +func (r *Repository) addDatasetVersionInjections(items []model.DatasetVersionInjection) error { if len(items) == 0 { return nil } @@ -344,7 +365,7 @@ func (r *Repository) AddDatasetVersionInjections(items []model.DatasetVersionInj return nil } -func (r *Repository) ClearDatasetVersionInjections(datasetVersionIDs []int, injectionIDs []int) error { +func (r *Repository) clearDatasetVersionInjections(datasetVersionIDs []int, injectionIDs []int) error { if len(datasetVersionIDs) == 0 { return nil } diff --git a/src/service/common/dataset.go b/src/module/dataset/resolve.go similarity index 71% rename from src/service/common/dataset.go rename to src/module/dataset/resolve.go index 73925757..357f9fef 100644 --- a/src/service/common/dataset.go +++ b/src/module/dataset/resolve.go @@ -1,30 +1,23 @@ -package common +package datasetmodule import ( "aegis/dto" "aegis/model" - "aegis/repository" "fmt" - - "gorm.io/gorm" ) -// mapRefsToDatasetVersions maps dataset refs to their corresponding dataset versions -func MapRefsToDatasetVersionsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID int) (map[*dto.DatasetRef]model.DatasetVersion, error) { - versions, err := getUniqueVersionsForDatasetRefsWithDB(db, refs, userID) +func (r *Repository) ResolveDatasetVersions(refs []*dto.DatasetRef, userID int) (map[*dto.DatasetRef]model.DatasetVersion, error) { + versions, err := getUniqueVersionsForDatasetRefs(r, refs, userID) if err != nil { return nil, fmt.Errorf("failed to batch get dataset versions: %w", err) } flatMap := make(map[string][]model.DatasetVersion) hierarchicalMap := make(map[string]map[string]model.DatasetVersion) - for _, version := range versions { datasetName := version.Dataset.Name versionName := version.Name - flatMap[datasetName] = append(flatMap[datasetName], version) - if _, exists := hierarchicalMap[datasetName]; !exists { hierarchicalMap[datasetName] = make(map[string]model.DatasetVersion) } @@ -38,11 +31,9 @@ func MapRefsToDatasetVersionsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID if _, exists := hierarchicalMap[ref.Name]; !exists { return nil, fmt.Errorf("dataset not found: %s", ref.Name) } - if _, exists := hierarchicalMap[ref.Name][ref.Version]; !exists { return nil, fmt.Errorf("dataset version not found: %s:%s", ref.Name, ref.Version) } - result = hierarchicalMap[ref.Name][ref.Version] } else { if _, exists := flatMap[ref.Name]; !exists { @@ -50,21 +41,18 @@ func MapRefsToDatasetVersionsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID } result = flatMap[ref.Name][0] } - results[ref] = result } - return results, nil } -func getUniqueVersionsForDatasetRefsWithDB(db *gorm.DB, refs []*dto.DatasetRef, userID int) ([]model.DatasetVersion, error) { +func getUniqueVersionsForDatasetRefs(repo *Repository, refs []*dto.DatasetRef, userID int) ([]model.DatasetVersion, error) { datasetNamesSet := make(map[string]struct{}, len(refs)) for _, ref := range refs { if ref.Name != "" { datasetNamesSet[ref.Name] = struct{}{} } } - if len(datasetNamesSet) == 0 { return []model.DatasetVersion{}, nil } @@ -73,11 +61,5 @@ func getUniqueVersionsForDatasetRefsWithDB(db *gorm.DB, refs []*dto.DatasetRef, for name := range datasetNamesSet { requiredNames = append(requiredNames, name) } - - versions, err := repository.BatchGetDatasetVersions(db, requiredNames, userID) - if err != nil { - return nil, fmt.Errorf("failed to batch get dataset versions: %w", err) - } - - return versions, nil + return repo.batchGetDatasetVersions(requiredNames, userID) } diff --git a/src/module/dataset/service.go b/src/module/dataset/service.go index 7530ed82..a5046832 100644 --- a/src/module/dataset/service.go +++ b/src/module/dataset/service.go @@ -9,7 +9,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/service/common" + labelmodule "aegis/module/label" "aegis/utils" "gorm.io/gorm" @@ -35,8 +35,8 @@ func (s *Service) CreateDataset(_ context.Context, req *CreateDatasetReq, userID versions = append(versions, *req.VersionReq.ConvertToDatasetVersion()) } - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) createdDataset, err := s.createDatasetCore(repo, dataset, versions, userID) if err != nil { return fmt.Errorf("failed to create dataset: %w", err) @@ -51,15 +51,15 @@ func (s *Service) CreateDataset(_ context.Context, req *CreateDatasetReq, userID } func (s *Service) DeleteDataset(_ context.Context, datasetID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if _, err := repo.BatchDeleteDatasetVersions(datasetID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if _, err := repo.batchDeleteDatasetVersions(datasetID); err != nil { return fmt.Errorf("failed to delete dataset versions: %w", err) } - if _, err := repo.RemoveUsersFromDataset(datasetID); err != nil { + if _, err := repo.removeUsersFromDataset(datasetID); err != nil { return fmt.Errorf("failed to remove all users from dataset: %w", err) } - rows, err := repo.DeleteDataset(datasetID) + rows, err := repo.deleteDataset(datasetID) if err != nil { return fmt.Errorf("failed to delete dataset: %w", err) } @@ -71,7 +71,7 @@ func (s *Service) DeleteDataset(_ context.Context, datasetID int) error { } func (s *Service) GetDataset(_ context.Context, datasetID int) (*DatasetDetailResp, error) { - dataset, err := s.repo.GetDatasetByID(datasetID) + dataset, err := s.repo.getDatasetByID(datasetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) @@ -79,7 +79,7 @@ func (s *Service) GetDataset(_ context.Context, datasetID int) (*DatasetDetailRe return nil, fmt.Errorf("failed to get dataset: %w", err) } - versions, err := s.repo.ListDatasetVersionsByDatasetID(dataset.ID) + versions, err := s.repo.listDatasetVersionsByDatasetID(dataset.ID) if err != nil { return nil, fmt.Errorf("failed to get dataset versions: %w", err) } @@ -95,7 +95,7 @@ func (s *Service) GetDataset(_ context.Context, datasetID int) (*DatasetDetailRe func (s *Service) ListDatasets(_ context.Context, req *ListDatasetReq) (*dto.ListResp[DatasetResp], error) { limit, offset := req.ToGormParams() - datasets, total, err := s.repo.ListDatasets(limit, offset, req.Type, req.IsPublic, req.Status) + datasets, total, err := s.repo.listDatasets(limit, offset, req.Type, req.IsPublic, req.Status) if err != nil { return nil, fmt.Errorf("failed to list datasets: %w", err) } @@ -105,7 +105,7 @@ func (s *Service) ListDatasets(_ context.Context, req *ListDatasetReq) (*dto.Lis datasetIDs = append(datasetIDs, dataset.ID) } - labelsMap, err := s.repo.ListDatasetLabels(datasetIDs) + labelsMap, err := s.repo.listDatasetLabels(datasetIDs) if err != nil { return nil, fmt.Errorf("failed to list dataset labels: %w", err) } @@ -129,7 +129,7 @@ func (s *Service) SearchDatasets(_ context.Context, req *SearchDatasetReq) (*dto return nil, fmt.Errorf("search dataset request is nil") } - results, total, err := s.repo.SearchDatasets(req.ConvertToSearchReq()) + results, total, err := s.repo.searchDatasets(req.ConvertToSearchReq()) if err != nil { return nil, fmt.Errorf("failed to search datasets: %w", err) } @@ -148,9 +148,9 @@ func (s *Service) SearchDatasets(_ context.Context, req *SearchDatasetReq) (*dto func (s *Service) UpdateDataset(_ context.Context, req *UpdateDatasetReq, datasetID int) (*DatasetResp, error) { var updatedDataset *model.Dataset - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - dataset, err := repo.GetDatasetByID(datasetID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + dataset, err := repo.getDatasetByID(datasetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) @@ -159,7 +159,7 @@ func (s *Service) UpdateDataset(_ context.Context, req *UpdateDatasetReq, datase } req.PatchDatasetModel(dataset) - if err := repo.UpdateDataset(dataset); err != nil { + if err := repo.updateDataset(dataset); err != nil { return fmt.Errorf("failed to update dataset: %w", err) } @@ -178,9 +178,9 @@ func (s *Service) ManageDatasetLabels(_ context.Context, req *ManageDatasetLabel } var managedDataset *model.Dataset - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - dataset, err := repo.GetDatasetByID(datasetID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + dataset, err := repo.getDatasetByID(datasetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) @@ -189,7 +189,7 @@ func (s *Service) ManageDatasetLabels(_ context.Context, req *ManageDatasetLabel } if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -202,29 +202,29 @@ func (s *Service) ManageDatasetLabels(_ context.Context, req *ManageDatasetLabel }) } - if err := repo.AddDatasetLabels(datasetLabels); err != nil { + if err := repo.addDatasetLabels(datasetLabels); err != nil { return fmt.Errorf("failed to add dataset labels: %w", err) } } if len(req.RemoveLabels) > 0 { - labelIDs, err := repo.ListLabelIDsByKeyAndDatasetID(datasetID, req.RemoveLabels) + labelIDs, err := repo.listLabelIDsByKeyAndDatasetID(datasetID, req.RemoveLabels) if err != nil { return fmt.Errorf("failed to find label ids by keys: %w", err) } if len(labelIDs) > 0 { - if err := repo.ClearDatasetLabels([]int{datasetID}, labelIDs); err != nil { + if err := repo.clearDatasetLabels([]int{datasetID}, labelIDs); err != nil { return fmt.Errorf("failed to clear dataset labels: %w", err) } - if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + if err := repo.batchDecreaseLabelUsages(labelIDs, 1); err != nil { return fmt.Errorf("failed to decrease label usage counts: %w", err) } } } - labels, err := repo.ListLabelsByDatasetID(dataset.ID) + labels, err := repo.listLabelsByDatasetID(dataset.ID) if err != nil { return fmt.Errorf("failed to get dataset labels: %w", err) } @@ -249,8 +249,8 @@ func (s *Service) CreateDatasetVersion(_ context.Context, req *CreateDatasetVers version.UserID = userID var createdVersion *model.DatasetVersion - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) versions, err := s.createDatasetVersionsCore(repo, []model.DatasetVersion{*version}) if err != nil { return fmt.Errorf("failed to create dataset version: %w", err) @@ -273,7 +273,7 @@ func (s *Service) CreateDatasetVersion(_ context.Context, req *CreateDatasetVers } func (s *Service) DeleteDatasetVersion(_ context.Context, versionID int) error { - rows, err := s.repo.DeleteDatasetVersion(versionID) + rows, err := s.repo.deleteDatasetVersion(versionID) if err != nil { return fmt.Errorf("failed to delete dataset version: %w", err) } @@ -284,14 +284,14 @@ func (s *Service) DeleteDatasetVersion(_ context.Context, versionID int) error { } func (s *Service) GetDatasetVersion(_ context.Context, datasetID, versionID int) (*DatasetVersionDetailResp, error) { - if _, err := s.repo.GetDatasetByID(datasetID); err != nil { + if _, err := s.repo.getDatasetByID(datasetID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) } return nil, fmt.Errorf("failed to get dataset: %w", err) } - version, err := s.repo.GetDatasetVersionByID(versionID) + version, err := s.repo.getDatasetVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) @@ -305,7 +305,7 @@ func (s *Service) GetDatasetVersion(_ context.Context, datasetID, versionID int) func (s *Service) ListDatasetVersions(_ context.Context, req *ListDatasetVersionReq, datasetID int) (*dto.ListResp[DatasetVersionResp], error) { limit, offset := req.ToGormParams() - versions, total, err := s.repo.ListDatasetVersions(limit, offset, datasetID, req.Status) + versions, total, err := s.repo.listDatasetVersions(limit, offset, datasetID, req.Status) if err != nil { return nil, fmt.Errorf("failed to list dataset versions: %w", err) } @@ -325,9 +325,9 @@ func (s *Service) UpdateDatasetVersion(_ context.Context, req *UpdateDatasetVers _ = datasetID var updatedVersion *model.DatasetVersion - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - version, err := repo.GetDatasetVersionByID(versionID) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + version, err := repo.getDatasetVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) @@ -336,7 +336,7 @@ func (s *Service) UpdateDatasetVersion(_ context.Context, req *UpdateDatasetVers } req.PatchDatasetVersionModel(version) - if err := repo.UpdateDatasetVersion(version); err != nil { + if err := repo.updateDatasetVersion(version); err != nil { return fmt.Errorf("failed to update dataset version: %w", err) } @@ -350,7 +350,7 @@ func (s *Service) UpdateDatasetVersion(_ context.Context, req *UpdateDatasetVers } func (s *Service) GetDatasetVersionFilename(_ context.Context, datasetID, versionID int) (string, error) { - dataset, err := s.repo.GetDatasetByID(datasetID) + dataset, err := s.repo.getDatasetByID(datasetID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return "", fmt.Errorf("%w: dataset id: %d", consts.ErrNotFound, datasetID) @@ -358,7 +358,7 @@ func (s *Service) GetDatasetVersionFilename(_ context.Context, datasetID, versio return "", fmt.Errorf("failed to get dataset: %w", err) } - version, err := s.repo.GetDatasetVersionByID(versionID) + version, err := s.repo.getDatasetVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return "", fmt.Errorf("%w: version id: %d", consts.ErrNotFound, versionID) @@ -392,9 +392,9 @@ func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageD } var managedVersion *model.DatasetVersion - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - version, err := repo.GetDatasetVersionByID(versionID) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + version, err := repo.getDatasetVersionByID(versionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: dataset version id: %d", consts.ErrNotFound, versionID) @@ -409,7 +409,7 @@ func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageD } if len(req.RemoveDatapacks) > 0 { - injectionIDMap, err := repo.ListInjectionIDsByNames(req.RemoveDatapacks) + injectionIDMap, err := repo.listInjectionIDsByNames(req.RemoveDatapacks) if err != nil { return fmt.Errorf("failed to list injections by names: %w", err) } @@ -426,7 +426,7 @@ func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageD injectionIDs = append(injectionIDs, injectionID) } - if err := repo.ClearDatasetVersionInjections([]int{version.ID}, injectionIDs); err != nil { + if err := repo.clearDatasetVersionInjections([]int{version.ID}, injectionIDs); err != nil { return fmt.Errorf("failed to remove dataset version datapacks: %w", err) } } @@ -438,7 +438,7 @@ func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageD version.Datapacks = datapacks version.FileCount = version.FileCount + len(req.AddDatapacks) - len(req.RemoveDatapacks) - if err := repo.UpdateDatasetVersion(version); err != nil { + if err := repo.updateDatasetVersion(version); err != nil { return fmt.Errorf("failed to update dataset version file count: %w", err) } @@ -453,7 +453,7 @@ func (s *Service) ManageDatasetVersionInjections(_ context.Context, req *ManageD } func (s *Service) createDatasetCore(repo *Repository, dataset *model.Dataset, versions []model.DatasetVersion, userID int) (*model.Dataset, error) { - role, err := repo.GetRoleByName(consts.RoleDatasetAdmin.String()) + role, err := repo.getRoleByName(consts.RoleDatasetAdmin.String()) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: role %v not found", consts.ErrNotFound, consts.RoleDatasetAdmin) @@ -461,14 +461,14 @@ func (s *Service) createDatasetCore(repo *Repository, dataset *model.Dataset, ve return nil, fmt.Errorf("failed to get dataset owner role: %w", err) } - if err := repo.CreateDataset(dataset); err != nil { + if err := repo.createDataset(dataset); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, consts.ErrAlreadyExists } return nil, err } - if err := repo.CreateUserDataset(&model.UserDataset{ + if err := repo.createUserDataset(&model.UserDataset{ UserID: userID, DatasetID: dataset.ID, RoleID: role.ID, @@ -496,7 +496,7 @@ func (s *Service) createDatasetVersionsCore(repo *Repository, versions []model.D return nil, nil } - if err := repo.BatchCreateDatasetVersions(versions); err != nil { + if err := repo.batchCreateDatasetVersions(versions); err != nil { return nil, fmt.Errorf("failed to create dataset versions: %w", err) } @@ -504,7 +504,7 @@ func (s *Service) createDatasetVersionsCore(repo *Repository, versions []model.D } func (s *Service) linkDatapacksToDatasetVersion(repo *Repository, versionID int, datapacks []string) error { - injectionIDMap, err := repo.ListInjectionIDsByNames(datapacks) + injectionIDMap, err := repo.listInjectionIDsByNames(datapacks) if err != nil { return fmt.Errorf("failed to list injections by names: %w", err) } @@ -521,7 +521,7 @@ func (s *Service) linkDatapacksToDatasetVersion(repo *Repository, versionID int, }) } - if err := repo.AddDatasetVersionInjections(items); err != nil { + if err := repo.addDatasetVersionInjections(items); err != nil { return fmt.Errorf("failed to add dataset version injections: %w", err) } diff --git a/src/module/evaluation/execution_query.go b/src/module/evaluation/execution_query.go new file mode 100644 index 00000000..0e718648 --- /dev/null +++ b/src/module/evaluation/execution_query.go @@ -0,0 +1,76 @@ +package evaluationmodule + +import ( + "context" + "fmt" + + "aegis/internalclient/orchestratorclient" + executionmodule "aegis/module/execution" + + "go.uber.org/fx" +) + +type executionQuerySource interface { + ListEvaluationExecutionsByDatapack(context.Context, *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) + ListEvaluationExecutionsByDataset(context.Context, *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) +} + +type executionQueryAdapter struct { + orchestrator *orchestratorclient.Client + local *executionmodule.Service + requireRemote bool +} + +type executionQuerySourceParams struct { + fx.In + + Orchestrator *orchestratorclient.Client `optional:"true"` + Local *executionmodule.Service `optional:"true"` +} + +func newExecutionQuerySource(params executionQuerySourceParams) executionQuerySource { + return executionQueryAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: false, + } +} + +func newRemoteExecutionQuerySource(params executionQuerySourceParams) executionQuerySource { + return executionQueryAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: true, + } +} + +func (a executionQueryAdapter) ListEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.ListEvaluationExecutionsByDatapack(ctx, req) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service query source is not configured") + } + if a.local == nil { + return nil, fmt.Errorf("evaluation execution query source is not configured") + } + return a.local.ListEvaluationExecutionsByDatapack(ctx, req) +} + +func (a executionQueryAdapter) ListEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.ListEvaluationExecutionsByDataset(ctx, req) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service query source is not configured") + } + if a.local == nil { + return nil, fmt.Errorf("evaluation execution query source is not configured") + } + return a.local.ListEvaluationExecutionsByDataset(ctx, req) +} + +// RemoteQueryOption forces the dedicated resource-service path to use orchestrator RPC only. +func RemoteQueryOption() fx.Option { + return fx.Decorate(newRemoteExecutionQuerySource) +} diff --git a/src/module/evaluation/handler.go b/src/module/evaluation/handler.go index 32b299c3..8ed01bdf 100644 --- a/src/module/evaluation/handler.go +++ b/src/module/evaluation/handler.go @@ -12,10 +12,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/evaluation/handler_service.go b/src/module/evaluation/handler_service.go new file mode 100644 index 00000000..0800d053 --- /dev/null +++ b/src/module/evaluation/handler_service.go @@ -0,0 +1,20 @@ +package evaluationmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures evaluation operations consumed by the HTTP handler. +type HandlerService interface { + ListDatapackEvaluationResults(context.Context, *BatchEvaluateDatapackReq, int) (*BatchEvaluateDatapackResp, error) + ListDatasetEvaluationResults(context.Context, *BatchEvaluateDatasetReq, int) (*BatchEvaluateDatasetResp, error) + ListEvaluations(context.Context, *ListEvaluationReq) (*dto.ListResp[EvaluationResp], error) + GetEvaluation(context.Context, int) (*EvaluationResp, error) + DeleteEvaluation(context.Context, int) error +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/evaluation/module.go b/src/module/evaluation/module.go index 03ef4844..07d21d53 100644 --- a/src/module/evaluation/module.go +++ b/src/module/evaluation/module.go @@ -4,6 +4,8 @@ import "go.uber.org/fx" var Module = fx.Module("evaluation", fx.Provide(NewRepository), + fx.Provide(newExecutionQuerySource), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/evaluation/service.go b/src/module/evaluation/service.go index a1c8084d..77f903ef 100644 --- a/src/module/evaluation/service.go +++ b/src/module/evaluation/service.go @@ -8,24 +8,27 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" + containermodule "aegis/module/container" + datasetmodule "aegis/module/dataset" executionmodule "aegis/module/execution" - "aegis/repository" - "aegis/service/common" - chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type Service struct { - repo *Repository + repo *Repository + query executionQuerySource } -func NewService(repo *Repository) *Service { - return &Service{repo: repo} +func NewService(repo *Repository, query executionQuerySource) *Service { + return &Service{ + repo: repo, + query: query, + } } -func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEvaluateDatapackReq, userID int) (*BatchEvaluateDatapackResp, error) { +func (s *Service) ListDatapackEvaluationResults(ctx context.Context, req *BatchEvaluateDatapackReq, userID int) (*BatchEvaluateDatapackResp, error) { if req == nil { return nil, fmt.Errorf("batch evaluate datapack request is nil") } @@ -35,7 +38,7 @@ func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEva algorithms = append(algorithms, &req.Specs[i].Algorithm) } - algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(s.repo.db, algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := containermodule.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -53,8 +56,11 @@ func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEva continue } - labelConditions := dto.ConvertLabelItemsToConditions(spec.FilterLabels) - executions, err := repository.ListExecutionsByDatapackFilter(s.repo.db, algorithmVersion.ID, spec.Datapack, labelConditions) + executions, err := s.listEvaluationExecutionsByDatapack(ctx, &executionmodule.EvaluationExecutionsByDatapackReq{ + AlgorithmVersionID: algorithmVersion.ID, + DatapackName: spec.Datapack, + FilterLabels: spec.FilterLabels, + }) if err != nil { failedItems = append(failedItems, fmt.Sprintf("%s - failed to query executions: %v", specIdentifier, err)) continue @@ -66,7 +72,7 @@ func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEva refs := make([]executionmodule.ExecutionRef, 0, len(executions)) for _, execution := range executions { - refs = append(refs, executionmodule.NewExecutionGranularityRef(&execution)) + refs = append(refs, execution.ExecutionRef) } evaluateRef := EvaluateDatapackRef{ @@ -74,14 +80,8 @@ func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEva ExecutionRefs: refs, } - datapack := executions[0].Datapack - if datapack != nil { - groundtruths, err := getGroundtruths(datapack) - if err != nil { - logrus.Warnf("failed to get groundtruth for datapack %s: %v", spec.Datapack, err) - } else { - evaluateRef.Groundtruths = groundtruths - } + if len(executions[0].Groundtruths) > 0 { + evaluateRef.Groundtruths = executions[0].Groundtruths } successItems = append(successItems, EvaluateDatapackItem{ @@ -109,7 +109,7 @@ func (s *Service) ListDatapackEvaluationResults(_ context.Context, req *BatchEva }, nil } -func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEvaluateDatasetReq, userID int) (*BatchEvaluateDatasetResp, error) { +func (s *Service) ListDatasetEvaluationResults(ctx context.Context, req *BatchEvaluateDatasetReq, userID int) (*BatchEvaluateDatasetResp, error) { if req == nil { return nil, fmt.Errorf("batch evaluate datapack request is nil") } @@ -121,12 +121,12 @@ func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEval datasets = append(datasets, &req.Specs[i].Dataset) } - algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(s.repo.db, algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := containermodule.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } - datasetVersionResults, err := common.MapRefsToDatasetVersionsWithDB(s.repo.db, datasets, userID) + datasetVersionResults, err := datasetmodule.NewRepository(s.repo.db).ResolveDatasetVersions(datasets, userID) if err != nil { return nil, fmt.Errorf("failed to map dataset refs to versions: %w", err) } @@ -150,8 +150,11 @@ func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEval continue } - labelConditions := dto.ConvertLabelItemsToConditions(spec.FilterLabels) - executions, err := repository.ListExecutionsByDatasetFilter(s.repo.db, algorithmVersion.ID, datasetVersion.ID, labelConditions) + executions, err := s.listEvaluationExecutionsByDataset(ctx, &executionmodule.EvaluationExecutionsByDatasetReq{ + AlgorithmVersionID: algorithmVersion.ID, + DatasetVersionID: datasetVersion.ID, + FilterLabels: spec.FilterLabels, + }) if err != nil { failedItems = append(failedItems, fmt.Sprintf("%s - failed to query executions: %v", specIdentifier, err)) continue @@ -161,11 +164,11 @@ func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEval continue } - executionMap := make(map[string][]model.Execution) + executionMap := make(map[string][]executionmodule.EvaluationExecutionItem) for _, execution := range executions { - name := execution.Datapack.Name + name := execution.Datapack if _, exists := executionMap[name]; !exists { - executionMap[name] = make([]model.Execution, 0) + executionMap[name] = make([]executionmodule.EvaluationExecutionItem, 0) } executionMap[name] = append(executionMap[name], execution) } @@ -181,7 +184,7 @@ func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEval for datapackName, groupedExecutions := range executionMap { refs := make([]executionmodule.ExecutionRef, 0, len(groupedExecutions)) for _, execution := range groupedExecutions { - refs = append(refs, executionmodule.NewExecutionGranularityRef(&execution)) + refs = append(refs, execution.ExecutionRef) } evaluateRef := EvaluateDatapackRef{ @@ -189,14 +192,8 @@ func (s *Service) ListDatasetEvaluationResults(_ context.Context, req *BatchEval ExecutionRefs: refs, } - datapack := groupedExecutions[0].Datapack - if datapack != nil { - groundtruths, err := getGroundtruths(datapack) - if err != nil { - logrus.Warnf("failed to get groundtruth for datapack %s: %v", datapackName, err) - } else { - evaluateRef.Groundtruths = groundtruths - } + if len(groupedExecutions[0].Groundtruths) > 0 { + evaluateRef.Groundtruths = groupedExecutions[0].Groundtruths } evaluateRefs = append(evaluateRefs, evaluateRef) @@ -262,6 +259,20 @@ func (s *Service) DeleteEvaluation(_ context.Context, id int) error { return s.repo.DeleteEvaluation(id) } +func (s *Service) listEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { + if s.query == nil { + return nil, fmt.Errorf("evaluation execution query source is not configured") + } + return s.query.ListEvaluationExecutionsByDatapack(ctx, req) +} + +func (s *Service) listEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { + if s.query == nil { + return nil, fmt.Errorf("evaluation execution query source is not configured") + } + return s.query.ListEvaluationExecutionsByDataset(ctx, req) +} + func persistEvaluations[T any](db *gorm.DB, evalType string, items []T, toEval func(*T) *model.Evaluation) { if len(items) == 0 { return @@ -284,11 +295,3 @@ func persistEvaluations[T any](db *gorm.DB, evalType string, items []T, toEval f logrus.Warnf("failed to batch persist %d %s evaluations: %v", len(evals), evalType, err) } } - -func getGroundtruths(datapack *model.FaultInjection) ([]chaos.Groundtruth, error) { - chaosGroundtruths := make([]chaos.Groundtruth, 0, len(datapack.Groundtruths)) - for _, gt := range datapack.Groundtruths { - chaosGroundtruths = append(chaosGroundtruths, *gt.ConvertToChaosGroundtruth()) - } - return chaosGroundtruths, nil -} diff --git a/src/module/evaluation/service_test.go b/src/module/evaluation/service_test.go new file mode 100644 index 00000000..920163ef --- /dev/null +++ b/src/module/evaluation/service_test.go @@ -0,0 +1,21 @@ +package evaluationmodule + +import ( + "testing" + + executionmodule "aegis/module/execution" +) + +func TestListEvaluationExecutionsRequiresQuerySource(t *testing.T) { + service := &Service{} + + _, err := service.listEvaluationExecutionsByDatapack(t.Context(), &executionmodule.EvaluationExecutionsByDatapackReq{}) + if err == nil { + t.Fatalf("expected datapack query to fail without orchestrator or execution service") + } + + _, err = service.listEvaluationExecutionsByDataset(t.Context(), &executionmodule.EvaluationExecutionsByDatasetReq{}) + if err == nil { + t.Fatalf("expected dataset query to fail without orchestrator or execution service") + } +} diff --git a/src/module/execution/api_types.go b/src/module/execution/api_types.go index 6af3cd98..0c649554 100644 --- a/src/module/execution/api_types.go +++ b/src/module/execution/api_types.go @@ -9,6 +9,8 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" ) // ExecutionRef represents execution granularity results for evaluation. @@ -46,6 +48,27 @@ func NewExecutionGranularityRef(execution *model.Execution) ExecutionRef { return *ref } +// EvaluationExecutionsByDatapackReq resolves execution results for one algorithm/datapack pair. +type EvaluationExecutionsByDatapackReq struct { + AlgorithmVersionID int `json:"algorithm_version_id"` + DatapackName string `json:"datapack_name"` + FilterLabels []dto.LabelItem `json:"filter_labels,omitempty"` +} + +// EvaluationExecutionsByDatasetReq resolves execution results for one algorithm/dataset pair. +type EvaluationExecutionsByDatasetReq struct { + AlgorithmVersionID int `json:"algorithm_version_id"` + DatasetVersionID int `json:"dataset_version_id"` + FilterLabels []dto.LabelItem `json:"filter_labels,omitempty"` +} + +// EvaluationExecutionItem is the orchestrator-owned execution payload used by evaluation queries. +type EvaluationExecutionItem struct { + Datapack string `json:"datapack"` + Groundtruths []chaos.Groundtruth `json:"groundtruths,omitempty"` + ExecutionRef +} + // BatchDeleteExecutionReq represents the request to batch delete executions. type BatchDeleteExecutionReq struct { IDs []int `json:"ids" binding:"omitempty"` diff --git a/src/module/execution/handler.go b/src/module/execution/handler.go index 04e91046..797f4346 100644 --- a/src/module/execution/handler.go +++ b/src/module/execution/handler.go @@ -17,10 +17,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/execution/handler_service.go b/src/module/execution/handler_service.go new file mode 100644 index 00000000..f27c665a --- /dev/null +++ b/src/module/execution/handler_service.go @@ -0,0 +1,24 @@ +package executionmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the execution operations consumed by the HTTP handler. +type HandlerService interface { + ListProjectExecutions(context.Context, *ListExecutionReq, int) (*dto.ListResp[ExecutionResp], error) + SubmitAlgorithmExecution(context.Context, *SubmitExecutionReq, string, int) (*SubmitExecutionResp, error) + ListExecutions(context.Context, *ListExecutionReq) (*dto.ListResp[ExecutionResp], error) + GetExecution(context.Context, int) (*ExecutionDetailResp, error) + ListAvailableLabels(context.Context) ([]dto.LabelItem, error) + ManageLabels(context.Context, *ManageExecutionLabelReq, int) (*ExecutionResp, error) + BatchDelete(context.Context, *BatchDeleteExecutionReq) error + UploadDetectorResults(context.Context, *UploadDetectorResultReq, int) (*UploadExecutionResultResp, error) + UploadGranularityResults(context.Context, *UploadGranularityResultReq, int) (*UploadExecutionResultResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/execution/module.go b/src/module/execution/module.go index 9044df26..02411c83 100644 --- a/src/module/execution/module.go +++ b/src/module/execution/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("execution", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/execution/repository.go b/src/module/execution/repository.go index 37f9ba30..1da0684e 100644 --- a/src/module/execution/repository.go +++ b/src/module/execution/repository.go @@ -21,14 +21,6 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - func (r *Repository) getProjectByName(name string) (*model.Project, error) { var project model.Project if err := r.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { @@ -133,6 +125,87 @@ func (r *Repository) getExecutionView(executionID int) (*model.Execution, []mode return &execution, labels, nil } +func (r *Repository) listEvaluationExecutionsByDatapack(algorithmVersionID int, datapackName string, filterLabels []dto.LabelItem) ([]model.Execution, error) { + var executions []model.Execution + + query := r.db.Model(&model.Execution{}). + Preload("DetectorResults"). + Preload("GranularityResults"). + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Groundtruths"). + Joins("JOIN fault_injections fi ON executions.datapack_id = fi.id"). + Where( + "executions.algorithm_version_id = ? AND fi.name = ? AND executions.status != ?", + algorithmVersionID, datapackName, consts.CommonDeleted, + ) + + if len(filterLabels) > 0 { + query = query. + Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). + Joins("JOIN labels l ON l.id = eil.label_id") + + var whereConditions *gorm.DB + for _, label := range filterLabels { + if whereConditions == nil { + whereConditions = r.db.Where("l.label_key = ? AND l.label_value = ?", label.Key, label.Value) + } else { + whereConditions = whereConditions.Or("l.label_key = ? AND l.label_value = ?", label.Key, label.Value) + } + } + + if whereConditions != nil { + query = query.Where(whereConditions) + } + query = query.Group("executions.id").Having("COUNT(executions.id) = ?", len(filterLabels)) + } + + if err := query.Order("executions.updated_at DESC").Find(&executions).Error; err != nil { + return nil, fmt.Errorf("failed to list evaluation executions for algorithm %d and datapack %s: %w", algorithmVersionID, datapackName, err) + } + return executions, nil +} + +func (r *Repository) listEvaluationExecutionsByDataset(algorithmVersionID, datasetVersionID int, filterLabels []dto.LabelItem) ([]model.Execution, error) { + var executions []model.Execution + + query := r.db.Model(&model.Execution{}). + Preload("DetectorResults"). + Preload("GranularityResults"). + Preload("AlgorithmVersion.Container"). + Preload("Datapack.Groundtruths"). + Preload("DatasetVersion"). + Preload("DatasetVersion.Injections"). + Where( + "executions.algorithm_version_id = ? AND executions.dataset_version_id = ? AND executions.status != ?", + algorithmVersionID, datasetVersionID, consts.CommonDeleted, + ) + + if len(filterLabels) > 0 { + query = query. + Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). + Joins("JOIN labels l ON l.id = eil.label_id") + + var whereConditions *gorm.DB + for _, label := range filterLabels { + if whereConditions == nil { + whereConditions = r.db.Where("l.label_key = ? AND l.label_value = ?", label.Key, label.Value) + } else { + whereConditions = whereConditions.Or("l.label_key = ? AND l.label_value = ?", label.Key, label.Value) + } + } + + if whereConditions != nil { + query = query.Where(whereConditions) + } + query = query.Group("executions.id").Having("COUNT(executions.id) = ?", len(filterLabels)) + } + + if err := query.Order("executions.updated_at DESC").Find(&executions).Error; err != nil { + return nil, fmt.Errorf("failed to list evaluation executions for algorithm %d and dataset version %d: %w", algorithmVersionID, datasetVersionID, err) + } + return executions, nil +} + func (r *Repository) getExecutionResultView(executionID int) (*model.Execution, []model.Label, []model.DetectorResult, []model.GranularityResult, error) { execution, labels, err := r.getExecutionView(executionID) if err != nil { @@ -184,38 +257,7 @@ func (r *Repository) listExecutionLabelIDsByKeys(executionID int, keys []string) return labelIDs, nil } -func (r *Repository) loadExecutionLabelIDsByItems(conditions []map[string]string, category consts.LabelCategory) (map[string]int, error) { - if len(conditions) == 0 { - return map[string]int{}, nil - } - - query := r.db.Model(&model.Label{}). - Where("status != ? AND category = ?", consts.CommonDeleted, category) - orBuilder := r.db.Where("1 = 0") - for _, condition := range conditions { - andBuilder := r.db.Where("1 = 1") - if key, ok := condition["key"]; ok { - andBuilder = andBuilder.Where("label_key = ?", key) - } - if value, ok := condition["value"]; ok { - andBuilder = andBuilder.Where("label_value = ?", value) - } - orBuilder = orBuilder.Or(andBuilder) - } - - var labels []model.Label - if err := query.Where(orBuilder).Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list label IDs by conditions: %w", err) - } - - result := make(map[string]int, len(labels)) - for _, label := range labels { - result[label.Key+":"+label.Value] = label.ID - } - return result, nil -} - -func (r *Repository) AddExecutionLabels(executionID int, labelIDs []int) error { +func (r *Repository) addExecutionLabels(executionID int, labelIDs []int) error { if len(labelIDs) == 0 { return nil } @@ -236,7 +278,7 @@ func (r *Repository) AddExecutionLabels(executionID int, labelIDs []int) error { return nil } -func (r *Repository) ClearExecutionLabels(executionIDs []int, labelIDs []int) error { +func (r *Repository) clearExecutionLabels(executionIDs []int, labelIDs []int) error { if len(executionIDs) == 0 { return nil } @@ -251,7 +293,7 @@ func (r *Repository) ClearExecutionLabels(executionIDs []int, labelIDs []int) er return nil } -func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { +func (r *Repository) batchDecreaseLabelUsages(labelIDs []int, decrement int) error { if len(labelIDs) == 0 { return nil } @@ -266,7 +308,7 @@ func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) err return nil } -func (r *Repository) ListExecutionIDsByLabelItems(labelItems []dto.LabelItem) ([]int, error) { +func (r *Repository) listExecutionIDsByLabelItems(labelItems []dto.LabelItem) ([]int, error) { labelConditions := make([]map[string]string, 0, len(labelItems)) for _, item := range labelItems { labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) @@ -295,7 +337,7 @@ func (r *Repository) ListExecutionIDsByLabelItems(labelItems []dto.LabelItem) ([ return executionIDs, nil } -func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { +func (r *Repository) batchDeleteExecutions(executionIDs []int) error { if len(executionIDs) == 0 { return nil } @@ -311,7 +353,7 @@ func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { return nil } -func (r *Repository) UpdateExecutionDuration(executionID int, duration float64) error { +func (r *Repository) updateExecutionDuration(executionID int, duration float64) error { var execution model.Execution if err := r.db. Preload("AlgorithmVersion.Container"). @@ -346,7 +388,35 @@ func (r *Repository) UpdateExecutionDuration(executionID int, duration float64) return nil } -func (r *Repository) SaveDetectorResults(results []model.DetectorResult) error { +func (r *Repository) loadExecution(executionID int) (*model.Execution, error) { + var execution model.Execution + if err := r.db.Where("id = ? AND status != ?", executionID, consts.CommonDeleted).First(&execution).Error; err != nil { + return nil, fmt.Errorf("failed to find execution %d: %w", executionID, err) + } + return &execution, nil +} + +func (r *Repository) createExecutionRecord(execution *model.Execution) error { + if err := r.db.Create(execution).Error; err != nil { + return fmt.Errorf("failed to create execution: %w", err) + } + return nil +} + +func (r *Repository) updateExecutionFields(executionID int, fields map[string]any) error { + result := r.db.Model(&model.Execution{}). + Where("id = ? AND status != ?", executionID, consts.CommonDeleted). + Updates(fields) + if err := result.Error; err != nil { + return fmt.Errorf("failed to update execution %d: %w", executionID, err) + } + if result.RowsAffected == 0 { + return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) + } + return nil +} + +func (r *Repository) saveDetectorResults(results []model.DetectorResult) error { if len(results) == 0 { return fmt.Errorf("no detector results to save") } @@ -356,7 +426,7 @@ func (r *Repository) SaveDetectorResults(results []model.DetectorResult) error { return nil } -func (r *Repository) SaveGranularityResults(results []model.GranularityResult) error { +func (r *Repository) saveGranularityResults(results []model.GranularityResult) error { if len(results) == 0 { return fmt.Errorf("no granularity results to create") } diff --git a/src/module/execution/runtime_types.go b/src/module/execution/runtime_types.go new file mode 100644 index 00000000..4d1631b0 --- /dev/null +++ b/src/module/execution/runtime_types.go @@ -0,0 +1,21 @@ +package executionmodule + +import ( + "aegis/consts" + "aegis/dto" +) + +// RuntimeCreateExecutionReq captures execution writes initiated by runtime-worker-service. +type RuntimeCreateExecutionReq struct { + TaskID string `json:"task_id"` + AlgorithmVersionID int `json:"algorithm_version_id"` + DatapackID int `json:"datapack_id"` + DatasetVersionID *int `json:"dataset_version_id,omitempty"` + Labels []dto.LabelItem `json:"labels,omitempty"` +} + +// RuntimeUpdateExecutionStateReq captures execution state mutations initiated by runtime-worker-service. +type RuntimeUpdateExecutionStateReq struct { + ExecutionID int `json:"execution_id"` + State consts.ExecutionState `json:"state"` +} diff --git a/src/module/execution/service.go b/src/module/execution/service.go index d092a5aa..5cc1c191 100644 --- a/src/module/execution/service.go +++ b/src/module/execution/service.go @@ -10,9 +10,13 @@ import ( "aegis/dto" redisinfra "aegis/infra/redis" "aegis/model" + containermodule "aegis/module/container" + injectionmodule "aegis/module/injection" + labelmodule "aegis/module/label" "aegis/service/common" "aegis/utils" + chaos "github.com/OperationsPAI/chaos-experiment/handler" "gorm.io/gorm" ) @@ -67,7 +71,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu refs = append(refs, &req.Specs[i].Algorithm.ContainerRef) } - algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -77,7 +81,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu var allExecutionItems []SubmitExecutionItem for idx, spec := range req.Specs { - datapacks, datasetID, err := common.ExtractDatapacks(s.repo.db, spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) + datapacks, datasetID, err := injectionmodule.NewRepository(s.repo.db).ResolveDatapacks(spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) if err != nil { return nil, fmt.Errorf("failed to extract datapacks: %w", err) } @@ -93,7 +97,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu } algorithmItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.Algorithm.EnvVars, &algorithmVersion) + envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.Algorithm.EnvVars, &algorithmVersion) if err != nil { return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) } @@ -183,6 +187,30 @@ func (s *Service) GetExecution(_ context.Context, id int) (*ExecutionDetailResp, return resp, nil } +func (s *Service) ListEvaluationExecutionsByDatapack(_ context.Context, req *EvaluationExecutionsByDatapackReq) ([]EvaluationExecutionItem, error) { + if req == nil { + return nil, fmt.Errorf("evaluation datapack query is nil") + } + + executions, err := s.repo.listEvaluationExecutionsByDatapack(req.AlgorithmVersionID, req.DatapackName, req.FilterLabels) + if err != nil { + return nil, err + } + return buildEvaluationExecutionItems(executions), nil +} + +func (s *Service) ListEvaluationExecutionsByDataset(_ context.Context, req *EvaluationExecutionsByDatasetReq) ([]EvaluationExecutionItem, error) { + if req == nil { + return nil, fmt.Errorf("evaluation dataset query is nil") + } + + executions, err := s.repo.listEvaluationExecutionsByDataset(req.AlgorithmVersionID, req.DatasetVersionID, req.FilterLabels) + if err != nil { + return nil, err + } + return buildEvaluationExecutionItems(executions), nil +} + func (s *Service) ListAvailableLabels(_ context.Context) ([]dto.LabelItem, error) { labels, err := s.repo.listAvailableExecutionLabels() if err != nil { @@ -203,8 +231,8 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, var managedExecution *model.Execution var managedLabels []model.Label - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) execution, _, err := repo.getExecutionView(executionID) if err != nil { if errors.Is(err, consts.ErrNotFound) { @@ -214,7 +242,7 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, } if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -223,7 +251,7 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, for _, label := range labels { labelIDs = append(labelIDs, label.ID) } - if err := repo.AddExecutionLabels(execution.ID, labelIDs); err != nil { + if err := repo.addExecutionLabels(execution.ID, labelIDs); err != nil { return fmt.Errorf("failed to add execution labels: %w", err) } } @@ -235,10 +263,10 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, } if len(labelIDs) > 0 { - if err := repo.ClearExecutionLabels([]int{executionID}, labelIDs); err != nil { + if err := repo.clearExecutionLabels([]int{executionID}, labelIDs); err != nil { return fmt.Errorf("failed to clear execution labels: %w", err) } - if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + if err := repo.batchDecreaseLabelUsages(labelIDs, 1); err != nil { return fmt.Errorf("failed to decrease label usage counts: %w", err) } } @@ -266,10 +294,35 @@ func (s *Service) BatchDelete(_ context.Context, req *BatchDeleteExecutionReq) e return s.batchDeleteByLabels(req.Labels) } +func buildEvaluationExecutionItems(executions []model.Execution) []EvaluationExecutionItem { + items := make([]EvaluationExecutionItem, 0, len(executions)) + for _, execution := range executions { + item := EvaluationExecutionItem{ + Datapack: execution.Datapack.Name, + Groundtruths: collectGroundtruths(execution.Datapack), + ExecutionRef: NewExecutionGranularityRef(&execution), + } + items = append(items, item) + } + return items +} + +func collectGroundtruths(datapack *model.FaultInjection) []chaos.Groundtruth { + if datapack == nil || len(datapack.Groundtruths) == 0 { + return nil + } + + items := make([]chaos.Groundtruth, 0, len(datapack.Groundtruths)) + for _, gt := range datapack.Groundtruths { + items = append(items, *gt.ConvertToChaosGroundtruth()) + } + return items +} + func (s *Service) UploadDetectorResults(_ context.Context, req *UploadDetectorResultReq, executionID int) (*UploadExecutionResultResp, error) { - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.UpdateExecutionDuration(executionID, req.Duration); err != nil { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.updateExecutionDuration(executionID, req.Duration); err != nil { return err } @@ -277,7 +330,7 @@ func (s *Service) UploadDetectorResults(_ context.Context, req *UploadDetectorRe for _, item := range req.Results { results = append(results, *item.ConvertToDetectorResult(executionID)) } - if err := repo.SaveDetectorResults(results); err != nil { + if err := repo.saveDetectorResults(results); err != nil { return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) } return nil @@ -294,9 +347,9 @@ func (s *Service) UploadDetectorResults(_ context.Context, req *UploadDetectorRe } func (s *Service) UploadGranularityResults(_ context.Context, req *UploadGranularityResultReq, executionID int) (*UploadExecutionResultResp, error) { - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.UpdateExecutionDuration(executionID, req.Duration); err != nil { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.updateExecutionDuration(executionID, req.Duration); err != nil { return err } @@ -304,7 +357,7 @@ func (s *Service) UploadGranularityResults(_ context.Context, req *UploadGranula for _, item := range req.Results { results = append(results, *item.ConvertToGranularityResult(executionID)) } - if err := repo.SaveGranularityResults(results); err != nil { + if err := repo.saveGranularityResults(results); err != nil { return fmt.Errorf("failed to save detector results for execution %d: %w", executionID, err) } return nil @@ -319,12 +372,90 @@ func (s *Service) UploadGranularityResults(_ context.Context, req *UploadGranula }, nil } +func (s *Service) CreateExecutionRecord(_ context.Context, req *RuntimeCreateExecutionReq) (int, error) { + if req == nil { + return 0, fmt.Errorf("runtime create execution request is nil") + } + if req.TaskID == "" { + return 0, fmt.Errorf("%w: task_id is required", consts.ErrBadRequest) + } + if req.AlgorithmVersionID <= 0 || req.DatapackID <= 0 { + return 0, fmt.Errorf("%w: algorithm_version_id and datapack_id are required", consts.ErrBadRequest) + } + + var createdExecutionID int + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + execution := &model.Execution{ + TaskID: &req.TaskID, + AlgorithmVersionID: req.AlgorithmVersionID, + DatapackID: req.DatapackID, + DatasetVersionID: req.DatasetVersionID, + State: consts.ExecutionInitial, + Status: consts.CommonEnabled, + } + + if err := repo.createExecutionRecord(execution); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: execution already exists for task %s", consts.ErrAlreadyExists, req.TaskID) + } + return err + } + + if len(req.Labels) > 0 { + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.ExecutionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + labelIDs := make([]int, 0, len(labels)) + for _, label := range labels { + labelIDs = append(labelIDs, label.ID) + } + if err := repo.addExecutionLabels(execution.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add execution labels: %w", err) + } + } + + createdExecutionID = execution.ID + return nil + }) + if err != nil { + return 0, err + } + return createdExecutionID, nil +} + +func (s *Service) UpdateExecutionState(_ context.Context, req *RuntimeUpdateExecutionStateReq) error { + if req == nil { + return fmt.Errorf("runtime update execution state request is nil") + } + if req.ExecutionID <= 0 { + return fmt.Errorf("%w: execution_id is required", consts.ErrBadRequest) + } + + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + execution, err := repo.loadExecution(req.ExecutionID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, req.ExecutionID) + } + return err + } + if execution.State != consts.ExecutionInitial { + return fmt.Errorf("cannot change state of execution %d from %s to %s", req.ExecutionID, consts.GetExecutionStateName(execution.State), consts.GetExecutionStateName(req.State)) + } + return repo.updateExecutionFields(req.ExecutionID, map[string]any{"state": req.State}) + }) +} + func (s *Service) batchDeleteByIDs(executionIDs []int) error { if len(executionIDs) == 0 { return nil } - return s.repo.Transaction(func(tx *gorm.DB) error { - return s.repo.withDB(tx).BatchDeleteExecutions(executionIDs) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + return NewRepository(tx).batchDeleteExecutions(executionIDs) }) } @@ -332,7 +463,7 @@ func (s *Service) batchDeleteByLabels(labelItems []dto.LabelItem) error { if len(labelItems) == 0 { return nil } - executionIDs, err := s.repo.ListExecutionIDsByLabelItems(labelItems) + executionIDs, err := s.repo.listExecutionIDsByLabelItems(labelItems) if err != nil { return fmt.Errorf("failed to list execution ids by labels: %w", err) } diff --git a/src/module/group/handler.go b/src/module/group/handler.go index 15f43b1c..7ae2fa0d 100644 --- a/src/module/group/handler.go +++ b/src/module/group/handler.go @@ -19,10 +19,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } @@ -113,7 +113,7 @@ func (h *Handler) GetGroupStream(c *gin.Context) { "stream_key": streamKey, }) - processor, err := h.service.NewGroupStreamProcessor(groupID) + processor, err := h.service.NewGroupStreamProcessor(ctx, groupID) if err != nil { logEntry.Errorf("Failed to initialize group stream processor: %v", err) dto.ErrorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to initialize group stream: %v", err)) diff --git a/src/module/group/handler_service.go b/src/module/group/handler_service.go new file mode 100644 index 00000000..5154dcdc --- /dev/null +++ b/src/module/group/handler_service.go @@ -0,0 +1,19 @@ +package groupmodule + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +// HandlerService captures group operations consumed by HTTP handlers and gateway adapters. +type HandlerService interface { + GetGroupStats(context.Context, *GetGroupStatsReq) (*GroupStats, error) + NewGroupStreamProcessor(context.Context, string) (*GroupStreamProcessor, error) + ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/group/module.go b/src/module/group/module.go index a9704595..ce600619 100644 --- a/src/module/group/module.go +++ b/src/module/group/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("group", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/group/service.go b/src/module/group/service.go index dad3cea5..9f4a79e3 100644 --- a/src/module/group/service.go +++ b/src/module/group/service.go @@ -60,19 +60,23 @@ func (s *Service) GetGroupStats(_ context.Context, req *GetGroupStatsReq) (*Grou }, nil } -func (s *Service) NewGroupStreamProcessor(groupID string) (*GroupStreamProcessor, error) { +func (s *Service) NewGroupStreamProcessor(_ context.Context, groupID string) (*GroupStreamProcessor, error) { + total, err := s.GetGroupTraceCount(groupID) + if err != nil { + return nil, err + } + return NewGroupStreamProcessor(int(total)), nil +} + +func (s *Service) GetGroupTraceCount(groupID string) (int64, error) { total, err := s.repo.CountTracesByGroupID(groupID) if err != nil { - return nil, fmt.Errorf("failed to count traces for group %s: %w", groupID, err) + return 0, fmt.Errorf("failed to count traces for group %s: %w", groupID, err) } if total == 0 { - return nil, fmt.Errorf("the group %s does not exist", groupID) + return 0, fmt.Errorf("the group %s does not exist", groupID) } - - return &GroupStreamProcessor{ - totalTraces: int(total), - finishedCount: 0, - }, nil + return total, nil } func (s *Service) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { @@ -92,6 +96,13 @@ type GroupStreamProcessor struct { finishedCount int } +func NewGroupStreamProcessor(totalTraces int) *GroupStreamProcessor { + return &GroupStreamProcessor{ + totalTraces: totalTraces, + finishedCount: 0, + } +} + func (p *GroupStreamProcessor) ProcessGroupMessage(msg redis.XMessage) (*GroupStreamEvent, error) { traceID, ok := msg.Values[consts.RdbEventTraceID].(string) if !ok || traceID == "" { diff --git a/src/module/injection/handler.go b/src/module/injection/handler.go index 4b24ee84..9dc26122 100644 --- a/src/module/injection/handler.go +++ b/src/module/injection/handler.go @@ -24,10 +24,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/injection/handler_service.go b/src/module/injection/handler_service.go new file mode 100644 index 00000000..aaa2b831 --- /dev/null +++ b/src/module/injection/handler_service.go @@ -0,0 +1,38 @@ +package injectionmodule + +import ( + "archive/zip" + "context" + "io" + + "aegis/dto" + "aegis/utils" +) + +// HandlerService captures the injection operations consumed by the HTTP handler. +type HandlerService interface { + ListProjectInjections(context.Context, *ListInjectionReq, int) (*dto.ListResp[InjectionResp], error) + Search(context.Context, *SearchInjectionReq, *int) (*dto.SearchResp[InjectionDetailResp], error) + ListNoIssues(context.Context, *ListInjectionNoIssuesReq, *int) ([]InjectionNoIssuesResp, error) + ListWithIssues(context.Context, *ListInjectionWithIssuesReq, *int) ([]InjectionWithIssuesResp, error) + SubmitFaultInjection(context.Context, *SubmitInjectionReq, string, int, *int) (*SubmitInjectionResp, error) + SubmitDatapackBuilding(context.Context, *SubmitDatapackBuildingReq, string, int, *int) (*SubmitDatapackBuildingResp, error) + ListInjections(context.Context, *ListInjectionReq) (*dto.ListResp[InjectionResp], error) + GetInjection(context.Context, int) (*InjectionDetailResp, error) + ManageLabels(context.Context, *ManageInjectionLabelReq, int) (*InjectionResp, error) + BatchManageLabels(context.Context, *BatchManageInjectionLabelReq) (*BatchManageInjectionLabelResp, error) + BatchDelete(context.Context, *BatchDeleteInjectionReq) error + Clone(context.Context, int, *CloneInjectionReq) (*InjectionDetailResp, error) + GetLogs(context.Context, int) (*InjectionLogsResp, error) + GetDatapackFilename(context.Context, int) (string, error) + DownloadDatapack(context.Context, *zip.Writer, []utils.ExculdeRule, int) error + GetDatapackFiles(context.Context, int, string) (*DatapackFilesResp, error) + DownloadDatapackFile(context.Context, int, string) (string, string, int64, io.ReadSeekCloser, error) + QueryDatapackFile(context.Context, int, string) (string, int64, io.ReadCloser, error) + UpdateGroundtruth(context.Context, int, *UpdateGroundtruthReq) error + UploadDatapack(context.Context, *UploadDatapackReq, io.Reader, int64) (*UploadDatapackResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/injection/module.go b/src/module/injection/module.go index cabed310..bd28c7b6 100644 --- a/src/module/injection/module.go +++ b/src/module/injection/module.go @@ -6,5 +6,6 @@ var Module = fx.Module("injection", fx.Provide(NewRepository), fx.Provide(NewDatapackStore), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/injection/repository.go b/src/module/injection/repository.go index 48a695a6..46484ccc 100644 --- a/src/module/injection/repository.go +++ b/src/module/injection/repository.go @@ -4,7 +4,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/repository" + "aegis/searchx" "encoding/json" "fmt" "strings" @@ -21,15 +21,7 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) LoadInjection(id int) (*model.FaultInjection, error) { +func (r *Repository) loadInjection(id int) (*model.FaultInjection, error) { var injection model.FaultInjection if err := r.db. Preload("Task"). @@ -43,7 +35,7 @@ func (r *Repository) LoadInjection(id int) (*model.FaultInjection, error) { return &injection, nil } -func (r *Repository) FindInjectionByName(name string, preload bool) (*model.FaultInjection, error) { +func (r *Repository) findInjectionByName(name string, preload bool) (*model.FaultInjection, error) { query := r.db if preload { query = query.Preload("Labels") @@ -57,14 +49,14 @@ func (r *Repository) FindInjectionByName(name string, preload bool) (*model.Faul return &injection, nil } -func (r *Repository) CreateInjectionRecord(injection *model.FaultInjection) error { +func (r *Repository) createInjectionRecord(injection *model.FaultInjection) error { if err := r.db.Create(injection).Error; err != nil { return fmt.Errorf("failed to create injection: %w", err) } return nil } -func (r *Repository) UpdateGroundtruth(id int, groundtruths []model.Groundtruth, source string) error { +func (r *Repository) updateGroundtruth(id int, groundtruths []model.Groundtruth, source string) error { groundtruthJSON, err := json.Marshal(groundtruths) if err != nil { return fmt.Errorf("failed to marshal groundtruths: %w", err) @@ -85,7 +77,20 @@ func (r *Repository) UpdateGroundtruth(id int, groundtruths []model.Groundtruth, return nil } -func (r *Repository) AddInjectionLabels(injectionID int, labelIDs []int) error { +func (r *Repository) updateInjectionFields(id int, fields map[string]any) error { + result := r.db.Model(&model.FaultInjection{}). + Where("id = ? AND status != ?", id, consts.CommonDeleted). + Updates(fields) + if result.Error != nil { + return fmt.Errorf("failed to update injection %d: %w", id, result.Error) + } + if result.RowsAffected == 0 { + return fmt.Errorf("%w: injection %d not found", consts.ErrNotFound, id) + } + return nil +} + +func (r *Repository) addInjectionLabels(injectionID int, labelIDs []int) error { if len(labelIDs) == 0 { return nil } @@ -103,7 +108,7 @@ func (r *Repository) AddInjectionLabels(injectionID int, labelIDs []int) error { return nil } -func (r *Repository) ResolveProject(name string) (*model.Project, error) { +func (r *Repository) resolveProject(name string) (*model.Project, error) { var project model.Project if err := r.db.Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&project).Error; err != nil { return nil, fmt.Errorf("failed to find project with name %s: %w", name, err) @@ -111,7 +116,7 @@ func (r *Repository) ResolveProject(name string) (*model.Project, error) { return &project, nil } -func (r *Repository) LoadTask(taskID string) (*model.Task, error) { +func (r *Repository) loadTask(taskID string) (*model.Task, error) { var task model.Task if err := r.db. Preload("FaultInjection.Benchmark.Container"). @@ -126,7 +131,7 @@ func (r *Repository) LoadTask(taskID string) (*model.Task, error) { return &task, nil } -func (r *Repository) LoadPedestalHelmConfig(versionID int) (*model.HelmConfig, error) { +func (r *Repository) loadPedestalHelmConfig(versionID int) (*model.HelmConfig, error) { var helmConfig model.HelmConfig if err := r.db.Preload("ContainerVersion"). Where("container_version_id = ?", versionID). @@ -136,7 +141,7 @@ func (r *Repository) LoadPedestalHelmConfig(versionID int) (*model.HelmConfig, e return &helmConfig, nil } -func (r *Repository) ListExistingEngineConfigs(configs []string) ([]string, error) { +func (r *Repository) listExistingEngineConfigs(configs []string) ([]string, error) { if len(configs) == 0 { return []string{}, nil } @@ -157,7 +162,7 @@ func (r *Repository) ListExistingEngineConfigs(configs []string) ([]string, erro return existing, nil } -func (r *Repository) ClearInjectionLabels(injectionIDs []int, labelIDs []int) error { +func (r *Repository) clearInjectionLabels(injectionIDs []int, labelIDs []int) error { if len(injectionIDs) == 0 { return nil } @@ -172,7 +177,7 @@ func (r *Repository) ClearInjectionLabels(injectionIDs []int, labelIDs []int) er return nil } -func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) error { +func (r *Repository) batchDecreaseLabelUsages(labelIDs []int, decrement int) error { if len(labelIDs) == 0 { return nil } @@ -186,7 +191,7 @@ func (r *Repository) BatchDecreaseLabelUsages(labelIDs []int, decrement int) err return nil } -func (r *Repository) ListExecutionsByDatapackIDs(datapackIDs []int) ([]model.Execution, error) { +func (r *Repository) listExecutionsByDatapackIDs(datapackIDs []int) ([]model.Execution, error) { if len(datapackIDs) == 0 { return []model.Execution{}, nil } @@ -205,7 +210,7 @@ func (r *Repository) ListExecutionsByDatapackIDs(datapackIDs []int) ([]model.Exe return executions, nil } -func (r *Repository) RemoveLabelsFromExecutions(executionIDs []int) error { +func (r *Repository) removeLabelsFromExecutions(executionIDs []int) error { if len(executionIDs) == 0 { return nil } @@ -216,7 +221,7 @@ func (r *Repository) RemoveLabelsFromExecutions(executionIDs []int) error { return nil } -func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { +func (r *Repository) batchDeleteExecutions(executionIDs []int) error { if len(executionIDs) == 0 { return nil } @@ -228,7 +233,7 @@ func (r *Repository) BatchDeleteExecutions(executionIDs []int) error { return nil } -func (r *Repository) BatchDeleteInjections(injectionIDs []int) error { +func (r *Repository) batchDeleteInjections(injectionIDs []int) error { if len(injectionIDs) == 0 { return nil } @@ -240,8 +245,8 @@ func (r *Repository) BatchDeleteInjections(injectionIDs []int) error { return nil } -func (r *Repository) DeleteInjectionsCascade(injectionIDs []int) error { - executions, err := r.ListExecutionsByDatapackIDs(injectionIDs) +func (r *Repository) deleteInjectionsCascade(injectionIDs []int) error { + executions, err := r.listExecutionsByDatapackIDs(injectionIDs) if err != nil { return fmt.Errorf("failed to list executions by datapack ids: %w", err) } @@ -252,25 +257,25 @@ func (r *Repository) DeleteInjectionsCascade(injectionIDs []int) error { } if len(executionIDs) > 0 { - if err := r.RemoveLabelsFromExecutions(executionIDs); err != nil { + if err := r.removeLabelsFromExecutions(executionIDs); err != nil { return fmt.Errorf("failed to remove execution labels: %w", err) } - if err := r.BatchDeleteExecutions(executionIDs); err != nil { + if err := r.batchDeleteExecutions(executionIDs); err != nil { return fmt.Errorf("failed to delete executions: %w", err) } } - if err := r.ClearInjectionLabels(injectionIDs, nil); err != nil { + if err := r.clearInjectionLabels(injectionIDs, nil); err != nil { return fmt.Errorf("failed to clear injection labels: %w", err) } - if err := r.BatchDeleteInjections(injectionIDs); err != nil { + if err := r.batchDeleteInjections(injectionIDs); err != nil { return fmt.Errorf("failed to delete injections: %w", err) } return nil } -func (r *Repository) GetInjectionWithLabels(injectionID int) (*model.FaultInjection, error) { - injection, err := r.LoadInjection(injectionID) +func (r *Repository) getInjectionWithLabels(injectionID int) (*model.FaultInjection, error) { + injection, err := r.loadInjection(injectionID) if err != nil { return nil, err } @@ -286,7 +291,7 @@ func (r *Repository) GetInjectionWithLabels(injectionID int) (*model.FaultInject return injection, nil } -func (r *Repository) LoadInjectionLabelIDsByItems(conditions []map[string]string, category consts.LabelCategory) (map[string]int, error) { +func (r *Repository) loadInjectionLabelIDsByItems(conditions []map[string]string, category consts.LabelCategory) (map[string]int, error) { if len(conditions) == 0 { return map[string]int{}, nil } @@ -318,8 +323,8 @@ func (r *Repository) LoadInjectionLabelIDsByItems(conditions []map[string]string return result, nil } -func (r *Repository) LoadExistingInjectionsByID(injectionIDs []int) (map[int]*model.FaultInjection, error) { - injections, err := r.ListFaultInjectionsByIDWithLabels(injectionIDs) +func (r *Repository) loadExistingInjectionsByID(injectionIDs []int) (map[int]*model.FaultInjection, error) { + injections, err := r.listFaultInjectionsByIDWithLabels(injectionIDs) if err != nil { return nil, err } @@ -332,7 +337,7 @@ func (r *Repository) LoadExistingInjectionsByID(injectionIDs []int) (map[int]*mo return result, nil } -func (r *Repository) ListInjectionsView(limit, offset int, filterOptions *ListInjectionFilters) ([]model.FaultInjection, int64, error) { +func (r *Repository) listInjectionsView(limit, offset int, filterOptions *ListInjectionFilters) ([]model.FaultInjection, int64, error) { query := r.db.Model(&model.FaultInjection{}). Preload("Benchmark.Container"). Preload("Pedestal.Container"). @@ -393,7 +398,7 @@ func (r *Repository) ListInjectionsView(limit, offset int, filterOptions *ListIn return injections, total, nil } -func (r *Repository) ListProjectInjectionsView(projectID, limit, offset int) ([]model.FaultInjection, int64, error) { +func (r *Repository) listProjectInjectionsView(projectID, limit, offset int) ([]model.FaultInjection, int64, error) { baseQuery := r.db.Model(&model.FaultInjection{}). Joins("JOIN tasks ON tasks.id = fault_injections.task_id"). Joins("JOIN traces on traces.id = tasks.trace_id"). @@ -433,13 +438,13 @@ func (r *Repository) ListProjectInjectionsView(projectID, limit, offset int) ([] return injections, total, nil } -func (r *Repository) SearchInjections(req *SearchInjectionReq, projectID *int) ([]model.FaultInjection, int64, error) { +func (r *Repository) searchInjections(req *SearchInjectionReq, projectID *int) ([]model.FaultInjection, int64, error) { searchReq := req.ConvertToSearchReq() if projectID != nil { searchReq.AddFilter("project_id", dto.OpEqual, *projectID) } - qb := repository.NewSearchQueryBuilder(r.db, consts.InjectionAllowedFields) + qb := searchx.NewQueryBuilder(r.db, consts.InjectionAllowedFields) qb.ApplySearchReq(searchReq.Filters, searchReq.Keyword, searchReq.Sort, searchReq.GroupBy, model.FaultInjection{}) qb.ApplyIncludes(searchReq.Includes) qb.ApplyIncludeFields(searchReq.IncludeFields) @@ -489,7 +494,7 @@ func (r *Repository) SearchInjections(req *SearchInjectionReq, projectID *int) ( return filtered, total, nil } -func (r *Repository) ListIssuesFreeInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionNoIssues, error) { +func (r *Repository) listIssuesFreeInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionNoIssues, error) { var injections []model.FaultInjectionNoIssues query := r.db.Model(&model.FaultInjectionNoIssues{}). Joins("JOIN fault_injections fi ON fi.id = fault_injection_no_issues.datapack_id"). @@ -523,7 +528,7 @@ func (r *Repository) ListIssuesFreeInjections(labelConditions []map[string]strin return injections, nil } -func (r *Repository) ListIssueInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionWithIssues, error) { +func (r *Repository) listIssueInjections(labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionWithIssues, error) { var injections []model.FaultInjectionWithIssues query := r.db.Model(&model.FaultInjectionWithIssues{}). Joins("JOIN fault_injections fi ON fi.id = fault_injection_with_issues.datapack_id"). @@ -552,7 +557,7 @@ func (r *Repository) ListIssueInjections(labelConditions []map[string]string, st return injections, nil } -func (r *Repository) ListInjectionLabelIDsByKeys(injectionID int, keys []string) ([]int, error) { +func (r *Repository) listInjectionLabelIDsByKeys(injectionID int, keys []string) ([]int, error) { var labelIDs []int if err := r.db.Table("labels l"). Select("l.id"). @@ -564,7 +569,7 @@ func (r *Repository) ListInjectionLabelIDsByKeys(injectionID int, keys []string) return labelIDs, nil } -func (r *Repository) ListFaultInjectionsByIDWithLabels(injectionIDs []int) ([]model.FaultInjection, error) { +func (r *Repository) listFaultInjectionsByIDWithLabels(injectionIDs []int) ([]model.FaultInjection, error) { if len(injectionIDs) == 0 { return []model.FaultInjection{}, nil } @@ -594,7 +599,7 @@ func (r *Repository) ListFaultInjectionsByIDWithLabels(injectionIDs []int) ([]mo return injections, nil } -func (r *Repository) ListInjectionIDsByLabelConditions(labelConditions []map[string]string) ([]int, error) { +func (r *Repository) listInjectionIDsByLabelConditions(labelConditions []map[string]string) ([]int, error) { return r.listInjectionIDsByLabels(labelConditions) } diff --git a/src/service/common/datapack_resolver.go b/src/module/injection/resolve.go similarity index 81% rename from src/service/common/datapack_resolver.go rename to src/module/injection/resolve.go index d614e3f8..f61874ad 100644 --- a/src/service/common/datapack_resolver.go +++ b/src/module/injection/resolve.go @@ -1,13 +1,11 @@ -package common +package injectionmodule import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/repository" + datasetmodule "aegis/module/dataset" "fmt" - - "gorm.io/gorm" ) var taskTypeDatapackStates = map[consts.TaskType][]consts.DatapackState{ @@ -23,16 +21,7 @@ var taskTypeDatapackStates = map[consts.TaskType][]consts.DatapackState{ }, } -func hasLabelKeyValue(labels []model.Label, key, value string) bool { - for _, label := range labels { - if label.Key == key && label.Value == value { - return true - } - } - return false -} - -func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.DatasetRef, userID int, taskType consts.TaskType) ([]model.FaultInjection, *int, error) { +func (r *Repository) ResolveDatapacks(datapackName *string, datasetRef *dto.DatasetRef, userID int, taskType consts.TaskType) ([]model.FaultInjection, *int, error) { states, exists := taskTypeDatapackStates[taskType] if !exists { return nil, nil, fmt.Errorf("unsupported task type: %s", consts.GetTaskTypeName(taskType)) @@ -55,7 +44,7 @@ func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset } if datapackName != nil { - datapack, err := repository.GetInjectionByName(db, *datapackName, true) + datapack, err := r.findInjectionByName(*datapackName, true) if err != nil { return nil, nil, fmt.Errorf("failed to get datapack: %w", err) } @@ -66,7 +55,7 @@ func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset } if datasetRef != nil { - datasetVersionResults, err := MapRefsToDatasetVersionsWithDB(db, []*dto.DatasetRef{datasetRef}, userID) + datasetVersionResults, err := datasetmodule.NewRepository(r.db).ResolveDatasetVersions([]*dto.DatasetRef{datasetRef}, userID) if err != nil { return nil, nil, fmt.Errorf("failed to get dataset versions: %w", err) } @@ -76,7 +65,7 @@ func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset return nil, nil, fmt.Errorf("dataset version not found for %v", datasetRef) } - datapacks, err := repository.ListInjectionsByDatasetVersionID(db, version.ID, true) + datapacks, err := datasetmodule.NewRepository(r.db).ListInjectionsByDatasetVersionID(version.ID, true) if err != nil { return nil, nil, fmt.Errorf("failed to get dataset datapacks: %s", err.Error()) } @@ -94,3 +83,12 @@ func ExtractDatapacks(db *gorm.DB, datapackName *string, datasetRef *dto.Dataset return nil, nil, fmt.Errorf("either datapack or dataset must be specified") } + +func hasLabelKeyValue(labels []model.Label, key, value string) bool { + for _, label := range labels { + if label.Key == key && label.Value == value { + return true + } + } + return false +} diff --git a/src/module/injection/runtime_types.go b/src/module/injection/runtime_types.go new file mode 100644 index 00000000..6f2a3f11 --- /dev/null +++ b/src/module/injection/runtime_types.go @@ -0,0 +1,42 @@ +package injectionmodule + +import ( + "time" + + "aegis/consts" + "aegis/dto" + "aegis/model" + + chaos "github.com/OperationsPAI/chaos-experiment/handler" +) + +// RuntimeCreateInjectionReq captures fault injection writes initiated by runtime-worker-service. +type RuntimeCreateInjectionReq struct { + Name string `json:"name"` + FaultType chaos.ChaosType `json:"fault_type"` + Category chaos.SystemType `json:"category"` + Description string `json:"description"` + DisplayConfig string `json:"display_config"` + EngineConfig string `json:"engine_config"` + Groundtruths []model.Groundtruth `json:"groundtruths"` + GroundtruthSource string `json:"groundtruth_source"` + PreDuration int `json:"pre_duration"` + TaskID string `json:"task_id"` + BenchmarkID *int `json:"benchmark_id,omitempty"` + PedestalID *int `json:"pedestal_id,omitempty"` + Labels []dto.LabelItem `json:"labels,omitempty"` + State consts.DatapackState `json:"state"` +} + +// RuntimeUpdateInjectionStateReq captures datapack state mutations initiated by runtime-worker-service. +type RuntimeUpdateInjectionStateReq struct { + Name string `json:"name"` + State consts.DatapackState `json:"state"` +} + +// RuntimeUpdateInjectionTimestampReq captures datapack timestamp updates initiated by runtime-worker-service. +type RuntimeUpdateInjectionTimestampReq struct { + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} diff --git a/src/module/injection/service.go b/src/module/injection/service.go index 20659443..fbe8010c 100644 --- a/src/module/injection/service.go +++ b/src/module/injection/service.go @@ -15,6 +15,8 @@ import ( lokiinfra "aegis/infra/loki" redisinfra "aegis/infra/redis" "aegis/model" + containermodule "aegis/module/container" + labelmodule "aegis/module/label" "aegis/service/common" "aegis/utils" @@ -43,7 +45,7 @@ func (s *Service) ListProjectInjections(ctx context.Context, req *ListInjectionR } limit, offset := req.ToGormParams() - injections, total, err := s.repo.ListProjectInjectionsView(projectID, limit, offset) + injections, total, err := s.repo.listProjectInjectionsView(projectID, limit, offset) if err != nil { return nil, fmt.Errorf("failed to list injections for project %d: %w", projectID, err) } @@ -63,7 +65,7 @@ func (s *Service) Search(ctx context.Context, req *SearchInjectionReq, projectID if req == nil { return nil, fmt.Errorf("search injection request is nil") } - injections, total, err := s.repo.SearchInjections(req, projectID) + injections, total, err := s.repo.searchInjections(req, projectID) if err != nil { return nil, fmt.Errorf("failed to search injections: %w", err) } @@ -99,7 +101,7 @@ func (s *Service) ListNoIssues(ctx context.Context, req *ListInjectionNoIssuesRe return nil, fmt.Errorf("invalid time range: %w", err) } - records, err := s.repo.ListIssuesFreeInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) + records, err := s.repo.listIssuesFreeInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) if err != nil { return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) } @@ -131,7 +133,7 @@ func (s *Service) ListWithIssues(ctx context.Context, req *ListInjectionWithIssu return nil, fmt.Errorf("invalid time range: %w", err) } - records, err := s.repo.ListIssueInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) + records, err := s.repo.listIssueInjections(labelConditions, &opts.CustomStartTime, &opts.CustomEndTime, projectID) if err != nil { return nil, fmt.Errorf("failed to list fault injections without issues: %w", err) } @@ -154,7 +156,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection db := s.repo.db if projectID == nil { - project, err := s.repo.ResolveProject(req.ProjectName) + project, err := s.repo.resolveProject(req.ProjectName) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) @@ -164,7 +166,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection projectID = &project.ID } - pedestalVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, []*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) + pedestalVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) if err != nil { return nil, fmt.Errorf("failed to map pedestal container ref to version: %w", err) } @@ -173,7 +175,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection return nil, fmt.Errorf("pedestal version not found for container: %s (version: %s)", req.Pedestal.Name, req.Pedestal.Version) } - helmConfig, err := s.repo.LoadPedestalHelmConfig(pedestalVersion.ID) + helmConfig, err := s.repo.loadPedestalHelmConfig(pedestalVersion.ID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: helm config not found for pedestal version id %d", consts.ErrNotFound, pedestalVersion.ID) @@ -182,7 +184,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } params := flattenYAMLToParameters(req.Pedestal.Payload, "") - helmValues, err := common.ListHelmConfigValuesWithDB(db, params, helmConfig) + helmValues, err := containermodule.NewRepository(db).ListHelmConfigValues(params, helmConfig) if err != nil { return nil, fmt.Errorf("failed to render pedestal helm values: %w", err) } @@ -193,7 +195,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection pedestalItem := dto.NewContainerVersionItem(&pedestalVersion) pedestalItem.Extra = helmConfigItem - benchmarkVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, []*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) + benchmarkVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) if err != nil { return nil, fmt.Errorf("failed to map benchmark container ref to version: %w", err) } @@ -203,7 +205,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := common.ListContainerVersionEnvVarsWithDB(db, req.Benchmark.EnvVars, &benchmarkVersion) + envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(req.Benchmark.EnvVars, &benchmarkVersion) if err != nil { return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) } @@ -243,7 +245,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection refs = append(refs, &req.Algorithms[i].ContainerRef) } - algorithmVersionsResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeAlgorithm, userID) + algorithmVersionsResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -257,7 +259,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } algorithmVersionItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.EnvVars, &algorithmVersion) + envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.EnvVars, &algorithmVersion) if err != nil { return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) } @@ -331,7 +333,7 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac db := s.repo.db if projectID == nil { - project, err := s.repo.ResolveProject(req.ProjectName) + project, err := s.repo.resolveProject(req.ProjectName) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: project %s not found", consts.ErrNotFound, req.ProjectName) @@ -346,14 +348,14 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac refs = append(refs, &req.Specs[i].Benchmark.ContainerRef) } - benchmarkVersionResults, err := common.MapRefsToContainerVersionsWithDB(db, refs, consts.ContainerTypeBenchmark, userID) + benchmarkVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeBenchmark, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } var allBuildingItems []SubmitBuildingItem for idx, spec := range req.Specs { - datapacks, datasetVersionID, err := common.ExtractDatapacks(s.repo.db, spec.Datapack, spec.Dataset, userID, consts.TaskTypeBuildDatapack) + datapacks, datasetVersionID, err := s.repo.ResolveDatapacks(spec.Datapack, spec.Dataset, userID, consts.TaskTypeBuildDatapack) if err != nil { return nil, fmt.Errorf("failed to extract datapacks: %w", err) } @@ -364,7 +366,7 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac } benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := common.ListContainerVersionEnvVarsWithDB(db, spec.Benchmark.EnvVars, &benchmarkVersion) + envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.Benchmark.EnvVars, &benchmarkVersion) if err != nil { return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) } @@ -413,7 +415,7 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac func (s *Service) ListInjections(_ context.Context, req *ListInjectionReq) (*dto.ListResp[InjectionResp], error) { limit, offset := req.ToGormParams() - injections, total, err := s.repo.ListInjectionsView(limit, offset, req.ToFilterOptions()) + injections, total, err := s.repo.listInjectionsView(limit, offset, req.ToFilterOptions()) if err != nil { return nil, fmt.Errorf("failed to list injections: %w", err) } @@ -430,7 +432,7 @@ func (s *Service) ListInjections(_ context.Context, req *ListInjectionReq) (*dto } func (s *Service) GetInjection(_ context.Context, id int) (*InjectionDetailResp, error) { - injection, err := s.repo.GetInjectionWithLabels(id) + injection, err := s.repo.getInjectionWithLabels(id) if err != nil { if errors.Is(err, consts.ErrNotFound) { return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -446,9 +448,9 @@ func (s *Service) GetMetadata(_ context.Context) (*InjectionMetadataResp, error) func (s *Service) ManageLabels(_ context.Context, req *ManageInjectionLabelReq, id int) (*InjectionResp, error) { var managedInjection *model.FaultInjection - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - injection, err := repo.LoadInjection(id) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + injection, err := repo.loadInjection(id) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -457,7 +459,7 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageInjectionLabelReq, } if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -465,27 +467,27 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageInjectionLabelReq, for _, label := range labels { labelIDs = append(labelIDs, label.ID) } - if err := repo.AddInjectionLabels(injection.ID, labelIDs); err != nil { + if err := repo.addInjectionLabels(injection.ID, labelIDs); err != nil { return fmt.Errorf("failed to add injection labels: %w", err) } } if len(req.RemoveLabels) > 0 { - labelIDs, err := repo.ListInjectionLabelIDsByKeys(injection.ID, req.RemoveLabels) + labelIDs, err := repo.listInjectionLabelIDsByKeys(injection.ID, req.RemoveLabels) if err != nil { return fmt.Errorf("failed to find label ids by keys: %w", err) } if len(labelIDs) > 0 { - if err := repo.ClearInjectionLabels([]int{id}, labelIDs); err != nil { + if err := repo.clearInjectionLabels([]int{id}, labelIDs); err != nil { return fmt.Errorf("failed to clear injection labels: %w", err) } - if err := repo.BatchDecreaseLabelUsages(labelIDs, 1); err != nil { + if err := repo.batchDecreaseLabelUsages(labelIDs, 1); err != nil { return fmt.Errorf("failed to decrease label usage counts: %w", err) } } } - managedInjection, err = repo.GetInjectionWithLabels(id) + managedInjection, err = repo.getInjectionWithLabels(id) if err != nil { return fmt.Errorf("failed to reload injection labels: %w", err) } @@ -506,8 +508,8 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection return resp, nil } - return resp, s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + return resp, s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) allInjectionIDs := make([]int, 0, len(req.Items)) operationMap := make(map[int]*InjectionLabelOperation, len(req.Items)) for i := range req.Items { @@ -516,7 +518,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection operationMap[item.InjectionID] = item } - foundIDMap, err := repo.LoadExistingInjectionsByID(allInjectionIDs) + foundIDMap, err := repo.loadExistingInjectionsByID(allInjectionIDs) if err != nil { return fmt.Errorf("failed to list injections: %w", err) } @@ -557,7 +559,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection var labelMap map[string]int if len(allAddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -573,7 +575,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection for _, item := range allRemoveLabels { labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) } - removeLabelMap, err = repo.LoadInjectionLabelIDsByItems(labelConditions, consts.InjectionCategory) + removeLabelMap, err = repo.loadInjectionLabelIDsByItems(labelConditions, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to find labels to remove: %w", err) } @@ -589,7 +591,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection } } if len(labelIDsToAdd) > 0 { - if err := repo.AddInjectionLabels(injectionID, labelIDsToAdd); err != nil { + if err := repo.addInjectionLabels(injectionID, labelIDsToAdd); err != nil { resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to add labels - %s", injectionID, err.Error())) resp.FailedCount++ delete(foundIDMap, injectionID) @@ -606,7 +608,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection } } if len(labelIDsToRemove) > 0 { - if err := repo.ClearInjectionLabels([]int{injectionID}, labelIDsToRemove); err != nil { + if err := repo.clearInjectionLabels([]int{injectionID}, labelIDsToRemove); err != nil { resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("Injection ID %d: failed to remove labels - %s", injectionID, err.Error())) resp.FailedCount++ delete(foundIDMap, injectionID) @@ -621,7 +623,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection for id := range foundIDMap { successIDs = append(successIDs, id) } - updatedInjections, err := repo.ListFaultInjectionsByIDWithLabels(successIDs) + updatedInjections, err := repo.listFaultInjectionsByIDWithLabels(successIDs) if err != nil { return fmt.Errorf("failed to fetch updated injections: %w", err) } @@ -644,7 +646,7 @@ func (s *Service) BatchDelete(ctx context.Context, req *BatchDeleteInjectionReq) } func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*InjectionDetailResp, error) { - original, err := s.repo.LoadInjection(id) + original, err := s.repo.loadInjection(id) if err != nil { if errors.Is(err, consts.ErrNotFound) { return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -669,16 +671,16 @@ func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*Inj Status: consts.CommonEnabled, } - err = s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.CreateInjectionRecord(cloned); err != nil { + err = s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.createInjectionRecord(cloned); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, cloned.Name) } return fmt.Errorf("failed to create injection: %w", err) } if len(req.Labels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -686,7 +688,7 @@ func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*Inj for _, label := range labels { labelIDs = append(labelIDs, label.ID) } - if err := repo.AddInjectionLabels(cloned.ID, labelIDs); err != nil { + if err := repo.addInjectionLabels(cloned.ID, labelIDs); err != nil { return fmt.Errorf("failed to add injection labels: %w", err) } } @@ -696,7 +698,7 @@ func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*Inj return nil, err } - cloned, err = s.repo.GetInjectionWithLabels(cloned.ID) + cloned, err = s.repo.getInjectionWithLabels(cloned.ID) if err != nil { return nil, fmt.Errorf("failed to get cloned injection labels: %w", err) } @@ -704,7 +706,7 @@ func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*Inj } func (s *Service) GetLogs(ctx context.Context, id int) (*InjectionLogsResp, error) { - injection, err := s.repo.LoadInjection(id) + injection, err := s.repo.loadInjection(id) if err != nil { if errors.Is(err, consts.ErrNotFound) { return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -718,7 +720,7 @@ func (s *Service) GetLogs(ctx context.Context, id int) (*InjectionLogsResp, erro } resp.TaskID = *injection.TaskID - task, taskErr := s.repo.LoadTask(*injection.TaskID) + task, taskErr := s.repo.loadTask(*injection.TaskID) if taskErr != nil { return resp, nil } @@ -739,7 +741,7 @@ func (s *Service) GetLogs(ctx context.Context, id int) (*InjectionLogsResp, erro } func (s *Service) GetDatapackFilename(_ context.Context, id int) (string, error) { - injection, err := s.repo.LoadInjection(id) + injection, err := s.repo.loadInjection(id) if err != nil { if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { return "", fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -791,10 +793,134 @@ func (s *Service) QueryDatapackFile(ctx context.Context, id int, filePath string } func (s *Service) UpdateGroundtruth(_ context.Context, id int, req *UpdateGroundtruthReq) error { - if _, err := s.repo.LoadInjection(id); err != nil { + if _, err := s.repo.loadInjection(id); err != nil { return err } - return s.repo.UpdateGroundtruth(id, req.Groundtruths, consts.GroundtruthSourceManual) + return s.repo.updateGroundtruth(id, req.Groundtruths, consts.GroundtruthSourceManual) +} + +func (s *Service) CreateInjectionRecord(_ context.Context, req *RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { + if req == nil { + return nil, fmt.Errorf("runtime create injection request is nil") + } + if req.Name == "" || req.TaskID == "" { + return nil, fmt.Errorf("%w: name and task_id are required", consts.ErrBadRequest) + } + + var created *model.FaultInjection + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + injection := &model.FaultInjection{ + Name: req.Name, + Source: consts.DatapackSourceInjection, + FaultType: req.FaultType, + Category: req.Category, + Description: req.Description, + DisplayConfig: utils.StringPtr(req.DisplayConfig), + EngineConfig: req.EngineConfig, + Groundtruths: req.Groundtruths, + GroundtruthSource: req.GroundtruthSource, + PreDuration: req.PreDuration, + TaskID: utils.StringPtr(req.TaskID), + BenchmarkID: req.BenchmarkID, + PedestalID: req.PedestalID, + State: req.State, + Status: consts.CommonEnabled, + } + + if err := repo.createInjectionRecord(injection); err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + return fmt.Errorf("%w: injection %s already exists", consts.ErrAlreadyExists, req.Name) + } + return err + } + + if len(req.Labels) > 0 { + createdLabels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) + if err != nil { + return fmt.Errorf("failed to create or update labels: %w", err) + } + + labelIDs := make([]int, 0, len(createdLabels)) + for _, label := range createdLabels { + labelIDs = append(labelIDs, label.ID) + } + + if err := repo.addInjectionLabels(injection.ID, labelIDs); err != nil { + return fmt.Errorf("failed to add injection labels: %w", err) + } + } + + created = injection + return nil + }) + if err != nil { + return nil, err + } + + item := dto.NewInjectionItem(created) + return &item, nil +} + +func (s *Service) UpdateInjectionState(_ context.Context, req *RuntimeUpdateInjectionStateReq) error { + if req == nil { + return fmt.Errorf("runtime update injection state request is nil") + } + if req.Name == "" { + return fmt.Errorf("%w: name is required", consts.ErrBadRequest) + } + + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + injection, err := repo.findInjectionByName(req.Name, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: injection %s not found", consts.ErrNotFound, req.Name) + } + return err + } + return repo.updateInjectionFields(injection.ID, map[string]any{"state": req.State}) + }) +} + +func (s *Service) UpdateInjectionTimestamps(_ context.Context, req *RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { + if req == nil { + return nil, fmt.Errorf("runtime update injection timestamp request is nil") + } + if req.Name == "" { + return nil, fmt.Errorf("%w: name is required", consts.ErrBadRequest) + } + + var updated *model.FaultInjection + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + injection, err := repo.findInjectionByName(req.Name, false) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("%w: injection %s not found", consts.ErrNotFound, req.Name) + } + return err + } + if err := repo.updateInjectionFields(injection.ID, map[string]any{ + "start_time": req.StartTime, + "end_time": req.EndTime, + }); err != nil { + return err + } + + reloaded, err := repo.loadInjection(injection.ID) + if err != nil { + return err + } + updated = reloaded + return nil + }) + if err != nil { + return nil, err + } + + item := dto.NewInjectionItem(updated) + return &item, nil } func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file io.Reader, fileSize int64) (*UploadDatapackResp, error) { @@ -810,7 +936,7 @@ func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file return nil, fmt.Errorf("%w: %s", consts.ErrBadRequest, err.Error()) } - existing, _ := s.repo.FindInjectionByName(req.Name, false) + existing, _ := s.repo.findInjectionByName(req.Name, false) if existing != nil { return nil, fmt.Errorf("%w: injection with name %s already exists", consts.ErrAlreadyExists, req.Name) } @@ -874,14 +1000,14 @@ func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file Status: consts.CommonEnabled, } - err = s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.CreateInjectionRecord(injection); err != nil { + err = s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.createInjectionRecord(injection); err != nil { return err } if len(labels) > 0 { - createdLabels, err := common.CreateOrUpdateLabelsFromItems(tx, labels, consts.InjectionCategory) + createdLabels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, labels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -891,7 +1017,7 @@ func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file labelIDs = append(labelIDs, label.ID) } - if err := repo.AddInjectionLabels(injection.ID, labelIDs); err != nil { + if err := repo.addInjectionLabels(injection.ID, labelIDs); err != nil { return fmt.Errorf("failed to add injection labels: %w", err) } } @@ -909,7 +1035,7 @@ func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file } func (s *Service) getReadyDatapack(id int) (*model.FaultInjection, error) { - injection, err := s.repo.LoadInjection(id) + injection, err := s.repo.loadInjection(id) if err != nil { if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: injection id: %d", consts.ErrNotFound, id) @@ -926,9 +1052,9 @@ func (s *Service) batchDeleteByIDs(injectionIDs []int) error { if len(injectionIDs) == 0 { return nil } - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - return repo.DeleteInjectionsCascade(injectionIDs) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + return repo.deleteInjectionsCascade(injectionIDs) }) } @@ -940,7 +1066,7 @@ func (s *Service) batchDeleteByLabels(labelItems []dto.LabelItem) error { for _, item := range labelItems { labelConditions = append(labelConditions, map[string]string{"key": item.Key, "value": item.Value}) } - injectionIDs, err := s.repo.ListInjectionIDsByLabelConditions(labelConditions) + injectionIDs, err := s.repo.listInjectionIDsByLabelConditions(labelConditions) if err != nil { return fmt.Errorf("failed to list injection ids by labels: %w", err) } diff --git a/src/module/injection/submit.go b/src/module/injection/submit.go index 2c64fc3b..1f922c58 100644 --- a/src/module/injection/submit.go +++ b/src/module/injection/submit.go @@ -152,7 +152,7 @@ func (s *Service) removeDuplicated(items []injectionProcessItem) ([]injectionPro existed := make(map[string]struct{}) for start := 0; start < len(keys); start += 100 { end := min(start+100, len(keys)) - existing, err := s.repo.ListExistingEngineConfigs(keys[start:end]) + existing, err := s.repo.listExistingEngineConfigs(keys[start:end]) if err != nil { return nil, nil, nil, err } diff --git a/src/module/label/core.go b/src/module/label/core.go index ba7d03a1..e11ace30 100644 --- a/src/module/label/core.go +++ b/src/module/label/core.go @@ -2,15 +2,18 @@ package labelmodule import ( "aegis/consts" + "aegis/dto" "aegis/model" + "aegis/utils" "errors" "fmt" + "sort" "gorm.io/gorm" ) -func CreateLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { - query := db.Where("label_key = ? AND label_value = ?", label.Key, label.Value). +func (r *Repository) CreateLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { + query := r.useDB(db).Where("label_key = ? AND label_value = ?", label.Key, label.Value). Where("status != ?", consts.CommonDeleted) var existingLabel model.Label @@ -20,7 +23,7 @@ func CreateLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { } if errors.Is(err, gorm.ErrRecordNotFound) { - if err := db.Omit(labelKeyOmitFields).Create(label).Error; err != nil { + if err := r.useDB(db).Omit(labelKeyOmitFields).Create(label).Error; err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, fmt.Errorf("%w: label with key %s and value %s already exists", consts.ErrAlreadyExists, label.Key, label.Value) } @@ -33,8 +36,67 @@ func CreateLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { existingLabel.Description = label.Description existingLabel.Color = label.Color existingLabel.Status = consts.CommonEnabled - if err := db.Omit(labelKeyOmitFields).Save(&existingLabel).Error; err != nil { + if err := r.useDB(db).Omit(labelKeyOmitFields).Save(&existingLabel).Error; err != nil { return nil, fmt.Errorf("failed to update existing label: %w", err) } return &existingLabel, nil } + +func (r *Repository) CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, category consts.LabelCategory) ([]model.Label, error) { + if len(labelItems) == 0 { + return []model.Label{}, nil + } + + repo := r + if db != nil { + repo = NewRepository(db) + } + kvMap := make(map[string]dto.LabelItem, len(labelItems)) + for _, item := range labelItems { + kvMap[item.Key] = item + } + + existingLabels, err := repo.listLabelsByConditions(dto.ConvertLabelItemsToConditions(labelItems)) + if err != nil { + return nil, fmt.Errorf("failed to find existing labels: %w", err) + } + + result := make([]model.Label, 0, len(labelItems)) + existingIDs := make([]int, 0, len(existingLabels)) + for _, existing := range existingLabels { + if item, ok := kvMap[existing.Key]; ok && item.Value == existing.Value { + result = append(result, existing) + existingIDs = append(existingIDs, existing.ID) + delete(kvMap, existing.Key) + } + } + + if len(existingIDs) > 0 { + if err := repo.batchIncreaseLabelUsages(existingIDs, 1); err != nil { + return nil, fmt.Errorf("failed to increase usage for existing labels: %w", err) + } + } + + if len(kvMap) > 0 { + newLabels := make([]model.Label, 0, len(kvMap)) + for key, item := range kvMap { + newLabels = append(newLabels, model.Label{ + Key: key, + Value: item.Value, + Category: category, + Description: fmt.Sprintf(consts.CustomLabelDescriptionTemplate, key, consts.GetLabelCategoryName(category)), + Color: utils.GenerateColorFromKey(key), + Usage: consts.DefaultLabelUsage, + IsSystem: item.IsSystem, + Status: consts.CommonEnabled, + }) + } + if err := repo.batchCreateLabels(newLabels); err != nil { + return nil, fmt.Errorf("failed to create new labels: %w", err) + } + result = append(result, newLabels...) + } + + sort.Slice(result, func(i, j int) bool { return result[i].ID < result[j].ID }) + return result, nil +} diff --git a/src/module/label/handler.go b/src/module/label/handler.go index b35516d7..0c96f8f8 100644 --- a/src/module/label/handler.go +++ b/src/module/label/handler.go @@ -12,10 +12,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { return &Handler{service: service} } +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } // BatchDeleteLabels handles batch deletion of labels // diff --git a/src/module/label/handler_service.go b/src/module/label/handler_service.go new file mode 100644 index 00000000..733d7d93 --- /dev/null +++ b/src/module/label/handler_service.go @@ -0,0 +1,21 @@ +package labelmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the label operations consumed by HTTP and resource gRPC handlers. +type HandlerService interface { + BatchDelete(context.Context, []int) error + Create(context.Context, *CreateLabelReq) (*LabelResp, error) + Delete(context.Context, int) error + GetDetail(context.Context, int) (*LabelDetailResp, error) + List(context.Context, *ListLabelReq) (*dto.ListResp[LabelResp], error) + Update(context.Context, *UpdateLabelReq, int) (*LabelResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/label/module.go b/src/module/label/module.go index 2f705aa4..46c7b518 100644 --- a/src/module/label/module.go +++ b/src/module/label/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("label", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/label/repository.go b/src/module/label/repository.go index e38e816c..9baea135 100644 --- a/src/module/label/repository.go +++ b/src/module/label/repository.go @@ -5,6 +5,7 @@ import ( "aegis/model" "errors" "fmt" + "strings" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -25,10 +26,6 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - func (r *Repository) ListLabelsByID(db *gorm.DB, labelIDs []int) ([]model.Label, error) { if len(labelIDs) == 0 { return []model.Label{}, nil @@ -84,6 +81,55 @@ func (r *Repository) GetLabelByKeyAndValue(db *gorm.DB, key, value string, statu return &label, nil } +func (r *Repository) batchCreateLabels(labels []model.Label) error { + if len(labels) == 0 { + return nil + } + if err := r.db.Omit(labelKeyOmitFields).Create(&labels).Error; err != nil { + return fmt.Errorf("failed to batch upsert labels: %w", err) + } + return nil +} + +func (r *Repository) batchIncreaseLabelUsages(labelIDs []int, increment int) error { + if len(labelIDs) == 0 { + return nil + } + + expr := gorm.Expr("usage_count + ?", increment) + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + UpdateColumn("usage_count", expr).Error; err != nil { + return fmt.Errorf("failed to batch increase label usages: %w", err) + } + return nil +} + +func (r *Repository) listLabelsByConditions(conditions []map[string]string) ([]model.Label, error) { + if len(conditions) == 0 { + return []model.Label{}, nil + } + + var labels []model.Label + query := r.db.Model(&model.Label{}) + var whereClauses []string + var whereArgs []any + + for _, condition := range conditions { + whereClauses = append(whereClauses, "(label_key = ? AND label_value = ?)") + whereArgs = append(whereArgs, condition["key"], condition["value"]) + } + + if len(whereClauses) > 0 { + query = query.Where(strings.Join(whereClauses, " OR "), whereArgs...) + } + + if err := query.Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels by conditions: %w", err) + } + return labels, nil +} + func (r *Repository) CreateLabel(db *gorm.DB, label *model.Label) error { if err := r.useDB(db).Omit(labelKeyOmitFields).Create(label).Error; err != nil { return fmt.Errorf("failed to create label: %w", err) diff --git a/src/module/label/service.go b/src/module/label/service.go index 227f1658..3cb26410 100644 --- a/src/module/label/service.go +++ b/src/module/label/service.go @@ -25,7 +25,7 @@ func (s *Service) BatchDelete(_ context.Context, ids []int) error { return nil } - return s.repo.Transaction(func(tx *gorm.DB) error { + return s.repo.db.Transaction(func(tx *gorm.DB) error { labels, err := s.repo.ListLabelsByID(tx, ids) if err != nil { return fmt.Errorf("failed to list labels by IDs: %w", err) @@ -92,7 +92,7 @@ func (s *Service) Create(_ context.Context, req *CreateLabelReq) (*LabelResp, er label := req.ConvertToLabel() var createdLabel *model.Label - err := s.repo.Transaction(func(tx *gorm.DB) error { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { item, err := s.createLabelCore(tx, label) if err != nil { return fmt.Errorf("failed to create label: %w", err) @@ -108,7 +108,7 @@ func (s *Service) Create(_ context.Context, req *CreateLabelReq) (*LabelResp, er } func (s *Service) Delete(_ context.Context, id int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { + return s.repo.db.Transaction(func(tx *gorm.DB) error { label, err := s.repo.GetLabelByID(tx, id) if err != nil { if errors.Is(err, consts.ErrNotFound) { @@ -188,7 +188,7 @@ func (s *Service) Update(_ context.Context, req *UpdateLabelReq, id int) (*Label } var updatedLabel *model.Label - err := s.repo.Transaction(func(tx *gorm.DB) error { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { existingLabel, err := s.repo.GetLabelByID(tx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -218,7 +218,7 @@ type labelRemovalOps struct { } func (s *Service) createLabelCore(db *gorm.DB, label *model.Label) (*model.Label, error) { - return CreateLabelCore(db, label) + return s.repo.CreateLabelCore(db, label) } func (s *Service) removeAssociationsFromLabels(db *gorm.DB, labelIDs []int, ops labelRemovalOps) (map[int]int64, error) { diff --git a/src/module/metric/handler.go b/src/module/metric/handler.go index 7ff39f80..624b8137 100644 --- a/src/module/metric/handler.go +++ b/src/module/metric/handler.go @@ -10,10 +10,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/metric/handler_service.go b/src/module/metric/handler_service.go new file mode 100644 index 00000000..faa0aefa --- /dev/null +++ b/src/module/metric/handler_service.go @@ -0,0 +1,14 @@ +package metricmodule + +import "context" + +// HandlerService captures the metric operations consumed by the HTTP handler. +type HandlerService interface { + GetInjectionMetrics(context.Context, *GetMetricsReq) (*InjectionMetrics, error) + GetExecutionMetrics(context.Context, *GetMetricsReq) (*ExecutionMetrics, error) + GetAlgorithmMetrics(context.Context, *GetMetricsReq) (*AlgorithmMetrics, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/metric/module.go b/src/module/metric/module.go index 085fd509..3efa27a2 100644 --- a/src/module/metric/module.go +++ b/src/module/metric/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("metric", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/metric/service.go b/src/module/metric/service.go index f8837dd9..4c8add1d 100644 --- a/src/module/metric/service.go +++ b/src/module/metric/service.go @@ -104,6 +104,9 @@ func (s *Service) GetAlgorithmMetrics(_ context.Context, req *GetMetricsReq) (*A } for _, algo := range algorithms { + if req.AlgorithmID != nil && algo.ID != *req.AlgorithmID { + continue + } executions, err := s.repo.ListExecutions(func(db *gorm.DB) *gorm.DB { query := db.Where("algorithm_id = ?", algo.ID) if req.StartTime != nil { diff --git a/src/module/notification/handler.go b/src/module/notification/handler.go index 168536de..92f99142 100644 --- a/src/module/notification/handler.go +++ b/src/module/notification/handler.go @@ -17,10 +17,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/notification/handler_service.go b/src/module/notification/handler_service.go new file mode 100644 index 00000000..725492d2 --- /dev/null +++ b/src/module/notification/handler_service.go @@ -0,0 +1,17 @@ +package notificationmodule + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +// HandlerService captures notification stream operations consumed by HTTP handlers and gateway adapters. +type HandlerService interface { + ReadStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/notification/module.go b/src/module/notification/module.go index b081c0cf..2c4c8fc2 100644 --- a/src/module/notification/module.go +++ b/src/module/notification/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("notification", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/project/api_types.go b/src/module/project/api_types.go index 913f604e..3e72b992 100644 --- a/src/module/project/api_types.go +++ b/src/module/project/api_types.go @@ -47,14 +47,19 @@ func (req *CreateProjectReq) ConvertToProject() *model.Project { // ListProjectReq represents project list query parameters. type ListProjectReq struct { dto.PaginationReq - IsPublic *bool `form:"is_public" binding:"omitempty"` - Status *consts.StatusType `form:"status" binding:"omitempty"` + IsPublic *bool `form:"is_public" binding:"omitempty"` + Status *consts.StatusType `form:"status" binding:"omitempty"` + TeamID *int `form:"team_id" binding:"omitempty"` + IncludeStatistics *bool `form:"include_statistics" binding:"omitempty"` } func (req *ListProjectReq) Validate() error { if err := req.PaginationReq.Validate(); err != nil { return err } + if req.TeamID != nil && *req.TeamID <= 0 { + return fmt.Errorf("team_id must be greater than 0") + } return validateStatus(req.Status, false) } diff --git a/src/module/project/handler.go b/src/module/project/handler.go index 537f1f79..7fd1e109 100644 --- a/src/module/project/handler.go +++ b/src/module/project/handler.go @@ -13,10 +13,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/project/handler_service.go b/src/module/project/handler_service.go new file mode 100644 index 00000000..83987f93 --- /dev/null +++ b/src/module/project/handler_service.go @@ -0,0 +1,21 @@ +package projectmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the project operations consumed by the HTTP handler. +type HandlerService interface { + CreateProject(context.Context, *CreateProjectReq, int) (*ProjectResp, error) + DeleteProject(context.Context, int) error + GetProjectDetail(context.Context, int) (*ProjectDetailResp, error) + ListProjects(context.Context, *ListProjectReq) (*dto.ListResp[ProjectResp], error) + UpdateProject(context.Context, *UpdateProjectReq, int) (*ProjectResp, error) + ManageProjectLabels(context.Context, *ManageProjectLabelReq, int) (*ProjectResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/project/module.go b/src/module/project/module.go index 3187e816..5acbdeae 100644 --- a/src/module/project/module.go +++ b/src/module/project/module.go @@ -7,7 +7,9 @@ import ( var Module = fx.Module("project", fx.Provide( NewRepository, + newProjectStatisticsSource, NewService, + AsHandlerService, NewHandler, ), ) diff --git a/src/module/project/project_statistics.go b/src/module/project/project_statistics.go new file mode 100644 index 00000000..1a6689d9 --- /dev/null +++ b/src/module/project/project_statistics.go @@ -0,0 +1,61 @@ +package projectmodule + +import ( + "context" + "fmt" + + "aegis/dto" + "aegis/internalclient/orchestratorclient" + + "go.uber.org/fx" +) + +type projectStatisticsSource interface { + ListProjectStatistics(context.Context, []int) (map[int]*dto.ProjectStatistics, error) +} + +type projectStatisticsSourceParams struct { + fx.In + + Repository *Repository + Orchestrator *orchestratorclient.Client `optional:"true"` +} + +type projectStatisticsAdapter struct { + orchestrator *orchestratorclient.Client + repository *Repository + requireRemote bool +} + +func newProjectStatisticsSource(params projectStatisticsSourceParams) projectStatisticsSource { + return projectStatisticsAdapter{ + orchestrator: params.Orchestrator, + repository: params.Repository, + } +} + +func newRemoteProjectStatisticsSource(params projectStatisticsSourceParams) projectStatisticsSource { + return projectStatisticsAdapter{ + orchestrator: params.Orchestrator, + repository: params.Repository, + requireRemote: true, + } +} + +func (a projectStatisticsAdapter) ListProjectStatistics(ctx context.Context, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.ListProjectStatistics(ctx, projectIDs) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service project statistics source is not configured") + } + if a.repository == nil { + return nil, fmt.Errorf("project statistics source is not configured") + } + return a.repository.ListProjectStatistics(projectIDs) +} + +// RemoteStatisticsOption forces the dedicated resource-service path to use orchestrator RPC only. +func RemoteStatisticsOption() fx.Option { + return fx.Decorate(newRemoteProjectStatisticsSource) +} diff --git a/src/module/project/repository.go b/src/module/project/repository.go index 57204569..28ed5b9d 100644 --- a/src/module/project/repository.go +++ b/src/module/project/repository.go @@ -18,14 +18,6 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - func (r *Repository) createProjectWithOwner(project *model.Project, userID int) error { var role model.Role if err := r.db.Where("name = ? AND status != ?", consts.RoleProjectAdmin.String(), consts.CommonDeleted). @@ -64,32 +56,32 @@ func (r *Repository) deleteProjectCascade(projectID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) loadProjectDetail(projectID int) (*model.Project, *dto.ProjectStatistics, int, error) { +func (r *Repository) loadProjectDetailBase(projectID int) (*model.Project, int, error) { project, err := r.loadProjectRecord(projectID) if err != nil { - return nil, nil, 0, err + return nil, 0, err } - statsMap, err := r.listProjectStatistics([]int{project.ID}) - if err != nil { - return nil, nil, 0, err - } - - userCount, err := r.countProjectUsers(project.ID) - if err != nil { - return nil, nil, 0, err + var userCount int64 + if err := r.db.Model(&model.UserProject{}). + Where("project_id = ? AND status = ?", project.ID, consts.CommonEnabled). + Count(&userCount).Error; err != nil { + return nil, 0, err } - return project, statsMap[project.ID], userCount, nil + return project, int(userCount), nil } -func (r *Repository) listProjectViews(limit, offset int, isPublic *bool, status *consts.StatusType) ([]model.Project, map[int]*dto.ProjectStatistics, int64, error) { +func (r *Repository) listProjectViews(limit, offset int, isPublic *bool, status *consts.StatusType, teamID *int) ([]model.Project, int64, error) { var ( projects []model.Project total int64 ) query := r.db.Model(&model.Project{}) + if teamID != nil { + query = query.Where("team_id = ?", *teamID) + } if isPublic != nil { query = query.Where("is_public = ?", *isPublic) } @@ -98,10 +90,10 @@ func (r *Repository) listProjectViews(limit, offset int, isPublic *bool, status } if err := query.Count(&total).Error; err != nil { - return nil, nil, 0, fmt.Errorf("failed to count projects: %w", err) + return nil, 0, fmt.Errorf("failed to count projects: %w", err) } if err := query.Limit(limit).Offset(offset).Find(&projects).Error; err != nil { - return nil, nil, 0, fmt.Errorf("failed to list projects: %w", err) + return nil, 0, fmt.Errorf("failed to list projects: %w", err) } projectIDs := make([]int, 0, len(projects)) @@ -109,21 +101,35 @@ func (r *Repository) listProjectViews(limit, offset int, isPublic *bool, status projectIDs = append(projectIDs, project.ID) } - labelsMap, err := r.listProjectLabels(projectIDs) - if err != nil { - return nil, nil, 0, err + type projectLabelResult struct { + model.Label + ProjectID int `gorm:"column:project_id"` } - statsMap, err := r.listProjectStatistics(projectIDs) - if err != nil { - return nil, nil, 0, err + labelsMap := make(map[int][]model.Label, len(projectIDs)) + for _, projectID := range projectIDs { + labelsMap[projectID] = []model.Label{} + } + if len(projectIDs) > 0 { + var flatResults []projectLabelResult + if err := r.db.Model(&model.Label{}). + Joins("JOIN project_labels pl ON pl.label_id = labels.id"). + Where("pl.project_id IN (?)", projectIDs). + Select("labels.*, pl.project_id"). + Find(&flatResults).Error; err != nil { + return nil, 0, fmt.Errorf("failed to batch query project labels: %w", err) + } + + for _, result := range flatResults { + labelsMap[result.ProjectID] = append(labelsMap[result.ProjectID], result.Label) + } } for i := range projects { projects[i].Labels = labelsMap[projects[i].ID] } - return projects, statsMap, total, nil + return projects, total, nil } func (r *Repository) updateMutableProject(projectID int, patch func(*model.Project)) (*model.Project, error) { @@ -143,62 +149,52 @@ func (r *Repository) manageProjectLabels(projectID int, addLabelIDs []int, remov if err != nil { return nil, err } - if err := r.addProjectLabels(projectID, addLabelIDs); err != nil { - return nil, err - } - if err := r.removeProjectLabelsByKeys(projectID, removeKeys); err != nil { - return nil, err - } - labels, err := r.listLabelsByProjectID(project.ID) - if err != nil { - return nil, err - } - project.Labels = labels - return project, nil -} -func (r *Repository) addProjectLabels(projectID int, labelIDs []int) error { - if len(labelIDs) == 0 { - return nil + if len(addLabelIDs) > 0 { + projectLabels := make([]model.ProjectLabel, 0, len(addLabelIDs)) + for _, labelID := range addLabelIDs { + projectLabels = append(projectLabels, model.ProjectLabel{ + ProjectID: projectID, + LabelID: labelID, + }) + } + if err := r.db.Create(&projectLabels).Error; err != nil { + return nil, fmt.Errorf("failed to add project-label associations: %w", err) + } + } + + if len(removeKeys) > 0 { + var labelIDs []int + if err := r.db.Table("labels l"). + Select("l.id"). + Joins("JOIN project_labels pl ON pl.label_id = l.id"). + Where("pl.project_id = ? AND l.label_key IN (?)", projectID, removeKeys). + Pluck("l.id", &labelIDs).Error; err != nil { + return nil, fmt.Errorf("failed to find label IDs by key '%v': %w", removeKeys, err) + } + if len(labelIDs) > 0 { + if err := r.db.Table("project_labels"). + Where("project_id = ? AND label_id IN (?)", projectID, labelIDs). + Delete(nil).Error; err != nil { + return nil, fmt.Errorf("failed to clear project labels: %w", err) + } + if err := r.db.Model(&model.Label{}). + Where("id IN (?)", labelIDs). + UpdateColumn("usage_count", gorm.Expr("GREATEST(0, usage_count - ?)", 1)).Error; err != nil { + return nil, fmt.Errorf("failed to decrease label usage counts: %w", err) + } + } } - projectLabels := make([]model.ProjectLabel, 0, len(labelIDs)) - for _, labelID := range labelIDs { - projectLabels = append(projectLabels, model.ProjectLabel{ - ProjectID: projectID, - LabelID: labelID, - }) - } - if err := r.db.Create(&projectLabels).Error; err != nil { - return fmt.Errorf("failed to add project-label associations: %w", err) - } - return nil -} - -func (r *Repository) removeProjectLabelsByKeys(projectID int, keys []string) error { - if len(keys) == 0 { - return nil - } - - labelIDs, err := r.listProjectLabelIDsByKeys(projectID, keys) - if err != nil { - return fmt.Errorf("failed to find label ids by keys: %w", err) - } - if len(labelIDs) == 0 { - return nil - } - - if err := r.db.Table("project_labels"). - Where("project_id = ? AND label_id IN (?)", projectID, labelIDs). - Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear project labels: %w", err) - } + var labels []model.Label if err := r.db.Model(&model.Label{}). - Where("id IN (?)", labelIDs). - UpdateColumn("usage_count", gorm.Expr("GREATEST(0, usage_count - ?)", 1)).Error; err != nil { - return fmt.Errorf("failed to decrease label usage counts: %w", err) + Joins("JOIN project_labels pl ON pl.label_id = labels.id"). + Where("pl.project_id = ?", project.ID). + Find(&labels).Error; err != nil { + return nil, fmt.Errorf("failed to list labels for project %d: %w", project.ID, err) } - return nil + project.Labels = labels + return project, nil } func (r *Repository) loadProjectRecord(projectID int) (*model.Project, error) { @@ -209,17 +205,7 @@ func (r *Repository) loadProjectRecord(projectID int) (*model.Project, error) { return &project, nil } -func (r *Repository) countProjectUsers(projectID int) (int, error) { - var userCount int64 - if err := r.db.Model(&model.UserProject{}). - Where("project_id = ? AND status = ?", projectID, consts.CommonEnabled). - Count(&userCount).Error; err != nil { - return 0, fmt.Errorf("failed to count project users: %w", err) - } - return int(userCount), nil -} - -func (r *Repository) listProjectStatistics(projectIDs []int) (map[int]*dto.ProjectStatistics, error) { +func (r *Repository) ListProjectStatistics(projectIDs []int) (map[int]*dto.ProjectStatistics, error) { statsMap := make(map[int]*dto.ProjectStatistics, len(projectIDs)) for _, projectID := range projectIDs { statsMap[projectID] = &dto.ProjectStatistics{} @@ -237,7 +223,7 @@ func (r *Repository) listProjectStatistics(projectIDs []int) (map[int]*dto.Proje Select("tr.project_id, COUNT(*) as count, MAX(fi.updated_at) as last_at"). Joins("JOIN tasks t ON fi.task_id = t.id"). Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). + Where("tr.project_id IN (?) AND fi.status != ?", projectIDs, consts.CommonDeleted). Group("tr.project_id"). Scan(&injectionStats).Error; err != nil { return nil, fmt.Errorf("failed to batch get injection statistics: %w", err) @@ -256,7 +242,7 @@ func (r *Repository) listProjectStatistics(projectIDs []int) (map[int]*dto.Proje Select("tr.project_id, COUNT(*) as count, MAX(e.updated_at) as last_at"). Joins("JOIN tasks t ON e.task_id = t.id"). Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). + Where("tr.project_id IN (?) AND e.status != ?", projectIDs, consts.CommonDeleted). Group("tr.project_id"). Scan(&executionStats).Error; err != nil { return nil, fmt.Errorf("failed to batch get execution statistics: %w", err) @@ -268,55 +254,3 @@ func (r *Repository) listProjectStatistics(projectIDs []int) (map[int]*dto.Proje return statsMap, nil } - -func (r *Repository) listProjectLabels(projectIDs []int) (map[int][]model.Label, error) { - labelsMap := make(map[int][]model.Label, len(projectIDs)) - for _, projectID := range projectIDs { - labelsMap[projectID] = []model.Label{} - } - if len(projectIDs) == 0 { - return labelsMap, nil - } - - type projectLabelResult struct { - model.Label - ProjectID int `gorm:"column:project_id"` - } - - var flatResults []projectLabelResult - if err := r.db.Model(&model.Label{}). - Joins("JOIN project_labels pl ON pl.label_id = labels.id"). - Where("pl.project_id IN (?)", projectIDs). - Select("labels.*, pl.project_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query project labels: %w", err) - } - - for _, result := range flatResults { - labelsMap[result.ProjectID] = append(labelsMap[result.ProjectID], result.Label) - } - return labelsMap, nil -} - -func (r *Repository) listLabelsByProjectID(projectID int) ([]model.Label, error) { - var labels []model.Label - if err := r.db.Model(&model.Label{}). - Joins("JOIN project_labels pl ON pl.label_id = labels.id"). - Where("pl.project_id = ?", projectID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels for project %d: %w", projectID, err) - } - return labels, nil -} - -func (r *Repository) listProjectLabelIDsByKeys(projectID int, keys []string) ([]int, error) { - var labelIDs []int - if err := r.db.Table("labels l"). - Select("l.id"). - Joins("JOIN project_labels pl ON pl.label_id = l.id"). - Where("pl.project_id = ? AND l.label_key IN (?)", projectID, keys). - Pluck("l.id", &labelIDs).Error; err != nil { - return nil, fmt.Errorf("failed to find label IDs by key '%v': %w", keys, err) - } - return labelIDs, nil -} diff --git a/src/module/project/service.go b/src/module/project/service.go index 7759cf44..24ea94ed 100644 --- a/src/module/project/service.go +++ b/src/module/project/service.go @@ -8,17 +8,21 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/service/common" + labelmodule "aegis/module/label" "gorm.io/gorm" ) type Service struct { repository *Repository + stats projectStatisticsSource } -func NewService(repository *Repository) *Service { - return &Service{repository: repository} +func NewService(repository *Repository, stats projectStatisticsSource) *Service { + return &Service{ + repository: repository, + stats: stats, + } } func (s *Service) CreateProject(ctx context.Context, req *CreateProjectReq, userID int) (*ProjectResp, error) { @@ -29,8 +33,8 @@ func (s *Service) CreateProject(ctx context.Context, req *CreateProjectReq, user project := req.ConvertToProject() var createdProject *model.Project - err := s.repository.Transaction(func(tx *gorm.DB) error { - if err := s.repository.withDB(tx).createProjectWithOwner(project, userID); err != nil { + err := s.repository.db.Transaction(func(tx *gorm.DB) error { + if err := NewRepository(tx).createProjectWithOwner(project, userID); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: project with name %s already exists", consts.ErrAlreadyExists, project.Name) } @@ -50,8 +54,8 @@ func (s *Service) CreateProject(ctx context.Context, req *CreateProjectReq, user } func (s *Service) DeleteProject(ctx context.Context, projectID int) error { - return s.repository.Transaction(func(tx *gorm.DB) error { - rows, err := s.repository.withDB(tx).deleteProjectCascade(projectID) + return s.repository.db.Transaction(func(tx *gorm.DB) error { + rows, err := NewRepository(tx).deleteProjectCascade(projectID) if err != nil { return err } @@ -64,13 +68,21 @@ func (s *Service) DeleteProject(ctx context.Context, projectID int) error { } func (s *Service) GetProjectDetail(ctx context.Context, projectID int) (*ProjectDetailResp, error) { - project, stats, userCount, err := s.repository.loadProjectDetail(projectID) + project, userCount, err := s.repository.loadProjectDetailBase(projectID) if err != nil { if errors.Is(err, consts.ErrNotFound) { return nil, fmt.Errorf("%w: project with ID %d not found", consts.ErrNotFound, projectID) } return nil, fmt.Errorf("failed to get project: %w", err) } + statsMap, err := s.stats.ListProjectStatistics(ctx, []int{project.ID}) + if err != nil { + return nil, fmt.Errorf("failed to get project statistics: %w", err) + } + stats := statsMap[project.ID] + if stats == nil { + stats = &dto.ProjectStatistics{} + } resp := NewProjectDetailResp(project, stats) resp.UserCount = userCount @@ -83,12 +95,33 @@ func (s *Service) ListProjects(ctx context.Context, req *ListProjectReq) (*dto.L } limit, offset := req.ToGormParams() + includeStatistics := req.IncludeStatistics == nil || *req.IncludeStatistics - projects, statsMap, total, err := s.repository.listProjectViews(limit, offset, req.IsPublic, req.Status) + projects, total, err := s.repository.listProjectViews(limit, offset, req.IsPublic, req.Status, req.TeamID) if err != nil { return nil, fmt.Errorf("failed to list projects: %w", err) } + statsMap := make(map[int]*dto.ProjectStatistics, len(projects)) + for i := range projects { + statsMap[projects[i].ID] = &dto.ProjectStatistics{} + } + if includeStatistics && len(projects) > 0 { + projectIDs := make([]int, 0, len(projects)) + for i := range projects { + projectIDs = append(projectIDs, projects[i].ID) + } + statsMap, err = s.stats.ListProjectStatistics(ctx, projectIDs) + if err != nil { + return nil, fmt.Errorf("failed to list project statistics: %w", err) + } + for _, projectID := range projectIDs { + if statsMap[projectID] == nil { + statsMap[projectID] = &dto.ProjectStatistics{} + } + } + } + projectResps := make([]ProjectResp, 0, len(projects)) for i := range projects { var stats *dto.ProjectStatistics @@ -118,8 +151,8 @@ func (s *Service) UpdateProject(ctx context.Context, req *UpdateProjectReq, proj var updatedProject *model.Project - err := s.repository.Transaction(func(tx *gorm.DB) error { - project, err := s.repository.withDB(tx).updateMutableProject(projectID, func(existingProject *model.Project) { + err := s.repository.db.Transaction(func(tx *gorm.DB) error { + project, err := NewRepository(tx).updateMutableProject(projectID, func(existingProject *model.Project) { req.PatchProjectModel(existingProject) }) if err != nil { @@ -141,11 +174,11 @@ func (s *Service) ManageProjectLabels(ctx context.Context, req *ManageProjectLab } var managedProject *model.Project - err := s.repository.Transaction(func(tx *gorm.DB) error { - repo := s.repository.withDB(tx) + err := s.repository.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) addLabelIDs := make([]int, 0, len(req.AddLabels)) if len(req.AddLabels) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) + labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/project/service_test.go b/src/module/project/service_test.go index dbfccd2a..613cea25 100644 --- a/src/module/project/service_test.go +++ b/src/module/project/service_test.go @@ -24,7 +24,9 @@ func newProjectService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { }), &gorm.Config{}) require.NoError(t, err) - return NewService(NewRepository(db)), mock, func() { + repo := NewRepository(db) + stats := newProjectStatisticsSource(projectStatisticsSourceParams{Repository: repo}) + return NewService(repo, stats), mock, func() { _ = sqlDB.Close() } } @@ -50,11 +52,11 @@ func TestProjectServiceListProjectsSuccess(t *testing.T) { WillReturnRows(sqlmock.NewRows([]string{ "id", "label_key", "label_value", "category", "description", "color", "usage_count", "is_system", "status", "created_at", "updated_at", "project_id", }).AddRow(10, "env", "prod", consts.ProjectCategory, "", "#1890ff", 1, false, consts.CommonEnabled, now, now, 1)) - mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). - WithArgs(1). + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) AND fi\\.status != \\? GROUP BY `tr`\\.`project_id`"). + WithArgs(1, consts.CommonDeleted). WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 2, now)) - mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). - WithArgs(1). + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) AND e\\.status != \\? GROUP BY `tr`\\.`project_id`"). + WithArgs(1, consts.CommonDeleted). WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 3, now)) resp, err := service.ListProjects(t.Context(), &ListProjectReq{ @@ -113,15 +115,15 @@ func TestProjectServiceGetProjectDetailSuccess(t *testing.T) { WillReturnRows(sqlmock.NewRows([]string{ "id", "name", "description", "team_id", "is_public", "status", "created_at", "updated_at", }).AddRow(1, "demo-project", "demo", nil, true, consts.CommonEnabled, now, now)) - mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). - WithArgs(1). - WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 2, now)) - mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) GROUP BY `tr`\\.`project_id`"). - WithArgs(1). - WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 3, now)) mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `user_projects` WHERE project_id = ? AND status = ?")). WithArgs(1, consts.CommonEnabled). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(4)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(fi\\.updated_at\\) as last_at FROM fault_injections fi .* WHERE tr\\.project_id IN \\(\\?\\) AND fi\\.status != \\? GROUP BY `tr`\\.`project_id`"). + WithArgs(1, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 2, now)) + mock.ExpectQuery("SELECT tr\\.project_id, COUNT\\(\\*\\) as count, MAX\\(e\\.updated_at\\) as last_at FROM executions e .* WHERE tr\\.project_id IN \\(\\?\\) AND e\\.status != \\? GROUP BY `tr`\\.`project_id`"). + WithArgs(1, consts.CommonDeleted). + WillReturnRows(sqlmock.NewRows([]string{"project_id", "count", "last_at"}).AddRow(1, 3, now)) resp, err := service.GetProjectDetail(t.Context(), 1) @@ -185,7 +187,7 @@ func TestProjectServiceDeleteProjectSuccess(t *testing.T) { } func TestProjectServiceManageLabelsNilRequest(t *testing.T) { - service := NewService(nil) + service := NewService(nil, nil) _, err := service.ManageProjectLabels(t.Context(), nil, 1) diff --git a/src/module/rbac/handler.go b/src/module/rbac/handler.go index 7a506e53..1d436700 100644 --- a/src/module/rbac/handler.go +++ b/src/module/rbac/handler.go @@ -12,10 +12,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/rbac/handler_service.go b/src/module/rbac/handler_service.go new file mode 100644 index 00000000..df23835e --- /dev/null +++ b/src/module/rbac/handler_service.go @@ -0,0 +1,29 @@ +package rbacmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the RBAC operations consumed by the HTTP handler. +type HandlerService interface { + CreateRole(context.Context, *CreateRoleReq) (*RoleResp, error) + DeleteRole(context.Context, int) error + GetRole(context.Context, int) (*RoleDetailResp, error) + ListRoles(context.Context, *ListRoleReq) (*dto.ListResp[RoleResp], error) + UpdateRole(context.Context, *UpdateRoleReq, int) (*RoleResp, error) + AssignRolePermissions(context.Context, []int, int) error + RemoveRolePermissions(context.Context, []int, int) error + ListUsersFromRole(context.Context, int) ([]UserListItem, error) + GetPermission(context.Context, int) (*PermissionDetailResp, error) + ListPermissions(context.Context, *ListPermissionReq) (*dto.ListResp[PermissionResp], error) + ListRolesFromPermission(context.Context, int) ([]RoleResp, error) + GetResource(context.Context, int) (*ResourceResp, error) + ListResources(context.Context, *ListResourceReq) (*dto.ListResp[ResourceResp], error) + ListResourcePermissions(context.Context, int) ([]PermissionResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/rbac/module.go b/src/module/rbac/module.go index 68d583f0..e4a268bd 100644 --- a/src/module/rbac/module.go +++ b/src/module/rbac/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("rbac", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/rbac/repository.go b/src/module/rbac/repository.go index ceb74125..6bcda444 100644 --- a/src/module/rbac/repository.go +++ b/src/module/rbac/repository.go @@ -16,14 +16,6 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - func (r *Repository) createRoleRecord(role *model.Role) error { if err := r.db.Create(role).Error; err != nil { return fmt.Errorf("failed to create role: %w", err) @@ -133,34 +125,7 @@ func (r *Repository) updateMutableRole(roleID int, patch func(*model.Role)) (*mo return role, nil } -func (r *Repository) loadAssignablePermissions(permissionIDs []int) (map[int]model.Permission, error) { - if len(permissionIDs) == 0 { - return map[int]model.Permission{}, nil - } - - unique := make(map[int]struct{}, len(permissionIDs)) - for _, id := range permissionIDs { - unique[id] = struct{}{} - } - - deduplicatedIDs := make([]int, 0, len(unique)) - for id := range unique { - deduplicatedIDs = append(deduplicatedIDs, id) - } - - permissions, err := r.listPermissionsByIDs(deduplicatedIDs) - if err != nil { - return nil, fmt.Errorf("failed to list permissions by ids: %w", err) - } - - result := make(map[int]model.Permission, len(permissions)) - for _, permission := range permissions { - result[permission.ID] = permission - } - return result, nil -} - -func (r *Repository) AssignRolePermissions(roleID int, permissionIDs []int) error { +func (r *Repository) assignRolePermissions(roleID int, permissionIDs []int) error { role, err := r.loadRole(roleID) if err != nil { return err @@ -169,7 +134,7 @@ func (r *Repository) AssignRolePermissions(roleID int, permissionIDs []int) erro return fmt.Errorf("%w: cannot assign permissions to system role", consts.ErrPermissionDenied) } - permissionMap, err := r.loadAssignablePermissions(permissionIDs) + permissionMap, err := r.buildAssignablePermissionMap(permissionIDs) if err != nil { return err } @@ -194,7 +159,7 @@ func (r *Repository) AssignRolePermissions(roleID int, permissionIDs []int) erro return nil } -func (r *Repository) RemoveRolePermissions(roleID int, permissionIDs []int) error { +func (r *Repository) removeRolePermissions(roleID int, permissionIDs []int) error { role, err := r.loadRole(roleID) if err != nil { return err @@ -203,7 +168,7 @@ func (r *Repository) RemoveRolePermissions(roleID int, permissionIDs []int) erro return fmt.Errorf("%w: cannot remove permissions of system role", consts.ErrPermissionDenied) } - permissionMap, err := r.loadAssignablePermissions(permissionIDs) + permissionMap, err := r.buildAssignablePermissionMap(permissionIDs) if err != nil { return err } @@ -356,3 +321,30 @@ func (r *Repository) listPermissionsByIDs(permissionIDs []int) ([]model.Permissi } return permissions, nil } + +func (r *Repository) buildAssignablePermissionMap(permissionIDs []int) (map[int]model.Permission, error) { + if len(permissionIDs) == 0 { + return map[int]model.Permission{}, nil + } + + unique := make(map[int]struct{}, len(permissionIDs)) + for _, id := range permissionIDs { + unique[id] = struct{}{} + } + + deduplicatedIDs := make([]int, 0, len(unique)) + for id := range unique { + deduplicatedIDs = append(deduplicatedIDs, id) + } + + permissions, err := r.listPermissionsByIDs(deduplicatedIDs) + if err != nil { + return nil, fmt.Errorf("failed to list permissions by ids: %w", err) + } + + result := make(map[int]model.Permission, len(permissions)) + for _, permission := range permissions { + result[permission.ID] = permission + } + return result, nil +} diff --git a/src/module/rbac/service.go b/src/module/rbac/service.go index 83187667..91ff744e 100644 --- a/src/module/rbac/service.go +++ b/src/module/rbac/service.go @@ -23,8 +23,8 @@ func NewService(repo *Repository) *Service { func (s *Service) CreateRole(_ context.Context, req *CreateRoleReq) (*RoleResp, error) { role := req.ConvertToRole() - if err := s.repo.Transaction(func(tx *gorm.DB) error { - if err := s.repo.withDB(tx).createRoleRecord(role); err != nil { + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + if err := NewRepository(tx).createRoleRecord(role); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: role with name %s already exists", consts.ErrAlreadyExists, role.Name) } @@ -39,8 +39,8 @@ func (s *Service) CreateRole(_ context.Context, req *CreateRoleReq) (*RoleResp, } func (s *Service) DeleteRole(_ context.Context, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - rows, err := s.repo.withDB(tx).deleteRoleCascade(roleID) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + rows, err := NewRepository(tx).deleteRoleCascade(roleID) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: role not found", consts.ErrNotFound) @@ -95,8 +95,8 @@ func (s *Service) ListRoles(_ context.Context, req *ListRoleReq) (*dto.ListResp[ func (s *Service) UpdateRole(_ context.Context, req *UpdateRoleReq, roleID int) (*RoleResp, error) { var updatedRole *model.Role - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) role, err := repo.updateMutableRole(roleID, func(existingRole *model.Role) { req.PatchRoleModel(existingRole) }) @@ -114,9 +114,9 @@ func (s *Service) UpdateRole(_ context.Context, req *UpdateRoleReq, roleID int) } func (s *Service) AssignRolePermissions(_ context.Context, permissionIDs []int, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.AssignRolePermissions(roleID, permissionIDs); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.assignRolePermissions(roleID, permissionIDs); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: role already has one or more of these permissions", consts.ErrAlreadyExists) } @@ -127,9 +127,9 @@ func (s *Service) AssignRolePermissions(_ context.Context, permissionIDs []int, } func (s *Service) RemoveRolePermissions(_ context.Context, permissionIDs []int, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.RemoveRolePermissions(roleID, permissionIDs); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.removeRolePermissions(roleID, permissionIDs); err != nil { return fmt.Errorf("failed to remove permissions from role: %w", err) } return nil diff --git a/src/module/system/handler.go b/src/module/system/handler.go index 4a4fd252..193c460b 100644 --- a/src/module/system/handler.go +++ b/src/module/system/handler.go @@ -13,10 +13,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } @@ -65,7 +65,11 @@ func (h *Handler) GetMetrics(c *gin.Context) { } c.Header("Deprecation", "true") c.Header("Link", `; rel="successor-version"`) - dto.SuccessResponse(c, h.service.GetMetrics()) + resp, err := h.service.GetMetrics(c.Request.Context()) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) } // GetSystemInfo handles basic system information @@ -85,7 +89,11 @@ func (h *Handler) GetMetrics(c *gin.Context) { func (h *Handler) GetSystemInfo(c *gin.Context) { c.Header("Deprecation", "true") c.Header("Link", `; rel="successor-version"`) - dto.SuccessResponse(c, h.service.GetSystemInfo()) + resp, err := h.service.GetSystemInfo(c.Request.Context()) + if httpx.HandleServiceError(c, err) { + return + } + dto.SuccessResponse(c, resp) } // ListNamespaceLocks handles listing of namespace locks @@ -153,7 +161,7 @@ func (h *Handler) GetAuditLog(c *gin.Context) { return } - resp, err := h.service.GetAuditLog(id) + resp, err := h.service.GetAuditLog(c.Request.Context(), id) if httpx.HandleServiceError(c, err) { return } @@ -194,7 +202,7 @@ func (h *Handler) ListAuditLogs(c *gin.Context) { return } - resp, err := h.service.ListAuditLogs(&req) + resp, err := h.service.ListAuditLogs(c.Request.Context(), &req) if httpx.HandleServiceError(c, err) { return } @@ -224,7 +232,7 @@ func (h *Handler) GetConfig(c *gin.Context) { return } - resp, err := h.service.GetConfig(configID) + resp, err := h.service.GetConfig(c.Request.Context(), configID) if httpx.HandleServiceError(c, err) { return } @@ -263,7 +271,7 @@ func (h *Handler) ListConfigs(c *gin.Context) { return } - resp, err := h.service.ListConfigs(&req) + resp, err := h.service.ListConfigs(c.Request.Context(), &req) if httpx.HandleServiceError(c, err) { return } @@ -350,7 +358,7 @@ func (h *Handler) RollbackConfigMetadata(c *gin.Context) { return } - resp, err := h.service.RollbackConfigMetadata(&req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + resp, err := h.service.RollbackConfigMetadata(c.Request.Context(), &req, configID, userID, c.ClientIP(), c.Request.UserAgent()) if httpx.HandleServiceError(c, err) { return } @@ -441,7 +449,7 @@ func (h *Handler) UpdateConfigMetadata(c *gin.Context) { return } - resp, err := h.service.UpdateConfigMetadata(&req, configID, userID, c.ClientIP(), c.Request.UserAgent()) + resp, err := h.service.UpdateConfigMetadata(c.Request.Context(), &req, configID, userID, c.ClientIP(), c.Request.UserAgent()) if httpx.HandleServiceError(c, err) { return } @@ -482,7 +490,7 @@ func (h *Handler) ListConfigHistories(c *gin.Context) { return } - resp, err := h.service.ListConfigHistories(&req, configID) + resp, err := h.service.ListConfigHistories(c.Request.Context(), &req, configID) if httpx.HandleServiceError(c, err) { return } diff --git a/src/module/system/handler_service.go b/src/module/system/handler_service.go new file mode 100644 index 00000000..5860a9ff --- /dev/null +++ b/src/module/system/handler_service.go @@ -0,0 +1,29 @@ +package systemmodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the system operations consumed by the HTTP handler. +type HandlerService interface { + GetHealth(context.Context) (*HealthCheckResp, error) + GetMetrics(context.Context) (*MonitoringMetricsResp, error) + GetSystemInfo(context.Context) (*SystemInfo, error) + ListNamespaceLocks(context.Context) (*ListNamespaceLockResp, error) + ListQueuedTasks(context.Context) (*QueuedTasksResp, error) + GetAuditLog(context.Context, int) (*AuditLogDetailResp, error) + ListAuditLogs(context.Context, *ListAuditLogReq) (*dto.ListResp[AuditLogResp], error) + GetConfig(context.Context, int) (*ConfigDetailResp, error) + ListConfigs(context.Context, *ListConfigReq) (*dto.ListResp[ConfigResp], error) + RollbackConfigValue(context.Context, *RollbackConfigReq, int, int, string, string) error + RollbackConfigMetadata(context.Context, *RollbackConfigReq, int, int, string, string) (*ConfigResp, error) + UpdateConfigValue(context.Context, *UpdateConfigValueReq, int, int, string, string) error + UpdateConfigMetadata(context.Context, *UpdateConfigMetadataReq, int, int, string, string) (*ConfigResp, error) + ListConfigHistories(context.Context, *ListConfigHistoryReq, int) (*dto.ListResp[ConfigHistoryResp], error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/system/module.go b/src/module/system/module.go index def22914..a3976b00 100644 --- a/src/module/system/module.go +++ b/src/module/system/module.go @@ -3,7 +3,11 @@ package systemmodule import "go.uber.org/fx" var Module = fx.Module("system", - fx.Provide(NewRepository), - fx.Provide(NewService), - fx.Provide(NewHandler), + fx.Provide( + NewRepository, + newRuntimeQuerySource, + NewService, + AsHandlerService, + NewHandler, + ), ) diff --git a/src/module/system/repository.go b/src/module/system/repository.go index 0cd6a6ea..88ccf041 100644 --- a/src/module/system/repository.go +++ b/src/module/system/repository.go @@ -16,15 +16,7 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) GetAuditLogByID(id int) (*model.AuditLog, error) { +func (r *Repository) getAuditLogByID(id int) (*model.AuditLog, error) { var auditLog model.AuditLog if err := r.db.Where("id = ?", id).First(&auditLog).Error; err != nil { return nil, fmt.Errorf("failed to get audit log: %w", err) @@ -32,7 +24,7 @@ func (r *Repository) GetAuditLogByID(id int) (*model.AuditLog, error) { return &auditLog, nil } -func (r *Repository) ListAuditLogs(limit, offset int, filters *ListAuditLogFilters) ([]model.AuditLog, int64, error) { +func (r *Repository) listAuditLogs(limit, offset int, filters *ListAuditLogFilters) ([]model.AuditLog, int64, error) { var ( logs []model.AuditLog total int64 @@ -75,7 +67,7 @@ func (r *Repository) ListAuditLogs(limit, offset int, filters *ListAuditLogFilte return logs, total, nil } -func (r *Repository) GetConfigByID(configID int, includeUser bool) (*model.DynamicConfig, error) { +func (r *Repository) getConfigByID(configID int, includeUser bool) (*model.DynamicConfig, error) { query := r.db if includeUser { query = query.Preload("UpdatedByUser") @@ -88,7 +80,7 @@ func (r *Repository) GetConfigByID(configID int, includeUser bool) (*model.Dynam return &cfg, nil } -func (r *Repository) ListConfigs(limit, offset int, valueType *consts.ConfigValueType, category *string, isSecret *bool, updatedBy *int) ([]model.DynamicConfig, int64, error) { +func (r *Repository) listConfigs(limit, offset int, valueType *consts.ConfigValueType, category *string, isSecret *bool, updatedBy *int) ([]model.DynamicConfig, int64, error) { var ( configs []model.DynamicConfig total int64 @@ -117,14 +109,14 @@ func (r *Repository) ListConfigs(limit, offset int, valueType *consts.ConfigValu return configs, total, nil } -func (r *Repository) UpdateConfig(config *model.DynamicConfig) error { +func (r *Repository) updateConfig(config *model.DynamicConfig) error { if err := r.db.Save(config).Error; err != nil { return fmt.Errorf("failed to update config: %w", err) } return nil } -func (r *Repository) GetConfigHistory(historyID int) (*model.ConfigHistory, error) { +func (r *Repository) getConfigHistory(historyID int) (*model.ConfigHistory, error) { var history model.ConfigHistory if err := r.db.Preload("Operator").Preload("Config").First(&history, historyID).Error; err != nil { return nil, fmt.Errorf("failed to find config history with id %d: %w", historyID, err) @@ -132,14 +124,14 @@ func (r *Repository) GetConfigHistory(historyID int) (*model.ConfigHistory, erro return &history, nil } -func (r *Repository) CreateConfigHistory(history *model.ConfigHistory) error { +func (r *Repository) createConfigHistory(history *model.ConfigHistory) error { if err := r.db.Create(history).Error; err != nil { return fmt.Errorf("failed to create config history: %w", err) } return nil } -func (r *Repository) ListConfigHistories(limit, offset int, configID int, changeType *consts.ConfigHistoryChangeType, operatorID *int) ([]model.ConfigHistory, int64, error) { +func (r *Repository) listConfigHistories(limit, offset int, configID int, changeType *consts.ConfigHistoryChangeType, operatorID *int) ([]model.ConfigHistory, int64, error) { var ( histories []model.ConfigHistory total int64 @@ -162,7 +154,7 @@ func (r *Repository) ListConfigHistories(limit, offset int, configID int, change return histories, total, nil } -func (r *Repository) ListConfigHistoriesByConfigID(configID int) ([]model.ConfigHistory, error) { +func (r *Repository) listConfigHistoriesByConfigID(configID int) ([]model.ConfigHistory, error) { var histories []model.ConfigHistory if err := r.db.Preload("Operator").Where("config_id = ?", configID).Order("created_at DESC").Find(&histories).Error; err != nil { return nil, fmt.Errorf("failed to list config histories for config %d: %w", configID, err) diff --git a/src/module/system/runtime_query.go b/src/module/system/runtime_query.go new file mode 100644 index 00000000..ba38d4b8 --- /dev/null +++ b/src/module/system/runtime_query.go @@ -0,0 +1,71 @@ +package systemmodule + +import ( + "context" + "fmt" + + "aegis/internalclient/runtimeclient" + systemmetricmodule "aegis/module/systemmetric" + taskmodule "aegis/module/task" + + "go.uber.org/fx" +) + +type runtimeQuerySource interface { + ListNamespaceLocks(context.Context) (*ListNamespaceLockResp, error) + ListQueuedTasks(context.Context) (*taskmodule.QueuedTasksResp, error) +} + +type runtimeQueryAdapter struct { + runtime *runtimeclient.Client + local *systemmetricmodule.Service + requireRemote bool +} + +type runtimeQuerySourceParams struct { + fx.In + + Runtime *runtimeclient.Client `optional:"true"` + Local *systemmetricmodule.Service +} + +func newRuntimeQuerySource(params runtimeQuerySourceParams) runtimeQuerySource { + return runtimeQueryAdapter{ + runtime: params.Runtime, + local: params.Local, + requireRemote: false, + } +} + +func newRemoteRuntimeQuerySource(params runtimeQuerySourceParams) runtimeQuerySource { + return runtimeQueryAdapter{ + runtime: params.Runtime, + local: params.Local, + requireRemote: true, + } +} + +func (a runtimeQueryAdapter) ListNamespaceLocks(ctx context.Context) (*ListNamespaceLockResp, error) { + if a.runtime != nil && a.runtime.Enabled() { + return a.runtime.GetNamespaceLocks(ctx) + } + if a.requireRemote { + return nil, fmt.Errorf("runtime-worker-service query source is not configured") + } + return a.local.ListNamespaceLocks(ctx) +} + +func (a runtimeQueryAdapter) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { + if a.runtime != nil && a.runtime.Enabled() { + return a.runtime.GetQueuedTasks(ctx) + } + if a.requireRemote { + return nil, fmt.Errorf("runtime-worker-service query source is not configured") + } + return a.local.ListQueuedTasks(ctx) +} + +// RemoteRuntimeQueryOption forces the dedicated system-service path to use runtime RPC only. +func RemoteRuntimeQueryOption() fx.Option { + return fx.Decorate(newRemoteRuntimeQuerySource) +} diff --git a/src/module/system/service.go b/src/module/system/service.go index c1939a01..25f06288 100644 --- a/src/module/system/service.go +++ b/src/module/system/service.go @@ -17,11 +17,11 @@ import ( k8sinfra "aegis/infra/k8s" redisinfra "aegis/infra/redis" "aegis/model" - systemmetricmodule "aegis/module/systemmetric" "aegis/service/common" "aegis/utils" "github.com/sirupsen/logrus" + "go.uber.org/fx" "gorm.io/gorm" ) @@ -44,7 +44,7 @@ type configHistoryParams struct { } type configHistoryWriter interface { - CreateConfigHistory(history *model.ConfigHistory) error + createConfigHistory(history *model.ConfigHistory) error } type Service struct { @@ -53,17 +53,28 @@ type Service struct { etcd *etcdinfra.Gateway k8s *k8sinfra.Gateway redis *redisinfra.Gateway - systemMetric *systemmetricmodule.Service + runtimeQuery runtimeQuerySource } -func NewService(repo *Repository, buildkit *buildkitinfra.Gateway, etcd *etcdinfra.Gateway, k8s *k8sinfra.Gateway, redis *redisinfra.Gateway, systemMetric *systemmetricmodule.Service) *Service { +type serviceParams struct { + fx.In + + Repo *Repository + Buildkit *buildkitinfra.Gateway + Etcd *etcdinfra.Gateway + K8s *k8sinfra.Gateway + Redis *redisinfra.Gateway + RuntimeQuery runtimeQuerySource +} + +func NewService(params serviceParams) *Service { return &Service{ - repo: repo, - buildkit: buildkit, - etcd: etcd, - k8s: k8s, - redis: redis, - systemMetric: systemMetric, + repo: params.Repo, + buildkit: params.Buildkit, + etcd: params.Etcd, + k8s: params.K8s, + redis: params.Redis, + runtimeQuery: params.RuntimeQuery, } } @@ -111,7 +122,7 @@ func (s *Service) GetHealth(ctx context.Context) (*HealthCheckResp, error) { }, nil } -func (s *Service) GetMetrics() *MonitoringMetricsResp { +func (s *Service) GetMetrics(_ context.Context) (*MonitoringMetricsResp, error) { return &MonitoringMetricsResp{ Timestamp: time.Now(), Metrics: map[string]MetricValue{ @@ -124,10 +135,10 @@ func (s *Service) GetMetrics() *MonitoringMetricsResp { "instance": "rcabench-01", "version": config.GetString("version"), }, - } + }, nil } -func (s *Service) GetSystemInfo() *SystemInfo { +func (s *Service) GetSystemInfo(_ context.Context) (*SystemInfo, error) { var memStats runtime.MemStats runtime.ReadMemStats(&memStats) return &SystemInfo{ @@ -135,19 +146,19 @@ func (s *Service) GetSystemInfo() *SystemInfo { MemoryUsage: float64(memStats.Alloc) / float64(memStats.Sys) * 100, DiskUsage: 45.8, LoadAverage: "1.2, 1.5, 1.8", - } + }, nil } func (s *Service) ListNamespaceLocks(ctx context.Context) (*ListNamespaceLockResp, error) { - return s.systemMetric.ListNamespaceLocks(ctx) + return s.runtimeQuery.ListNamespaceLocks(ctx) } func (s *Service) ListQueuedTasks(ctx context.Context) (*QueuedTasksResp, error) { - return s.systemMetric.ListQueuedTasks(ctx) + return s.runtimeQuery.ListQueuedTasks(ctx) } -func (s *Service) GetAuditLog(id int) (*AuditLogDetailResp, error) { - log, err := s.repo.GetAuditLogByID(id) +func (s *Service) GetAuditLog(_ context.Context, id int) (*AuditLogDetailResp, error) { + log, err := s.repo.getAuditLogByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: audit log with ID %d not found", consts.ErrNotFound, id) @@ -158,11 +169,11 @@ func (s *Service) GetAuditLog(id int) (*AuditLogDetailResp, error) { return NewAuditLogDetailResp(log), nil } -func (s *Service) ListAuditLogs(req *ListAuditLogReq) (*dto.ListResp[AuditLogResp], error) { +func (s *Service) ListAuditLogs(_ context.Context, req *ListAuditLogReq) (*dto.ListResp[AuditLogResp], error) { limit, offset := req.ToGormParams() filterOptions := req.ToFilterOptions() - logs, total, err := s.repo.ListAuditLogs(limit, offset, filterOptions) + logs, total, err := s.repo.listAuditLogs(limit, offset, filterOptions) if err != nil { return nil, fmt.Errorf("failed to list audit logs: %w", err) } @@ -170,13 +181,13 @@ func (s *Service) ListAuditLogs(req *ListAuditLogReq) (*dto.ListResp[AuditLogRes return buildAuditLogListResp(logs, req, total), nil } -func (s *Service) GetConfig(configID int) (*ConfigDetailResp, error) { - cfg, err := s.repo.GetConfigByID(configID, true) +func (s *Service) GetConfig(_ context.Context, configID int) (*ConfigDetailResp, error) { + cfg, err := s.repo.getConfigByID(configID, true) if err != nil { return nil, fmt.Errorf("failed to get config detail: %w", err) } - histories, err := s.repo.ListConfigHistoriesByConfigID(cfg.ID) + histories, err := s.repo.listConfigHistoriesByConfigID(cfg.ID) if err != nil { return nil, fmt.Errorf("failed to get config histories: %w", err) } @@ -184,10 +195,10 @@ func (s *Service) GetConfig(configID int) (*ConfigDetailResp, error) { return buildConfigDetailResp(cfg, histories), nil } -func (s *Service) ListConfigs(req *ListConfigReq) (*dto.ListResp[ConfigResp], error) { +func (s *Service) ListConfigs(_ context.Context, req *ListConfigReq) (*dto.ListResp[ConfigResp], error) { limit, offset := req.ToGormParams() - configs, total, err := s.repo.ListConfigs(limit, offset, req.ValueType, req.Category, req.IsSecret, req.UpdatedBy) + configs, total, err := s.repo.listConfigs(limit, offset, req.ValueType, req.Category, req.IsSecret, req.UpdatedBy) if err != nil { return nil, fmt.Errorf("failed to list configs: %w", err) } @@ -196,7 +207,7 @@ func (s *Service) ListConfigs(req *ListConfigReq) (*dto.ListResp[ConfigResp], er } func (s *Service) RollbackConfigValue(ctx context.Context, req *RollbackConfigReq, configID, userID int, ipAddress, userAgent string) error { - history, err := s.repo.GetConfigHistory(req.HistoryID) + history, err := s.repo.getConfigHistory(req.HistoryID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) @@ -208,7 +219,7 @@ func (s *Service) RollbackConfigValue(ctx context.Context, req *RollbackConfigRe return fmt.Errorf("history entry %d is not a value change (field: %v)", req.HistoryID, history.ChangeField) } - existingConfig, err := s.repo.GetConfigByID(configID, false) + existingConfig, err := s.repo.getConfigByID(configID, false) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) @@ -245,8 +256,8 @@ func (s *Service) RollbackConfigValue(ctx context.Context, req *RollbackConfigRe return s.propagateValueChange(ctx, existingConfig, newValue, "rollback") } -func (s *Service) RollbackConfigMetadata(req *RollbackConfigReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { - history, err := s.repo.GetConfigHistory(req.HistoryID) +func (s *Service) RollbackConfigMetadata(_ context.Context, req *RollbackConfigReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { + history, err := s.repo.getConfigHistory(req.HistoryID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: history entry with id %d not found", consts.ErrNotFound, req.HistoryID) @@ -258,7 +269,7 @@ func (s *Service) RollbackConfigMetadata(req *RollbackConfigReq, configID, userI return nil, fmt.Errorf("history entry %d is a value change, use RollbackConfigValue instead", req.HistoryID) } - existingConfig, err := s.repo.GetConfigByID(configID, false) + existingConfig, err := s.repo.getConfigByID(configID, false) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) @@ -292,7 +303,7 @@ func (s *Service) RollbackConfigMetadata(req *RollbackConfigReq, configID, userI } func (s *Service) UpdateConfigValue(ctx context.Context, req *UpdateConfigValueReq, configID, userID int, ipAddress, userAgent string) error { - existingConfig, err := s.repo.GetConfigByID(configID, false) + existingConfig, err := s.repo.getConfigByID(configID, false) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) @@ -333,8 +344,8 @@ func (s *Service) UpdateConfigValue(ctx context.Context, req *UpdateConfigValueR return s.propagateValueChange(ctx, existingConfig, newValue, "update") } -func (s *Service) UpdateConfigMetadata(req *UpdateConfigMetadataReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { - existingConfig, err := s.repo.GetConfigByID(configID, false) +func (s *Service) UpdateConfigMetadata(_ context.Context, req *UpdateConfigMetadataReq, configID, userID int, ipAddress, userAgent string) (*ConfigResp, error) { + existingConfig, err := s.repo.getConfigByID(configID, false) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("%w: configuration with id %d not found", consts.ErrNotFound, configID) @@ -348,11 +359,11 @@ func (s *Service) UpdateConfigMetadata(req *UpdateConfigMetadataReq, configID, u } var updatedConfig *model.DynamicConfig - err = s.repo.Transaction(func(tx *gorm.DB) error { - txRepo := s.repo.withDB(tx) + err = s.repo.db.Transaction(func(tx *gorm.DB) error { + txRepo := NewRepository(tx) existingConfig.UpdatedBy = utils.IntPtr(userID) - if err := txRepo.UpdateConfig(existingConfig); err != nil { + if err := txRepo.updateConfig(existingConfig); err != nil { return fmt.Errorf("failed to update config: %w", err) } @@ -382,10 +393,10 @@ func (s *Service) UpdateConfigMetadata(req *UpdateConfigMetadataReq, configID, u return NewConfigResp(updatedConfig), nil } -func (s *Service) ListConfigHistories(req *ListConfigHistoryReq, configID int) (*dto.ListResp[ConfigHistoryResp], error) { +func (s *Service) ListConfigHistories(_ context.Context, req *ListConfigHistoryReq, configID int) (*dto.ListResp[ConfigHistoryResp], error) { limit, offset := req.ToGormParams() - histories, total, err := s.repo.ListConfigHistories(limit, offset, configID, req.ChangeType, req.OperatorID) + histories, total, err := s.repo.listConfigHistories(limit, offset, configID, req.ChangeType, req.OperatorID) if err != nil { return nil, fmt.Errorf("failed to list config histories: %w", err) } @@ -462,7 +473,7 @@ func (s *Service) createConfigHistory(repo configHistoryWriter, params configHis RolledBackFromID: params.RollbackFromID, ChangeField: params.ConfigUpdateContext.ChangeField, } - if err := repo.CreateConfigHistory(entry); err != nil { + if err := repo.createConfigHistory(entry); err != nil { return fmt.Errorf("failed to create config history: %w", err) } return nil @@ -471,9 +482,9 @@ func (s *Service) createConfigHistory(repo configHistoryWriter, params configHis func (s *Service) createConfigRollback(cfg *model.DynamicConfig, historyID *int, updateContext configUpdateContext) (*model.DynamicConfig, error) { var updatedConfig *model.DynamicConfig - err := s.repo.Transaction(func(tx *gorm.DB) error { - txRepo := s.repo.withDB(tx) - if err := txRepo.UpdateConfig(cfg); err != nil { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + txRepo := NewRepository(tx) + if err := txRepo.updateConfig(cfg); err != nil { return fmt.Errorf("failed to update config: %w", err) } @@ -706,6 +717,9 @@ func (s *Service) checkJaegerHealth(parent context.Context) ServiceInfo { func (s *Service) checkKubernetesHealth(parent context.Context) ServiceInfo { start := time.Now() + if s.k8s == nil { + return ServiceInfo{Status: "unavailable", LastChecked: time.Now(), ResponseTime: time.Since(start).String(), Error: "Kubernetes gateway not configured"} + } ctx, cancel := context.WithTimeout(parent, 5*time.Second) defer cancel() if err := s.k8s.CheckHealth(ctx); err != nil { diff --git a/src/module/system/service_test.go b/src/module/system/service_test.go index 587c0705..2b2d61e6 100644 --- a/src/module/system/service_test.go +++ b/src/module/system/service_test.go @@ -1,6 +1,7 @@ package systemmodule import ( + "context" "testing" "time" @@ -15,7 +16,7 @@ type fakeConfigHistoryWriter struct { err error } -func (f *fakeConfigHistoryWriter) CreateConfigHistory(history *model.ConfigHistory) error { +func (f *fakeConfigHistoryWriter) createConfigHistory(history *model.ConfigHistory) error { f.history = history return f.err } @@ -23,7 +24,10 @@ func (f *fakeConfigHistoryWriter) CreateConfigHistory(history *model.ConfigHisto func TestGetMetricsReturnsExpectedLabels(t *testing.T) { svc := &Service{} - resp := svc.GetMetrics() + resp, err := svc.GetMetrics(context.Background()) + if err != nil { + t.Fatalf("GetMetrics() error = %v", err) + } if resp == nil { t.Fatal("expected metrics response") } @@ -38,7 +42,10 @@ func TestGetMetricsReturnsExpectedLabels(t *testing.T) { func TestGetSystemInfoReturnsLoadAverage(t *testing.T) { svc := &Service{} - resp := svc.GetSystemInfo() + resp, err := svc.GetSystemInfo(context.Background()) + if err != nil { + t.Fatalf("GetSystemInfo() error = %v", err) + } if resp == nil { t.Fatal("expected system info response") } diff --git a/src/module/systemmetric/handler.go b/src/module/systemmetric/handler.go index fa869e49..e7c3f356 100644 --- a/src/module/systemmetric/handler.go +++ b/src/module/systemmetric/handler.go @@ -9,10 +9,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/systemmetric/handler_service.go b/src/module/systemmetric/handler_service.go new file mode 100644 index 00000000..d2d3cc7f --- /dev/null +++ b/src/module/systemmetric/handler_service.go @@ -0,0 +1,13 @@ +package systemmetricmodule + +import "context" + +// HandlerService captures the system metric operations consumed by the HTTP handler. +type HandlerService interface { + GetSystemMetrics(context.Context) (*SystemMetricsResp, error) + GetSystemMetricsHistory(context.Context) (*SystemMetricsHistoryResp, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/systemmetric/module.go b/src/module/systemmetric/module.go index 7aa90f68..49e3986a 100644 --- a/src/module/systemmetric/module.go +++ b/src/module/systemmetric/module.go @@ -3,8 +3,11 @@ package systemmetricmodule import "go.uber.org/fx" var Module = fx.Module("system_metric", - fx.Provide(NewRepository), - fx.Provide(NewService), - fx.Provide(NewHandler), + fx.Provide( + NewRepository, + NewService, + AsHandlerService, + NewHandler, + ), fx.Invoke(RegisterMetricsCollector), ) diff --git a/src/module/systemmetric/service.go b/src/module/systemmetric/service.go index e1da82c1..6f2c391d 100644 --- a/src/module/systemmetric/service.go +++ b/src/module/systemmetric/service.go @@ -9,8 +9,8 @@ import ( "time" "aegis/consts" + "aegis/dto" redisinfra "aegis/infra/redis" - "aegis/model" taskmodule "aegis/module/task" "github.com/redis/go-redis/v9" @@ -154,11 +154,11 @@ func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksR readyTasks := make([]taskmodule.TaskResp, 0, len(readyTaskDatas)) for _, taskData := range readyTaskDatas { - var task model.Task - if err := json.Unmarshal([]byte(taskData), &task); err != nil { + taskResp, err := decodeQueuedTask(taskData) + if err != nil { return nil, err } - readyTasks = append(readyTasks, *taskmodule.NewTaskResp(&task)) + readyTasks = append(readyTasks, taskResp) } delayedTaskDatas, err := s.redis.ListDelayedTasks(ctx, 1000) @@ -171,11 +171,11 @@ func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksR delayedTasks := make([]taskmodule.TaskResp, 0, len(delayedTaskDatas)) for _, taskData := range delayedTaskDatas { - var task model.Task - if err := json.Unmarshal([]byte(taskData), &task); err != nil { + taskResp, err := decodeQueuedTask(taskData) + if err != nil { return nil, err } - delayedTasks = append(delayedTasks, *taskmodule.NewTaskResp(&task)) + delayedTasks = append(delayedTasks, taskResp) } return &taskmodule.QueuedTasksResp{ @@ -184,6 +184,26 @@ func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksR }, nil } +func decodeQueuedTask(taskData string) (taskmodule.TaskResp, error) { + var task dto.UnifiedTask + if err := json.Unmarshal([]byte(taskData), &task); err != nil { + return taskmodule.TaskResp{}, err + } + + return taskmodule.TaskResp{ + ID: task.TaskID, + Type: consts.GetTaskTypeName(task.Type), + Immediate: task.Immediate, + ExecuteTime: task.ExecuteTime, + CronExpr: task.CronExpr, + TraceID: task.TraceID, + GroupID: task.GroupID, + State: consts.GetTaskStateName(task.State), + Status: consts.GetStatusTypeName(consts.CommonEnabled), + ProjectID: task.ProjectID, + }, nil +} + func parseMetricValues(items []string) []MetricValue { metrics := make([]MetricValue, 0, len(items)) for _, item := range items { diff --git a/src/module/task/handler.go b/src/module/task/handler.go index 90fd31e7..99c7c993 100644 --- a/src/module/task/handler.go +++ b/src/module/task/handler.go @@ -23,10 +23,10 @@ var wsUpgrader = websocket.Upgrader{ } type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/task/handler_service.go b/src/module/task/handler_service.go new file mode 100644 index 00000000..6bc034bc --- /dev/null +++ b/src/module/task/handler_service.go @@ -0,0 +1,23 @@ +package taskmodule + +import ( + "context" + + "aegis/dto" + "aegis/model" + + "github.com/gorilla/websocket" +) + +// HandlerService captures task operations consumed by HTTP handlers and gateway adapters. +type HandlerService interface { + BatchDelete(context.Context, []string) error + GetDetail(context.Context, string) (*TaskDetailResp, error) + List(context.Context, *ListTaskReq) (*dto.ListResp[TaskResp], error) + GetForLogStream(context.Context, string) (*model.Task, error) + StreamLogs(context.Context, *websocket.Conn, *model.Task) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/task/log_types.go b/src/module/task/log_types.go index d4a5606c..3c643124 100644 --- a/src/module/task/log_types.go +++ b/src/module/task/log_types.go @@ -3,6 +3,7 @@ package taskmodule import ( "aegis/consts" "aegis/dto" + "time" ) // WSLogMessage is the WebSocket payload for task log streaming. @@ -12,3 +13,11 @@ type WSLogMessage struct { Message string `json:"message,omitempty"` Total int `json:"total,omitempty"` } + +// TaskLogPollResp represents one task log poll batch for remote websocket forwarding. +type TaskLogPollResp struct { + Logs []dto.LogEntry `json:"logs"` + Terminal bool `json:"terminal"` + State string `json:"state"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/src/module/task/module.go b/src/module/task/module.go index 3de1ac1f..0df7fe63 100644 --- a/src/module/task/module.go +++ b/src/module/task/module.go @@ -8,5 +8,6 @@ var Module = fx.Module("task", fx.Provide(NewLokiGateway), fx.Provide(NewTaskLogService), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/task/service.go b/src/module/task/service.go index bc591ea9..24a5f45c 100644 --- a/src/module/task/service.go +++ b/src/module/task/service.go @@ -89,6 +89,36 @@ func (s *Service) StreamLogs(ctx context.Context, conn *websocket.Conn, task *mo s.logService.StreamLogs(ctx, conn, task) } +func (s *Service) PollLogs(ctx context.Context, taskID string, after time.Time) (*TaskLogPollResp, error) { + task, err := s.repository.GetByID(taskID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: task id: %s", consts.ErrNotFound, taskID) + } + return nil, fmt.Errorf("failed to get task: %w", err) + } + + start := task.CreatedAt + if !after.IsZero() && after.After(start) { + start = after.Add(time.Nanosecond) + } + + lokiCtx, lokiCancel := context.WithTimeout(ctx, 10*time.Second) + defer lokiCancel() + + logEntries, err := s.loki.QueryJobLogs(lokiCtx, task.ID, start) + if err != nil { + return nil, fmt.Errorf("failed to query task logs: %w", err) + } + + return &TaskLogPollResp{ + Logs: logEntries, + Terminal: isTaskTerminal(task.State), + State: consts.GetTaskStateName(task.State), + CreatedAt: task.CreatedAt, + }, nil +} + func (s *Service) queryHistoricalLogs(ctx context.Context, task *model.Task) []string { lokiCtx, lokiCancel := context.WithTimeout(ctx, 10*time.Second) defer lokiCancel() diff --git a/src/module/team/handler.go b/src/module/team/handler.go index 61cbed14..536346e7 100644 --- a/src/module/team/handler.go +++ b/src/module/team/handler.go @@ -13,10 +13,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/team/handler_service.go b/src/module/team/handler_service.go new file mode 100644 index 00000000..88882617 --- /dev/null +++ b/src/module/team/handler_service.go @@ -0,0 +1,25 @@ +package teammodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the team operations consumed by the HTTP handler. +type HandlerService interface { + CreateTeam(context.Context, *CreateTeamReq, int) (*TeamResp, error) + DeleteTeam(context.Context, int) error + GetTeamDetail(context.Context, int) (*TeamDetailResp, error) + ListTeams(context.Context, *ListTeamReq, int, bool) (*dto.ListResp[TeamResp], error) + UpdateTeam(context.Context, *UpdateTeamReq, int) (*TeamResp, error) + ListTeamProjects(context.Context, *TeamProjectListReq, int) (*dto.ListResp[TeamProjectItem], error) + AddMember(context.Context, *AddTeamMemberReq, int) error + RemoveMember(context.Context, int, int, int) error + UpdateMemberRole(context.Context, *UpdateTeamMemberRoleReq, int, int, int) error + ListMembers(context.Context, *ListTeamMemberReq, int) (*dto.ListResp[TeamMemberResp], error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/team/module.go b/src/module/team/module.go index 98a6b978..77874c6d 100644 --- a/src/module/team/module.go +++ b/src/module/team/module.go @@ -4,6 +4,8 @@ import "go.uber.org/fx" var Module = fx.Module("team", fx.Provide(NewRepository), + fx.Provide(newProjectReader), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/team/project_reader.go b/src/module/team/project_reader.go new file mode 100644 index 00000000..abea64db --- /dev/null +++ b/src/module/team/project_reader.go @@ -0,0 +1,127 @@ +package teammodule + +import ( + "context" + "fmt" + + "aegis/consts" + "aegis/dto" + "aegis/internalclient/resourceclient" + "aegis/model" + projectmodule "aegis/module/project" + + "go.uber.org/fx" +) + +type projectReader interface { + CountProjects(context.Context, int) (int, error) + ListProjects(context.Context, *TeamProjectListReq, int) (*dto.ListResp[TeamProjectItem], error) +} + +type projectReaderParams struct { + fx.In + + Repository *Repository + Resource *resourceclient.Client `optional:"true"` +} + +type projectReaderAdapter struct { + repo *Repository + resource *resourceclient.Client + requireRemote bool +} + +func newProjectReader(params projectReaderParams) projectReader { + return projectReaderAdapter{ + repo: params.Repository, + resource: params.Resource, + } +} + +func newRemoteProjectReader(params projectReaderParams) projectReader { + return projectReaderAdapter{ + repo: params.Repository, + resource: params.Resource, + requireRemote: true, + } +} + +func (r projectReaderAdapter) CountProjects(ctx context.Context, teamID int) (int, error) { + if r.resource != nil && r.resource.Enabled() { + includeStatistics := false + resp, err := r.resource.ListProjects(ctx, &projectmodule.ListProjectReq{ + PaginationReq: dto.PaginationReq{Page: 1, Size: 10}, + TeamID: &teamID, + IncludeStatistics: &includeStatistics, + }) + if err != nil { + return 0, fmt.Errorf("list team projects via resource-service: %w", err) + } + if resp.Pagination == nil { + return len(resp.Items), nil + } + return int(resp.Pagination.Total), nil + } + if r.requireRemote { + return 0, fmt.Errorf("resource-service project reader is not configured") + } + + var projectCount int64 + if err := r.repo.db.Model(&model.Project{}). + Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted). + Count(&projectCount).Error; err != nil { + return 0, fmt.Errorf("failed to get team project count: %w", err) + } + return int(projectCount), nil +} + +func (r projectReaderAdapter) ListProjects(ctx context.Context, req *TeamProjectListReq, teamID int) (*dto.ListResp[TeamProjectItem], error) { + if r.resource != nil && r.resource.Enabled() { + if req == nil { + req = &TeamProjectListReq{} + } + + resourceReq := *req + resourceReq.TeamID = &teamID + resp, err := r.resource.ListProjects(ctx, &resourceReq) + if err != nil { + return nil, fmt.Errorf("list team projects via resource-service: %w", err) + } + + items := make([]TeamProjectItem, len(resp.Items)) + copy(items, resp.Items) + return &dto.ListResp[TeamProjectItem]{ + Items: items, + Pagination: resp.Pagination, + }, nil + } + if r.requireRemote { + return nil, fmt.Errorf("resource-service project reader is not configured") + } + + if req == nil { + req = &TeamProjectListReq{} + } + limit, offset := req.ToGormParams() + projects, statsMap, total, err := r.repo.listTeamProjectViews(teamID, limit, offset, req.IsPublic, req.Status) + if err != nil { + return nil, err + } + + items := make([]TeamProjectItem, 0, len(projects)) + for i := range projects { + items = append(items, *projectmodule.NewProjectResp(&projects[i], statsMap[projects[i].ID])) + } + + return &dto.ListResp[TeamProjectItem]{ + Items: items, + Pagination: req.ConvertToPaginationInfo(total), + }, nil +} + +// RemoteProjectReaderOption forces the dedicated iam-service path to use resource RPC only. +func RemoteProjectReaderOption() fx.Option { + return fx.Decorate(newRemoteProjectReader) +} + +var _ projectReader = (*projectReaderAdapter)(nil) diff --git a/src/module/team/repository.go b/src/module/team/repository.go index d32a64a8..67c5ebcf 100644 --- a/src/module/team/repository.go +++ b/src/module/team/repository.go @@ -4,9 +4,9 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" + projectmodule "aegis/module/project" "errors" "fmt" - "time" "gorm.io/gorm" ) @@ -19,14 +19,6 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - func (r *Repository) createTeamWithCreator(team *model.Team, userID int) error { var superAdminRole model.Role if err := r.db.Where("name = ? AND status != ?", consts.RoleSuperAdmin.String(), consts.CommonDeleted). @@ -49,29 +41,28 @@ func (r *Repository) createTeamWithCreator(team *model.Team, userID int) error { return nil } -func (r *Repository) loadTeamDetail(teamID int) (*model.Team, int, int, error) { +func (r *Repository) loadTeamDetailBase(teamID int) (*model.Team, int, error) { team, err := r.loadTeam(teamID) if err != nil { - return nil, 0, 0, err + return nil, 0, err } - userCount, err := r.countTeamUsers(teamID) - if err != nil { - return nil, 0, 0, err - } - projectCount, err := r.countTeamProjects(teamID) - if err != nil { - return nil, 0, 0, err + var userCount int64 + if err := r.db.Model(&model.UserTeam{}). + Where("team_id = ? AND status = ?", teamID, consts.CommonEnabled). + Count(&userCount).Error; err != nil { + return nil, 0, err } - return team, userCount, projectCount, nil + return team, int(userCount), nil } func (r *Repository) listVisibleTeams(limit, offset int, req *ListTeamReq, userID int, isAdmin bool) ([]model.Team, int64, error) { var teamIDs []int if !isAdmin { - teamIDs, err := r.listVisibleTeamIDsForUser(userID) - if err != nil { + if err := r.db.Model(&model.UserTeam{}). + Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). + Pluck("team_id", &teamIDs).Error; err != nil { return nil, 0, err } if len(teamIDs) == 0 { @@ -140,14 +131,14 @@ func (r *Repository) listTeamProjectViews(teamID, limit, offset int, isPublic *b projectIDs = append(projectIDs, project.ID) } - statsMap, err := listTeamProjectStatistics(r.db, projectIDs) + statsMap, err := projectmodule.NewRepository(r.db).ListProjectStatistics(projectIDs) if err != nil { return nil, nil, 0, err } return projects, statsMap, total, nil } -func (r *Repository) AddMember(teamID int, username string, roleID int) error { +func (r *Repository) addMember(teamID int, username string, roleID int) error { if _, err := r.loadTeam(teamID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return consts.ErrNotFound @@ -159,8 +150,9 @@ func (r *Repository) AddMember(teamID int, username string, roleID int) error { if err := r.db.Where("username = ?", username).First(&user).Error; err != nil { return fmt.Errorf("failed to find user with username %s: %w", username, err) } - if err := r.ensureRoleExists(roleID); err != nil { - return err + var role model.Role + if err := r.db.Where("id = ? AND status != ?", roleID, consts.CommonDeleted).First(&role).Error; err != nil { + return fmt.Errorf("failed to find role with id %d: %w", roleID, err) } if err := r.db.Omit("active_user_team").Create(&model.UserTeam{ @@ -174,7 +166,7 @@ func (r *Repository) AddMember(teamID int, username string, roleID int) error { return nil } -func (r *Repository) RemoveMember(teamID, userID int) (int64, error) { +func (r *Repository) removeMember(teamID, userID int) (int64, error) { if _, err := r.loadTeam(teamID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, consts.ErrNotFound @@ -191,15 +183,16 @@ func (r *Repository) RemoveMember(teamID, userID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) UpdateMemberRole(teamID, targetUserID, roleID int) error { +func (r *Repository) updateMemberRole(teamID, targetUserID, roleID int) error { if _, err := r.loadTeam(teamID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return consts.ErrNotFound } return err } - if err := r.ensureRoleExists(roleID); err != nil { - return err + var role model.Role + if err := r.db.Where("id = ? AND status != ?", roleID, consts.CommonDeleted).First(&role).Error; err != nil { + return fmt.Errorf("failed to find role with id %d: %w", roleID, err) } var userTeam model.UserTeam @@ -212,7 +205,7 @@ func (r *Repository) UpdateMemberRole(teamID, targetUserID, roleID int) error { return r.db.Save(&userTeam).Error } -func (r *Repository) ListTeamMembers(teamID, limit, offset int) ([]TeamMemberResp, int64, error) { +func (r *Repository) listTeamMembers(teamID, limit, offset int) ([]TeamMemberResp, int64, error) { if _, err := r.loadTeam(teamID); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, consts.ErrNotFound @@ -256,7 +249,7 @@ func (r *Repository) loadUserTeamMembership(userID, teamID int) (*model.UserTeam return &userTeam, nil } -func (r *Repository) DeleteTeam(teamID int) (int64, error) { +func (r *Repository) deleteTeam(teamID int) (int64, error) { result := r.db.Model(&model.Team{}). Where("id = ? AND status != ?", teamID, consts.CommonDeleted). Update("status", consts.CommonDeleted) @@ -274,14 +267,6 @@ func (r *Repository) isTeamPublic(teamID int) (bool, error) { return team.IsPublic, nil } -func (r *Repository) ensureRoleExists(roleID int) error { - var role model.Role - if err := r.db.Where("id = ? AND status != ?", roleID, consts.CommonDeleted).First(&role).Error; err != nil { - return fmt.Errorf("failed to find role with id %d: %w", roleID, err) - } - return nil -} - func (r *Repository) loadTeam(teamID int) (*model.Team, error) { var team model.Team if err := r.db.Where("id = ?", teamID).First(&team).Error; err != nil { @@ -289,83 +274,3 @@ func (r *Repository) loadTeam(teamID int) (*model.Team, error) { } return &team, nil } - -func (r *Repository) countTeamUsers(teamID int) (int, error) { - var userCount int64 - if err := r.db.Model(&model.UserTeam{}). - Where("team_id = ? AND status = ?", teamID, consts.CommonEnabled). - Count(&userCount).Error; err != nil { - return 0, fmt.Errorf("failed to get team user count: %w", err) - } - return int(userCount), nil -} - -func (r *Repository) countTeamProjects(teamID int) (int, error) { - var projectCount int64 - if err := r.db.Model(&model.Project{}). - Where("team_id = ? AND status != ?", teamID, consts.CommonDeleted). - Count(&projectCount).Error; err != nil { - return 0, fmt.Errorf("failed to get team project count: %w", err) - } - return int(projectCount), nil -} - -func (r *Repository) listVisibleTeamIDsForUser(userID int) ([]int, error) { - var teamIDs []int - if err := r.db.Model(&model.UserTeam{}). - Where("user_id = ? AND status = ?", userID, consts.CommonEnabled). - Pluck("team_id", &teamIDs).Error; err != nil { - return nil, fmt.Errorf("failed to get user teams: %w", err) - } - return teamIDs, nil -} - -func listTeamProjectStatistics(db *gorm.DB, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { - statsMap := make(map[int]*dto.ProjectStatistics, len(projectIDs)) - for _, projectID := range projectIDs { - statsMap[projectID] = &dto.ProjectStatistics{} - } - if len(projectIDs) == 0 { - return statsMap, nil - } - - var injectionStats []struct { - ProjectID int - Count int64 - LastAt *time.Time - } - if err := db.Table("fault_injections fi"). - Select("tr.project_id, COUNT(*) as count, MAX(fi.updated_at) as last_at"). - Joins("JOIN tasks t ON fi.task_id = t.id"). - Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). - Group("tr.project_id"). - Scan(&injectionStats).Error; err != nil { - return nil, fmt.Errorf("failed to batch get injection statistics: %w", err) - } - for _, stat := range injectionStats { - statsMap[stat.ProjectID].InjectionCount = int(stat.Count) - statsMap[stat.ProjectID].LastInjectionAt = stat.LastAt - } - - var executionStats []struct { - ProjectID int - Count int64 - LastAt *time.Time - } - if err := db.Table("executions e"). - Select("tr.project_id, COUNT(*) as count, MAX(e.updated_at) as last_at"). - Joins("JOIN tasks t ON e.task_id = t.id"). - Joins("JOIN traces tr ON t.trace_id = tr.id"). - Where("tr.project_id IN (?)", projectIDs). - Group("tr.project_id"). - Scan(&executionStats).Error; err != nil { - return nil, fmt.Errorf("failed to batch get execution statistics: %w", err) - } - for _, stat := range executionStats { - statsMap[stat.ProjectID].ExecutionCount = int(stat.Count) - statsMap[stat.ProjectID].LastExecutionAt = stat.LastAt - } - - return statsMap, nil -} diff --git a/src/module/team/service.go b/src/module/team/service.go index b661990b..276bf733 100644 --- a/src/module/team/service.go +++ b/src/module/team/service.go @@ -8,24 +8,27 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - projectmodule "aegis/module/project" "gorm.io/gorm" ) type Service struct { - repo *Repository + repo *Repository + projects projectReader } -func NewService(repo *Repository) *Service { - return &Service{repo: repo} +func NewService(repo *Repository, projects projectReader) *Service { + return &Service{ + repo: repo, + projects: projects, + } } func (s *Service) CreateTeam(_ context.Context, req *CreateTeamReq, userID int) (*TeamResp, error) { team := req.ConvertToTeam() - err := s.repo.Transaction(func(tx *gorm.DB) error { - if err := s.repo.withDB(tx).createTeamWithCreator(team, userID); err != nil { + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + if err := NewRepository(tx).createTeamWithCreator(team, userID); err != nil { if errors.Is(err, consts.ErrAlreadyExists) { return consts.ErrAlreadyExists } @@ -41,7 +44,7 @@ func (s *Service) CreateTeam(_ context.Context, req *CreateTeamReq, userID int) } func (s *Service) DeleteTeam(_ context.Context, teamID int) error { - rowsAffected, err := s.repo.DeleteTeam(teamID) + rowsAffected, err := s.repo.deleteTeam(teamID) if err != nil { return err } @@ -51,14 +54,18 @@ func (s *Service) DeleteTeam(_ context.Context, teamID int) error { return nil } -func (s *Service) GetTeamDetail(_ context.Context, teamID int) (*TeamDetailResp, error) { - team, userCount, projectCount, err := s.repo.loadTeamDetail(teamID) +func (s *Service) GetTeamDetail(ctx context.Context, teamID int) (*TeamDetailResp, error) { + team, userCount, err := s.repo.loadTeamDetailBase(teamID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, consts.ErrNotFound } return nil, err } + projectCount, err := s.projects.CountProjects(ctx, teamID) + if err != nil { + return nil, err + } resp := NewTeamDetailResp(team) resp.UserCount = userCount @@ -99,26 +106,12 @@ func (s *Service) UpdateTeam(_ context.Context, req *UpdateTeamReq, teamID int) return NewTeamResp(team), nil } -func (s *Service) ListTeamProjects(_ context.Context, req *TeamProjectListReq, teamID int) (*dto.ListResp[TeamProjectItem], error) { - limit, offset := req.ToGormParams() - projects, statsMap, total, err := s.repo.listTeamProjectViews(teamID, limit, offset, req.IsPublic, req.Status) - if err != nil { - return nil, err - } - - items := make([]TeamProjectItem, 0, len(projects)) - for i := range projects { - items = append(items, *projectmodule.NewProjectResp(&projects[i], statsMap[projects[i].ID])) - } - - return &dto.ListResp[TeamProjectItem]{ - Items: items, - Pagination: req.ConvertToPaginationInfo(total), - }, nil +func (s *Service) ListTeamProjects(ctx context.Context, req *TeamProjectListReq, teamID int) (*dto.ListResp[TeamProjectItem], error) { + return s.projects.ListProjects(ctx, req, teamID) } func (s *Service) AddMember(_ context.Context, req *AddTeamMemberReq, teamID int) error { - if err := s.repo.AddMember(teamID, req.Username, req.RoleID); err != nil { + if err := s.repo.addMember(teamID, req.Username, req.RoleID); err != nil { if errors.Is(err, consts.ErrNotFound) { return consts.ErrNotFound } @@ -138,7 +131,7 @@ func (s *Service) RemoveMember(_ context.Context, teamID, currentUserID, targetU return fmt.Errorf("cannot remove yourself from the team") } - rowsAffected, err := s.repo.RemoveMember(teamID, targetUserID) + rowsAffected, err := s.repo.removeMember(teamID, targetUserID) if err != nil { if errors.Is(err, consts.ErrNotFound) || errors.Is(err, gorm.ErrRecordNotFound) { return consts.ErrNotFound @@ -154,7 +147,7 @@ func (s *Service) RemoveMember(_ context.Context, teamID, currentUserID, targetU func (s *Service) UpdateMemberRole(_ context.Context, req *UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { _ = currentUserID - if err := s.repo.UpdateMemberRole(teamID, targetUserID, req.RoleID); err != nil { + if err := s.repo.updateMemberRole(teamID, targetUserID, req.RoleID); err != nil { if errors.Is(err, consts.ErrNotFound) { return consts.ErrNotFound } @@ -168,7 +161,7 @@ func (s *Service) UpdateMemberRole(_ context.Context, req *UpdateTeamMemberRoleR func (s *Service) ListMembers(_ context.Context, req *ListTeamMemberReq, teamID int) (*dto.ListResp[TeamMemberResp], error) { limit, offset := req.ToGormParams() - members, total, err := s.repo.ListTeamMembers(teamID, limit, offset) + members, total, err := s.repo.listTeamMembers(teamID, limit, offset) if err != nil { return nil, err } diff --git a/src/module/team/service_test.go b/src/module/team/service_test.go index f7ed54fc..46653f9c 100644 --- a/src/module/team/service_test.go +++ b/src/module/team/service_test.go @@ -1,17 +1,29 @@ package teammodule import ( + "context" "regexp" "testing" "time" "aegis/consts" + "aegis/dto" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/require" "gorm.io/driver/mysql" "gorm.io/gorm" ) +type stubProjectReader struct{} + +func (stubProjectReader) CountProjects(context.Context, int) (int, error) { + return 0, nil +} + +func (stubProjectReader) ListProjects(context.Context, *TeamProjectListReq, int) (*dto.ListResp[TeamProjectItem], error) { + return &dto.ListResp[TeamProjectItem]{Items: []TeamProjectItem{}}, nil +} + func newTeamService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { t.Helper() @@ -24,7 +36,7 @@ func newTeamService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { }), &gorm.Config{}) require.NoError(t, err) - return NewService(NewRepository(db)), mock, func() { + return NewService(NewRepository(db), stubProjectReader{}), mock, func() { _ = sqlDB.Close() } } @@ -54,7 +66,7 @@ func TestTeamServiceListTeamsSuccess(t *testing.T) { } func TestTeamServiceRemoveMemberSelfRejected(t *testing.T) { - service := NewService(nil) + service := NewService(nil, stubProjectReader{}) err := service.RemoveMember(t.Context(), 1, 7, 7) diff --git a/src/module/trace/handler.go b/src/module/trace/handler.go index 679e020d..3be294b6 100644 --- a/src/module/trace/handler.go +++ b/src/module/trace/handler.go @@ -19,10 +19,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/trace/handler_service.go b/src/module/trace/handler_service.go new file mode 100644 index 00000000..9fa133a2 --- /dev/null +++ b/src/module/trace/handler_service.go @@ -0,0 +1,22 @@ +package tracemodule + +import ( + "context" + "time" + + "aegis/dto" + + "github.com/redis/go-redis/v9" +) + +// HandlerService captures trace operations consumed by HTTP handlers and gateway adapters. +type HandlerService interface { + GetTrace(context.Context, string) (*TraceDetailResp, error) + ListTraces(context.Context, *ListTraceReq) (*dto.ListResp[TraceResp], error) + GetTraceStreamProcessor(context.Context, string) (*StreamProcessor, error) + ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/trace/module.go b/src/module/trace/module.go index 8635116e..09d32c85 100644 --- a/src/module/trace/module.go +++ b/src/module/trace/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("trace", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/trace/service.go b/src/module/trace/service.go index 9a01ef94..f072f0a7 100644 --- a/src/module/trace/service.go +++ b/src/module/trace/service.go @@ -51,6 +51,14 @@ func (s *Service) ListTraces(_ context.Context, req *ListTraceReq) (*dto.ListRes } func (s *Service) GetTraceStreamProcessor(ctx context.Context, traceID string) (*StreamProcessor, error) { + algorithms, err := s.GetTraceStreamAlgorithms(ctx, traceID) + if err != nil { + return nil, err + } + return NewStreamProcessor(algorithms), nil +} + +func (s *Service) GetTraceStreamAlgorithms(ctx context.Context, traceID string) ([]dto.ContainerVersionItem, error) { trace, err := s.repo.GetTraceByID(traceID) if err != nil { return nil, fmt.Errorf("failed to fetch trace: %w", err) @@ -63,17 +71,17 @@ func (s *Service) GetTraceStreamProcessor(ctx context.Context, traceID string) ( } } - if len(algorithms) > 0 { - filtered := algorithms[:0] - for _, algorithm := range algorithms { - if algorithm.ContainerName != config.GetDetectorName() { - filtered = append(filtered, algorithm) - } - } - algorithms = filtered + if len(algorithms) == 0 { + return nil, nil } - return NewStreamProcessor(algorithms), nil + filtered := algorithms[:0] + for _, algorithm := range algorithms { + if algorithm.ContainerName != config.GetDetectorName() { + filtered = append(filtered, algorithm) + } + } + return filtered, nil } func (s *Service) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { diff --git a/src/module/user/handler.go b/src/module/user/handler.go index 9cee30cc..13742a47 100644 --- a/src/module/user/handler.go +++ b/src/module/user/handler.go @@ -12,10 +12,10 @@ import ( ) type Handler struct { - service *Service + service HandlerService } -func NewHandler(service *Service) *Handler { +func NewHandler(service HandlerService) *Handler { return &Handler{service: service} } diff --git a/src/module/user/handler_service.go b/src/module/user/handler_service.go new file mode 100644 index 00000000..34fcb4d0 --- /dev/null +++ b/src/module/user/handler_service.go @@ -0,0 +1,30 @@ +package usermodule + +import ( + "context" + + "aegis/dto" +) + +// HandlerService captures the user operations consumed by the HTTP handler. +type HandlerService interface { + CreateUser(context.Context, *CreateUserReq) (*UserResp, error) + DeleteUser(context.Context, int) error + GetUserDetail(context.Context, int) (*UserDetailResp, error) + ListUsers(context.Context, *ListUserReq) (*dto.ListResp[UserResp], error) + UpdateUser(context.Context, *UpdateUserReq, int) (*UserResp, error) + AssignRole(context.Context, int, int) error + RemoveRole(context.Context, int, int) error + AssignPermissions(context.Context, *AssignUserPermissionReq, int) error + RemovePermissions(context.Context, *RemoveUserPermissionReq, int) error + AssignContainer(context.Context, int, int, int) error + RemoveContainer(context.Context, int, int) error + AssignDataset(context.Context, int, int, int) error + RemoveDataset(context.Context, int, int) error + AssignProject(context.Context, int, int, int) error + RemoveProject(context.Context, int, int) error +} + +func AsHandlerService(service *Service) HandlerService { + return service +} diff --git a/src/module/user/module.go b/src/module/user/module.go index 30c5f35e..08c04c8d 100644 --- a/src/module/user/module.go +++ b/src/module/user/module.go @@ -5,5 +5,6 @@ import "go.uber.org/fx" var Module = fx.Module("user", fx.Provide(NewRepository), fx.Provide(NewService), + fx.Provide(AsHandlerService), fx.Provide(NewHandler), ) diff --git a/src/module/user/repository.go b/src/module/user/repository.go index 01a6ddac..518cb59b 100644 --- a/src/module/user/repository.go +++ b/src/module/user/repository.go @@ -16,30 +16,15 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) withDB(db *gorm.DB) *Repository { - return &Repository{db: db} -} - -func (r *Repository) Transaction(fn func(tx *gorm.DB) error) error { - return r.db.Transaction(fn) -} - -func (r *Repository) ensureUserUnique(username, email string) error { +func (r *Repository) createUserIfUnique(user *model.User) error { var existingByUsername model.User - if err := r.db.Where("username = ?", username).First(&existingByUsername).Error; err == nil { - return fmt.Errorf("%w: username %s already exists", consts.ErrAlreadyExists, username) + if err := r.db.Where("username = ?", user.Username).First(&existingByUsername).Error; err == nil { + return fmt.Errorf("%w: username %s already exists", consts.ErrAlreadyExists, user.Username) } var existingByEmail model.User - if err := r.db.Where("email = ?", email).First(&existingByEmail).Error; err == nil { - return fmt.Errorf("%w: email %s already exists", consts.ErrAlreadyExists, email) - } - return nil -} - -func (r *Repository) createUserIfUnique(user *model.User) error { - if err := r.ensureUserUnique(user.Username, user.Email); err != nil { - return err + if err := r.db.Where("email = ?", user.Email).First(&existingByEmail).Error; err == nil { + return fmt.Errorf("%w: email %s already exists", consts.ErrAlreadyExists, user.Email) } if err := r.db.Omit("active_username").Create(user).Error; err != nil { return fmt.Errorf("failed to create user: %w", err) @@ -55,7 +40,7 @@ func (r *Repository) getUserDetailBase(userID int) (*model.User, error) { return &user, nil } -func (r *Repository) DeleteUserCascade(userID int) (int64, error) { +func (r *Repository) deleteUserCascade(userID int) (int64, error) { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return 0, err } @@ -162,7 +147,7 @@ func (r *Repository) loadUserDetailRelations(userID int) ([]model.Role, []model. return roles, permissions, userContainers, userDatasets, userProjects, nil } -func (r *Repository) AssignGlobalRole(userID, roleID int) error { +func (r *Repository) assignGlobalRole(userID, roleID int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -175,7 +160,7 @@ func (r *Repository) AssignGlobalRole(userID, roleID int) error { return nil } -func (r *Repository) RemoveGlobalRole(userID, roleID int) error { +func (r *Repository) removeGlobalRole(userID, roleID int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -189,7 +174,7 @@ func (r *Repository) RemoveGlobalRole(userID, roleID int) error { return nil } -func (r *Repository) BuildUserPermissions(userID int, items []AssignUserPermissionItem) ([]model.UserPermission, error) { +func (r *Repository) buildUserPermissions(userID int, items []AssignUserPermissionItem) ([]model.UserPermission, error) { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return nil, err } @@ -235,7 +220,7 @@ func (r *Repository) BuildUserPermissions(userID int, items []AssignUserPermissi return userPermissions, nil } -func (r *Repository) BatchCreateUserPermissions(userPermissions []model.UserPermission) error { +func (r *Repository) batchCreateUserPermissions(userPermissions []model.UserPermission) error { if len(userPermissions) == 0 { return nil } @@ -245,7 +230,7 @@ func (r *Repository) BatchCreateUserPermissions(userPermissions []model.UserPerm return nil } -func (r *Repository) BatchDeleteUserPermissions(userID int, permissionIDs []int) error { +func (r *Repository) batchDeleteUserPermissions(userID int, permissionIDs []int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -271,7 +256,7 @@ func (r *Repository) BatchDeleteUserPermissions(userID int, permissionIDs []int) return nil } -func (r *Repository) AssignContainerRole(userID, containerID, roleID int) error { +func (r *Repository) assignContainerRole(userID, containerID, roleID int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -292,7 +277,7 @@ func (r *Repository) AssignContainerRole(userID, containerID, roleID int) error return nil } -func (r *Repository) RemoveContainerRole(userID, containerID int) (int64, error) { +func (r *Repository) removeContainerRole(userID, containerID int) (int64, error) { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return 0, err } @@ -308,7 +293,7 @@ func (r *Repository) RemoveContainerRole(userID, containerID int) (int64, error) return result.RowsAffected, nil } -func (r *Repository) AssignDatasetRole(userID, datasetID, roleID int) error { +func (r *Repository) assignDatasetRole(userID, datasetID, roleID int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -329,7 +314,7 @@ func (r *Repository) AssignDatasetRole(userID, datasetID, roleID int) error { return nil } -func (r *Repository) RemoveDatasetRole(userID, datasetID int) (int64, error) { +func (r *Repository) removeDatasetRole(userID, datasetID int) (int64, error) { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return 0, err } @@ -345,7 +330,7 @@ func (r *Repository) RemoveDatasetRole(userID, datasetID int) (int64, error) { return result.RowsAffected, nil } -func (r *Repository) AssignProjectRole(userID, projectID, roleID int) error { +func (r *Repository) assignProjectRole(userID, projectID, roleID int) error { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return err } @@ -366,7 +351,7 @@ func (r *Repository) AssignProjectRole(userID, projectID, roleID int) error { return nil } -func (r *Repository) RemoveProjectRole(userID, projectID int) (int64, error) { +func (r *Repository) removeProjectRole(userID, projectID int) (int64, error) { if err := r.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { return 0, err } diff --git a/src/module/user/service.go b/src/module/user/service.go index dd881044..b187a2af 100644 --- a/src/module/user/service.go +++ b/src/module/user/service.go @@ -37,8 +37,8 @@ func (s *Service) CreateUser(_ context.Context, req *CreateUserReq) (*UserResp, IsActive: true, } - if err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + if err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) return repo.createUserIfUnique(user) }); err != nil { return nil, err @@ -48,8 +48,8 @@ func (s *Service) CreateUser(_ context.Context, req *CreateUserReq) (*UserResp, } func (s *Service) DeleteUser(_ context.Context, userID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) if err := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: user not found", consts.ErrNotFound) @@ -57,7 +57,7 @@ func (s *Service) DeleteUser(_ context.Context, userID int) error { return fmt.Errorf("failed to get user: %w", err) } - rows, err := repo.DeleteUserCascade(userID) + rows, err := repo.deleteUserCascade(userID) if err != nil { return err } @@ -129,8 +129,8 @@ func (s *Service) UpdateUser(_ context.Context, req *UpdateUserReq, userID int) } var updatedUser *model.User - err := s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) + err := s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) user, err := repo.updateMutableUser(userID, func(existingUser *model.User) { req.PatchUserModel(existingUser) }) @@ -152,9 +152,9 @@ func (s *Service) UpdateUser(_ context.Context, req *UpdateUserReq, userID int) } func (s *Service) AssignRole(_ context.Context, userID, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.AssignGlobalRole(userID, roleID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.assignGlobalRole(userID, roleID); err != nil { if errors.Is(err, consts.ErrNotFound) { if userErr := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); userErr != nil { return fmt.Errorf("%w: user not found", consts.ErrNotFound) @@ -171,9 +171,9 @@ func (s *Service) AssignRole(_ context.Context, userID, roleID int) error { } func (s *Service) RemoveRole(_ context.Context, userID, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.RemoveGlobalRole(userID, roleID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.removeGlobalRole(userID, roleID); err != nil { if errors.Is(err, consts.ErrNotFound) { if userErr := repo.ensureActiveRecordExists(&model.User{}, userID, "user"); userErr != nil { return fmt.Errorf("%w: user not found", consts.ErrNotFound) @@ -187,9 +187,9 @@ func (s *Service) RemoveRole(_ context.Context, userID, roleID int) error { } func (s *Service) AssignPermissions(_ context.Context, req *AssignUserPermissionReq, userID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - userPermissions, err := repo.BuildUserPermissions(userID, req.Items) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + userPermissions, err := repo.buildUserPermissions(userID, req.Items) if err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: failed to resolve permission assignment targets", consts.ErrNotFound) @@ -197,7 +197,7 @@ func (s *Service) AssignPermissions(_ context.Context, req *AssignUserPermission return err } - if err := repo.BatchCreateUserPermissions(userPermissions); err != nil { + if err := repo.batchCreateUserPermissions(userPermissions); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return fmt.Errorf("%w: user already has one or more of these permissions", consts.ErrAlreadyExists) } @@ -208,9 +208,9 @@ func (s *Service) AssignPermissions(_ context.Context, req *AssignUserPermission } func (s *Service) RemovePermissions(_ context.Context, req *RemoveUserPermissionReq, userID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.BatchDeleteUserPermissions(userID, req.PermissionIDs); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.batchDeleteUserPermissions(userID, req.PermissionIDs); err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: failed to resolve user or permissions", consts.ErrNotFound) } @@ -221,9 +221,9 @@ func (s *Service) RemovePermissions(_ context.Context, req *RemoveUserPermission } func (s *Service) AssignContainer(_ context.Context, userID, containerID, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.AssignContainerRole(userID, containerID, roleID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.assignContainerRole(userID, containerID, roleID); err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: user/container/role not found", consts.ErrNotFound) } @@ -237,9 +237,9 @@ func (s *Service) AssignContainer(_ context.Context, userID, containerID, roleID } func (s *Service) RemoveContainer(_ context.Context, userID, containerID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - rows, err := repo.RemoveContainerRole(userID, containerID) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + rows, err := repo.removeContainerRole(userID, containerID) if err != nil { return fmt.Errorf("failed to remove user from container: %w", err) } @@ -251,9 +251,9 @@ func (s *Service) RemoveContainer(_ context.Context, userID, containerID int) er } func (s *Service) AssignDataset(_ context.Context, userID, datasetID, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.AssignDatasetRole(userID, datasetID, roleID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.assignDatasetRole(userID, datasetID, roleID); err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: user/dataset/role not found", consts.ErrNotFound) } @@ -267,9 +267,9 @@ func (s *Service) AssignDataset(_ context.Context, userID, datasetID, roleID int } func (s *Service) RemoveDataset(_ context.Context, userID, datasetID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - rows, err := repo.RemoveDatasetRole(userID, datasetID) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + rows, err := repo.removeDatasetRole(userID, datasetID) if err != nil { return fmt.Errorf("failed to remove user from dataset: %w", err) } @@ -281,9 +281,9 @@ func (s *Service) RemoveDataset(_ context.Context, userID, datasetID int) error } func (s *Service) AssignProject(_ context.Context, userID, projectID, roleID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - if err := repo.AssignProjectRole(userID, projectID, roleID); err != nil { + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + if err := repo.assignProjectRole(userID, projectID, roleID); err != nil { if errors.Is(err, consts.ErrNotFound) { return fmt.Errorf("%w: user/project/role not found", consts.ErrNotFound) } @@ -297,9 +297,9 @@ func (s *Service) AssignProject(_ context.Context, userID, projectID, roleID int } func (s *Service) RemoveProject(_ context.Context, userID, projectID int) error { - return s.repo.Transaction(func(tx *gorm.DB) error { - repo := s.repo.withDB(tx) - rows, err := repo.RemoveProjectRole(userID, projectID) + return s.repo.db.Transaction(func(tx *gorm.DB) error { + repo := NewRepository(tx) + rows, err := repo.removeProjectRole(userID, projectID) if err != nil { return fmt.Errorf("failed to remove user from project: %w", err) } diff --git a/src/proto/iam/v1/iam.pb.go b/src/proto/iam/v1/iam.pb.go new file mode 100644 index 00000000..f383ea53 --- /dev/null +++ b/src/proto/iam/v1/iam.pb.go @@ -0,0 +1,2209 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/iam/v1/iam.proto + +package iamv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type VerifyTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyTokenRequest) Reset() { + *x = VerifyTokenRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyTokenRequest) ProtoMessage() {} + +func (x *VerifyTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyTokenRequest.ProtoReflect.Descriptor instead. +func (*VerifyTokenRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{0} +} + +func (x *VerifyTokenRequest) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +type VerifyTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"` + IsActive bool `protobuf:"varint,6,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + IsAdmin bool `protobuf:"varint,7,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` + Roles []string `protobuf:"bytes,8,rep,name=roles,proto3" json:"roles,omitempty"` + ExpiresAtUnix int64 `protobuf:"varint,9,opt,name=expires_at_unix,json=expiresAtUnix,proto3" json:"expires_at_unix,omitempty"` + AuthType string `protobuf:"bytes,10,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` + AccessKeyId int64 `protobuf:"varint,11,opt,name=access_key_id,json=accessKeyId,proto3" json:"access_key_id,omitempty"` + TaskId string `protobuf:"bytes,12,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VerifyTokenResponse) Reset() { + *x = VerifyTokenResponse{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VerifyTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VerifyTokenResponse) ProtoMessage() {} + +func (x *VerifyTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VerifyTokenResponse.ProtoReflect.Descriptor instead. +func (*VerifyTokenResponse) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{1} +} + +func (x *VerifyTokenResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *VerifyTokenResponse) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + +func (x *VerifyTokenResponse) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *VerifyTokenResponse) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *VerifyTokenResponse) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *VerifyTokenResponse) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *VerifyTokenResponse) GetIsAdmin() bool { + if x != nil { + return x.IsAdmin + } + return false +} + +func (x *VerifyTokenResponse) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + +func (x *VerifyTokenResponse) GetExpiresAtUnix() int64 { + if x != nil { + return x.ExpiresAtUnix + } + return 0 +} + +func (x *VerifyTokenResponse) GetAuthType() string { + if x != nil { + return x.AuthType + } + return "" +} + +func (x *VerifyTokenResponse) GetAccessKeyId() int64 { + if x != nil { + return x.AccessKeyId + } + return 0 +} + +func (x *VerifyTokenResponse) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type CheckPermissionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Action string `protobuf:"bytes,2,opt,name=action,proto3" json:"action,omitempty"` + Scope string `protobuf:"bytes,3,opt,name=scope,proto3" json:"scope,omitempty"` + ResourceName string `protobuf:"bytes,4,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` + TeamId int64 `protobuf:"varint,5,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + ProjectId int64 `protobuf:"varint,6,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + ContainerId int64 `protobuf:"varint,7,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + DatasetId int64 `protobuf:"varint,8,opt,name=dataset_id,json=datasetId,proto3" json:"dataset_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckPermissionRequest) Reset() { + *x = CheckPermissionRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckPermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckPermissionRequest) ProtoMessage() {} + +func (x *CheckPermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckPermissionRequest.ProtoReflect.Descriptor instead. +func (*CheckPermissionRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckPermissionRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *CheckPermissionRequest) GetAction() string { + if x != nil { + return x.Action + } + return "" +} + +func (x *CheckPermissionRequest) GetScope() string { + if x != nil { + return x.Scope + } + return "" +} + +func (x *CheckPermissionRequest) GetResourceName() string { + if x != nil { + return x.ResourceName + } + return "" +} + +func (x *CheckPermissionRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *CheckPermissionRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *CheckPermissionRequest) GetContainerId() int64 { + if x != nil { + return x.ContainerId + } + return 0 +} + +func (x *CheckPermissionRequest) GetDatasetId() int64 { + if x != nil { + return x.DatasetId + } + return 0 +} + +type CheckPermissionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckPermissionResponse) Reset() { + *x = CheckPermissionResponse{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckPermissionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckPermissionResponse) ProtoMessage() {} + +func (x *CheckPermissionResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckPermissionResponse.ProtoReflect.Descriptor instead. +func (*CheckPermissionResponse) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{3} +} + +func (x *CheckPermissionResponse) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +type UserTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + TeamId int64 `protobuf:"varint,2,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserTeamRequest) Reset() { + *x = UserTeamRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserTeamRequest) ProtoMessage() {} + +func (x *UserTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserTeamRequest.ProtoReflect.Descriptor instead. +func (*UserTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{4} +} + +func (x *UserTeamRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserTeamRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +type TeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TeamRequest) Reset() { + *x = TeamRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TeamRequest) ProtoMessage() {} + +func (x *TeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TeamRequest.ProtoReflect.Descriptor instead. +func (*TeamRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{5} +} + +func (x *TeamRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +type UserProjectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ProjectId int64 `protobuf:"varint,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserProjectRequest) Reset() { + *x = UserProjectRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserProjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserProjectRequest) ProtoMessage() {} + +func (x *UserProjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserProjectRequest.ProtoReflect.Descriptor instead. +func (*UserProjectRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{6} +} + +func (x *UserProjectRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserProjectRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +type BoolResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value bool `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoolResponse) Reset() { + *x = BoolResponse{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoolResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoolResponse) ProtoMessage() {} + +func (x *BoolResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoolResponse.ProtoReflect.Descriptor instead. +func (*BoolResponse) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{7} +} + +func (x *BoolResponse) GetValue() bool { + if x != nil { + return x.Value + } + return false +} + +type ExchangeAccessKeyTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + Timestamp string `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Nonce string `protobuf:"bytes,3,opt,name=nonce,proto3" json:"nonce,omitempty"` + Signature string `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` + Method string `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,6,opt,name=path,proto3" json:"path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeAccessKeyTokenRequest) Reset() { + *x = ExchangeAccessKeyTokenRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeAccessKeyTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeAccessKeyTokenRequest) ProtoMessage() {} + +func (x *ExchangeAccessKeyTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExchangeAccessKeyTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeAccessKeyTokenRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{8} +} + +func (x *ExchangeAccessKeyTokenRequest) GetAccessKey() string { + if x != nil { + return x.AccessKey + } + return "" +} + +func (x *ExchangeAccessKeyTokenRequest) GetTimestamp() string { + if x != nil { + return x.Timestamp + } + return "" +} + +func (x *ExchangeAccessKeyTokenRequest) GetNonce() string { + if x != nil { + return x.Nonce + } + return "" +} + +func (x *ExchangeAccessKeyTokenRequest) GetSignature() string { + if x != nil { + return x.Signature + } + return "" +} + +func (x *ExchangeAccessKeyTokenRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ExchangeAccessKeyTokenRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +type ExchangeAccessKeyTokenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` + ExpiresAtUnix int64 `protobuf:"varint,3,opt,name=expires_at_unix,json=expiresAtUnix,proto3" json:"expires_at_unix,omitempty"` + AuthType string `protobuf:"bytes,4,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` + AccessKey string `protobuf:"bytes,5,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExchangeAccessKeyTokenResponse) Reset() { + *x = ExchangeAccessKeyTokenResponse{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExchangeAccessKeyTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExchangeAccessKeyTokenResponse) ProtoMessage() {} + +func (x *ExchangeAccessKeyTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExchangeAccessKeyTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeAccessKeyTokenResponse) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{9} +} + +func (x *ExchangeAccessKeyTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *ExchangeAccessKeyTokenResponse) GetTokenType() string { + if x != nil { + return x.TokenType + } + return "" +} + +func (x *ExchangeAccessKeyTokenResponse) GetExpiresAtUnix() int64 { + if x != nil { + return x.ExpiresAtUnix + } + return 0 +} + +func (x *ExchangeAccessKeyTokenResponse) GetAuthType() string { + if x != nil { + return x.AuthType + } + return "" +} + +func (x *ExchangeAccessKeyTokenResponse) GetAccessKey() string { + if x != nil { + return x.AccessKey + } + return "" +} + +type MutationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Body *structpb.Struct `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutationRequest) Reset() { + *x = MutationRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutationRequest) ProtoMessage() {} + +func (x *MutationRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutationRequest.ProtoReflect.Descriptor instead. +func (*MutationRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{10} +} + +func (x *MutationRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type QueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRequest) Reset() { + *x = QueryRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRequest) ProtoMessage() {} + +func (x *QueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. +func (*QueryRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{11} +} + +func (x *QueryRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type IDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IDRequest) Reset() { + *x = IDRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IDRequest) ProtoMessage() {} + +func (x *IDRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IDRequest.ProtoReflect.Descriptor instead. +func (*IDRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{12} +} + +func (x *IDRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type UpdateByIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateByIDRequest) Reset() { + *x = UpdateByIDRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateByIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateByIDRequest) ProtoMessage() {} + +func (x *UpdateByIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateByIDRequest.ProtoReflect.Descriptor instead. +func (*UpdateByIDRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{13} +} + +func (x *UpdateByIDRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateByIDRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type UserIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserIDRequest) Reset() { + *x = UserIDRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserIDRequest) ProtoMessage() {} + +func (x *UserIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserIDRequest.ProtoReflect.Descriptor instead. +func (*UserIDRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{14} +} + +func (x *UserIDRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +type UserQueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Query *structpb.Struct `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserQueryRequest) Reset() { + *x = UserQueryRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserQueryRequest) ProtoMessage() {} + +func (x *UserQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserQueryRequest.ProtoReflect.Descriptor instead. +func (*UserQueryRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{15} +} + +func (x *UserQueryRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserQueryRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type UserBodyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserBodyRequest) Reset() { + *x = UserBodyRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserBodyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserBodyRequest) ProtoMessage() {} + +func (x *UserBodyRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserBodyRequest.ProtoReflect.Descriptor instead. +func (*UserBodyRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{16} +} + +func (x *UserBodyRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserBodyRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type UserScopedIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Id int64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserScopedIDRequest) Reset() { + *x = UserScopedIDRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserScopedIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserScopedIDRequest) ProtoMessage() {} + +func (x *UserScopedIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserScopedIDRequest.ProtoReflect.Descriptor instead. +func (*UserScopedIDRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{17} +} + +func (x *UserScopedIDRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserScopedIDRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type UserRoleBindingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + RoleId int64 `protobuf:"varint,2,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserRoleBindingRequest) Reset() { + *x = UserRoleBindingRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserRoleBindingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserRoleBindingRequest) ProtoMessage() {} + +func (x *UserRoleBindingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserRoleBindingRequest.ProtoReflect.Descriptor instead. +func (*UserRoleBindingRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{18} +} + +func (x *UserRoleBindingRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserRoleBindingRequest) GetRoleId() int64 { + if x != nil { + return x.RoleId + } + return 0 +} + +type UserResourceBindingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ResourceId int64 `protobuf:"varint,2,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + RoleId int64 `protobuf:"varint,3,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserResourceBindingRequest) Reset() { + *x = UserResourceBindingRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserResourceBindingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserResourceBindingRequest) ProtoMessage() {} + +func (x *UserResourceBindingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserResourceBindingRequest.ProtoReflect.Descriptor instead. +func (*UserResourceBindingRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{19} +} + +func (x *UserResourceBindingRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserResourceBindingRequest) GetResourceId() int64 { + if x != nil { + return x.ResourceId + } + return 0 +} + +func (x *UserResourceBindingRequest) GetRoleId() int64 { + if x != nil { + return x.RoleId + } + return 0 +} + +type LogoutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + TokenId string `protobuf:"bytes,2,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` + ExpiresAtUnix int64 `protobuf:"varint,3,opt,name=expires_at_unix,json=expiresAtUnix,proto3" json:"expires_at_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LogoutRequest) Reset() { + *x = LogoutRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LogoutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LogoutRequest) ProtoMessage() {} + +func (x *LogoutRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LogoutRequest.ProtoReflect.Descriptor instead. +func (*LogoutRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{20} +} + +func (x *LogoutRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *LogoutRequest) GetTokenId() string { + if x != nil { + return x.TokenId + } + return "" +} + +func (x *LogoutRequest) GetExpiresAtUnix() int64 { + if x != nil { + return x.ExpiresAtUnix + } + return 0 +} + +type RolePermissionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RoleId int64 `protobuf:"varint,1,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` + PermissionIds []int64 `protobuf:"varint,2,rep,packed,name=permission_ids,json=permissionIds,proto3" json:"permission_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RolePermissionsRequest) Reset() { + *x = RolePermissionsRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RolePermissionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RolePermissionsRequest) ProtoMessage() {} + +func (x *RolePermissionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RolePermissionsRequest.ProtoReflect.Descriptor instead. +func (*RolePermissionsRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{21} +} + +func (x *RolePermissionsRequest) GetRoleId() int64 { + if x != nil { + return x.RoleId + } + return 0 +} + +func (x *RolePermissionsRequest) GetPermissionIds() []int64 { + if x != nil { + return x.PermissionIds + } + return nil +} + +type CreateTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateTeamRequest) Reset() { + *x = CreateTeamRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTeamRequest) ProtoMessage() {} + +func (x *CreateTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTeamRequest.ProtoReflect.Descriptor instead. +func (*CreateTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{22} +} + +func (x *CreateTeamRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *CreateTeamRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type ListTeamsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + IsAdmin bool `protobuf:"varint,2,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` + Query *structpb.Struct `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamsRequest) Reset() { + *x = ListTeamsRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamsRequest) ProtoMessage() {} + +func (x *ListTeamsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTeamsRequest.ProtoReflect.Descriptor instead. +func (*ListTeamsRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{23} +} + +func (x *ListTeamsRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *ListTeamsRequest) GetIsAdmin() bool { + if x != nil { + return x.IsAdmin + } + return false +} + +func (x *ListTeamsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type UpdateTeamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTeamRequest) Reset() { + *x = UpdateTeamRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTeamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTeamRequest) ProtoMessage() {} + +func (x *UpdateTeamRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateTeamRequest.ProtoReflect.Descriptor instead. +func (*UpdateTeamRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{24} +} + +func (x *UpdateTeamRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *UpdateTeamRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type ListTeamProjectsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Query *structpb.Struct `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamProjectsRequest) Reset() { + *x = ListTeamProjectsRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamProjectsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamProjectsRequest) ProtoMessage() {} + +func (x *ListTeamProjectsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTeamProjectsRequest.ProtoReflect.Descriptor instead. +func (*ListTeamProjectsRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{25} +} + +func (x *ListTeamProjectsRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *ListTeamProjectsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type AddTeamMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddTeamMemberRequest) Reset() { + *x = AddTeamMemberRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddTeamMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddTeamMemberRequest) ProtoMessage() {} + +func (x *AddTeamMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddTeamMemberRequest.ProtoReflect.Descriptor instead. +func (*AddTeamMemberRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{26} +} + +func (x *AddTeamMemberRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *AddTeamMemberRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type RemoveTeamMemberRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + CurrentUserId int64 `protobuf:"varint,2,opt,name=current_user_id,json=currentUserId,proto3" json:"current_user_id,omitempty"` + TargetUserId int64 `protobuf:"varint,3,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveTeamMemberRequest) Reset() { + *x = RemoveTeamMemberRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveTeamMemberRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveTeamMemberRequest) ProtoMessage() {} + +func (x *RemoveTeamMemberRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveTeamMemberRequest.ProtoReflect.Descriptor instead. +func (*RemoveTeamMemberRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{27} +} + +func (x *RemoveTeamMemberRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *RemoveTeamMemberRequest) GetCurrentUserId() int64 { + if x != nil { + return x.CurrentUserId + } + return 0 +} + +func (x *RemoveTeamMemberRequest) GetTargetUserId() int64 { + if x != nil { + return x.TargetUserId + } + return 0 +} + +type UpdateTeamMemberRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"` + CurrentUserId int64 `protobuf:"varint,3,opt,name=current_user_id,json=currentUserId,proto3" json:"current_user_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateTeamMemberRoleRequest) Reset() { + *x = UpdateTeamMemberRoleRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateTeamMemberRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTeamMemberRoleRequest) ProtoMessage() {} + +func (x *UpdateTeamMemberRoleRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateTeamMemberRoleRequest.ProtoReflect.Descriptor instead. +func (*UpdateTeamMemberRoleRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{28} +} + +func (x *UpdateTeamMemberRoleRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *UpdateTeamMemberRoleRequest) GetTargetUserId() int64 { + if x != nil { + return x.TargetUserId + } + return 0 +} + +func (x *UpdateTeamMemberRoleRequest) GetCurrentUserId() int64 { + if x != nil { + return x.CurrentUserId + } + return 0 +} + +func (x *UpdateTeamMemberRoleRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type ListTeamMembersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TeamId int64 `protobuf:"varint,1,opt,name=team_id,json=teamId,proto3" json:"team_id,omitempty"` + Query *structpb.Struct `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTeamMembersRequest) Reset() { + *x = ListTeamMembersRequest{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTeamMembersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTeamMembersRequest) ProtoMessage() {} + +func (x *ListTeamMembersRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTeamMembersRequest.ProtoReflect.Descriptor instead. +func (*ListTeamMembersRequest) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{29} +} + +func (x *ListTeamMembersRequest) GetTeamId() int64 { + if x != nil { + return x.TeamId + } + return 0 +} + +func (x *ListTeamMembersRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type StructResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StructResponse) Reset() { + *x = StructResponse{} + mi := &file_proto_iam_v1_iam_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StructResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StructResponse) ProtoMessage() {} + +func (x *StructResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_iam_v1_iam_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StructResponse.ProtoReflect.Descriptor instead. +func (*StructResponse) Descriptor() ([]byte, []int) { + return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{30} +} + +func (x *StructResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +var File_proto_iam_v1_iam_proto protoreflect.FileDescriptor + +const file_proto_iam_v1_iam_proto_rawDesc = "" + + "\n" + + "\x16proto/iam/v1/iam.proto\x12\x06iam.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\"*\n" + + "\x12VerifyTokenRequest\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\xe5\x02\n" + + "\x13VerifyTokenResponse\x12\x14\n" + + "\x05valid\x18\x01 \x01(\bR\x05valid\x12\x1d\n" + + "\n" + + "token_type\x18\x02 \x01(\tR\ttokenType\x12\x17\n" + + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x1a\n" + + "\busername\x18\x04 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x05 \x01(\tR\x05email\x12\x1b\n" + + "\tis_active\x18\x06 \x01(\bR\bisActive\x12\x19\n" + + "\bis_admin\x18\a \x01(\bR\aisAdmin\x12\x14\n" + + "\x05roles\x18\b \x03(\tR\x05roles\x12&\n" + + "\x0fexpires_at_unix\x18\t \x01(\x03R\rexpiresAtUnix\x12\x1b\n" + + "\tauth_type\x18\n" + + " \x01(\tR\bauthType\x12\"\n" + + "\raccess_key_id\x18\v \x01(\x03R\vaccessKeyId\x12\x17\n" + + "\atask_id\x18\f \x01(\tR\x06taskId\"\xfe\x01\n" + + "\x16CheckPermissionRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x16\n" + + "\x06action\x18\x02 \x01(\tR\x06action\x12\x14\n" + + "\x05scope\x18\x03 \x01(\tR\x05scope\x12#\n" + + "\rresource_name\x18\x04 \x01(\tR\fresourceName\x12\x17\n" + + "\ateam_id\x18\x05 \x01(\x03R\x06teamId\x12\x1d\n" + + "\n" + + "project_id\x18\x06 \x01(\x03R\tprojectId\x12!\n" + + "\fcontainer_id\x18\a \x01(\x03R\vcontainerId\x12\x1d\n" + + "\n" + + "dataset_id\x18\b \x01(\x03R\tdatasetId\"3\n" + + "\x17CheckPermissionResponse\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\"C\n" + + "\x0fUserTeamRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" + + "\ateam_id\x18\x02 \x01(\x03R\x06teamId\"&\n" + + "\vTeamRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\"L\n" + + "\x12UserProjectRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1d\n" + + "\n" + + "project_id\x18\x02 \x01(\x03R\tprojectId\"$\n" + + "\fBoolResponse\x12\x14\n" + + "\x05value\x18\x01 \x01(\bR\x05value\"\xbc\x01\n" + + "\x1dExchangeAccessKeyTokenRequest\x12\x1d\n" + + "\n" + + "access_key\x18\x01 \x01(\tR\taccessKey\x12\x1c\n" + + "\ttimestamp\x18\x02 \x01(\tR\ttimestamp\x12\x14\n" + + "\x05nonce\x18\x03 \x01(\tR\x05nonce\x12\x1c\n" + + "\tsignature\x18\x04 \x01(\tR\tsignature\x12\x16\n" + + "\x06method\x18\x05 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x06 \x01(\tR\x04path\"\xb9\x01\n" + + "\x1eExchangeAccessKeyTokenResponse\x12\x14\n" + + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1d\n" + + "\n" + + "token_type\x18\x02 \x01(\tR\ttokenType\x12&\n" + + "\x0fexpires_at_unix\x18\x03 \x01(\x03R\rexpiresAtUnix\x12\x1b\n" + + "\tauth_type\x18\x04 \x01(\tR\bauthType\x12\x1d\n" + + "\n" + + "access_key\x18\x05 \x01(\tR\taccessKey\">\n" + + "\x0fMutationRequest\x12+\n" + + "\x04body\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04body\"=\n" + + "\fQueryRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"\x1b\n" + + "\tIDRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"P\n" + + "\x11UpdateByIDRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\"(\n" + + "\rUserIDRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\"Z\n" + + "\x10UserQueryRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12-\n" + + "\x05query\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x05query\"W\n" + + "\x0fUserBodyRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\">\n" + + "\x13UserScopedIDRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x0e\n" + + "\x02id\x18\x02 \x01(\x03R\x02id\"J\n" + + "\x16UserRoleBindingRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x17\n" + + "\arole_id\x18\x02 \x01(\x03R\x06roleId\"o\n" + + "\x1aUserResourceBindingRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x1f\n" + + "\vresource_id\x18\x02 \x01(\x03R\n" + + "resourceId\x12\x17\n" + + "\arole_id\x18\x03 \x01(\x03R\x06roleId\"k\n" + + "\rLogoutRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x19\n" + + "\btoken_id\x18\x02 \x01(\tR\atokenId\x12&\n" + + "\x0fexpires_at_unix\x18\x03 \x01(\x03R\rexpiresAtUnix\"X\n" + + "\x16RolePermissionsRequest\x12\x17\n" + + "\arole_id\x18\x01 \x01(\x03R\x06roleId\x12%\n" + + "\x0epermission_ids\x18\x02 \x03(\x03R\rpermissionIds\"Y\n" + + "\x11CreateTeamRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\"u\n" + + "\x10ListTeamsRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x19\n" + + "\bis_admin\x18\x02 \x01(\bR\aisAdmin\x12-\n" + + "\x05query\x18\x03 \x01(\v2\x17.google.protobuf.StructR\x05query\"Y\n" + + "\x11UpdateTeamRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\"a\n" + + "\x17ListTeamProjectsRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12-\n" + + "\x05query\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x05query\"\\\n" + + "\x14AddTeamMemberRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\"\x80\x01\n" + + "\x17RemoveTeamMemberRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12&\n" + + "\x0fcurrent_user_id\x18\x02 \x01(\x03R\rcurrentUserId\x12$\n" + + "\x0etarget_user_id\x18\x03 \x01(\x03R\ftargetUserId\"\xb1\x01\n" + + "\x1bUpdateTeamMemberRoleRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12$\n" + + "\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12&\n" + + "\x0fcurrent_user_id\x18\x03 \x01(\x03R\rcurrentUserId\x12+\n" + + "\x04body\x18\x04 \x01(\v2\x17.google.protobuf.StructR\x04body\"`\n" + + "\x16ListTeamMembersRequest\x12\x17\n" + + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12-\n" + + "\x05query\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x05query\"=\n" + + "\x0eStructResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xa3 \n" + + "\n" + + "IAMService\x12F\n" + + "\vVerifyToken\x12\x1a.iam.v1.VerifyTokenRequest\x1a\x1b.iam.v1.VerifyTokenResponse\x12R\n" + + "\x0fCheckPermission\x12\x1e.iam.v1.CheckPermissionRequest\x1a\x1f.iam.v1.CheckPermissionResponse\x128\n" + + "\x05Login\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x12;\n" + + "\bRegister\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\fRefreshToken\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x127\n" + + "\x06Logout\x12\x15.iam.v1.LogoutRequest\x1a\x16.google.protobuf.Empty\x12A\n" + + "\x0eChangePassword\x12\x17.iam.v1.UserBodyRequest\x1a\x16.google.protobuf.Empty\x12;\n" + + "\n" + + "GetProfile\x12\x15.iam.v1.UserIDRequest\x1a\x16.iam.v1.StructResponse\x12B\n" + + "\x0fCreateAccessKey\x12\x17.iam.v1.UserBodyRequest\x1a\x16.iam.v1.StructResponse\x12B\n" + + "\x0eListAccessKeys\x12\x18.iam.v1.UserQueryRequest\x1a\x16.iam.v1.StructResponse\x12C\n" + + "\fGetAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12F\n" + + "\x0fDeleteAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12G\n" + + "\x10DisableAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12F\n" + + "\x0fEnableAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12F\n" + + "\x0fRotateAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12@\n" + + "\x0fIsUserTeamAdmin\x12\x17.iam.v1.UserTeamRequest\x1a\x14.iam.v1.BoolResponse\x12=\n" + + "\fIsUserInTeam\x12\x17.iam.v1.UserTeamRequest\x1a\x14.iam.v1.BoolResponse\x129\n" + + "\fIsTeamPublic\x12\x13.iam.v1.TeamRequest\x1a\x14.iam.v1.BoolResponse\x12F\n" + + "\x12IsUserProjectAdmin\x12\x1a.iam.v1.UserProjectRequest\x1a\x14.iam.v1.BoolResponse\x12C\n" + + "\x0fIsUserInProject\x12\x1a.iam.v1.UserProjectRequest\x1a\x14.iam.v1.BoolResponse\x12g\n" + + "\x16ExchangeAccessKeyToken\x12%.iam.v1.ExchangeAccessKeyTokenRequest\x1a&.iam.v1.ExchangeAccessKeyTokenResponse\x12=\n" + + "\n" + + "CreateUser\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x127\n" + + "\n" + + "DeleteUser\x12\x11.iam.v1.IDRequest\x1a\x16.google.protobuf.Empty\x124\n" + + "\aGetUser\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x129\n" + + "\tListUsers\x12\x14.iam.v1.QueryRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\n" + + "UpdateUser\x12\x19.iam.v1.UpdateByIDRequest\x1a\x16.iam.v1.StructResponse\x12H\n" + + "\x0eAssignUserRole\x12\x1e.iam.v1.UserRoleBindingRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x0eRemoveUserRole\x12\x1e.iam.v1.UserRoleBindingRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x15AssignUserPermissions\x12\x17.iam.v1.UserBodyRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x15RemoveUserPermissions\x12\x17.iam.v1.UserBodyRequest\x1a\x16.google.protobuf.Empty\x12Q\n" + + "\x13AssignUserContainer\x12\".iam.v1.UserResourceBindingRequest\x1a\x16.google.protobuf.Empty\x12J\n" + + "\x13RemoveUserContainer\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12O\n" + + "\x11AssignUserDataset\x12\".iam.v1.UserResourceBindingRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x11RemoveUserDataset\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12O\n" + + "\x11AssignUserProject\x12\".iam.v1.UserResourceBindingRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x11RemoveUserProject\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12=\n" + + "\n" + + "CreateRole\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x127\n" + + "\n" + + "DeleteRole\x12\x11.iam.v1.IDRequest\x1a\x16.google.protobuf.Empty\x124\n" + + "\aGetRole\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x129\n" + + "\tListRoles\x12\x14.iam.v1.QueryRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\n" + + "UpdateRole\x12\x19.iam.v1.UpdateByIDRequest\x1a\x16.iam.v1.StructResponse\x12O\n" + + "\x15AssignRolePermissions\x12\x1e.iam.v1.RolePermissionsRequest\x1a\x16.google.protobuf.Empty\x12O\n" + + "\x15RemoveRolePermissions\x12\x1e.iam.v1.RolePermissionsRequest\x1a\x16.google.protobuf.Empty\x12>\n" + + "\x11ListUsersFromRole\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x12:\n" + + "\rGetPermission\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\x0fListPermissions\x12\x14.iam.v1.QueryRequest\x1a\x16.iam.v1.StructResponse\x12D\n" + + "\x17ListRolesFromPermission\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x128\n" + + "\vGetResource\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x12=\n" + + "\rListResources\x12\x14.iam.v1.QueryRequest\x1a\x16.iam.v1.StructResponse\x12D\n" + + "\x17ListResourcePermissions\x12\x11.iam.v1.IDRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\n" + + "CreateTeam\x12\x19.iam.v1.CreateTeamRequest\x1a\x16.iam.v1.StructResponse\x129\n" + + "\n" + + "DeleteTeam\x12\x13.iam.v1.TeamRequest\x1a\x16.google.protobuf.Empty\x126\n" + + "\aGetTeam\x12\x13.iam.v1.TeamRequest\x1a\x16.iam.v1.StructResponse\x12=\n" + + "\tListTeams\x12\x18.iam.v1.ListTeamsRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\n" + + "UpdateTeam\x12\x19.iam.v1.UpdateTeamRequest\x1a\x16.iam.v1.StructResponse\x12K\n" + + "\x10ListTeamProjects\x12\x1f.iam.v1.ListTeamProjectsRequest\x1a\x16.iam.v1.StructResponse\x12E\n" + + "\rAddTeamMember\x12\x1c.iam.v1.AddTeamMemberRequest\x1a\x16.google.protobuf.Empty\x12K\n" + + "\x10RemoveTeamMember\x12\x1f.iam.v1.RemoveTeamMemberRequest\x1a\x16.google.protobuf.Empty\x12S\n" + + "\x14UpdateTeamMemberRole\x12#.iam.v1.UpdateTeamMemberRoleRequest\x1a\x16.google.protobuf.Empty\x12I\n" + + "\x0fListTeamMembers\x12\x1e.iam.v1.ListTeamMembersRequest\x1a\x16.iam.v1.StructResponseB\x1aZ\x18aegis/proto/iam/v1;iamv1b\x06proto3" + +var ( + file_proto_iam_v1_iam_proto_rawDescOnce sync.Once + file_proto_iam_v1_iam_proto_rawDescData []byte +) + +func file_proto_iam_v1_iam_proto_rawDescGZIP() []byte { + file_proto_iam_v1_iam_proto_rawDescOnce.Do(func() { + file_proto_iam_v1_iam_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_iam_v1_iam_proto_rawDesc), len(file_proto_iam_v1_iam_proto_rawDesc))) + }) + return file_proto_iam_v1_iam_proto_rawDescData +} + +var file_proto_iam_v1_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_proto_iam_v1_iam_proto_goTypes = []any{ + (*VerifyTokenRequest)(nil), // 0: iam.v1.VerifyTokenRequest + (*VerifyTokenResponse)(nil), // 1: iam.v1.VerifyTokenResponse + (*CheckPermissionRequest)(nil), // 2: iam.v1.CheckPermissionRequest + (*CheckPermissionResponse)(nil), // 3: iam.v1.CheckPermissionResponse + (*UserTeamRequest)(nil), // 4: iam.v1.UserTeamRequest + (*TeamRequest)(nil), // 5: iam.v1.TeamRequest + (*UserProjectRequest)(nil), // 6: iam.v1.UserProjectRequest + (*BoolResponse)(nil), // 7: iam.v1.BoolResponse + (*ExchangeAccessKeyTokenRequest)(nil), // 8: iam.v1.ExchangeAccessKeyTokenRequest + (*ExchangeAccessKeyTokenResponse)(nil), // 9: iam.v1.ExchangeAccessKeyTokenResponse + (*MutationRequest)(nil), // 10: iam.v1.MutationRequest + (*QueryRequest)(nil), // 11: iam.v1.QueryRequest + (*IDRequest)(nil), // 12: iam.v1.IDRequest + (*UpdateByIDRequest)(nil), // 13: iam.v1.UpdateByIDRequest + (*UserIDRequest)(nil), // 14: iam.v1.UserIDRequest + (*UserQueryRequest)(nil), // 15: iam.v1.UserQueryRequest + (*UserBodyRequest)(nil), // 16: iam.v1.UserBodyRequest + (*UserScopedIDRequest)(nil), // 17: iam.v1.UserScopedIDRequest + (*UserRoleBindingRequest)(nil), // 18: iam.v1.UserRoleBindingRequest + (*UserResourceBindingRequest)(nil), // 19: iam.v1.UserResourceBindingRequest + (*LogoutRequest)(nil), // 20: iam.v1.LogoutRequest + (*RolePermissionsRequest)(nil), // 21: iam.v1.RolePermissionsRequest + (*CreateTeamRequest)(nil), // 22: iam.v1.CreateTeamRequest + (*ListTeamsRequest)(nil), // 23: iam.v1.ListTeamsRequest + (*UpdateTeamRequest)(nil), // 24: iam.v1.UpdateTeamRequest + (*ListTeamProjectsRequest)(nil), // 25: iam.v1.ListTeamProjectsRequest + (*AddTeamMemberRequest)(nil), // 26: iam.v1.AddTeamMemberRequest + (*RemoveTeamMemberRequest)(nil), // 27: iam.v1.RemoveTeamMemberRequest + (*UpdateTeamMemberRoleRequest)(nil), // 28: iam.v1.UpdateTeamMemberRoleRequest + (*ListTeamMembersRequest)(nil), // 29: iam.v1.ListTeamMembersRequest + (*StructResponse)(nil), // 30: iam.v1.StructResponse + (*structpb.Struct)(nil), // 31: google.protobuf.Struct + (*emptypb.Empty)(nil), // 32: google.protobuf.Empty +} +var file_proto_iam_v1_iam_proto_depIdxs = []int32{ + 31, // 0: iam.v1.MutationRequest.body:type_name -> google.protobuf.Struct + 31, // 1: iam.v1.QueryRequest.query:type_name -> google.protobuf.Struct + 31, // 2: iam.v1.UpdateByIDRequest.body:type_name -> google.protobuf.Struct + 31, // 3: iam.v1.UserQueryRequest.query:type_name -> google.protobuf.Struct + 31, // 4: iam.v1.UserBodyRequest.body:type_name -> google.protobuf.Struct + 31, // 5: iam.v1.CreateTeamRequest.body:type_name -> google.protobuf.Struct + 31, // 6: iam.v1.ListTeamsRequest.query:type_name -> google.protobuf.Struct + 31, // 7: iam.v1.UpdateTeamRequest.body:type_name -> google.protobuf.Struct + 31, // 8: iam.v1.ListTeamProjectsRequest.query:type_name -> google.protobuf.Struct + 31, // 9: iam.v1.AddTeamMemberRequest.body:type_name -> google.protobuf.Struct + 31, // 10: iam.v1.UpdateTeamMemberRoleRequest.body:type_name -> google.protobuf.Struct + 31, // 11: iam.v1.ListTeamMembersRequest.query:type_name -> google.protobuf.Struct + 31, // 12: iam.v1.StructResponse.data:type_name -> google.protobuf.Struct + 0, // 13: iam.v1.IAMService.VerifyToken:input_type -> iam.v1.VerifyTokenRequest + 2, // 14: iam.v1.IAMService.CheckPermission:input_type -> iam.v1.CheckPermissionRequest + 10, // 15: iam.v1.IAMService.Login:input_type -> iam.v1.MutationRequest + 10, // 16: iam.v1.IAMService.Register:input_type -> iam.v1.MutationRequest + 10, // 17: iam.v1.IAMService.RefreshToken:input_type -> iam.v1.MutationRequest + 20, // 18: iam.v1.IAMService.Logout:input_type -> iam.v1.LogoutRequest + 16, // 19: iam.v1.IAMService.ChangePassword:input_type -> iam.v1.UserBodyRequest + 14, // 20: iam.v1.IAMService.GetProfile:input_type -> iam.v1.UserIDRequest + 16, // 21: iam.v1.IAMService.CreateAccessKey:input_type -> iam.v1.UserBodyRequest + 15, // 22: iam.v1.IAMService.ListAccessKeys:input_type -> iam.v1.UserQueryRequest + 17, // 23: iam.v1.IAMService.GetAccessKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 24: iam.v1.IAMService.DeleteAccessKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 25: iam.v1.IAMService.DisableAccessKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 26: iam.v1.IAMService.EnableAccessKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 27: iam.v1.IAMService.RotateAccessKey:input_type -> iam.v1.UserScopedIDRequest + 4, // 28: iam.v1.IAMService.IsUserTeamAdmin:input_type -> iam.v1.UserTeamRequest + 4, // 29: iam.v1.IAMService.IsUserInTeam:input_type -> iam.v1.UserTeamRequest + 5, // 30: iam.v1.IAMService.IsTeamPublic:input_type -> iam.v1.TeamRequest + 6, // 31: iam.v1.IAMService.IsUserProjectAdmin:input_type -> iam.v1.UserProjectRequest + 6, // 32: iam.v1.IAMService.IsUserInProject:input_type -> iam.v1.UserProjectRequest + 8, // 33: iam.v1.IAMService.ExchangeAccessKeyToken:input_type -> iam.v1.ExchangeAccessKeyTokenRequest + 10, // 34: iam.v1.IAMService.CreateUser:input_type -> iam.v1.MutationRequest + 12, // 35: iam.v1.IAMService.DeleteUser:input_type -> iam.v1.IDRequest + 12, // 36: iam.v1.IAMService.GetUser:input_type -> iam.v1.IDRequest + 11, // 37: iam.v1.IAMService.ListUsers:input_type -> iam.v1.QueryRequest + 13, // 38: iam.v1.IAMService.UpdateUser:input_type -> iam.v1.UpdateByIDRequest + 18, // 39: iam.v1.IAMService.AssignUserRole:input_type -> iam.v1.UserRoleBindingRequest + 18, // 40: iam.v1.IAMService.RemoveUserRole:input_type -> iam.v1.UserRoleBindingRequest + 16, // 41: iam.v1.IAMService.AssignUserPermissions:input_type -> iam.v1.UserBodyRequest + 16, // 42: iam.v1.IAMService.RemoveUserPermissions:input_type -> iam.v1.UserBodyRequest + 19, // 43: iam.v1.IAMService.AssignUserContainer:input_type -> iam.v1.UserResourceBindingRequest + 17, // 44: iam.v1.IAMService.RemoveUserContainer:input_type -> iam.v1.UserScopedIDRequest + 19, // 45: iam.v1.IAMService.AssignUserDataset:input_type -> iam.v1.UserResourceBindingRequest + 17, // 46: iam.v1.IAMService.RemoveUserDataset:input_type -> iam.v1.UserScopedIDRequest + 19, // 47: iam.v1.IAMService.AssignUserProject:input_type -> iam.v1.UserResourceBindingRequest + 17, // 48: iam.v1.IAMService.RemoveUserProject:input_type -> iam.v1.UserScopedIDRequest + 10, // 49: iam.v1.IAMService.CreateRole:input_type -> iam.v1.MutationRequest + 12, // 50: iam.v1.IAMService.DeleteRole:input_type -> iam.v1.IDRequest + 12, // 51: iam.v1.IAMService.GetRole:input_type -> iam.v1.IDRequest + 11, // 52: iam.v1.IAMService.ListRoles:input_type -> iam.v1.QueryRequest + 13, // 53: iam.v1.IAMService.UpdateRole:input_type -> iam.v1.UpdateByIDRequest + 21, // 54: iam.v1.IAMService.AssignRolePermissions:input_type -> iam.v1.RolePermissionsRequest + 21, // 55: iam.v1.IAMService.RemoveRolePermissions:input_type -> iam.v1.RolePermissionsRequest + 12, // 56: iam.v1.IAMService.ListUsersFromRole:input_type -> iam.v1.IDRequest + 12, // 57: iam.v1.IAMService.GetPermission:input_type -> iam.v1.IDRequest + 11, // 58: iam.v1.IAMService.ListPermissions:input_type -> iam.v1.QueryRequest + 12, // 59: iam.v1.IAMService.ListRolesFromPermission:input_type -> iam.v1.IDRequest + 12, // 60: iam.v1.IAMService.GetResource:input_type -> iam.v1.IDRequest + 11, // 61: iam.v1.IAMService.ListResources:input_type -> iam.v1.QueryRequest + 12, // 62: iam.v1.IAMService.ListResourcePermissions:input_type -> iam.v1.IDRequest + 22, // 63: iam.v1.IAMService.CreateTeam:input_type -> iam.v1.CreateTeamRequest + 5, // 64: iam.v1.IAMService.DeleteTeam:input_type -> iam.v1.TeamRequest + 5, // 65: iam.v1.IAMService.GetTeam:input_type -> iam.v1.TeamRequest + 23, // 66: iam.v1.IAMService.ListTeams:input_type -> iam.v1.ListTeamsRequest + 24, // 67: iam.v1.IAMService.UpdateTeam:input_type -> iam.v1.UpdateTeamRequest + 25, // 68: iam.v1.IAMService.ListTeamProjects:input_type -> iam.v1.ListTeamProjectsRequest + 26, // 69: iam.v1.IAMService.AddTeamMember:input_type -> iam.v1.AddTeamMemberRequest + 27, // 70: iam.v1.IAMService.RemoveTeamMember:input_type -> iam.v1.RemoveTeamMemberRequest + 28, // 71: iam.v1.IAMService.UpdateTeamMemberRole:input_type -> iam.v1.UpdateTeamMemberRoleRequest + 29, // 72: iam.v1.IAMService.ListTeamMembers:input_type -> iam.v1.ListTeamMembersRequest + 1, // 73: iam.v1.IAMService.VerifyToken:output_type -> iam.v1.VerifyTokenResponse + 3, // 74: iam.v1.IAMService.CheckPermission:output_type -> iam.v1.CheckPermissionResponse + 30, // 75: iam.v1.IAMService.Login:output_type -> iam.v1.StructResponse + 30, // 76: iam.v1.IAMService.Register:output_type -> iam.v1.StructResponse + 30, // 77: iam.v1.IAMService.RefreshToken:output_type -> iam.v1.StructResponse + 32, // 78: iam.v1.IAMService.Logout:output_type -> google.protobuf.Empty + 32, // 79: iam.v1.IAMService.ChangePassword:output_type -> google.protobuf.Empty + 30, // 80: iam.v1.IAMService.GetProfile:output_type -> iam.v1.StructResponse + 30, // 81: iam.v1.IAMService.CreateAccessKey:output_type -> iam.v1.StructResponse + 30, // 82: iam.v1.IAMService.ListAccessKeys:output_type -> iam.v1.StructResponse + 30, // 83: iam.v1.IAMService.GetAccessKey:output_type -> iam.v1.StructResponse + 32, // 84: iam.v1.IAMService.DeleteAccessKey:output_type -> google.protobuf.Empty + 32, // 85: iam.v1.IAMService.DisableAccessKey:output_type -> google.protobuf.Empty + 32, // 86: iam.v1.IAMService.EnableAccessKey:output_type -> google.protobuf.Empty + 30, // 87: iam.v1.IAMService.RotateAccessKey:output_type -> iam.v1.StructResponse + 7, // 88: iam.v1.IAMService.IsUserTeamAdmin:output_type -> iam.v1.BoolResponse + 7, // 89: iam.v1.IAMService.IsUserInTeam:output_type -> iam.v1.BoolResponse + 7, // 90: iam.v1.IAMService.IsTeamPublic:output_type -> iam.v1.BoolResponse + 7, // 91: iam.v1.IAMService.IsUserProjectAdmin:output_type -> iam.v1.BoolResponse + 7, // 92: iam.v1.IAMService.IsUserInProject:output_type -> iam.v1.BoolResponse + 9, // 93: iam.v1.IAMService.ExchangeAccessKeyToken:output_type -> iam.v1.ExchangeAccessKeyTokenResponse + 30, // 94: iam.v1.IAMService.CreateUser:output_type -> iam.v1.StructResponse + 32, // 95: iam.v1.IAMService.DeleteUser:output_type -> google.protobuf.Empty + 30, // 96: iam.v1.IAMService.GetUser:output_type -> iam.v1.StructResponse + 30, // 97: iam.v1.IAMService.ListUsers:output_type -> iam.v1.StructResponse + 30, // 98: iam.v1.IAMService.UpdateUser:output_type -> iam.v1.StructResponse + 32, // 99: iam.v1.IAMService.AssignUserRole:output_type -> google.protobuf.Empty + 32, // 100: iam.v1.IAMService.RemoveUserRole:output_type -> google.protobuf.Empty + 32, // 101: iam.v1.IAMService.AssignUserPermissions:output_type -> google.protobuf.Empty + 32, // 102: iam.v1.IAMService.RemoveUserPermissions:output_type -> google.protobuf.Empty + 32, // 103: iam.v1.IAMService.AssignUserContainer:output_type -> google.protobuf.Empty + 32, // 104: iam.v1.IAMService.RemoveUserContainer:output_type -> google.protobuf.Empty + 32, // 105: iam.v1.IAMService.AssignUserDataset:output_type -> google.protobuf.Empty + 32, // 106: iam.v1.IAMService.RemoveUserDataset:output_type -> google.protobuf.Empty + 32, // 107: iam.v1.IAMService.AssignUserProject:output_type -> google.protobuf.Empty + 32, // 108: iam.v1.IAMService.RemoveUserProject:output_type -> google.protobuf.Empty + 30, // 109: iam.v1.IAMService.CreateRole:output_type -> iam.v1.StructResponse + 32, // 110: iam.v1.IAMService.DeleteRole:output_type -> google.protobuf.Empty + 30, // 111: iam.v1.IAMService.GetRole:output_type -> iam.v1.StructResponse + 30, // 112: iam.v1.IAMService.ListRoles:output_type -> iam.v1.StructResponse + 30, // 113: iam.v1.IAMService.UpdateRole:output_type -> iam.v1.StructResponse + 32, // 114: iam.v1.IAMService.AssignRolePermissions:output_type -> google.protobuf.Empty + 32, // 115: iam.v1.IAMService.RemoveRolePermissions:output_type -> google.protobuf.Empty + 30, // 116: iam.v1.IAMService.ListUsersFromRole:output_type -> iam.v1.StructResponse + 30, // 117: iam.v1.IAMService.GetPermission:output_type -> iam.v1.StructResponse + 30, // 118: iam.v1.IAMService.ListPermissions:output_type -> iam.v1.StructResponse + 30, // 119: iam.v1.IAMService.ListRolesFromPermission:output_type -> iam.v1.StructResponse + 30, // 120: iam.v1.IAMService.GetResource:output_type -> iam.v1.StructResponse + 30, // 121: iam.v1.IAMService.ListResources:output_type -> iam.v1.StructResponse + 30, // 122: iam.v1.IAMService.ListResourcePermissions:output_type -> iam.v1.StructResponse + 30, // 123: iam.v1.IAMService.CreateTeam:output_type -> iam.v1.StructResponse + 32, // 124: iam.v1.IAMService.DeleteTeam:output_type -> google.protobuf.Empty + 30, // 125: iam.v1.IAMService.GetTeam:output_type -> iam.v1.StructResponse + 30, // 126: iam.v1.IAMService.ListTeams:output_type -> iam.v1.StructResponse + 30, // 127: iam.v1.IAMService.UpdateTeam:output_type -> iam.v1.StructResponse + 30, // 128: iam.v1.IAMService.ListTeamProjects:output_type -> iam.v1.StructResponse + 32, // 129: iam.v1.IAMService.AddTeamMember:output_type -> google.protobuf.Empty + 32, // 130: iam.v1.IAMService.RemoveTeamMember:output_type -> google.protobuf.Empty + 32, // 131: iam.v1.IAMService.UpdateTeamMemberRole:output_type -> google.protobuf.Empty + 30, // 132: iam.v1.IAMService.ListTeamMembers:output_type -> iam.v1.StructResponse + 73, // [73:133] is the sub-list for method output_type + 13, // [13:73] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_proto_iam_v1_iam_proto_init() } +func file_proto_iam_v1_iam_proto_init() { + if File_proto_iam_v1_iam_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iam_v1_iam_proto_rawDesc), len(file_proto_iam_v1_iam_proto_rawDesc)), + NumEnums: 0, + NumMessages: 31, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_iam_v1_iam_proto_goTypes, + DependencyIndexes: file_proto_iam_v1_iam_proto_depIdxs, + MessageInfos: file_proto_iam_v1_iam_proto_msgTypes, + }.Build() + File_proto_iam_v1_iam_proto = out.File + file_proto_iam_v1_iam_proto_goTypes = nil + file_proto_iam_v1_iam_proto_depIdxs = nil +} diff --git a/src/proto/iam/v1/iam.proto b/src/proto/iam/v1/iam.proto new file mode 100644 index 00000000..db4053b8 --- /dev/null +++ b/src/proto/iam/v1/iam.proto @@ -0,0 +1,246 @@ +syntax = "proto3"; + +package iam.v1; + +option go_package = "aegis/proto/iam/v1;iamv1"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/struct.proto"; + +service IAMService { + rpc VerifyToken(VerifyTokenRequest) returns (VerifyTokenResponse); + rpc CheckPermission(CheckPermissionRequest) returns (CheckPermissionResponse); + rpc Login(MutationRequest) returns (StructResponse); + rpc Register(MutationRequest) returns (StructResponse); + rpc RefreshToken(MutationRequest) returns (StructResponse); + rpc Logout(LogoutRequest) returns (google.protobuf.Empty); + rpc ChangePassword(UserBodyRequest) returns (google.protobuf.Empty); + rpc GetProfile(UserIDRequest) returns (StructResponse); + rpc CreateAccessKey(UserBodyRequest) returns (StructResponse); + rpc ListAccessKeys(UserQueryRequest) returns (StructResponse); + rpc GetAccessKey(UserScopedIDRequest) returns (StructResponse); + rpc DeleteAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc DisableAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc EnableAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc RotateAccessKey(UserScopedIDRequest) returns (StructResponse); + rpc IsUserTeamAdmin(UserTeamRequest) returns (BoolResponse); + rpc IsUserInTeam(UserTeamRequest) returns (BoolResponse); + rpc IsTeamPublic(TeamRequest) returns (BoolResponse); + rpc IsUserProjectAdmin(UserProjectRequest) returns (BoolResponse); + rpc IsUserInProject(UserProjectRequest) returns (BoolResponse); + rpc ExchangeAccessKeyToken(ExchangeAccessKeyTokenRequest) returns (ExchangeAccessKeyTokenResponse); + rpc CreateUser(MutationRequest) returns (StructResponse); + rpc DeleteUser(IDRequest) returns (google.protobuf.Empty); + rpc GetUser(IDRequest) returns (StructResponse); + rpc ListUsers(QueryRequest) returns (StructResponse); + rpc UpdateUser(UpdateByIDRequest) returns (StructResponse); + rpc AssignUserRole(UserRoleBindingRequest) returns (google.protobuf.Empty); + rpc RemoveUserRole(UserRoleBindingRequest) returns (google.protobuf.Empty); + rpc AssignUserPermissions(UserBodyRequest) returns (google.protobuf.Empty); + rpc RemoveUserPermissions(UserBodyRequest) returns (google.protobuf.Empty); + rpc AssignUserContainer(UserResourceBindingRequest) returns (google.protobuf.Empty); + rpc RemoveUserContainer(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc AssignUserDataset(UserResourceBindingRequest) returns (google.protobuf.Empty); + rpc RemoveUserDataset(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc AssignUserProject(UserResourceBindingRequest) returns (google.protobuf.Empty); + rpc RemoveUserProject(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc CreateRole(MutationRequest) returns (StructResponse); + rpc DeleteRole(IDRequest) returns (google.protobuf.Empty); + rpc GetRole(IDRequest) returns (StructResponse); + rpc ListRoles(QueryRequest) returns (StructResponse); + rpc UpdateRole(UpdateByIDRequest) returns (StructResponse); + rpc AssignRolePermissions(RolePermissionsRequest) returns (google.protobuf.Empty); + rpc RemoveRolePermissions(RolePermissionsRequest) returns (google.protobuf.Empty); + rpc ListUsersFromRole(IDRequest) returns (StructResponse); + rpc GetPermission(IDRequest) returns (StructResponse); + rpc ListPermissions(QueryRequest) returns (StructResponse); + rpc ListRolesFromPermission(IDRequest) returns (StructResponse); + rpc GetResource(IDRequest) returns (StructResponse); + rpc ListResources(QueryRequest) returns (StructResponse); + rpc ListResourcePermissions(IDRequest) returns (StructResponse); + rpc CreateTeam(CreateTeamRequest) returns (StructResponse); + rpc DeleteTeam(TeamRequest) returns (google.protobuf.Empty); + rpc GetTeam(TeamRequest) returns (StructResponse); + rpc ListTeams(ListTeamsRequest) returns (StructResponse); + rpc UpdateTeam(UpdateTeamRequest) returns (StructResponse); + rpc ListTeamProjects(ListTeamProjectsRequest) returns (StructResponse); + rpc AddTeamMember(AddTeamMemberRequest) returns (google.protobuf.Empty); + rpc RemoveTeamMember(RemoveTeamMemberRequest) returns (google.protobuf.Empty); + rpc UpdateTeamMemberRole(UpdateTeamMemberRoleRequest) returns (google.protobuf.Empty); + rpc ListTeamMembers(ListTeamMembersRequest) returns (StructResponse); +} + +message VerifyTokenRequest { + string token = 1; +} + +message VerifyTokenResponse { + bool valid = 1; + string token_type = 2; + int64 user_id = 3; + string username = 4; + string email = 5; + bool is_active = 6; + bool is_admin = 7; + repeated string roles = 8; + int64 expires_at_unix = 9; + string auth_type = 10; + int64 access_key_id = 11; + string task_id = 12; +} + +message CheckPermissionRequest { + int64 user_id = 1; + string action = 2; + string scope = 3; + string resource_name = 4; + int64 team_id = 5; + int64 project_id = 6; + int64 container_id = 7; + int64 dataset_id = 8; +} + +message CheckPermissionResponse { + bool allowed = 1; +} + +message UserTeamRequest { + int64 user_id = 1; + int64 team_id = 2; +} + +message TeamRequest { + int64 team_id = 1; +} + +message UserProjectRequest { + int64 user_id = 1; + int64 project_id = 2; +} + +message BoolResponse { + bool value = 1; +} + +message ExchangeAccessKeyTokenRequest { + string access_key = 1; + string timestamp = 2; + string nonce = 3; + string signature = 4; + string method = 5; + string path = 6; +} + +message ExchangeAccessKeyTokenResponse { + string token = 1; + string token_type = 2; + int64 expires_at_unix = 3; + string auth_type = 4; + string access_key = 5; +} + +message MutationRequest { + google.protobuf.Struct body = 1; +} + +message QueryRequest { + google.protobuf.Struct query = 1; +} + +message IDRequest { + int64 id = 1; +} + +message UpdateByIDRequest { + int64 id = 1; + google.protobuf.Struct body = 2; +} + +message UserIDRequest { + int64 user_id = 1; +} + +message UserQueryRequest { + int64 user_id = 1; + google.protobuf.Struct query = 2; +} + +message UserBodyRequest { + int64 user_id = 1; + google.protobuf.Struct body = 2; +} + +message UserScopedIDRequest { + int64 user_id = 1; + int64 id = 2; +} + +message UserRoleBindingRequest { + int64 user_id = 1; + int64 role_id = 2; +} + +message UserResourceBindingRequest { + int64 user_id = 1; + int64 resource_id = 2; + int64 role_id = 3; +} + +message LogoutRequest { + int64 user_id = 1; + string token_id = 2; + int64 expires_at_unix = 3; +} + +message RolePermissionsRequest { + int64 role_id = 1; + repeated int64 permission_ids = 2; +} + +message CreateTeamRequest { + int64 user_id = 1; + google.protobuf.Struct body = 2; +} + +message ListTeamsRequest { + int64 user_id = 1; + bool is_admin = 2; + google.protobuf.Struct query = 3; +} + +message UpdateTeamRequest { + int64 team_id = 1; + google.protobuf.Struct body = 2; +} + +message ListTeamProjectsRequest { + int64 team_id = 1; + google.protobuf.Struct query = 2; +} + +message AddTeamMemberRequest { + int64 team_id = 1; + google.protobuf.Struct body = 2; +} + +message RemoveTeamMemberRequest { + int64 team_id = 1; + int64 current_user_id = 2; + int64 target_user_id = 3; +} + +message UpdateTeamMemberRoleRequest { + int64 team_id = 1; + int64 target_user_id = 2; + int64 current_user_id = 3; + google.protobuf.Struct body = 4; +} + +message ListTeamMembersRequest { + int64 team_id = 1; + google.protobuf.Struct query = 2; +} + +message StructResponse { + google.protobuf.Struct data = 1; +} diff --git a/src/proto/iam/v1/iam_grpc.pb.go b/src/proto/iam/v1/iam_grpc.pb.go new file mode 100644 index 00000000..eb326758 --- /dev/null +++ b/src/proto/iam/v1/iam_grpc.pb.go @@ -0,0 +1,2364 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v5.29.3 +// source: proto/iam/v1/iam.proto + +package iamv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + IAMService_VerifyToken_FullMethodName = "/iam.v1.IAMService/VerifyToken" + IAMService_CheckPermission_FullMethodName = "/iam.v1.IAMService/CheckPermission" + IAMService_Login_FullMethodName = "/iam.v1.IAMService/Login" + IAMService_Register_FullMethodName = "/iam.v1.IAMService/Register" + IAMService_RefreshToken_FullMethodName = "/iam.v1.IAMService/RefreshToken" + IAMService_Logout_FullMethodName = "/iam.v1.IAMService/Logout" + IAMService_ChangePassword_FullMethodName = "/iam.v1.IAMService/ChangePassword" + IAMService_GetProfile_FullMethodName = "/iam.v1.IAMService/GetProfile" + IAMService_CreateAccessKey_FullMethodName = "/iam.v1.IAMService/CreateAccessKey" + IAMService_ListAccessKeys_FullMethodName = "/iam.v1.IAMService/ListAccessKeys" + IAMService_GetAccessKey_FullMethodName = "/iam.v1.IAMService/GetAccessKey" + IAMService_DeleteAccessKey_FullMethodName = "/iam.v1.IAMService/DeleteAccessKey" + IAMService_DisableAccessKey_FullMethodName = "/iam.v1.IAMService/DisableAccessKey" + IAMService_EnableAccessKey_FullMethodName = "/iam.v1.IAMService/EnableAccessKey" + IAMService_RotateAccessKey_FullMethodName = "/iam.v1.IAMService/RotateAccessKey" + IAMService_IsUserTeamAdmin_FullMethodName = "/iam.v1.IAMService/IsUserTeamAdmin" + IAMService_IsUserInTeam_FullMethodName = "/iam.v1.IAMService/IsUserInTeam" + IAMService_IsTeamPublic_FullMethodName = "/iam.v1.IAMService/IsTeamPublic" + IAMService_IsUserProjectAdmin_FullMethodName = "/iam.v1.IAMService/IsUserProjectAdmin" + IAMService_IsUserInProject_FullMethodName = "/iam.v1.IAMService/IsUserInProject" + IAMService_ExchangeAccessKeyToken_FullMethodName = "/iam.v1.IAMService/ExchangeAccessKeyToken" + IAMService_CreateUser_FullMethodName = "/iam.v1.IAMService/CreateUser" + IAMService_DeleteUser_FullMethodName = "/iam.v1.IAMService/DeleteUser" + IAMService_GetUser_FullMethodName = "/iam.v1.IAMService/GetUser" + IAMService_ListUsers_FullMethodName = "/iam.v1.IAMService/ListUsers" + IAMService_UpdateUser_FullMethodName = "/iam.v1.IAMService/UpdateUser" + IAMService_AssignUserRole_FullMethodName = "/iam.v1.IAMService/AssignUserRole" + IAMService_RemoveUserRole_FullMethodName = "/iam.v1.IAMService/RemoveUserRole" + IAMService_AssignUserPermissions_FullMethodName = "/iam.v1.IAMService/AssignUserPermissions" + IAMService_RemoveUserPermissions_FullMethodName = "/iam.v1.IAMService/RemoveUserPermissions" + IAMService_AssignUserContainer_FullMethodName = "/iam.v1.IAMService/AssignUserContainer" + IAMService_RemoveUserContainer_FullMethodName = "/iam.v1.IAMService/RemoveUserContainer" + IAMService_AssignUserDataset_FullMethodName = "/iam.v1.IAMService/AssignUserDataset" + IAMService_RemoveUserDataset_FullMethodName = "/iam.v1.IAMService/RemoveUserDataset" + IAMService_AssignUserProject_FullMethodName = "/iam.v1.IAMService/AssignUserProject" + IAMService_RemoveUserProject_FullMethodName = "/iam.v1.IAMService/RemoveUserProject" + IAMService_CreateRole_FullMethodName = "/iam.v1.IAMService/CreateRole" + IAMService_DeleteRole_FullMethodName = "/iam.v1.IAMService/DeleteRole" + IAMService_GetRole_FullMethodName = "/iam.v1.IAMService/GetRole" + IAMService_ListRoles_FullMethodName = "/iam.v1.IAMService/ListRoles" + IAMService_UpdateRole_FullMethodName = "/iam.v1.IAMService/UpdateRole" + IAMService_AssignRolePermissions_FullMethodName = "/iam.v1.IAMService/AssignRolePermissions" + IAMService_RemoveRolePermissions_FullMethodName = "/iam.v1.IAMService/RemoveRolePermissions" + IAMService_ListUsersFromRole_FullMethodName = "/iam.v1.IAMService/ListUsersFromRole" + IAMService_GetPermission_FullMethodName = "/iam.v1.IAMService/GetPermission" + IAMService_ListPermissions_FullMethodName = "/iam.v1.IAMService/ListPermissions" + IAMService_ListRolesFromPermission_FullMethodName = "/iam.v1.IAMService/ListRolesFromPermission" + IAMService_GetResource_FullMethodName = "/iam.v1.IAMService/GetResource" + IAMService_ListResources_FullMethodName = "/iam.v1.IAMService/ListResources" + IAMService_ListResourcePermissions_FullMethodName = "/iam.v1.IAMService/ListResourcePermissions" + IAMService_CreateTeam_FullMethodName = "/iam.v1.IAMService/CreateTeam" + IAMService_DeleteTeam_FullMethodName = "/iam.v1.IAMService/DeleteTeam" + IAMService_GetTeam_FullMethodName = "/iam.v1.IAMService/GetTeam" + IAMService_ListTeams_FullMethodName = "/iam.v1.IAMService/ListTeams" + IAMService_UpdateTeam_FullMethodName = "/iam.v1.IAMService/UpdateTeam" + IAMService_ListTeamProjects_FullMethodName = "/iam.v1.IAMService/ListTeamProjects" + IAMService_AddTeamMember_FullMethodName = "/iam.v1.IAMService/AddTeamMember" + IAMService_RemoveTeamMember_FullMethodName = "/iam.v1.IAMService/RemoveTeamMember" + IAMService_UpdateTeamMemberRole_FullMethodName = "/iam.v1.IAMService/UpdateTeamMemberRole" + IAMService_ListTeamMembers_FullMethodName = "/iam.v1.IAMService/ListTeamMembers" +) + +// IAMServiceClient is the client API for IAMService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type IAMServiceClient interface { + VerifyToken(ctx context.Context, in *VerifyTokenRequest, opts ...grpc.CallOption) (*VerifyTokenResponse, error) + CheckPermission(ctx context.Context, in *CheckPermissionRequest, opts ...grpc.CallOption) (*CheckPermissionResponse, error) + Login(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + Register(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + RefreshToken(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ChangePassword(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetProfile(ctx context.Context, in *UserIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + CreateAccessKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListAccessKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + DeleteAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DisableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + EnableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RotateAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + IsUserTeamAdmin(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) + IsUserInTeam(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) + IsTeamPublic(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) + IsUserProjectAdmin(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) + IsUserInProject(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) + ExchangeAccessKeyToken(ctx context.Context, in *ExchangeAccessKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAccessKeyTokenResponse, error) + CreateUser(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + DeleteUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListUsers(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateUser(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + AssignUserRole(ctx context.Context, in *UserRoleBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveUserRole(ctx context.Context, in *UserRoleBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + AssignUserPermissions(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveUserPermissions(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + AssignUserContainer(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveUserContainer(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + AssignUserDataset(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveUserDataset(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + AssignUserProject(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveUserProject(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + CreateRole(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + DeleteRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListRoles(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateRole(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + AssignRolePermissions(ctx context.Context, in *RolePermissionsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveRolePermissions(ctx context.Context, in *RolePermissionsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListUsersFromRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetPermission(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListPermissions(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListRolesFromPermission(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetResource(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListResources(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListResourcePermissions(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) + CreateTeam(ctx context.Context, in *CreateTeamRequest, opts ...grpc.CallOption) (*StructResponse, error) + DeleteTeam(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + GetTeam(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListTeams(ctx context.Context, in *ListTeamsRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateTeam(ctx context.Context, in *UpdateTeamRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListTeamProjects(ctx context.Context, in *ListTeamProjectsRequest, opts ...grpc.CallOption) (*StructResponse, error) + AddTeamMember(ctx context.Context, in *AddTeamMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RemoveTeamMember(ctx context.Context, in *RemoveTeamMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + UpdateTeamMemberRole(ctx context.Context, in *UpdateTeamMemberRoleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListTeamMembers(ctx context.Context, in *ListTeamMembersRequest, opts ...grpc.CallOption) (*StructResponse, error) +} + +type iAMServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewIAMServiceClient(cc grpc.ClientConnInterface) IAMServiceClient { + return &iAMServiceClient{cc} +} + +func (c *iAMServiceClient) VerifyToken(ctx context.Context, in *VerifyTokenRequest, opts ...grpc.CallOption) (*VerifyTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VerifyTokenResponse) + err := c.cc.Invoke(ctx, IAMService_VerifyToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) CheckPermission(ctx context.Context, in *CheckPermissionRequest, opts ...grpc.CallOption) (*CheckPermissionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckPermissionResponse) + err := c.cc.Invoke(ctx, IAMService_CheckPermission_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) Login(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_Login_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) Register(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_Register_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RefreshToken(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_RefreshToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_Logout_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ChangePassword(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_ChangePassword_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetProfile(ctx context.Context, in *UserIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) CreateAccessKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_CreateAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListAccessKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListAccessKeys_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) DeleteAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_DeleteAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) DisableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_DisableAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) EnableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_EnableAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RotateAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_RotateAccessKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) IsUserTeamAdmin(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BoolResponse) + err := c.cc.Invoke(ctx, IAMService_IsUserTeamAdmin_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) IsUserInTeam(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BoolResponse) + err := c.cc.Invoke(ctx, IAMService_IsUserInTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) IsTeamPublic(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BoolResponse) + err := c.cc.Invoke(ctx, IAMService_IsTeamPublic_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) IsUserProjectAdmin(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BoolResponse) + err := c.cc.Invoke(ctx, IAMService_IsUserProjectAdmin_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) IsUserInProject(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BoolResponse) + err := c.cc.Invoke(ctx, IAMService_IsUserInProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ExchangeAccessKeyToken(ctx context.Context, in *ExchangeAccessKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAccessKeyTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExchangeAccessKeyTokenResponse) + err := c.cc.Invoke(ctx, IAMService_ExchangeAccessKeyToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) CreateUser(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_CreateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) DeleteUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_DeleteUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListUsers(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) UpdateUser(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_UpdateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignUserRole(ctx context.Context, in *UserRoleBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignUserRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveUserRole(ctx context.Context, in *UserRoleBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveUserRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignUserPermissions(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignUserPermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveUserPermissions(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveUserPermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignUserContainer(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignUserContainer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveUserContainer(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveUserContainer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignUserDataset(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignUserDataset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveUserDataset(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveUserDataset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignUserProject(ctx context.Context, in *UserResourceBindingRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignUserProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveUserProject(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveUserProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) CreateRole(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_CreateRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) DeleteRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_DeleteRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListRoles(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListRoles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) UpdateRole(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_UpdateRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AssignRolePermissions(ctx context.Context, in *RolePermissionsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AssignRolePermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveRolePermissions(ctx context.Context, in *RolePermissionsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveRolePermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListUsersFromRole(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListUsersFromRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetPermission(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetPermission_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListPermissions(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListPermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListRolesFromPermission(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListRolesFromPermission_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetResource(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetResource_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListResources(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListResources_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListResourcePermissions(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListResourcePermissions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) CreateTeam(ctx context.Context, in *CreateTeamRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_CreateTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) DeleteTeam(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_DeleteTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) GetTeam(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_GetTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListTeams(ctx context.Context, in *ListTeamsRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListTeams_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) UpdateTeam(ctx context.Context, in *UpdateTeamRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_UpdateTeam_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListTeamProjects(ctx context.Context, in *ListTeamProjectsRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListTeamProjects_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) AddTeamMember(ctx context.Context, in *AddTeamMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_AddTeamMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RemoveTeamMember(ctx context.Context, in *RemoveTeamMemberRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RemoveTeamMember_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) UpdateTeamMemberRole(ctx context.Context, in *UpdateTeamMemberRoleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_UpdateTeamMemberRole_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) ListTeamMembers(ctx context.Context, in *ListTeamMembersRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, IAMService_ListTeamMembers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// IAMServiceServer is the server API for IAMService service. +// All implementations must embed UnimplementedIAMServiceServer +// for forward compatibility. +type IAMServiceServer interface { + VerifyToken(context.Context, *VerifyTokenRequest) (*VerifyTokenResponse, error) + CheckPermission(context.Context, *CheckPermissionRequest) (*CheckPermissionResponse, error) + Login(context.Context, *MutationRequest) (*StructResponse, error) + Register(context.Context, *MutationRequest) (*StructResponse, error) + RefreshToken(context.Context, *MutationRequest) (*StructResponse, error) + Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) + ChangePassword(context.Context, *UserBodyRequest) (*emptypb.Empty, error) + GetProfile(context.Context, *UserIDRequest) (*StructResponse, error) + CreateAccessKey(context.Context, *UserBodyRequest) (*StructResponse, error) + ListAccessKeys(context.Context, *UserQueryRequest) (*StructResponse, error) + GetAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) + DeleteAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + DisableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + EnableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + RotateAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) + IsUserTeamAdmin(context.Context, *UserTeamRequest) (*BoolResponse, error) + IsUserInTeam(context.Context, *UserTeamRequest) (*BoolResponse, error) + IsTeamPublic(context.Context, *TeamRequest) (*BoolResponse, error) + IsUserProjectAdmin(context.Context, *UserProjectRequest) (*BoolResponse, error) + IsUserInProject(context.Context, *UserProjectRequest) (*BoolResponse, error) + ExchangeAccessKeyToken(context.Context, *ExchangeAccessKeyTokenRequest) (*ExchangeAccessKeyTokenResponse, error) + CreateUser(context.Context, *MutationRequest) (*StructResponse, error) + DeleteUser(context.Context, *IDRequest) (*emptypb.Empty, error) + GetUser(context.Context, *IDRequest) (*StructResponse, error) + ListUsers(context.Context, *QueryRequest) (*StructResponse, error) + UpdateUser(context.Context, *UpdateByIDRequest) (*StructResponse, error) + AssignUserRole(context.Context, *UserRoleBindingRequest) (*emptypb.Empty, error) + RemoveUserRole(context.Context, *UserRoleBindingRequest) (*emptypb.Empty, error) + AssignUserPermissions(context.Context, *UserBodyRequest) (*emptypb.Empty, error) + RemoveUserPermissions(context.Context, *UserBodyRequest) (*emptypb.Empty, error) + AssignUserContainer(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) + RemoveUserContainer(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + AssignUserDataset(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) + RemoveUserDataset(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + AssignUserProject(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) + RemoveUserProject(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + CreateRole(context.Context, *MutationRequest) (*StructResponse, error) + DeleteRole(context.Context, *IDRequest) (*emptypb.Empty, error) + GetRole(context.Context, *IDRequest) (*StructResponse, error) + ListRoles(context.Context, *QueryRequest) (*StructResponse, error) + UpdateRole(context.Context, *UpdateByIDRequest) (*StructResponse, error) + AssignRolePermissions(context.Context, *RolePermissionsRequest) (*emptypb.Empty, error) + RemoveRolePermissions(context.Context, *RolePermissionsRequest) (*emptypb.Empty, error) + ListUsersFromRole(context.Context, *IDRequest) (*StructResponse, error) + GetPermission(context.Context, *IDRequest) (*StructResponse, error) + ListPermissions(context.Context, *QueryRequest) (*StructResponse, error) + ListRolesFromPermission(context.Context, *IDRequest) (*StructResponse, error) + GetResource(context.Context, *IDRequest) (*StructResponse, error) + ListResources(context.Context, *QueryRequest) (*StructResponse, error) + ListResourcePermissions(context.Context, *IDRequest) (*StructResponse, error) + CreateTeam(context.Context, *CreateTeamRequest) (*StructResponse, error) + DeleteTeam(context.Context, *TeamRequest) (*emptypb.Empty, error) + GetTeam(context.Context, *TeamRequest) (*StructResponse, error) + ListTeams(context.Context, *ListTeamsRequest) (*StructResponse, error) + UpdateTeam(context.Context, *UpdateTeamRequest) (*StructResponse, error) + ListTeamProjects(context.Context, *ListTeamProjectsRequest) (*StructResponse, error) + AddTeamMember(context.Context, *AddTeamMemberRequest) (*emptypb.Empty, error) + RemoveTeamMember(context.Context, *RemoveTeamMemberRequest) (*emptypb.Empty, error) + UpdateTeamMemberRole(context.Context, *UpdateTeamMemberRoleRequest) (*emptypb.Empty, error) + ListTeamMembers(context.Context, *ListTeamMembersRequest) (*StructResponse, error) + mustEmbedUnimplementedIAMServiceServer() +} + +// UnimplementedIAMServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedIAMServiceServer struct{} + +func (UnimplementedIAMServiceServer) VerifyToken(context.Context, *VerifyTokenRequest) (*VerifyTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method VerifyToken not implemented") +} +func (UnimplementedIAMServiceServer) CheckPermission(context.Context, *CheckPermissionRequest) (*CheckPermissionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CheckPermission not implemented") +} +func (UnimplementedIAMServiceServer) Login(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedIAMServiceServer) Register(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Register not implemented") +} +func (UnimplementedIAMServiceServer) RefreshToken(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RefreshToken not implemented") +} +func (UnimplementedIAMServiceServer) Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method Logout not implemented") +} +func (UnimplementedIAMServiceServer) ChangePassword(context.Context, *UserBodyRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method ChangePassword not implemented") +} +func (UnimplementedIAMServiceServer) GetProfile(context.Context, *UserIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProfile not implemented") +} +func (UnimplementedIAMServiceServer) CreateAccessKey(context.Context, *UserBodyRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) ListAccessKeys(context.Context, *UserQueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAccessKeys not implemented") +} +func (UnimplementedIAMServiceServer) GetAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) DeleteAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) DisableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DisableAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) EnableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method EnableAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) RotateAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RotateAccessKey not implemented") +} +func (UnimplementedIAMServiceServer) IsUserTeamAdmin(context.Context, *UserTeamRequest) (*BoolResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsUserTeamAdmin not implemented") +} +func (UnimplementedIAMServiceServer) IsUserInTeam(context.Context, *UserTeamRequest) (*BoolResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsUserInTeam not implemented") +} +func (UnimplementedIAMServiceServer) IsTeamPublic(context.Context, *TeamRequest) (*BoolResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsTeamPublic not implemented") +} +func (UnimplementedIAMServiceServer) IsUserProjectAdmin(context.Context, *UserProjectRequest) (*BoolResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsUserProjectAdmin not implemented") +} +func (UnimplementedIAMServiceServer) IsUserInProject(context.Context, *UserProjectRequest) (*BoolResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsUserInProject not implemented") +} +func (UnimplementedIAMServiceServer) ExchangeAccessKeyToken(context.Context, *ExchangeAccessKeyTokenRequest) (*ExchangeAccessKeyTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExchangeAccessKeyToken not implemented") +} +func (UnimplementedIAMServiceServer) CreateUser(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedIAMServiceServer) DeleteUser(context.Context, *IDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented") +} +func (UnimplementedIAMServiceServer) GetUser(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedIAMServiceServer) ListUsers(context.Context, *QueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedIAMServiceServer) UpdateUser(context.Context, *UpdateByIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateUser not implemented") +} +func (UnimplementedIAMServiceServer) AssignUserRole(context.Context, *UserRoleBindingRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignUserRole not implemented") +} +func (UnimplementedIAMServiceServer) RemoveUserRole(context.Context, *UserRoleBindingRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveUserRole not implemented") +} +func (UnimplementedIAMServiceServer) AssignUserPermissions(context.Context, *UserBodyRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignUserPermissions not implemented") +} +func (UnimplementedIAMServiceServer) RemoveUserPermissions(context.Context, *UserBodyRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveUserPermissions not implemented") +} +func (UnimplementedIAMServiceServer) AssignUserContainer(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignUserContainer not implemented") +} +func (UnimplementedIAMServiceServer) RemoveUserContainer(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveUserContainer not implemented") +} +func (UnimplementedIAMServiceServer) AssignUserDataset(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignUserDataset not implemented") +} +func (UnimplementedIAMServiceServer) RemoveUserDataset(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveUserDataset not implemented") +} +func (UnimplementedIAMServiceServer) AssignUserProject(context.Context, *UserResourceBindingRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignUserProject not implemented") +} +func (UnimplementedIAMServiceServer) RemoveUserProject(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveUserProject not implemented") +} +func (UnimplementedIAMServiceServer) CreateRole(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateRole not implemented") +} +func (UnimplementedIAMServiceServer) DeleteRole(context.Context, *IDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRole not implemented") +} +func (UnimplementedIAMServiceServer) GetRole(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRole not implemented") +} +func (UnimplementedIAMServiceServer) ListRoles(context.Context, *QueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRoles not implemented") +} +func (UnimplementedIAMServiceServer) UpdateRole(context.Context, *UpdateByIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateRole not implemented") +} +func (UnimplementedIAMServiceServer) AssignRolePermissions(context.Context, *RolePermissionsRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AssignRolePermissions not implemented") +} +func (UnimplementedIAMServiceServer) RemoveRolePermissions(context.Context, *RolePermissionsRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveRolePermissions not implemented") +} +func (UnimplementedIAMServiceServer) ListUsersFromRole(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListUsersFromRole not implemented") +} +func (UnimplementedIAMServiceServer) GetPermission(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPermission not implemented") +} +func (UnimplementedIAMServiceServer) ListPermissions(context.Context, *QueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListPermissions not implemented") +} +func (UnimplementedIAMServiceServer) ListRolesFromPermission(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRolesFromPermission not implemented") +} +func (UnimplementedIAMServiceServer) GetResource(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetResource not implemented") +} +func (UnimplementedIAMServiceServer) ListResources(context.Context, *QueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListResources not implemented") +} +func (UnimplementedIAMServiceServer) ListResourcePermissions(context.Context, *IDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListResourcePermissions not implemented") +} +func (UnimplementedIAMServiceServer) CreateTeam(context.Context, *CreateTeamRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateTeam not implemented") +} +func (UnimplementedIAMServiceServer) DeleteTeam(context.Context, *TeamRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteTeam not implemented") +} +func (UnimplementedIAMServiceServer) GetTeam(context.Context, *TeamRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetTeam not implemented") +} +func (UnimplementedIAMServiceServer) ListTeams(context.Context, *ListTeamsRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTeams not implemented") +} +func (UnimplementedIAMServiceServer) UpdateTeam(context.Context, *UpdateTeamRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateTeam not implemented") +} +func (UnimplementedIAMServiceServer) ListTeamProjects(context.Context, *ListTeamProjectsRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTeamProjects not implemented") +} +func (UnimplementedIAMServiceServer) AddTeamMember(context.Context, *AddTeamMemberRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AddTeamMember not implemented") +} +func (UnimplementedIAMServiceServer) RemoveTeamMember(context.Context, *RemoveTeamMemberRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveTeamMember not implemented") +} +func (UnimplementedIAMServiceServer) UpdateTeamMemberRole(context.Context, *UpdateTeamMemberRoleRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateTeamMemberRole not implemented") +} +func (UnimplementedIAMServiceServer) ListTeamMembers(context.Context, *ListTeamMembersRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTeamMembers not implemented") +} +func (UnimplementedIAMServiceServer) mustEmbedUnimplementedIAMServiceServer() {} +func (UnimplementedIAMServiceServer) testEmbeddedByValue() {} + +// UnsafeIAMServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to IAMServiceServer will +// result in compilation errors. +type UnsafeIAMServiceServer interface { + mustEmbedUnimplementedIAMServiceServer() +} + +func RegisterIAMServiceServer(s grpc.ServiceRegistrar, srv IAMServiceServer) { + // If the following call panics, it indicates UnimplementedIAMServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&IAMService_ServiceDesc, srv) +} + +func _IAMService_VerifyToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VerifyTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).VerifyToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_VerifyToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).VerifyToken(ctx, req.(*VerifyTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_CheckPermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckPermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).CheckPermission(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_CheckPermission_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).CheckPermission(ctx, req.(*CheckPermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_Login_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).Login(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).Register(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_Register_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).Register(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RefreshToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RefreshToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RefreshToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RefreshToken(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LogoutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).Logout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_Logout_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).Logout(ctx, req.(*LogoutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ChangePassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserBodyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ChangePassword(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ChangePassword_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ChangePassword(ctx, req.(*UserBodyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetProfile(ctx, req.(*UserIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_CreateAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserBodyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).CreateAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_CreateAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).CreateAccessKey(ctx, req.(*UserBodyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListAccessKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserQueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListAccessKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListAccessKeys_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListAccessKeys(ctx, req.(*UserQueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetAccessKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_DeleteAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).DeleteAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_DeleteAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).DeleteAccessKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_DisableAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).DisableAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_DisableAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).DisableAccessKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_EnableAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).EnableAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_EnableAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).EnableAccessKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RotateAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RotateAccessKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RotateAccessKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RotateAccessKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_IsUserTeamAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).IsUserTeamAdmin(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_IsUserTeamAdmin_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).IsUserTeamAdmin(ctx, req.(*UserTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_IsUserInTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).IsUserInTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_IsUserInTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).IsUserInTeam(ctx, req.(*UserTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_IsTeamPublic_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).IsTeamPublic(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_IsTeamPublic_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).IsTeamPublic(ctx, req.(*TeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_IsUserProjectAdmin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserProjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).IsUserProjectAdmin(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_IsUserProjectAdmin_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).IsUserProjectAdmin(ctx, req.(*UserProjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_IsUserInProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserProjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).IsUserInProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_IsUserInProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).IsUserInProject(ctx, req.(*UserProjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ExchangeAccessKeyToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangeAccessKeyTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ExchangeAccessKeyToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ExchangeAccessKeyToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ExchangeAccessKeyToken(ctx, req.(*ExchangeAccessKeyTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_CreateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).CreateUser(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).DeleteUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_DeleteUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).DeleteUser(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetUser(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListUsers(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateByIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).UpdateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_UpdateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).UpdateUser(ctx, req.(*UpdateByIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignUserRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserRoleBindingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignUserRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignUserRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignUserRole(ctx, req.(*UserRoleBindingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveUserRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserRoleBindingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveUserRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveUserRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveUserRole(ctx, req.(*UserRoleBindingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignUserPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserBodyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignUserPermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignUserPermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignUserPermissions(ctx, req.(*UserBodyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveUserPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserBodyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveUserPermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveUserPermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveUserPermissions(ctx, req.(*UserBodyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignUserContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserResourceBindingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignUserContainer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignUserContainer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignUserContainer(ctx, req.(*UserResourceBindingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveUserContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveUserContainer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveUserContainer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveUserContainer(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignUserDataset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserResourceBindingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignUserDataset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignUserDataset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignUserDataset(ctx, req.(*UserResourceBindingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveUserDataset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveUserDataset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveUserDataset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveUserDataset(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignUserProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserResourceBindingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignUserProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignUserProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignUserProject(ctx, req.(*UserResourceBindingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveUserProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveUserProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveUserProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveUserProject(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_CreateRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).CreateRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_CreateRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).CreateRole(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_DeleteRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).DeleteRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_DeleteRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).DeleteRole(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetRole(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListRoles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListRoles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListRoles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListRoles(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_UpdateRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateByIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).UpdateRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_UpdateRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).UpdateRole(ctx, req.(*UpdateByIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AssignRolePermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RolePermissionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AssignRolePermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AssignRolePermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AssignRolePermissions(ctx, req.(*RolePermissionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveRolePermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RolePermissionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveRolePermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveRolePermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveRolePermissions(ctx, req.(*RolePermissionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListUsersFromRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListUsersFromRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListUsersFromRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListUsersFromRole(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetPermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetPermission(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetPermission_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetPermission(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListPermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListPermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListPermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListPermissions(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListRolesFromPermission_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListRolesFromPermission(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListRolesFromPermission_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListRolesFromPermission(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetResource_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetResource(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListResources(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListResources_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListResources(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListResourcePermissions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListResourcePermissions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListResourcePermissions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListResourcePermissions(ctx, req.(*IDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_CreateTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).CreateTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_CreateTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).CreateTeam(ctx, req.(*CreateTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_DeleteTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).DeleteTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_DeleteTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).DeleteTeam(ctx, req.(*TeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_GetTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).GetTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_GetTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).GetTeam(ctx, req.(*TeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListTeams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTeamsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListTeams(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListTeams_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListTeams(ctx, req.(*ListTeamsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_UpdateTeam_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateTeamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).UpdateTeam(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_UpdateTeam_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).UpdateTeam(ctx, req.(*UpdateTeamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListTeamProjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTeamProjectsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListTeamProjects(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListTeamProjects_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListTeamProjects(ctx, req.(*ListTeamProjectsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_AddTeamMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddTeamMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).AddTeamMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_AddTeamMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).AddTeamMember(ctx, req.(*AddTeamMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RemoveTeamMember_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveTeamMemberRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RemoveTeamMember(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RemoveTeamMember_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RemoveTeamMember(ctx, req.(*RemoveTeamMemberRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_UpdateTeamMemberRole_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateTeamMemberRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).UpdateTeamMemberRole(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_UpdateTeamMemberRole_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).UpdateTeamMemberRole(ctx, req.(*UpdateTeamMemberRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_ListTeamMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTeamMembersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).ListTeamMembers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_ListTeamMembers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).ListTeamMembers(ctx, req.(*ListTeamMembersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// IAMService_ServiceDesc is the grpc.ServiceDesc for IAMService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var IAMService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "iam.v1.IAMService", + HandlerType: (*IAMServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "VerifyToken", + Handler: _IAMService_VerifyToken_Handler, + }, + { + MethodName: "CheckPermission", + Handler: _IAMService_CheckPermission_Handler, + }, + { + MethodName: "Login", + Handler: _IAMService_Login_Handler, + }, + { + MethodName: "Register", + Handler: _IAMService_Register_Handler, + }, + { + MethodName: "RefreshToken", + Handler: _IAMService_RefreshToken_Handler, + }, + { + MethodName: "Logout", + Handler: _IAMService_Logout_Handler, + }, + { + MethodName: "ChangePassword", + Handler: _IAMService_ChangePassword_Handler, + }, + { + MethodName: "GetProfile", + Handler: _IAMService_GetProfile_Handler, + }, + { + MethodName: "CreateAccessKey", + Handler: _IAMService_CreateAccessKey_Handler, + }, + { + MethodName: "ListAccessKeys", + Handler: _IAMService_ListAccessKeys_Handler, + }, + { + MethodName: "GetAccessKey", + Handler: _IAMService_GetAccessKey_Handler, + }, + { + MethodName: "DeleteAccessKey", + Handler: _IAMService_DeleteAccessKey_Handler, + }, + { + MethodName: "DisableAccessKey", + Handler: _IAMService_DisableAccessKey_Handler, + }, + { + MethodName: "EnableAccessKey", + Handler: _IAMService_EnableAccessKey_Handler, + }, + { + MethodName: "RotateAccessKey", + Handler: _IAMService_RotateAccessKey_Handler, + }, + { + MethodName: "IsUserTeamAdmin", + Handler: _IAMService_IsUserTeamAdmin_Handler, + }, + { + MethodName: "IsUserInTeam", + Handler: _IAMService_IsUserInTeam_Handler, + }, + { + MethodName: "IsTeamPublic", + Handler: _IAMService_IsTeamPublic_Handler, + }, + { + MethodName: "IsUserProjectAdmin", + Handler: _IAMService_IsUserProjectAdmin_Handler, + }, + { + MethodName: "IsUserInProject", + Handler: _IAMService_IsUserInProject_Handler, + }, + { + MethodName: "ExchangeAccessKeyToken", + Handler: _IAMService_ExchangeAccessKeyToken_Handler, + }, + { + MethodName: "CreateUser", + Handler: _IAMService_CreateUser_Handler, + }, + { + MethodName: "DeleteUser", + Handler: _IAMService_DeleteUser_Handler, + }, + { + MethodName: "GetUser", + Handler: _IAMService_GetUser_Handler, + }, + { + MethodName: "ListUsers", + Handler: _IAMService_ListUsers_Handler, + }, + { + MethodName: "UpdateUser", + Handler: _IAMService_UpdateUser_Handler, + }, + { + MethodName: "AssignUserRole", + Handler: _IAMService_AssignUserRole_Handler, + }, + { + MethodName: "RemoveUserRole", + Handler: _IAMService_RemoveUserRole_Handler, + }, + { + MethodName: "AssignUserPermissions", + Handler: _IAMService_AssignUserPermissions_Handler, + }, + { + MethodName: "RemoveUserPermissions", + Handler: _IAMService_RemoveUserPermissions_Handler, + }, + { + MethodName: "AssignUserContainer", + Handler: _IAMService_AssignUserContainer_Handler, + }, + { + MethodName: "RemoveUserContainer", + Handler: _IAMService_RemoveUserContainer_Handler, + }, + { + MethodName: "AssignUserDataset", + Handler: _IAMService_AssignUserDataset_Handler, + }, + { + MethodName: "RemoveUserDataset", + Handler: _IAMService_RemoveUserDataset_Handler, + }, + { + MethodName: "AssignUserProject", + Handler: _IAMService_AssignUserProject_Handler, + }, + { + MethodName: "RemoveUserProject", + Handler: _IAMService_RemoveUserProject_Handler, + }, + { + MethodName: "CreateRole", + Handler: _IAMService_CreateRole_Handler, + }, + { + MethodName: "DeleteRole", + Handler: _IAMService_DeleteRole_Handler, + }, + { + MethodName: "GetRole", + Handler: _IAMService_GetRole_Handler, + }, + { + MethodName: "ListRoles", + Handler: _IAMService_ListRoles_Handler, + }, + { + MethodName: "UpdateRole", + Handler: _IAMService_UpdateRole_Handler, + }, + { + MethodName: "AssignRolePermissions", + Handler: _IAMService_AssignRolePermissions_Handler, + }, + { + MethodName: "RemoveRolePermissions", + Handler: _IAMService_RemoveRolePermissions_Handler, + }, + { + MethodName: "ListUsersFromRole", + Handler: _IAMService_ListUsersFromRole_Handler, + }, + { + MethodName: "GetPermission", + Handler: _IAMService_GetPermission_Handler, + }, + { + MethodName: "ListPermissions", + Handler: _IAMService_ListPermissions_Handler, + }, + { + MethodName: "ListRolesFromPermission", + Handler: _IAMService_ListRolesFromPermission_Handler, + }, + { + MethodName: "GetResource", + Handler: _IAMService_GetResource_Handler, + }, + { + MethodName: "ListResources", + Handler: _IAMService_ListResources_Handler, + }, + { + MethodName: "ListResourcePermissions", + Handler: _IAMService_ListResourcePermissions_Handler, + }, + { + MethodName: "CreateTeam", + Handler: _IAMService_CreateTeam_Handler, + }, + { + MethodName: "DeleteTeam", + Handler: _IAMService_DeleteTeam_Handler, + }, + { + MethodName: "GetTeam", + Handler: _IAMService_GetTeam_Handler, + }, + { + MethodName: "ListTeams", + Handler: _IAMService_ListTeams_Handler, + }, + { + MethodName: "UpdateTeam", + Handler: _IAMService_UpdateTeam_Handler, + }, + { + MethodName: "ListTeamProjects", + Handler: _IAMService_ListTeamProjects_Handler, + }, + { + MethodName: "AddTeamMember", + Handler: _IAMService_AddTeamMember_Handler, + }, + { + MethodName: "RemoveTeamMember", + Handler: _IAMService_RemoveTeamMember_Handler, + }, + { + MethodName: "UpdateTeamMemberRole", + Handler: _IAMService_UpdateTeamMemberRole_Handler, + }, + { + MethodName: "ListTeamMembers", + Handler: _IAMService_ListTeamMembers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/iam/v1/iam.proto", +} diff --git a/src/proto/orchestrator/v1/orchestrator.pb.go b/src/proto/orchestrator/v1/orchestrator.pb.go new file mode 100644 index 00000000..39d82bae --- /dev/null +++ b/src/proto/orchestrator/v1/orchestrator.pb.go @@ -0,0 +1,1903 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/orchestrator/v1/orchestrator.proto + +package orchestratorv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{0} +} + +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + AppId string `protobuf:"bytes,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + TimestampUnix int64 `protobuf:"varint,4,opt,name=timestamp_unix,json=timestampUnix,proto3" json:"timestamp_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *PingResponse) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *PingResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PingResponse) GetTimestampUnix() int64 { + if x != nil { + return x.TimestampUnix + } + return 0 +} + +type SubmitExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,10,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitExecutionRequest) Reset() { + *x = SubmitExecutionRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitExecutionRequest) ProtoMessage() {} + +func (x *SubmitExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitExecutionRequest.ProtoReflect.Descriptor instead. +func (*SubmitExecutionRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{2} +} + +func (x *SubmitExecutionRequest) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitExecutionRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *SubmitExecutionRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type SubmitExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + Items []*SubmittedExecutionItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitExecutionResponse) Reset() { + *x = SubmitExecutionResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitExecutionResponse) ProtoMessage() {} + +func (x *SubmitExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitExecutionResponse.ProtoReflect.Descriptor instead. +func (*SubmitExecutionResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{3} +} + +func (x *SubmitExecutionResponse) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitExecutionResponse) GetItems() []*SubmittedExecutionItem { + if x != nil { + return x.Items + } + return nil +} + +type SubmittedExecutionItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index int64 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + TraceId string `protobuf:"bytes,2,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + AlgorithmId int64 `protobuf:"varint,4,opt,name=algorithm_id,json=algorithmId,proto3" json:"algorithm_id,omitempty"` + AlgorithmVersionId int64 `protobuf:"varint,5,opt,name=algorithm_version_id,json=algorithmVersionId,proto3" json:"algorithm_version_id,omitempty"` + DatapackId int64 `protobuf:"varint,6,opt,name=datapack_id,json=datapackId,proto3" json:"datapack_id,omitempty"` + DatasetId int64 `protobuf:"varint,7,opt,name=dataset_id,json=datasetId,proto3" json:"dataset_id,omitempty"` + HasDatapackId bool `protobuf:"varint,8,opt,name=has_datapack_id,json=hasDatapackId,proto3" json:"has_datapack_id,omitempty"` + HasDatasetId bool `protobuf:"varint,9,opt,name=has_dataset_id,json=hasDatasetId,proto3" json:"has_dataset_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmittedExecutionItem) Reset() { + *x = SubmittedExecutionItem{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmittedExecutionItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmittedExecutionItem) ProtoMessage() {} + +func (x *SubmittedExecutionItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmittedExecutionItem.ProtoReflect.Descriptor instead. +func (*SubmittedExecutionItem) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{4} +} + +func (x *SubmittedExecutionItem) GetIndex() int64 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *SubmittedExecutionItem) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +func (x *SubmittedExecutionItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *SubmittedExecutionItem) GetAlgorithmId() int64 { + if x != nil { + return x.AlgorithmId + } + return 0 +} + +func (x *SubmittedExecutionItem) GetAlgorithmVersionId() int64 { + if x != nil { + return x.AlgorithmVersionId + } + return 0 +} + +func (x *SubmittedExecutionItem) GetDatapackId() int64 { + if x != nil { + return x.DatapackId + } + return 0 +} + +func (x *SubmittedExecutionItem) GetDatasetId() int64 { + if x != nil { + return x.DatasetId + } + return 0 +} + +func (x *SubmittedExecutionItem) GetHasDatapackId() bool { + if x != nil { + return x.HasDatapackId + } + return false +} + +func (x *SubmittedExecutionItem) GetHasDatasetId() bool { + if x != nil { + return x.HasDatasetId + } + return false +} + +type SubmitFaultInjectionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,10,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitFaultInjectionRequest) Reset() { + *x = SubmitFaultInjectionRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitFaultInjectionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitFaultInjectionRequest) ProtoMessage() {} + +func (x *SubmitFaultInjectionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitFaultInjectionRequest.ProtoReflect.Descriptor instead. +func (*SubmitFaultInjectionRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{5} +} + +func (x *SubmitFaultInjectionRequest) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitFaultInjectionRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *SubmitFaultInjectionRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *SubmitFaultInjectionRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type SubmitFaultInjectionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + Items []*SubmittedInjectionItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + OriginalCount int64 `protobuf:"varint,3,opt,name=original_count,json=originalCount,proto3" json:"original_count,omitempty"` + Warnings *InjectionWarnings `protobuf:"bytes,4,opt,name=warnings,proto3" json:"warnings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitFaultInjectionResponse) Reset() { + *x = SubmitFaultInjectionResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitFaultInjectionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitFaultInjectionResponse) ProtoMessage() {} + +func (x *SubmitFaultInjectionResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitFaultInjectionResponse.ProtoReflect.Descriptor instead. +func (*SubmitFaultInjectionResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{6} +} + +func (x *SubmitFaultInjectionResponse) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitFaultInjectionResponse) GetItems() []*SubmittedInjectionItem { + if x != nil { + return x.Items + } + return nil +} + +func (x *SubmitFaultInjectionResponse) GetOriginalCount() int64 { + if x != nil { + return x.OriginalCount + } + return 0 +} + +func (x *SubmitFaultInjectionResponse) GetWarnings() *InjectionWarnings { + if x != nil { + return x.Warnings + } + return nil +} + +type SubmittedInjectionItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index int64 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + TraceId string `protobuf:"bytes,2,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmittedInjectionItem) Reset() { + *x = SubmittedInjectionItem{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmittedInjectionItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmittedInjectionItem) ProtoMessage() {} + +func (x *SubmittedInjectionItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmittedInjectionItem.ProtoReflect.Descriptor instead. +func (*SubmittedInjectionItem) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{7} +} + +func (x *SubmittedInjectionItem) GetIndex() int64 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *SubmittedInjectionItem) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +func (x *SubmittedInjectionItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type InjectionWarnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + DuplicateServicesInBatch []string `protobuf:"bytes,1,rep,name=duplicate_services_in_batch,json=duplicateServicesInBatch,proto3" json:"duplicate_services_in_batch,omitempty"` + DuplicateBatchesInRequest []int64 `protobuf:"varint,2,rep,packed,name=duplicate_batches_in_request,json=duplicateBatchesInRequest,proto3" json:"duplicate_batches_in_request,omitempty"` + BatchesExistInDatabase []int64 `protobuf:"varint,3,rep,packed,name=batches_exist_in_database,json=batchesExistInDatabase,proto3" json:"batches_exist_in_database,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InjectionWarnings) Reset() { + *x = InjectionWarnings{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InjectionWarnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InjectionWarnings) ProtoMessage() {} + +func (x *InjectionWarnings) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InjectionWarnings.ProtoReflect.Descriptor instead. +func (*InjectionWarnings) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{8} +} + +func (x *InjectionWarnings) GetDuplicateServicesInBatch() []string { + if x != nil { + return x.DuplicateServicesInBatch + } + return nil +} + +func (x *InjectionWarnings) GetDuplicateBatchesInRequest() []int64 { + if x != nil { + return x.DuplicateBatchesInRequest + } + return nil +} + +func (x *InjectionWarnings) GetBatchesExistInDatabase() []int64 { + if x != nil { + return x.BatchesExistInDatabase + } + return nil +} + +type SubmitDatapackBuildingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + ProjectId int64 `protobuf:"varint,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,10,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitDatapackBuildingRequest) Reset() { + *x = SubmitDatapackBuildingRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitDatapackBuildingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitDatapackBuildingRequest) ProtoMessage() {} + +func (x *SubmitDatapackBuildingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitDatapackBuildingRequest.ProtoReflect.Descriptor instead. +func (*SubmitDatapackBuildingRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{9} +} + +func (x *SubmitDatapackBuildingRequest) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitDatapackBuildingRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *SubmitDatapackBuildingRequest) GetProjectId() int64 { + if x != nil { + return x.ProjectId + } + return 0 +} + +func (x *SubmitDatapackBuildingRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type SubmitDatapackBuildingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + Items []*SubmittedBuildingItem `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmitDatapackBuildingResponse) Reset() { + *x = SubmitDatapackBuildingResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmitDatapackBuildingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitDatapackBuildingResponse) ProtoMessage() {} + +func (x *SubmitDatapackBuildingResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitDatapackBuildingResponse.ProtoReflect.Descriptor instead. +func (*SubmitDatapackBuildingResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{10} +} + +func (x *SubmitDatapackBuildingResponse) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +func (x *SubmitDatapackBuildingResponse) GetItems() []*SubmittedBuildingItem { + if x != nil { + return x.Items + } + return nil +} + +type SubmittedBuildingItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Index int64 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"` + TraceId string `protobuf:"bytes,2,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + TaskId string `protobuf:"bytes,3,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubmittedBuildingItem) Reset() { + *x = SubmittedBuildingItem{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubmittedBuildingItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmittedBuildingItem) ProtoMessage() {} + +func (x *SubmittedBuildingItem) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmittedBuildingItem.ProtoReflect.Descriptor instead. +func (*SubmittedBuildingItem) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{11} +} + +func (x *SubmittedBuildingItem) GetIndex() int64 { + if x != nil { + return x.Index + } + return 0 +} + +func (x *SubmittedBuildingItem) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +func (x *SubmittedBuildingItem) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type CancelTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelTaskRequest) Reset() { + *x = CancelTaskRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelTaskRequest) ProtoMessage() {} + +func (x *CancelTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelTaskRequest.ProtoReflect.Descriptor instead. +func (*CancelTaskRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{12} +} + +func (x *CancelTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type CancelTaskResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cancelled bool `protobuf:"varint,1,opt,name=cancelled,proto3" json:"cancelled,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CancelTaskResponse) Reset() { + *x = CancelTaskResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CancelTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CancelTaskResponse) ProtoMessage() {} + +func (x *CancelTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CancelTaskResponse.ProtoReflect.Descriptor instead. +func (*CancelTaskResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{13} +} + +func (x *CancelTaskResponse) GetCancelled() bool { + if x != nil { + return x.Cancelled + } + return false +} + +type MutationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Body *structpb.Struct `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutationRequest) Reset() { + *x = MutationRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutationRequest) ProtoMessage() {} + +func (x *MutationRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutationRequest.ProtoReflect.Descriptor instead. +func (*MutationRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{14} +} + +func (x *MutationRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type GetExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ExecutionId int64 `protobuf:"varint,1,opt,name=execution_id,json=executionId,proto3" json:"execution_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetExecutionRequest) Reset() { + *x = GetExecutionRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetExecutionRequest) ProtoMessage() {} + +func (x *GetExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetExecutionRequest.ProtoReflect.Descriptor instead. +func (*GetExecutionRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{15} +} + +func (x *GetExecutionRequest) GetExecutionId() int64 { + if x != nil { + return x.ExecutionId + } + return 0 +} + +type ListProjectStatisticsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProjectIds []int64 `protobuf:"varint,1,rep,packed,name=project_ids,json=projectIds,proto3" json:"project_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProjectStatisticsRequest) Reset() { + *x = ListProjectStatisticsRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProjectStatisticsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProjectStatisticsRequest) ProtoMessage() {} + +func (x *ListProjectStatisticsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProjectStatisticsRequest.ProtoReflect.Descriptor instead. +func (*ListProjectStatisticsRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{16} +} + +func (x *ListProjectStatisticsRequest) GetProjectIds() []int64 { + if x != nil { + return x.ProjectIds + } + return nil +} + +type GetTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTaskRequest) Reset() { + *x = GetTaskRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTaskRequest) ProtoMessage() {} + +func (x *GetTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTaskRequest.ProtoReflect.Descriptor instead. +func (*GetTaskRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{17} +} + +func (x *GetTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type PollTaskLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + AfterUnixNano int64 `protobuf:"varint,2,opt,name=after_unix_nano,json=afterUnixNano,proto3" json:"after_unix_nano,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollTaskLogsRequest) Reset() { + *x = PollTaskLogsRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollTaskLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollTaskLogsRequest) ProtoMessage() {} + +func (x *PollTaskLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollTaskLogsRequest.ProtoReflect.Descriptor instead. +func (*PollTaskLogsRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{18} +} + +func (x *PollTaskLogsRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +func (x *PollTaskLogsRequest) GetAfterUnixNano() int64 { + if x != nil { + return x.AfterUnixNano + } + return 0 +} + +type ListTasksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTasksRequest) Reset() { + *x = ListTasksRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTasksRequest) ProtoMessage() {} + +func (x *ListTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTasksRequest.ProtoReflect.Descriptor instead. +func (*ListTasksRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{19} +} + +func (x *ListTasksRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type GetTraceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TraceId string `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTraceRequest) Reset() { + *x = GetTraceRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTraceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTraceRequest) ProtoMessage() {} + +func (x *GetTraceRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTraceRequest.ProtoReflect.Descriptor instead. +func (*GetTraceRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{20} +} + +func (x *GetTraceRequest) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +type ListTracesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTracesRequest) Reset() { + *x = ListTracesRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTracesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTracesRequest) ProtoMessage() {} + +func (x *ListTracesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTracesRequest.ProtoReflect.Descriptor instead. +func (*ListTracesRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{21} +} + +func (x *ListTracesRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type GetGroupStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGroupStatsRequest) Reset() { + *x = GetGroupStatsRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGroupStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGroupStatsRequest) ProtoMessage() {} + +func (x *GetGroupStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGroupStatsRequest.ProtoReflect.Descriptor instead. +func (*GetGroupStatsRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{22} +} + +func (x *GetGroupStatsRequest) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +type GetTraceStreamStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TraceId string `protobuf:"bytes,1,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetTraceStreamStateRequest) Reset() { + *x = GetTraceStreamStateRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetTraceStreamStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTraceStreamStateRequest) ProtoMessage() {} + +func (x *GetTraceStreamStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTraceStreamStateRequest.ProtoReflect.Descriptor instead. +func (*GetTraceStreamStateRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{23} +} + +func (x *GetTraceStreamStateRequest) GetTraceId() string { + if x != nil { + return x.TraceId + } + return "" +} + +type GetGroupStreamStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + GroupId string `protobuf:"bytes,1,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGroupStreamStateRequest) Reset() { + *x = GetGroupStreamStateRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGroupStreamStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGroupStreamStateRequest) ProtoMessage() {} + +func (x *GetGroupStreamStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGroupStreamStateRequest.ProtoReflect.Descriptor instead. +func (*GetGroupStreamStateRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{24} +} + +func (x *GetGroupStreamStateRequest) GetGroupId() string { + if x != nil { + return x.GroupId + } + return "" +} + +type ReadStreamMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + StreamKey string `protobuf:"bytes,1,opt,name=stream_key,json=streamKey,proto3" json:"stream_key,omitempty"` + LastId string `protobuf:"bytes,2,opt,name=last_id,json=lastId,proto3" json:"last_id,omitempty"` + Count int64 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` + BlockMillis int64 `protobuf:"varint,4,opt,name=block_millis,json=blockMillis,proto3" json:"block_millis,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadStreamMessagesRequest) Reset() { + *x = ReadStreamMessagesRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadStreamMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadStreamMessagesRequest) ProtoMessage() {} + +func (x *ReadStreamMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadStreamMessagesRequest.ProtoReflect.Descriptor instead. +func (*ReadStreamMessagesRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{25} +} + +func (x *ReadStreamMessagesRequest) GetStreamKey() string { + if x != nil { + return x.StreamKey + } + return "" +} + +func (x *ReadStreamMessagesRequest) GetLastId() string { + if x != nil { + return x.LastId + } + return "" +} + +func (x *ReadStreamMessagesRequest) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *ReadStreamMessagesRequest) GetBlockMillis() int64 { + if x != nil { + return x.BlockMillis + } + return 0 +} + +type ListDeadLetterTasksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit int64 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDeadLetterTasksRequest) Reset() { + *x = ListDeadLetterTasksRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDeadLetterTasksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDeadLetterTasksRequest) ProtoMessage() {} + +func (x *ListDeadLetterTasksRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDeadLetterTasksRequest.ProtoReflect.Descriptor instead. +func (*ListDeadLetterTasksRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{26} +} + +func (x *ListDeadLetterTasksRequest) GetLimit() int64 { + if x != nil { + return x.Limit + } + return 0 +} + +type RetryTaskRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + TaskId string `protobuf:"bytes,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryTaskRequest) Reset() { + *x = RetryTaskRequest{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryTaskRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryTaskRequest) ProtoMessage() {} + +func (x *RetryTaskRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryTaskRequest.ProtoReflect.Descriptor instead. +func (*RetryTaskRequest) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{27} +} + +func (x *RetryTaskRequest) GetTaskId() string { + if x != nil { + return x.TaskId + } + return "" +} + +type RetryTaskResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + Queue string `protobuf:"bytes,2,opt,name=queue,proto3" json:"queue,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryTaskResponse) Reset() { + *x = RetryTaskResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryTaskResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryTaskResponse) ProtoMessage() {} + +func (x *RetryTaskResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryTaskResponse.ProtoReflect.Descriptor instead. +func (*RetryTaskResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{28} +} + +func (x *RetryTaskResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *RetryTaskResponse) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +type StructResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StructResponse) Reset() { + *x = StructResponse{} + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StructResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StructResponse) ProtoMessage() {} + +func (x *StructResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_orchestrator_v1_orchestrator_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StructResponse.ProtoReflect.Descriptor instead. +func (*StructResponse) Descriptor() ([]byte, []int) { + return file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP(), []int{29} +} + +func (x *StructResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +var File_proto_orchestrator_v1_orchestrator_proto protoreflect.FileDescriptor + +const file_proto_orchestrator_v1_orchestrator_proto_rawDesc = "" + + "\n" + + "(proto/orchestrator/v1/orchestrator.proto\x12\x0forchestrator.v1\x1a\x1cgoogle/protobuf/struct.proto\"\r\n" + + "\vPingRequest\"~\n" + + "\fPingResponse\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x15\n" + + "\x06app_id\x18\x02 \x01(\tR\x05appId\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12%\n" + + "\x0etimestamp_unix\x18\x04 \x01(\x03R\rtimestampUnix\"y\n" + + "\x16SubmitExecutionRequest\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12+\n" + + "\x04body\x18\n" + + " \x01(\v2\x17.google.protobuf.StructR\x04body\"s\n" + + "\x17SubmitExecutionResponse\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12=\n" + + "\x05items\x18\x02 \x03(\v2'.orchestrator.v1.SubmittedExecutionItemR\x05items\"\xc5\x02\n" + + "\x16SubmittedExecutionItem\x12\x14\n" + + "\x05index\x18\x01 \x01(\x03R\x05index\x12\x19\n" + + "\btrace_id\x18\x02 \x01(\tR\atraceId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\x12!\n" + + "\falgorithm_id\x18\x04 \x01(\x03R\valgorithmId\x120\n" + + "\x14algorithm_version_id\x18\x05 \x01(\x03R\x12algorithmVersionId\x12\x1f\n" + + "\vdatapack_id\x18\x06 \x01(\x03R\n" + + "datapackId\x12\x1d\n" + + "\n" + + "dataset_id\x18\a \x01(\x03R\tdatasetId\x12&\n" + + "\x0fhas_datapack_id\x18\b \x01(\bR\rhasDatapackId\x12$\n" + + "\x0ehas_dataset_id\x18\t \x01(\bR\fhasDatasetId\"\x9d\x01\n" + + "\x1bSubmitFaultInjectionRequest\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1d\n" + + "\n" + + "project_id\x18\x03 \x01(\x03R\tprojectId\x12+\n" + + "\x04body\x18\n" + + " \x01(\v2\x17.google.protobuf.StructR\x04body\"\xdf\x01\n" + + "\x1cSubmitFaultInjectionResponse\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12=\n" + + "\x05items\x18\x02 \x03(\v2'.orchestrator.v1.SubmittedInjectionItemR\x05items\x12%\n" + + "\x0eoriginal_count\x18\x03 \x01(\x03R\roriginalCount\x12>\n" + + "\bwarnings\x18\x04 \x01(\v2\".orchestrator.v1.InjectionWarningsR\bwarnings\"b\n" + + "\x16SubmittedInjectionItem\x12\x14\n" + + "\x05index\x18\x01 \x01(\x03R\x05index\x12\x19\n" + + "\btrace_id\x18\x02 \x01(\tR\atraceId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\"\xce\x01\n" + + "\x11InjectionWarnings\x12=\n" + + "\x1bduplicate_services_in_batch\x18\x01 \x03(\tR\x18duplicateServicesInBatch\x12?\n" + + "\x1cduplicate_batches_in_request\x18\x02 \x03(\x03R\x19duplicateBatchesInRequest\x129\n" + + "\x19batches_exist_in_database\x18\x03 \x03(\x03R\x16batchesExistInDatabase\"\x9f\x01\n" + + "\x1dSubmitDatapackBuildingRequest\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x1d\n" + + "\n" + + "project_id\x18\x03 \x01(\x03R\tprojectId\x12+\n" + + "\x04body\x18\n" + + " \x01(\v2\x17.google.protobuf.StructR\x04body\"y\n" + + "\x1eSubmitDatapackBuildingResponse\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\x12<\n" + + "\x05items\x18\x02 \x03(\v2&.orchestrator.v1.SubmittedBuildingItemR\x05items\"a\n" + + "\x15SubmittedBuildingItem\x12\x14\n" + + "\x05index\x18\x01 \x01(\x03R\x05index\x12\x19\n" + + "\btrace_id\x18\x02 \x01(\tR\atraceId\x12\x17\n" + + "\atask_id\x18\x03 \x01(\tR\x06taskId\",\n" + + "\x11CancelTaskRequest\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\"2\n" + + "\x12CancelTaskResponse\x12\x1c\n" + + "\tcancelled\x18\x01 \x01(\bR\tcancelled\">\n" + + "\x0fMutationRequest\x12+\n" + + "\x04body\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04body\"8\n" + + "\x13GetExecutionRequest\x12!\n" + + "\fexecution_id\x18\x01 \x01(\x03R\vexecutionId\"?\n" + + "\x1cListProjectStatisticsRequest\x12\x1f\n" + + "\vproject_ids\x18\x01 \x03(\x03R\n" + + "projectIds\")\n" + + "\x0eGetTaskRequest\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\"V\n" + + "\x13PollTaskLogsRequest\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\x12&\n" + + "\x0fafter_unix_nano\x18\x02 \x01(\x03R\rafterUnixNano\"A\n" + + "\x10ListTasksRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\",\n" + + "\x0fGetTraceRequest\x12\x19\n" + + "\btrace_id\x18\x01 \x01(\tR\atraceId\"B\n" + + "\x11ListTracesRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"1\n" + + "\x14GetGroupStatsRequest\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\"7\n" + + "\x1aGetTraceStreamStateRequest\x12\x19\n" + + "\btrace_id\x18\x01 \x01(\tR\atraceId\"7\n" + + "\x1aGetGroupStreamStateRequest\x12\x19\n" + + "\bgroup_id\x18\x01 \x01(\tR\agroupId\"\x8c\x01\n" + + "\x19ReadStreamMessagesRequest\x12\x1d\n" + + "\n" + + "stream_key\x18\x01 \x01(\tR\tstreamKey\x12\x17\n" + + "\alast_id\x18\x02 \x01(\tR\x06lastId\x12\x14\n" + + "\x05count\x18\x03 \x01(\x03R\x05count\x12!\n" + + "\fblock_millis\x18\x04 \x01(\x03R\vblockMillis\"2\n" + + "\x1aListDeadLetterTasksRequest\x12\x14\n" + + "\x05limit\x18\x01 \x01(\x03R\x05limit\"+\n" + + "\x10RetryTaskRequest\x12\x17\n" + + "\atask_id\x18\x01 \x01(\tR\x06taskId\"E\n" + + "\x11RetryTaskResponse\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\bR\baccepted\x12\x14\n" + + "\x05queue\x18\x02 \x01(\tR\x05queue\"=\n" + + "\x0eStructResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xc0\x15\n" + + "\x13OrchestratorService\x12C\n" + + "\x04Ping\x12\x1c.orchestrator.v1.PingRequest\x1a\x1d.orchestrator.v1.PingResponse\x12d\n" + + "\x0fSubmitExecution\x12'.orchestrator.v1.SubmitExecutionRequest\x1a(.orchestrator.v1.SubmitExecutionResponse\x12s\n" + + "\x14SubmitFaultInjection\x12,.orchestrator.v1.SubmitFaultInjectionRequest\x1a-.orchestrator.v1.SubmitFaultInjectionResponse\x12y\n" + + "\x16SubmitDatapackBuilding\x12..orchestrator.v1.SubmitDatapackBuildingRequest\x1a/.orchestrator.v1.SubmitDatapackBuildingResponse\x12T\n" + + "\x0fCreateExecution\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12T\n" + + "\x0fCreateInjection\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12Y\n" + + "\x14UpdateExecutionState\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12Y\n" + + "\x14UpdateInjectionState\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12^\n" + + "\x19UpdateInjectionTimestamps\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12X\n" + + "\x13GetInjectionMetrics\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12X\n" + + "\x13GetExecutionMetrics\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12U\n" + + "\n" + + "CancelTask\x12\".orchestrator.v1.CancelTaskRequest\x1a#.orchestrator.v1.CancelTaskResponse\x12U\n" + + "\fGetExecution\x12$.orchestrator.v1.GetExecutionRequest\x1a\x1f.orchestrator.v1.StructResponse\x12g\n" + + "\x15ListProjectStatistics\x12-.orchestrator.v1.ListProjectStatisticsRequest\x1a\x1f.orchestrator.v1.StructResponse\x12g\n" + + "\"ListEvaluationExecutionsByDatapack\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12f\n" + + "!ListEvaluationExecutionsByDataset\x12 .orchestrator.v1.MutationRequest\x1a\x1f.orchestrator.v1.StructResponse\x12K\n" + + "\aGetTask\x12\x1f.orchestrator.v1.GetTaskRequest\x1a\x1f.orchestrator.v1.StructResponse\x12U\n" + + "\fPollTaskLogs\x12$.orchestrator.v1.PollTaskLogsRequest\x1a\x1f.orchestrator.v1.StructResponse\x12O\n" + + "\tListTasks\x12!.orchestrator.v1.ListTasksRequest\x1a\x1f.orchestrator.v1.StructResponse\x12M\n" + + "\bGetTrace\x12 .orchestrator.v1.GetTraceRequest\x1a\x1f.orchestrator.v1.StructResponse\x12Q\n" + + "\n" + + "ListTraces\x12\".orchestrator.v1.ListTracesRequest\x1a\x1f.orchestrator.v1.StructResponse\x12W\n" + + "\rGetGroupStats\x12%.orchestrator.v1.GetGroupStatsRequest\x1a\x1f.orchestrator.v1.StructResponse\x12c\n" + + "\x13GetTraceStreamState\x12+.orchestrator.v1.GetTraceStreamStateRequest\x1a\x1f.orchestrator.v1.StructResponse\x12f\n" + + "\x17ReadTraceStreamMessages\x12*.orchestrator.v1.ReadStreamMessagesRequest\x1a\x1f.orchestrator.v1.StructResponse\x12c\n" + + "\x13GetGroupStreamState\x12+.orchestrator.v1.GetGroupStreamStateRequest\x1a\x1f.orchestrator.v1.StructResponse\x12f\n" + + "\x17ReadGroupStreamMessages\x12*.orchestrator.v1.ReadStreamMessagesRequest\x1a\x1f.orchestrator.v1.StructResponse\x12m\n" + + "\x1eReadNotificationStreamMessages\x12*.orchestrator.v1.ReadStreamMessagesRequest\x1a\x1f.orchestrator.v1.StructResponse\x12c\n" + + "\x13ListDeadLetterTasks\x12+.orchestrator.v1.ListDeadLetterTasksRequest\x1a\x1f.orchestrator.v1.StructResponse\x12R\n" + + "\tRetryTask\x12!.orchestrator.v1.RetryTaskRequest\x1a\".orchestrator.v1.RetryTaskResponseB,Z*aegis/proto/orchestrator/v1;orchestratorv1b\x06proto3" + +var ( + file_proto_orchestrator_v1_orchestrator_proto_rawDescOnce sync.Once + file_proto_orchestrator_v1_orchestrator_proto_rawDescData []byte +) + +func file_proto_orchestrator_v1_orchestrator_proto_rawDescGZIP() []byte { + file_proto_orchestrator_v1_orchestrator_proto_rawDescOnce.Do(func() { + file_proto_orchestrator_v1_orchestrator_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_orchestrator_v1_orchestrator_proto_rawDesc), len(file_proto_orchestrator_v1_orchestrator_proto_rawDesc))) + }) + return file_proto_orchestrator_v1_orchestrator_proto_rawDescData +} + +var file_proto_orchestrator_v1_orchestrator_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_proto_orchestrator_v1_orchestrator_proto_goTypes = []any{ + (*PingRequest)(nil), // 0: orchestrator.v1.PingRequest + (*PingResponse)(nil), // 1: orchestrator.v1.PingResponse + (*SubmitExecutionRequest)(nil), // 2: orchestrator.v1.SubmitExecutionRequest + (*SubmitExecutionResponse)(nil), // 3: orchestrator.v1.SubmitExecutionResponse + (*SubmittedExecutionItem)(nil), // 4: orchestrator.v1.SubmittedExecutionItem + (*SubmitFaultInjectionRequest)(nil), // 5: orchestrator.v1.SubmitFaultInjectionRequest + (*SubmitFaultInjectionResponse)(nil), // 6: orchestrator.v1.SubmitFaultInjectionResponse + (*SubmittedInjectionItem)(nil), // 7: orchestrator.v1.SubmittedInjectionItem + (*InjectionWarnings)(nil), // 8: orchestrator.v1.InjectionWarnings + (*SubmitDatapackBuildingRequest)(nil), // 9: orchestrator.v1.SubmitDatapackBuildingRequest + (*SubmitDatapackBuildingResponse)(nil), // 10: orchestrator.v1.SubmitDatapackBuildingResponse + (*SubmittedBuildingItem)(nil), // 11: orchestrator.v1.SubmittedBuildingItem + (*CancelTaskRequest)(nil), // 12: orchestrator.v1.CancelTaskRequest + (*CancelTaskResponse)(nil), // 13: orchestrator.v1.CancelTaskResponse + (*MutationRequest)(nil), // 14: orchestrator.v1.MutationRequest + (*GetExecutionRequest)(nil), // 15: orchestrator.v1.GetExecutionRequest + (*ListProjectStatisticsRequest)(nil), // 16: orchestrator.v1.ListProjectStatisticsRequest + (*GetTaskRequest)(nil), // 17: orchestrator.v1.GetTaskRequest + (*PollTaskLogsRequest)(nil), // 18: orchestrator.v1.PollTaskLogsRequest + (*ListTasksRequest)(nil), // 19: orchestrator.v1.ListTasksRequest + (*GetTraceRequest)(nil), // 20: orchestrator.v1.GetTraceRequest + (*ListTracesRequest)(nil), // 21: orchestrator.v1.ListTracesRequest + (*GetGroupStatsRequest)(nil), // 22: orchestrator.v1.GetGroupStatsRequest + (*GetTraceStreamStateRequest)(nil), // 23: orchestrator.v1.GetTraceStreamStateRequest + (*GetGroupStreamStateRequest)(nil), // 24: orchestrator.v1.GetGroupStreamStateRequest + (*ReadStreamMessagesRequest)(nil), // 25: orchestrator.v1.ReadStreamMessagesRequest + (*ListDeadLetterTasksRequest)(nil), // 26: orchestrator.v1.ListDeadLetterTasksRequest + (*RetryTaskRequest)(nil), // 27: orchestrator.v1.RetryTaskRequest + (*RetryTaskResponse)(nil), // 28: orchestrator.v1.RetryTaskResponse + (*StructResponse)(nil), // 29: orchestrator.v1.StructResponse + (*structpb.Struct)(nil), // 30: google.protobuf.Struct +} +var file_proto_orchestrator_v1_orchestrator_proto_depIdxs = []int32{ + 30, // 0: orchestrator.v1.SubmitExecutionRequest.body:type_name -> google.protobuf.Struct + 4, // 1: orchestrator.v1.SubmitExecutionResponse.items:type_name -> orchestrator.v1.SubmittedExecutionItem + 30, // 2: orchestrator.v1.SubmitFaultInjectionRequest.body:type_name -> google.protobuf.Struct + 7, // 3: orchestrator.v1.SubmitFaultInjectionResponse.items:type_name -> orchestrator.v1.SubmittedInjectionItem + 8, // 4: orchestrator.v1.SubmitFaultInjectionResponse.warnings:type_name -> orchestrator.v1.InjectionWarnings + 30, // 5: orchestrator.v1.SubmitDatapackBuildingRequest.body:type_name -> google.protobuf.Struct + 11, // 6: orchestrator.v1.SubmitDatapackBuildingResponse.items:type_name -> orchestrator.v1.SubmittedBuildingItem + 30, // 7: orchestrator.v1.MutationRequest.body:type_name -> google.protobuf.Struct + 30, // 8: orchestrator.v1.ListTasksRequest.query:type_name -> google.protobuf.Struct + 30, // 9: orchestrator.v1.ListTracesRequest.query:type_name -> google.protobuf.Struct + 30, // 10: orchestrator.v1.StructResponse.data:type_name -> google.protobuf.Struct + 0, // 11: orchestrator.v1.OrchestratorService.Ping:input_type -> orchestrator.v1.PingRequest + 2, // 12: orchestrator.v1.OrchestratorService.SubmitExecution:input_type -> orchestrator.v1.SubmitExecutionRequest + 5, // 13: orchestrator.v1.OrchestratorService.SubmitFaultInjection:input_type -> orchestrator.v1.SubmitFaultInjectionRequest + 9, // 14: orchestrator.v1.OrchestratorService.SubmitDatapackBuilding:input_type -> orchestrator.v1.SubmitDatapackBuildingRequest + 14, // 15: orchestrator.v1.OrchestratorService.CreateExecution:input_type -> orchestrator.v1.MutationRequest + 14, // 16: orchestrator.v1.OrchestratorService.CreateInjection:input_type -> orchestrator.v1.MutationRequest + 14, // 17: orchestrator.v1.OrchestratorService.UpdateExecutionState:input_type -> orchestrator.v1.MutationRequest + 14, // 18: orchestrator.v1.OrchestratorService.UpdateInjectionState:input_type -> orchestrator.v1.MutationRequest + 14, // 19: orchestrator.v1.OrchestratorService.UpdateInjectionTimestamps:input_type -> orchestrator.v1.MutationRequest + 14, // 20: orchestrator.v1.OrchestratorService.GetInjectionMetrics:input_type -> orchestrator.v1.MutationRequest + 14, // 21: orchestrator.v1.OrchestratorService.GetExecutionMetrics:input_type -> orchestrator.v1.MutationRequest + 12, // 22: orchestrator.v1.OrchestratorService.CancelTask:input_type -> orchestrator.v1.CancelTaskRequest + 15, // 23: orchestrator.v1.OrchestratorService.GetExecution:input_type -> orchestrator.v1.GetExecutionRequest + 16, // 24: orchestrator.v1.OrchestratorService.ListProjectStatistics:input_type -> orchestrator.v1.ListProjectStatisticsRequest + 14, // 25: orchestrator.v1.OrchestratorService.ListEvaluationExecutionsByDatapack:input_type -> orchestrator.v1.MutationRequest + 14, // 26: orchestrator.v1.OrchestratorService.ListEvaluationExecutionsByDataset:input_type -> orchestrator.v1.MutationRequest + 17, // 27: orchestrator.v1.OrchestratorService.GetTask:input_type -> orchestrator.v1.GetTaskRequest + 18, // 28: orchestrator.v1.OrchestratorService.PollTaskLogs:input_type -> orchestrator.v1.PollTaskLogsRequest + 19, // 29: orchestrator.v1.OrchestratorService.ListTasks:input_type -> orchestrator.v1.ListTasksRequest + 20, // 30: orchestrator.v1.OrchestratorService.GetTrace:input_type -> orchestrator.v1.GetTraceRequest + 21, // 31: orchestrator.v1.OrchestratorService.ListTraces:input_type -> orchestrator.v1.ListTracesRequest + 22, // 32: orchestrator.v1.OrchestratorService.GetGroupStats:input_type -> orchestrator.v1.GetGroupStatsRequest + 23, // 33: orchestrator.v1.OrchestratorService.GetTraceStreamState:input_type -> orchestrator.v1.GetTraceStreamStateRequest + 25, // 34: orchestrator.v1.OrchestratorService.ReadTraceStreamMessages:input_type -> orchestrator.v1.ReadStreamMessagesRequest + 24, // 35: orchestrator.v1.OrchestratorService.GetGroupStreamState:input_type -> orchestrator.v1.GetGroupStreamStateRequest + 25, // 36: orchestrator.v1.OrchestratorService.ReadGroupStreamMessages:input_type -> orchestrator.v1.ReadStreamMessagesRequest + 25, // 37: orchestrator.v1.OrchestratorService.ReadNotificationStreamMessages:input_type -> orchestrator.v1.ReadStreamMessagesRequest + 26, // 38: orchestrator.v1.OrchestratorService.ListDeadLetterTasks:input_type -> orchestrator.v1.ListDeadLetterTasksRequest + 27, // 39: orchestrator.v1.OrchestratorService.RetryTask:input_type -> orchestrator.v1.RetryTaskRequest + 1, // 40: orchestrator.v1.OrchestratorService.Ping:output_type -> orchestrator.v1.PingResponse + 3, // 41: orchestrator.v1.OrchestratorService.SubmitExecution:output_type -> orchestrator.v1.SubmitExecutionResponse + 6, // 42: orchestrator.v1.OrchestratorService.SubmitFaultInjection:output_type -> orchestrator.v1.SubmitFaultInjectionResponse + 10, // 43: orchestrator.v1.OrchestratorService.SubmitDatapackBuilding:output_type -> orchestrator.v1.SubmitDatapackBuildingResponse + 29, // 44: orchestrator.v1.OrchestratorService.CreateExecution:output_type -> orchestrator.v1.StructResponse + 29, // 45: orchestrator.v1.OrchestratorService.CreateInjection:output_type -> orchestrator.v1.StructResponse + 29, // 46: orchestrator.v1.OrchestratorService.UpdateExecutionState:output_type -> orchestrator.v1.StructResponse + 29, // 47: orchestrator.v1.OrchestratorService.UpdateInjectionState:output_type -> orchestrator.v1.StructResponse + 29, // 48: orchestrator.v1.OrchestratorService.UpdateInjectionTimestamps:output_type -> orchestrator.v1.StructResponse + 29, // 49: orchestrator.v1.OrchestratorService.GetInjectionMetrics:output_type -> orchestrator.v1.StructResponse + 29, // 50: orchestrator.v1.OrchestratorService.GetExecutionMetrics:output_type -> orchestrator.v1.StructResponse + 13, // 51: orchestrator.v1.OrchestratorService.CancelTask:output_type -> orchestrator.v1.CancelTaskResponse + 29, // 52: orchestrator.v1.OrchestratorService.GetExecution:output_type -> orchestrator.v1.StructResponse + 29, // 53: orchestrator.v1.OrchestratorService.ListProjectStatistics:output_type -> orchestrator.v1.StructResponse + 29, // 54: orchestrator.v1.OrchestratorService.ListEvaluationExecutionsByDatapack:output_type -> orchestrator.v1.StructResponse + 29, // 55: orchestrator.v1.OrchestratorService.ListEvaluationExecutionsByDataset:output_type -> orchestrator.v1.StructResponse + 29, // 56: orchestrator.v1.OrchestratorService.GetTask:output_type -> orchestrator.v1.StructResponse + 29, // 57: orchestrator.v1.OrchestratorService.PollTaskLogs:output_type -> orchestrator.v1.StructResponse + 29, // 58: orchestrator.v1.OrchestratorService.ListTasks:output_type -> orchestrator.v1.StructResponse + 29, // 59: orchestrator.v1.OrchestratorService.GetTrace:output_type -> orchestrator.v1.StructResponse + 29, // 60: orchestrator.v1.OrchestratorService.ListTraces:output_type -> orchestrator.v1.StructResponse + 29, // 61: orchestrator.v1.OrchestratorService.GetGroupStats:output_type -> orchestrator.v1.StructResponse + 29, // 62: orchestrator.v1.OrchestratorService.GetTraceStreamState:output_type -> orchestrator.v1.StructResponse + 29, // 63: orchestrator.v1.OrchestratorService.ReadTraceStreamMessages:output_type -> orchestrator.v1.StructResponse + 29, // 64: orchestrator.v1.OrchestratorService.GetGroupStreamState:output_type -> orchestrator.v1.StructResponse + 29, // 65: orchestrator.v1.OrchestratorService.ReadGroupStreamMessages:output_type -> orchestrator.v1.StructResponse + 29, // 66: orchestrator.v1.OrchestratorService.ReadNotificationStreamMessages:output_type -> orchestrator.v1.StructResponse + 29, // 67: orchestrator.v1.OrchestratorService.ListDeadLetterTasks:output_type -> orchestrator.v1.StructResponse + 28, // 68: orchestrator.v1.OrchestratorService.RetryTask:output_type -> orchestrator.v1.RetryTaskResponse + 40, // [40:69] is the sub-list for method output_type + 11, // [11:40] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name +} + +func init() { file_proto_orchestrator_v1_orchestrator_proto_init() } +func file_proto_orchestrator_v1_orchestrator_proto_init() { + if File_proto_orchestrator_v1_orchestrator_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_orchestrator_v1_orchestrator_proto_rawDesc), len(file_proto_orchestrator_v1_orchestrator_proto_rawDesc)), + NumEnums: 0, + NumMessages: 30, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_orchestrator_v1_orchestrator_proto_goTypes, + DependencyIndexes: file_proto_orchestrator_v1_orchestrator_proto_depIdxs, + MessageInfos: file_proto_orchestrator_v1_orchestrator_proto_msgTypes, + }.Build() + File_proto_orchestrator_v1_orchestrator_proto = out.File + file_proto_orchestrator_v1_orchestrator_proto_goTypes = nil + file_proto_orchestrator_v1_orchestrator_proto_depIdxs = nil +} diff --git a/src/proto/orchestrator/v1/orchestrator.proto b/src/proto/orchestrator/v1/orchestrator.proto new file mode 100644 index 00000000..9dd96070 --- /dev/null +++ b/src/proto/orchestrator/v1/orchestrator.proto @@ -0,0 +1,192 @@ +syntax = "proto3"; + +package orchestrator.v1; + +option go_package = "aegis/proto/orchestrator/v1;orchestratorv1"; + +import "google/protobuf/struct.proto"; + +service OrchestratorService { + rpc Ping(PingRequest) returns (PingResponse); + rpc SubmitExecution(SubmitExecutionRequest) returns (SubmitExecutionResponse); + rpc SubmitFaultInjection(SubmitFaultInjectionRequest) returns (SubmitFaultInjectionResponse); + rpc SubmitDatapackBuilding(SubmitDatapackBuildingRequest) returns (SubmitDatapackBuildingResponse); + rpc CreateExecution(MutationRequest) returns (StructResponse); + rpc CreateInjection(MutationRequest) returns (StructResponse); + rpc UpdateExecutionState(MutationRequest) returns (StructResponse); + rpc UpdateInjectionState(MutationRequest) returns (StructResponse); + rpc UpdateInjectionTimestamps(MutationRequest) returns (StructResponse); + rpc GetInjectionMetrics(MutationRequest) returns (StructResponse); + rpc GetExecutionMetrics(MutationRequest) returns (StructResponse); + rpc CancelTask(CancelTaskRequest) returns (CancelTaskResponse); + rpc GetExecution(GetExecutionRequest) returns (StructResponse); + rpc ListProjectStatistics(ListProjectStatisticsRequest) returns (StructResponse); + rpc ListEvaluationExecutionsByDatapack(MutationRequest) returns (StructResponse); + rpc ListEvaluationExecutionsByDataset(MutationRequest) returns (StructResponse); + rpc GetTask(GetTaskRequest) returns (StructResponse); + rpc PollTaskLogs(PollTaskLogsRequest) returns (StructResponse); + rpc ListTasks(ListTasksRequest) returns (StructResponse); + rpc GetTrace(GetTraceRequest) returns (StructResponse); + rpc ListTraces(ListTracesRequest) returns (StructResponse); + rpc GetGroupStats(GetGroupStatsRequest) returns (StructResponse); + rpc GetTraceStreamState(GetTraceStreamStateRequest) returns (StructResponse); + rpc ReadTraceStreamMessages(ReadStreamMessagesRequest) returns (StructResponse); + rpc GetGroupStreamState(GetGroupStreamStateRequest) returns (StructResponse); + rpc ReadGroupStreamMessages(ReadStreamMessagesRequest) returns (StructResponse); + rpc ReadNotificationStreamMessages(ReadStreamMessagesRequest) returns (StructResponse); + rpc ListDeadLetterTasks(ListDeadLetterTasksRequest) returns (StructResponse); + rpc RetryTask(RetryTaskRequest) returns (RetryTaskResponse); +} + +message PingRequest {} + +message PingResponse { + string service = 1; + string app_id = 2; + string status = 3; + int64 timestamp_unix = 4; +} + +message SubmitExecutionRequest { + string group_id = 1; + int64 user_id = 2; + google.protobuf.Struct body = 10; +} + +message SubmitExecutionResponse { + string group_id = 1; + repeated SubmittedExecutionItem items = 2; +} + +message SubmittedExecutionItem { + int64 index = 1; + string trace_id = 2; + string task_id = 3; + int64 algorithm_id = 4; + int64 algorithm_version_id = 5; + int64 datapack_id = 6; + int64 dataset_id = 7; + bool has_datapack_id = 8; + bool has_dataset_id = 9; +} + +message SubmitFaultInjectionRequest { + string group_id = 1; + int64 user_id = 2; + int64 project_id = 3; + google.protobuf.Struct body = 10; +} + +message SubmitFaultInjectionResponse { + string group_id = 1; + repeated SubmittedInjectionItem items = 2; + int64 original_count = 3; + InjectionWarnings warnings = 4; +} + +message SubmittedInjectionItem { + int64 index = 1; + string trace_id = 2; + string task_id = 3; +} + +message InjectionWarnings { + repeated string duplicate_services_in_batch = 1; + repeated int64 duplicate_batches_in_request = 2; + repeated int64 batches_exist_in_database = 3; +} + +message SubmitDatapackBuildingRequest { + string group_id = 1; + int64 user_id = 2; + int64 project_id = 3; + google.protobuf.Struct body = 10; +} + +message SubmitDatapackBuildingResponse { + string group_id = 1; + repeated SubmittedBuildingItem items = 2; +} + +message SubmittedBuildingItem { + int64 index = 1; + string trace_id = 2; + string task_id = 3; +} + +message CancelTaskRequest { + string task_id = 1; +} + +message CancelTaskResponse { + bool cancelled = 1; +} + +message MutationRequest { + google.protobuf.Struct body = 1; +} + +message GetExecutionRequest { + int64 execution_id = 1; +} + +message ListProjectStatisticsRequest { + repeated int64 project_ids = 1; +} + +message GetTaskRequest { + string task_id = 1; +} + +message PollTaskLogsRequest { + string task_id = 1; + int64 after_unix_nano = 2; +} + +message ListTasksRequest { + google.protobuf.Struct query = 1; +} + +message GetTraceRequest { + string trace_id = 1; +} + +message ListTracesRequest { + google.protobuf.Struct query = 1; +} + +message GetGroupStatsRequest { + string group_id = 1; +} + +message GetTraceStreamStateRequest { + string trace_id = 1; +} + +message GetGroupStreamStateRequest { + string group_id = 1; +} + +message ReadStreamMessagesRequest { + string stream_key = 1; + string last_id = 2; + int64 count = 3; + int64 block_millis = 4; +} + +message ListDeadLetterTasksRequest { + int64 limit = 1; +} + +message RetryTaskRequest { + string task_id = 1; +} + +message RetryTaskResponse { + bool accepted = 1; + string queue = 2; +} + +message StructResponse { + google.protobuf.Struct data = 1; +} diff --git a/src/proto/orchestrator/v1/orchestrator_grpc.pb.go b/src/proto/orchestrator/v1/orchestrator_grpc.pb.go new file mode 100644 index 00000000..ea1fc5b5 --- /dev/null +++ b/src/proto/orchestrator/v1/orchestrator_grpc.pb.go @@ -0,0 +1,1185 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v5.29.3 +// source: proto/orchestrator/v1/orchestrator.proto + +package orchestratorv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + OrchestratorService_Ping_FullMethodName = "/orchestrator.v1.OrchestratorService/Ping" + OrchestratorService_SubmitExecution_FullMethodName = "/orchestrator.v1.OrchestratorService/SubmitExecution" + OrchestratorService_SubmitFaultInjection_FullMethodName = "/orchestrator.v1.OrchestratorService/SubmitFaultInjection" + OrchestratorService_SubmitDatapackBuilding_FullMethodName = "/orchestrator.v1.OrchestratorService/SubmitDatapackBuilding" + OrchestratorService_CreateExecution_FullMethodName = "/orchestrator.v1.OrchestratorService/CreateExecution" + OrchestratorService_CreateInjection_FullMethodName = "/orchestrator.v1.OrchestratorService/CreateInjection" + OrchestratorService_UpdateExecutionState_FullMethodName = "/orchestrator.v1.OrchestratorService/UpdateExecutionState" + OrchestratorService_UpdateInjectionState_FullMethodName = "/orchestrator.v1.OrchestratorService/UpdateInjectionState" + OrchestratorService_UpdateInjectionTimestamps_FullMethodName = "/orchestrator.v1.OrchestratorService/UpdateInjectionTimestamps" + OrchestratorService_GetInjectionMetrics_FullMethodName = "/orchestrator.v1.OrchestratorService/GetInjectionMetrics" + OrchestratorService_GetExecutionMetrics_FullMethodName = "/orchestrator.v1.OrchestratorService/GetExecutionMetrics" + OrchestratorService_CancelTask_FullMethodName = "/orchestrator.v1.OrchestratorService/CancelTask" + OrchestratorService_GetExecution_FullMethodName = "/orchestrator.v1.OrchestratorService/GetExecution" + OrchestratorService_ListProjectStatistics_FullMethodName = "/orchestrator.v1.OrchestratorService/ListProjectStatistics" + OrchestratorService_ListEvaluationExecutionsByDatapack_FullMethodName = "/orchestrator.v1.OrchestratorService/ListEvaluationExecutionsByDatapack" + OrchestratorService_ListEvaluationExecutionsByDataset_FullMethodName = "/orchestrator.v1.OrchestratorService/ListEvaluationExecutionsByDataset" + OrchestratorService_GetTask_FullMethodName = "/orchestrator.v1.OrchestratorService/GetTask" + OrchestratorService_PollTaskLogs_FullMethodName = "/orchestrator.v1.OrchestratorService/PollTaskLogs" + OrchestratorService_ListTasks_FullMethodName = "/orchestrator.v1.OrchestratorService/ListTasks" + OrchestratorService_GetTrace_FullMethodName = "/orchestrator.v1.OrchestratorService/GetTrace" + OrchestratorService_ListTraces_FullMethodName = "/orchestrator.v1.OrchestratorService/ListTraces" + OrchestratorService_GetGroupStats_FullMethodName = "/orchestrator.v1.OrchestratorService/GetGroupStats" + OrchestratorService_GetTraceStreamState_FullMethodName = "/orchestrator.v1.OrchestratorService/GetTraceStreamState" + OrchestratorService_ReadTraceStreamMessages_FullMethodName = "/orchestrator.v1.OrchestratorService/ReadTraceStreamMessages" + OrchestratorService_GetGroupStreamState_FullMethodName = "/orchestrator.v1.OrchestratorService/GetGroupStreamState" + OrchestratorService_ReadGroupStreamMessages_FullMethodName = "/orchestrator.v1.OrchestratorService/ReadGroupStreamMessages" + OrchestratorService_ReadNotificationStreamMessages_FullMethodName = "/orchestrator.v1.OrchestratorService/ReadNotificationStreamMessages" + OrchestratorService_ListDeadLetterTasks_FullMethodName = "/orchestrator.v1.OrchestratorService/ListDeadLetterTasks" + OrchestratorService_RetryTask_FullMethodName = "/orchestrator.v1.OrchestratorService/RetryTask" +) + +// OrchestratorServiceClient is the client API for OrchestratorService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type OrchestratorServiceClient interface { + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + SubmitExecution(ctx context.Context, in *SubmitExecutionRequest, opts ...grpc.CallOption) (*SubmitExecutionResponse, error) + SubmitFaultInjection(ctx context.Context, in *SubmitFaultInjectionRequest, opts ...grpc.CallOption) (*SubmitFaultInjectionResponse, error) + SubmitDatapackBuilding(ctx context.Context, in *SubmitDatapackBuildingRequest, opts ...grpc.CallOption) (*SubmitDatapackBuildingResponse, error) + CreateExecution(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + CreateInjection(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateExecutionState(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateInjectionState(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + UpdateInjectionTimestamps(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetInjectionMetrics(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetExecutionMetrics(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + CancelTask(ctx context.Context, in *CancelTaskRequest, opts ...grpc.CallOption) (*CancelTaskResponse, error) + GetExecution(ctx context.Context, in *GetExecutionRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListProjectStatistics(ctx context.Context, in *ListProjectStatisticsRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListEvaluationExecutionsByDatapack(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListEvaluationExecutionsByDataset(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetTask(ctx context.Context, in *GetTaskRequest, opts ...grpc.CallOption) (*StructResponse, error) + PollTaskLogs(ctx context.Context, in *PollTaskLogsRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListTasks(ctx context.Context, in *ListTasksRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListTraces(ctx context.Context, in *ListTracesRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetGroupStats(ctx context.Context, in *GetGroupStatsRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetTraceStreamState(ctx context.Context, in *GetTraceStreamStateRequest, opts ...grpc.CallOption) (*StructResponse, error) + ReadTraceStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetGroupStreamState(ctx context.Context, in *GetGroupStreamStateRequest, opts ...grpc.CallOption) (*StructResponse, error) + ReadGroupStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) + ReadNotificationStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListDeadLetterTasks(ctx context.Context, in *ListDeadLetterTasksRequest, opts ...grpc.CallOption) (*StructResponse, error) + RetryTask(ctx context.Context, in *RetryTaskRequest, opts ...grpc.CallOption) (*RetryTaskResponse, error) +} + +type orchestratorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewOrchestratorServiceClient(cc grpc.ClientConnInterface) OrchestratorServiceClient { + return &orchestratorServiceClient{cc} +} + +func (c *orchestratorServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PingResponse) + err := c.cc.Invoke(ctx, OrchestratorService_Ping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) SubmitExecution(ctx context.Context, in *SubmitExecutionRequest, opts ...grpc.CallOption) (*SubmitExecutionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubmitExecutionResponse) + err := c.cc.Invoke(ctx, OrchestratorService_SubmitExecution_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) SubmitFaultInjection(ctx context.Context, in *SubmitFaultInjectionRequest, opts ...grpc.CallOption) (*SubmitFaultInjectionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubmitFaultInjectionResponse) + err := c.cc.Invoke(ctx, OrchestratorService_SubmitFaultInjection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) SubmitDatapackBuilding(ctx context.Context, in *SubmitDatapackBuildingRequest, opts ...grpc.CallOption) (*SubmitDatapackBuildingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SubmitDatapackBuildingResponse) + err := c.cc.Invoke(ctx, OrchestratorService_SubmitDatapackBuilding_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) CreateExecution(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_CreateExecution_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) CreateInjection(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_CreateInjection_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) UpdateExecutionState(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_UpdateExecutionState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) UpdateInjectionState(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_UpdateInjectionState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) UpdateInjectionTimestamps(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_UpdateInjectionTimestamps_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetInjectionMetrics(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetInjectionMetrics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetExecutionMetrics(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetExecutionMetrics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) CancelTask(ctx context.Context, in *CancelTaskRequest, opts ...grpc.CallOption) (*CancelTaskResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CancelTaskResponse) + err := c.cc.Invoke(ctx, OrchestratorService_CancelTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetExecution(ctx context.Context, in *GetExecutionRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetExecution_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListProjectStatistics(ctx context.Context, in *ListProjectStatisticsRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListProjectStatistics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListEvaluationExecutionsByDatapack(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListEvaluationExecutionsByDatapack_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListEvaluationExecutionsByDataset(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListEvaluationExecutionsByDataset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetTask(ctx context.Context, in *GetTaskRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) PollTaskLogs(ctx context.Context, in *PollTaskLogsRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_PollTaskLogs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListTasks(ctx context.Context, in *ListTasksRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetTrace(ctx context.Context, in *GetTraceRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetTrace_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListTraces(ctx context.Context, in *ListTracesRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListTraces_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetGroupStats(ctx context.Context, in *GetGroupStatsRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetGroupStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetTraceStreamState(ctx context.Context, in *GetTraceStreamStateRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetTraceStreamState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ReadTraceStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ReadTraceStreamMessages_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) GetGroupStreamState(ctx context.Context, in *GetGroupStreamStateRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_GetGroupStreamState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ReadGroupStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ReadGroupStreamMessages_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ReadNotificationStreamMessages(ctx context.Context, in *ReadStreamMessagesRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ReadNotificationStreamMessages_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) ListDeadLetterTasks(ctx context.Context, in *ListDeadLetterTasksRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, OrchestratorService_ListDeadLetterTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *orchestratorServiceClient) RetryTask(ctx context.Context, in *RetryTaskRequest, opts ...grpc.CallOption) (*RetryTaskResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RetryTaskResponse) + err := c.cc.Invoke(ctx, OrchestratorService_RetryTask_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// OrchestratorServiceServer is the server API for OrchestratorService service. +// All implementations must embed UnimplementedOrchestratorServiceServer +// for forward compatibility. +type OrchestratorServiceServer interface { + Ping(context.Context, *PingRequest) (*PingResponse, error) + SubmitExecution(context.Context, *SubmitExecutionRequest) (*SubmitExecutionResponse, error) + SubmitFaultInjection(context.Context, *SubmitFaultInjectionRequest) (*SubmitFaultInjectionResponse, error) + SubmitDatapackBuilding(context.Context, *SubmitDatapackBuildingRequest) (*SubmitDatapackBuildingResponse, error) + CreateExecution(context.Context, *MutationRequest) (*StructResponse, error) + CreateInjection(context.Context, *MutationRequest) (*StructResponse, error) + UpdateExecutionState(context.Context, *MutationRequest) (*StructResponse, error) + UpdateInjectionState(context.Context, *MutationRequest) (*StructResponse, error) + UpdateInjectionTimestamps(context.Context, *MutationRequest) (*StructResponse, error) + GetInjectionMetrics(context.Context, *MutationRequest) (*StructResponse, error) + GetExecutionMetrics(context.Context, *MutationRequest) (*StructResponse, error) + CancelTask(context.Context, *CancelTaskRequest) (*CancelTaskResponse, error) + GetExecution(context.Context, *GetExecutionRequest) (*StructResponse, error) + ListProjectStatistics(context.Context, *ListProjectStatisticsRequest) (*StructResponse, error) + ListEvaluationExecutionsByDatapack(context.Context, *MutationRequest) (*StructResponse, error) + ListEvaluationExecutionsByDataset(context.Context, *MutationRequest) (*StructResponse, error) + GetTask(context.Context, *GetTaskRequest) (*StructResponse, error) + PollTaskLogs(context.Context, *PollTaskLogsRequest) (*StructResponse, error) + ListTasks(context.Context, *ListTasksRequest) (*StructResponse, error) + GetTrace(context.Context, *GetTraceRequest) (*StructResponse, error) + ListTraces(context.Context, *ListTracesRequest) (*StructResponse, error) + GetGroupStats(context.Context, *GetGroupStatsRequest) (*StructResponse, error) + GetTraceStreamState(context.Context, *GetTraceStreamStateRequest) (*StructResponse, error) + ReadTraceStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) + GetGroupStreamState(context.Context, *GetGroupStreamStateRequest) (*StructResponse, error) + ReadGroupStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) + ReadNotificationStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) + ListDeadLetterTasks(context.Context, *ListDeadLetterTasksRequest) (*StructResponse, error) + RetryTask(context.Context, *RetryTaskRequest) (*RetryTaskResponse, error) + mustEmbedUnimplementedOrchestratorServiceServer() +} + +// UnimplementedOrchestratorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedOrchestratorServiceServer struct{} + +func (UnimplementedOrchestratorServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedOrchestratorServiceServer) SubmitExecution(context.Context, *SubmitExecutionRequest) (*SubmitExecutionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitExecution not implemented") +} +func (UnimplementedOrchestratorServiceServer) SubmitFaultInjection(context.Context, *SubmitFaultInjectionRequest) (*SubmitFaultInjectionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitFaultInjection not implemented") +} +func (UnimplementedOrchestratorServiceServer) SubmitDatapackBuilding(context.Context, *SubmitDatapackBuildingRequest) (*SubmitDatapackBuildingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SubmitDatapackBuilding not implemented") +} +func (UnimplementedOrchestratorServiceServer) CreateExecution(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateExecution not implemented") +} +func (UnimplementedOrchestratorServiceServer) CreateInjection(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateInjection not implemented") +} +func (UnimplementedOrchestratorServiceServer) UpdateExecutionState(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateExecutionState not implemented") +} +func (UnimplementedOrchestratorServiceServer) UpdateInjectionState(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateInjectionState not implemented") +} +func (UnimplementedOrchestratorServiceServer) UpdateInjectionTimestamps(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateInjectionTimestamps not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetInjectionMetrics(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetInjectionMetrics not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetExecutionMetrics(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetExecutionMetrics not implemented") +} +func (UnimplementedOrchestratorServiceServer) CancelTask(context.Context, *CancelTaskRequest) (*CancelTaskResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CancelTask not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetExecution(context.Context, *GetExecutionRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetExecution not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListProjectStatistics(context.Context, *ListProjectStatisticsRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListProjectStatistics not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListEvaluationExecutionsByDatapack(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListEvaluationExecutionsByDatapack not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListEvaluationExecutionsByDataset(context.Context, *MutationRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListEvaluationExecutionsByDataset not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetTask(context.Context, *GetTaskRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetTask not implemented") +} +func (UnimplementedOrchestratorServiceServer) PollTaskLogs(context.Context, *PollTaskLogsRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method PollTaskLogs not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListTasks(context.Context, *ListTasksRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTasks not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetTrace(context.Context, *GetTraceRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetTrace not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListTraces(context.Context, *ListTracesRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListTraces not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetGroupStats(context.Context, *GetGroupStatsRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetGroupStats not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetTraceStreamState(context.Context, *GetTraceStreamStateRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetTraceStreamState not implemented") +} +func (UnimplementedOrchestratorServiceServer) ReadTraceStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadTraceStreamMessages not implemented") +} +func (UnimplementedOrchestratorServiceServer) GetGroupStreamState(context.Context, *GetGroupStreamStateRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetGroupStreamState not implemented") +} +func (UnimplementedOrchestratorServiceServer) ReadGroupStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadGroupStreamMessages not implemented") +} +func (UnimplementedOrchestratorServiceServer) ReadNotificationStreamMessages(context.Context, *ReadStreamMessagesRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ReadNotificationStreamMessages not implemented") +} +func (UnimplementedOrchestratorServiceServer) ListDeadLetterTasks(context.Context, *ListDeadLetterTasksRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDeadLetterTasks not implemented") +} +func (UnimplementedOrchestratorServiceServer) RetryTask(context.Context, *RetryTaskRequest) (*RetryTaskResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RetryTask not implemented") +} +func (UnimplementedOrchestratorServiceServer) mustEmbedUnimplementedOrchestratorServiceServer() {} +func (UnimplementedOrchestratorServiceServer) testEmbeddedByValue() {} + +// UnsafeOrchestratorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to OrchestratorServiceServer will +// result in compilation errors. +type UnsafeOrchestratorServiceServer interface { + mustEmbedUnimplementedOrchestratorServiceServer() +} + +func RegisterOrchestratorServiceServer(s grpc.ServiceRegistrar, srv OrchestratorServiceServer) { + // If the following call panics, it indicates UnimplementedOrchestratorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&OrchestratorService_ServiceDesc, srv) +} + +func _OrchestratorService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).Ping(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_SubmitExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubmitExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).SubmitExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_SubmitExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).SubmitExecution(ctx, req.(*SubmitExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_SubmitFaultInjection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubmitFaultInjectionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).SubmitFaultInjection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_SubmitFaultInjection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).SubmitFaultInjection(ctx, req.(*SubmitFaultInjectionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_SubmitDatapackBuilding_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubmitDatapackBuildingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).SubmitDatapackBuilding(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_SubmitDatapackBuilding_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).SubmitDatapackBuilding(ctx, req.(*SubmitDatapackBuildingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_CreateExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).CreateExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_CreateExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).CreateExecution(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_CreateInjection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).CreateInjection(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_CreateInjection_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).CreateInjection(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_UpdateExecutionState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).UpdateExecutionState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_UpdateExecutionState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).UpdateExecutionState(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_UpdateInjectionState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).UpdateInjectionState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_UpdateInjectionState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).UpdateInjectionState(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_UpdateInjectionTimestamps_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).UpdateInjectionTimestamps(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_UpdateInjectionTimestamps_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).UpdateInjectionTimestamps(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetInjectionMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetInjectionMetrics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetInjectionMetrics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetInjectionMetrics(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetExecutionMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetExecutionMetrics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetExecutionMetrics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetExecutionMetrics(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_CancelTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CancelTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).CancelTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_CancelTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).CancelTask(ctx, req.(*CancelTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetExecution(ctx, req.(*GetExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListProjectStatistics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProjectStatisticsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListProjectStatistics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListProjectStatistics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListProjectStatistics(ctx, req.(*ListProjectStatisticsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListEvaluationExecutionsByDatapack_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListEvaluationExecutionsByDatapack(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListEvaluationExecutionsByDatapack_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListEvaluationExecutionsByDatapack(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListEvaluationExecutionsByDataset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListEvaluationExecutionsByDataset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListEvaluationExecutionsByDataset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListEvaluationExecutionsByDataset(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetTask(ctx, req.(*GetTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_PollTaskLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollTaskLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).PollTaskLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_PollTaskLogs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).PollTaskLogs(ctx, req.(*PollTaskLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTasksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListTasks(ctx, req.(*ListTasksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetTrace_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTraceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetTrace(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetTrace_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetTrace(ctx, req.(*GetTraceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListTraces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListTracesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListTraces(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListTraces_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListTraces(ctx, req.(*ListTracesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetGroupStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGroupStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetGroupStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetGroupStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetGroupStats(ctx, req.(*GetGroupStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetTraceStreamState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTraceStreamStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetTraceStreamState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetTraceStreamState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetTraceStreamState(ctx, req.(*GetTraceStreamStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ReadTraceStreamMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadStreamMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ReadTraceStreamMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ReadTraceStreamMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ReadTraceStreamMessages(ctx, req.(*ReadStreamMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_GetGroupStreamState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGroupStreamStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).GetGroupStreamState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_GetGroupStreamState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).GetGroupStreamState(ctx, req.(*GetGroupStreamStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ReadGroupStreamMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadStreamMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ReadGroupStreamMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ReadGroupStreamMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ReadGroupStreamMessages(ctx, req.(*ReadStreamMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ReadNotificationStreamMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReadStreamMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ReadNotificationStreamMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ReadNotificationStreamMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ReadNotificationStreamMessages(ctx, req.(*ReadStreamMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_ListDeadLetterTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDeadLetterTasksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).ListDeadLetterTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_ListDeadLetterTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).ListDeadLetterTasks(ctx, req.(*ListDeadLetterTasksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _OrchestratorService_RetryTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RetryTaskRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(OrchestratorServiceServer).RetryTask(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: OrchestratorService_RetryTask_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(OrchestratorServiceServer).RetryTask(ctx, req.(*RetryTaskRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// OrchestratorService_ServiceDesc is the grpc.ServiceDesc for OrchestratorService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var OrchestratorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "orchestrator.v1.OrchestratorService", + HandlerType: (*OrchestratorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _OrchestratorService_Ping_Handler, + }, + { + MethodName: "SubmitExecution", + Handler: _OrchestratorService_SubmitExecution_Handler, + }, + { + MethodName: "SubmitFaultInjection", + Handler: _OrchestratorService_SubmitFaultInjection_Handler, + }, + { + MethodName: "SubmitDatapackBuilding", + Handler: _OrchestratorService_SubmitDatapackBuilding_Handler, + }, + { + MethodName: "CreateExecution", + Handler: _OrchestratorService_CreateExecution_Handler, + }, + { + MethodName: "CreateInjection", + Handler: _OrchestratorService_CreateInjection_Handler, + }, + { + MethodName: "UpdateExecutionState", + Handler: _OrchestratorService_UpdateExecutionState_Handler, + }, + { + MethodName: "UpdateInjectionState", + Handler: _OrchestratorService_UpdateInjectionState_Handler, + }, + { + MethodName: "UpdateInjectionTimestamps", + Handler: _OrchestratorService_UpdateInjectionTimestamps_Handler, + }, + { + MethodName: "GetInjectionMetrics", + Handler: _OrchestratorService_GetInjectionMetrics_Handler, + }, + { + MethodName: "GetExecutionMetrics", + Handler: _OrchestratorService_GetExecutionMetrics_Handler, + }, + { + MethodName: "CancelTask", + Handler: _OrchestratorService_CancelTask_Handler, + }, + { + MethodName: "GetExecution", + Handler: _OrchestratorService_GetExecution_Handler, + }, + { + MethodName: "ListProjectStatistics", + Handler: _OrchestratorService_ListProjectStatistics_Handler, + }, + { + MethodName: "ListEvaluationExecutionsByDatapack", + Handler: _OrchestratorService_ListEvaluationExecutionsByDatapack_Handler, + }, + { + MethodName: "ListEvaluationExecutionsByDataset", + Handler: _OrchestratorService_ListEvaluationExecutionsByDataset_Handler, + }, + { + MethodName: "GetTask", + Handler: _OrchestratorService_GetTask_Handler, + }, + { + MethodName: "PollTaskLogs", + Handler: _OrchestratorService_PollTaskLogs_Handler, + }, + { + MethodName: "ListTasks", + Handler: _OrchestratorService_ListTasks_Handler, + }, + { + MethodName: "GetTrace", + Handler: _OrchestratorService_GetTrace_Handler, + }, + { + MethodName: "ListTraces", + Handler: _OrchestratorService_ListTraces_Handler, + }, + { + MethodName: "GetGroupStats", + Handler: _OrchestratorService_GetGroupStats_Handler, + }, + { + MethodName: "GetTraceStreamState", + Handler: _OrchestratorService_GetTraceStreamState_Handler, + }, + { + MethodName: "ReadTraceStreamMessages", + Handler: _OrchestratorService_ReadTraceStreamMessages_Handler, + }, + { + MethodName: "GetGroupStreamState", + Handler: _OrchestratorService_GetGroupStreamState_Handler, + }, + { + MethodName: "ReadGroupStreamMessages", + Handler: _OrchestratorService_ReadGroupStreamMessages_Handler, + }, + { + MethodName: "ReadNotificationStreamMessages", + Handler: _OrchestratorService_ReadNotificationStreamMessages_Handler, + }, + { + MethodName: "ListDeadLetterTasks", + Handler: _OrchestratorService_ListDeadLetterTasks_Handler, + }, + { + MethodName: "RetryTask", + Handler: _OrchestratorService_RetryTask_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/orchestrator/v1/orchestrator.proto", +} diff --git a/src/proto/resource/v1/resource.pb.go b/src/proto/resource/v1/resource.pb.go new file mode 100644 index 00000000..67722dd0 --- /dev/null +++ b/src/proto/resource/v1/resource.pb.go @@ -0,0 +1,976 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/resource/v1/resource.proto + +package resourcev1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{0} +} + +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + AppId string `protobuf:"bytes,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + TimestampUnix int64 `protobuf:"varint,4,opt,name=timestamp_unix,json=timestampUnix,proto3" json:"timestamp_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *PingResponse) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *PingResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PingResponse) GetTimestampUnix() int64 { + if x != nil { + return x.TimestampUnix + } + return 0 +} + +type ListProjectsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListProjectsRequest) Reset() { + *x = ListProjectsRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListProjectsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListProjectsRequest) ProtoMessage() {} + +func (x *ListProjectsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListProjectsRequest.ProtoReflect.Descriptor instead. +func (*ListProjectsRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{2} +} + +func (x *ListProjectsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type ListContainersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListContainersRequest) Reset() { + *x = ListContainersRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListContainersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListContainersRequest) ProtoMessage() {} + +func (x *ListContainersRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListContainersRequest.ProtoReflect.Descriptor instead. +func (*ListContainersRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{3} +} + +func (x *ListContainersRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type ListDatasetsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDatasetsRequest) Reset() { + *x = ListDatasetsRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDatasetsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDatasetsRequest) ProtoMessage() {} + +func (x *ListDatasetsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDatasetsRequest.ProtoReflect.Descriptor instead. +func (*ListDatasetsRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{4} +} + +func (x *ListDatasetsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type ListDatapackEvaluationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDatapackEvaluationsRequest) Reset() { + *x = ListDatapackEvaluationsRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDatapackEvaluationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDatapackEvaluationsRequest) ProtoMessage() {} + +func (x *ListDatapackEvaluationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDatapackEvaluationsRequest.ProtoReflect.Descriptor instead. +func (*ListDatapackEvaluationsRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{5} +} + +func (x *ListDatapackEvaluationsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +func (x *ListDatapackEvaluationsRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +type ListDatasetEvaluationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDatasetEvaluationsRequest) Reset() { + *x = ListDatasetEvaluationsRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDatasetEvaluationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDatasetEvaluationsRequest) ProtoMessage() {} + +func (x *ListDatasetEvaluationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDatasetEvaluationsRequest.ProtoReflect.Descriptor instead. +func (*ListDatasetEvaluationsRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{6} +} + +func (x *ListDatasetEvaluationsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +func (x *ListDatasetEvaluationsRequest) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +type ListEvaluationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListEvaluationsRequest) Reset() { + *x = ListEvaluationsRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListEvaluationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListEvaluationsRequest) ProtoMessage() {} + +func (x *ListEvaluationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListEvaluationsRequest.ProtoReflect.Descriptor instead. +func (*ListEvaluationsRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{7} +} + +func (x *ListEvaluationsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type MutationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Body *structpb.Struct `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MutationRequest) Reset() { + *x = MutationRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MutationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MutationRequest) ProtoMessage() {} + +func (x *MutationRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MutationRequest.ProtoReflect.Descriptor instead. +func (*MutationRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{8} +} + +func (x *MutationRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type QueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueryRequest) Reset() { + *x = QueryRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRequest) ProtoMessage() {} + +func (x *QueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueryRequest.ProtoReflect.Descriptor instead. +func (*QueryRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{9} +} + +func (x *QueryRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type GetResourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceRequest) Reset() { + *x = GetResourceRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceRequest) ProtoMessage() {} + +func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceRequest.ProtoReflect.Descriptor instead. +func (*GetResourceRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{10} +} + +func (x *GetResourceRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type UpdateByIDRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Body *structpb.Struct `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateByIDRequest) Reset() { + *x = UpdateByIDRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateByIDRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateByIDRequest) ProtoMessage() {} + +func (x *UpdateByIDRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateByIDRequest.ProtoReflect.Descriptor instead. +func (*UpdateByIDRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{11} +} + +func (x *UpdateByIDRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdateByIDRequest) GetBody() *structpb.Struct { + if x != nil { + return x.Body + } + return nil +} + +type BatchDeleteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ids []int64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchDeleteRequest) Reset() { + *x = BatchDeleteRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchDeleteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchDeleteRequest) ProtoMessage() {} + +func (x *BatchDeleteRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchDeleteRequest.ProtoReflect.Descriptor instead. +func (*BatchDeleteRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{12} +} + +func (x *BatchDeleteRequest) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + +type IDQueryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Query *structpb.Struct `protobuf:"bytes,2,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IDQueryRequest) Reset() { + *x = IDQueryRequest{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IDQueryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IDQueryRequest) ProtoMessage() {} + +func (x *IDQueryRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IDQueryRequest.ProtoReflect.Descriptor instead. +func (*IDQueryRequest) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{13} +} + +func (x *IDQueryRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IDQueryRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type ResourceItemResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceItemResponse) Reset() { + *x = ResourceItemResponse{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceItemResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceItemResponse) ProtoMessage() {} + +func (x *ResourceItemResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceItemResponse.ProtoReflect.Descriptor instead. +func (*ResourceItemResponse) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{14} +} + +func (x *ResourceItemResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +type ResourceListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceListResponse) Reset() { + *x = ResourceListResponse{} + mi := &file_proto_resource_v1_resource_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceListResponse) ProtoMessage() {} + +func (x *ResourceListResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_resource_v1_resource_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceListResponse.ProtoReflect.Descriptor instead. +func (*ResourceListResponse) Descriptor() ([]byte, []int) { + return file_proto_resource_v1_resource_proto_rawDescGZIP(), []int{15} +} + +func (x *ResourceListResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +var File_proto_resource_v1_resource_proto protoreflect.FileDescriptor + +const file_proto_resource_v1_resource_proto_rawDesc = "" + + "\n" + + " proto/resource/v1/resource.proto\x12\vresource.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\"\r\n" + + "\vPingRequest\"~\n" + + "\fPingResponse\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x15\n" + + "\x06app_id\x18\x02 \x01(\tR\x05appId\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12%\n" + + "\x0etimestamp_unix\x18\x04 \x01(\x03R\rtimestampUnix\"D\n" + + "\x13ListProjectsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"F\n" + + "\x15ListContainersRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"D\n" + + "\x13ListDatasetsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"h\n" + + "\x1eListDatapackEvaluationsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x03R\x06userId\"g\n" + + "\x1dListDatasetEvaluationsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\x12\x17\n" + + "\auser_id\x18\x02 \x01(\x03R\x06userId\"G\n" + + "\x16ListEvaluationsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\">\n" + + "\x0fMutationRequest\x12+\n" + + "\x04body\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04body\"=\n" + + "\fQueryRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"$\n" + + "\x12GetResourceRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"P\n" + + "\x11UpdateByIDRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12+\n" + + "\x04body\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x04body\"&\n" + + "\x12BatchDeleteRequest\x12\x10\n" + + "\x03ids\x18\x01 \x03(\x03R\x03ids\"O\n" + + "\x0eIDQueryRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12-\n" + + "\x05query\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x05query\"C\n" + + "\x14ResourceItemResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data\"C\n" + + "\x14ResourceListResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xce\x10\n" + + "\x0fResourceService\x12;\n" + + "\x04Ping\x12\x18.resource.v1.PingRequest\x1a\x19.resource.v1.PingResponse\x12S\n" + + "\fListProjects\x12 .resource.v1.ListProjectsRequest\x1a!.resource.v1.ResourceListResponse\x12P\n" + + "\n" + + "GetProject\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12W\n" + + "\x0eListContainers\x12\".resource.v1.ListContainersRequest\x1a!.resource.v1.ResourceListResponse\x12R\n" + + "\fGetContainer\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12S\n" + + "\fListDatasets\x12 .resource.v1.ListDatasetsRequest\x1a!.resource.v1.ResourceListResponse\x12P\n" + + "\n" + + "GetDataset\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12N\n" + + "\vCreateLabel\x12\x1c.resource.v1.MutationRequest\x1a!.resource.v1.ResourceItemResponse\x12N\n" + + "\bGetLabel\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12J\n" + + "\n" + + "ListLabels\x12\x19.resource.v1.QueryRequest\x1a!.resource.v1.ResourceListResponse\x12P\n" + + "\vUpdateLabel\x12\x1e.resource.v1.UpdateByIDRequest\x1a!.resource.v1.ResourceItemResponse\x12F\n" + + "\vDeleteLabel\x12\x1f.resource.v1.GetResourceRequest\x1a\x16.google.protobuf.Empty\x12L\n" + + "\x11BatchDeleteLabels\x12\x1f.resource.v1.BatchDeleteRequest\x1a\x16.google.protobuf.Empty\x12P\n" + + "\x10ListChaosSystems\x12\x19.resource.v1.QueryRequest\x1a!.resource.v1.ResourceListResponse\x12T\n" + + "\x0eGetChaosSystem\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12T\n" + + "\x11CreateChaosSystem\x12\x1c.resource.v1.MutationRequest\x1a!.resource.v1.ResourceItemResponse\x12V\n" + + "\x11UpdateChaosSystem\x12\x1e.resource.v1.UpdateByIDRequest\x1a!.resource.v1.ResourceItemResponse\x12L\n" + + "\x11DeleteChaosSystem\x12\x1f.resource.v1.GetResourceRequest\x1a\x16.google.protobuf.Empty\x12S\n" + + "\x19UpsertChaosSystemMetadata\x12\x1e.resource.v1.UpdateByIDRequest\x1a\x16.google.protobuf.Empty\x12Y\n" + + "\x17ListChaosSystemMetadata\x12\x1b.resource.v1.IDQueryRequest\x1a!.resource.v1.ResourceItemResponse\x12o\n" + + "\x1dListDatapackEvaluationResults\x12+.resource.v1.ListDatapackEvaluationsRequest\x1a!.resource.v1.ResourceItemResponse\x12m\n" + + "\x1cListDatasetEvaluationResults\x12*.resource.v1.ListDatasetEvaluationsRequest\x1a!.resource.v1.ResourceItemResponse\x12Y\n" + + "\x0fListEvaluations\x12#.resource.v1.ListEvaluationsRequest\x1a!.resource.v1.ResourceListResponse\x12S\n" + + "\rGetEvaluation\x12\x1f.resource.v1.GetResourceRequest\x1a!.resource.v1.ResourceItemResponse\x12K\n" + + "\x10DeleteEvaluation\x12\x1f.resource.v1.GetResourceRequest\x1a\x16.google.protobuf.EmptyB$Z\"aegis/proto/resource/v1;resourcev1b\x06proto3" + +var ( + file_proto_resource_v1_resource_proto_rawDescOnce sync.Once + file_proto_resource_v1_resource_proto_rawDescData []byte +) + +func file_proto_resource_v1_resource_proto_rawDescGZIP() []byte { + file_proto_resource_v1_resource_proto_rawDescOnce.Do(func() { + file_proto_resource_v1_resource_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_resource_v1_resource_proto_rawDesc), len(file_proto_resource_v1_resource_proto_rawDesc))) + }) + return file_proto_resource_v1_resource_proto_rawDescData +} + +var file_proto_resource_v1_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_proto_resource_v1_resource_proto_goTypes = []any{ + (*PingRequest)(nil), // 0: resource.v1.PingRequest + (*PingResponse)(nil), // 1: resource.v1.PingResponse + (*ListProjectsRequest)(nil), // 2: resource.v1.ListProjectsRequest + (*ListContainersRequest)(nil), // 3: resource.v1.ListContainersRequest + (*ListDatasetsRequest)(nil), // 4: resource.v1.ListDatasetsRequest + (*ListDatapackEvaluationsRequest)(nil), // 5: resource.v1.ListDatapackEvaluationsRequest + (*ListDatasetEvaluationsRequest)(nil), // 6: resource.v1.ListDatasetEvaluationsRequest + (*ListEvaluationsRequest)(nil), // 7: resource.v1.ListEvaluationsRequest + (*MutationRequest)(nil), // 8: resource.v1.MutationRequest + (*QueryRequest)(nil), // 9: resource.v1.QueryRequest + (*GetResourceRequest)(nil), // 10: resource.v1.GetResourceRequest + (*UpdateByIDRequest)(nil), // 11: resource.v1.UpdateByIDRequest + (*BatchDeleteRequest)(nil), // 12: resource.v1.BatchDeleteRequest + (*IDQueryRequest)(nil), // 13: resource.v1.IDQueryRequest + (*ResourceItemResponse)(nil), // 14: resource.v1.ResourceItemResponse + (*ResourceListResponse)(nil), // 15: resource.v1.ResourceListResponse + (*structpb.Struct)(nil), // 16: google.protobuf.Struct + (*emptypb.Empty)(nil), // 17: google.protobuf.Empty +} +var file_proto_resource_v1_resource_proto_depIdxs = []int32{ + 16, // 0: resource.v1.ListProjectsRequest.query:type_name -> google.protobuf.Struct + 16, // 1: resource.v1.ListContainersRequest.query:type_name -> google.protobuf.Struct + 16, // 2: resource.v1.ListDatasetsRequest.query:type_name -> google.protobuf.Struct + 16, // 3: resource.v1.ListDatapackEvaluationsRequest.query:type_name -> google.protobuf.Struct + 16, // 4: resource.v1.ListDatasetEvaluationsRequest.query:type_name -> google.protobuf.Struct + 16, // 5: resource.v1.ListEvaluationsRequest.query:type_name -> google.protobuf.Struct + 16, // 6: resource.v1.MutationRequest.body:type_name -> google.protobuf.Struct + 16, // 7: resource.v1.QueryRequest.query:type_name -> google.protobuf.Struct + 16, // 8: resource.v1.UpdateByIDRequest.body:type_name -> google.protobuf.Struct + 16, // 9: resource.v1.IDQueryRequest.query:type_name -> google.protobuf.Struct + 16, // 10: resource.v1.ResourceItemResponse.data:type_name -> google.protobuf.Struct + 16, // 11: resource.v1.ResourceListResponse.data:type_name -> google.protobuf.Struct + 0, // 12: resource.v1.ResourceService.Ping:input_type -> resource.v1.PingRequest + 2, // 13: resource.v1.ResourceService.ListProjects:input_type -> resource.v1.ListProjectsRequest + 10, // 14: resource.v1.ResourceService.GetProject:input_type -> resource.v1.GetResourceRequest + 3, // 15: resource.v1.ResourceService.ListContainers:input_type -> resource.v1.ListContainersRequest + 10, // 16: resource.v1.ResourceService.GetContainer:input_type -> resource.v1.GetResourceRequest + 4, // 17: resource.v1.ResourceService.ListDatasets:input_type -> resource.v1.ListDatasetsRequest + 10, // 18: resource.v1.ResourceService.GetDataset:input_type -> resource.v1.GetResourceRequest + 8, // 19: resource.v1.ResourceService.CreateLabel:input_type -> resource.v1.MutationRequest + 10, // 20: resource.v1.ResourceService.GetLabel:input_type -> resource.v1.GetResourceRequest + 9, // 21: resource.v1.ResourceService.ListLabels:input_type -> resource.v1.QueryRequest + 11, // 22: resource.v1.ResourceService.UpdateLabel:input_type -> resource.v1.UpdateByIDRequest + 10, // 23: resource.v1.ResourceService.DeleteLabel:input_type -> resource.v1.GetResourceRequest + 12, // 24: resource.v1.ResourceService.BatchDeleteLabels:input_type -> resource.v1.BatchDeleteRequest + 9, // 25: resource.v1.ResourceService.ListChaosSystems:input_type -> resource.v1.QueryRequest + 10, // 26: resource.v1.ResourceService.GetChaosSystem:input_type -> resource.v1.GetResourceRequest + 8, // 27: resource.v1.ResourceService.CreateChaosSystem:input_type -> resource.v1.MutationRequest + 11, // 28: resource.v1.ResourceService.UpdateChaosSystem:input_type -> resource.v1.UpdateByIDRequest + 10, // 29: resource.v1.ResourceService.DeleteChaosSystem:input_type -> resource.v1.GetResourceRequest + 11, // 30: resource.v1.ResourceService.UpsertChaosSystemMetadata:input_type -> resource.v1.UpdateByIDRequest + 13, // 31: resource.v1.ResourceService.ListChaosSystemMetadata:input_type -> resource.v1.IDQueryRequest + 5, // 32: resource.v1.ResourceService.ListDatapackEvaluationResults:input_type -> resource.v1.ListDatapackEvaluationsRequest + 6, // 33: resource.v1.ResourceService.ListDatasetEvaluationResults:input_type -> resource.v1.ListDatasetEvaluationsRequest + 7, // 34: resource.v1.ResourceService.ListEvaluations:input_type -> resource.v1.ListEvaluationsRequest + 10, // 35: resource.v1.ResourceService.GetEvaluation:input_type -> resource.v1.GetResourceRequest + 10, // 36: resource.v1.ResourceService.DeleteEvaluation:input_type -> resource.v1.GetResourceRequest + 1, // 37: resource.v1.ResourceService.Ping:output_type -> resource.v1.PingResponse + 15, // 38: resource.v1.ResourceService.ListProjects:output_type -> resource.v1.ResourceListResponse + 14, // 39: resource.v1.ResourceService.GetProject:output_type -> resource.v1.ResourceItemResponse + 15, // 40: resource.v1.ResourceService.ListContainers:output_type -> resource.v1.ResourceListResponse + 14, // 41: resource.v1.ResourceService.GetContainer:output_type -> resource.v1.ResourceItemResponse + 15, // 42: resource.v1.ResourceService.ListDatasets:output_type -> resource.v1.ResourceListResponse + 14, // 43: resource.v1.ResourceService.GetDataset:output_type -> resource.v1.ResourceItemResponse + 14, // 44: resource.v1.ResourceService.CreateLabel:output_type -> resource.v1.ResourceItemResponse + 14, // 45: resource.v1.ResourceService.GetLabel:output_type -> resource.v1.ResourceItemResponse + 15, // 46: resource.v1.ResourceService.ListLabels:output_type -> resource.v1.ResourceListResponse + 14, // 47: resource.v1.ResourceService.UpdateLabel:output_type -> resource.v1.ResourceItemResponse + 17, // 48: resource.v1.ResourceService.DeleteLabel:output_type -> google.protobuf.Empty + 17, // 49: resource.v1.ResourceService.BatchDeleteLabels:output_type -> google.protobuf.Empty + 15, // 50: resource.v1.ResourceService.ListChaosSystems:output_type -> resource.v1.ResourceListResponse + 14, // 51: resource.v1.ResourceService.GetChaosSystem:output_type -> resource.v1.ResourceItemResponse + 14, // 52: resource.v1.ResourceService.CreateChaosSystem:output_type -> resource.v1.ResourceItemResponse + 14, // 53: resource.v1.ResourceService.UpdateChaosSystem:output_type -> resource.v1.ResourceItemResponse + 17, // 54: resource.v1.ResourceService.DeleteChaosSystem:output_type -> google.protobuf.Empty + 17, // 55: resource.v1.ResourceService.UpsertChaosSystemMetadata:output_type -> google.protobuf.Empty + 14, // 56: resource.v1.ResourceService.ListChaosSystemMetadata:output_type -> resource.v1.ResourceItemResponse + 14, // 57: resource.v1.ResourceService.ListDatapackEvaluationResults:output_type -> resource.v1.ResourceItemResponse + 14, // 58: resource.v1.ResourceService.ListDatasetEvaluationResults:output_type -> resource.v1.ResourceItemResponse + 15, // 59: resource.v1.ResourceService.ListEvaluations:output_type -> resource.v1.ResourceListResponse + 14, // 60: resource.v1.ResourceService.GetEvaluation:output_type -> resource.v1.ResourceItemResponse + 17, // 61: resource.v1.ResourceService.DeleteEvaluation:output_type -> google.protobuf.Empty + 37, // [37:62] is the sub-list for method output_type + 12, // [12:37] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name +} + +func init() { file_proto_resource_v1_resource_proto_init() } +func file_proto_resource_v1_resource_proto_init() { + if File_proto_resource_v1_resource_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_resource_v1_resource_proto_rawDesc), len(file_proto_resource_v1_resource_proto_rawDesc)), + NumEnums: 0, + NumMessages: 16, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_resource_v1_resource_proto_goTypes, + DependencyIndexes: file_proto_resource_v1_resource_proto_depIdxs, + MessageInfos: file_proto_resource_v1_resource_proto_msgTypes, + }.Build() + File_proto_resource_v1_resource_proto = out.File + file_proto_resource_v1_resource_proto_goTypes = nil + file_proto_resource_v1_resource_proto_depIdxs = nil +} diff --git a/src/proto/resource/v1/resource.proto b/src/proto/resource/v1/resource.proto new file mode 100644 index 00000000..c6c2d654 --- /dev/null +++ b/src/proto/resource/v1/resource.proto @@ -0,0 +1,105 @@ +syntax = "proto3"; + +package resource.v1; + +option go_package = "aegis/proto/resource/v1;resourcev1"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/struct.proto"; + +service ResourceService { + rpc Ping(PingRequest) returns (PingResponse); + rpc ListProjects(ListProjectsRequest) returns (ResourceListResponse); + rpc GetProject(GetResourceRequest) returns (ResourceItemResponse); + rpc ListContainers(ListContainersRequest) returns (ResourceListResponse); + rpc GetContainer(GetResourceRequest) returns (ResourceItemResponse); + rpc ListDatasets(ListDatasetsRequest) returns (ResourceListResponse); + rpc GetDataset(GetResourceRequest) returns (ResourceItemResponse); + rpc CreateLabel(MutationRequest) returns (ResourceItemResponse); + rpc GetLabel(GetResourceRequest) returns (ResourceItemResponse); + rpc ListLabels(QueryRequest) returns (ResourceListResponse); + rpc UpdateLabel(UpdateByIDRequest) returns (ResourceItemResponse); + rpc DeleteLabel(GetResourceRequest) returns (google.protobuf.Empty); + rpc BatchDeleteLabels(BatchDeleteRequest) returns (google.protobuf.Empty); + rpc ListChaosSystems(QueryRequest) returns (ResourceListResponse); + rpc GetChaosSystem(GetResourceRequest) returns (ResourceItemResponse); + rpc CreateChaosSystem(MutationRequest) returns (ResourceItemResponse); + rpc UpdateChaosSystem(UpdateByIDRequest) returns (ResourceItemResponse); + rpc DeleteChaosSystem(GetResourceRequest) returns (google.protobuf.Empty); + rpc UpsertChaosSystemMetadata(UpdateByIDRequest) returns (google.protobuf.Empty); + rpc ListChaosSystemMetadata(IDQueryRequest) returns (ResourceItemResponse); + rpc ListDatapackEvaluationResults(ListDatapackEvaluationsRequest) returns (ResourceItemResponse); + rpc ListDatasetEvaluationResults(ListDatasetEvaluationsRequest) returns (ResourceItemResponse); + rpc ListEvaluations(ListEvaluationsRequest) returns (ResourceListResponse); + rpc GetEvaluation(GetResourceRequest) returns (ResourceItemResponse); + rpc DeleteEvaluation(GetResourceRequest) returns (google.protobuf.Empty); +} + +message PingRequest {} + +message PingResponse { + string service = 1; + string app_id = 2; + string status = 3; + int64 timestamp_unix = 4; +} + +message ListProjectsRequest { + google.protobuf.Struct query = 1; +} + +message ListContainersRequest { + google.protobuf.Struct query = 1; +} + +message ListDatasetsRequest { + google.protobuf.Struct query = 1; +} + +message ListDatapackEvaluationsRequest { + google.protobuf.Struct query = 1; + int64 user_id = 2; +} + +message ListDatasetEvaluationsRequest { + google.protobuf.Struct query = 1; + int64 user_id = 2; +} + +message ListEvaluationsRequest { + google.protobuf.Struct query = 1; +} + +message MutationRequest { + google.protobuf.Struct body = 1; +} + +message QueryRequest { + google.protobuf.Struct query = 1; +} + +message GetResourceRequest { + int64 id = 1; +} + +message UpdateByIDRequest { + int64 id = 1; + google.protobuf.Struct body = 2; +} + +message BatchDeleteRequest { + repeated int64 ids = 1; +} + +message IDQueryRequest { + int64 id = 1; + google.protobuf.Struct query = 2; +} + +message ResourceItemResponse { + google.protobuf.Struct data = 1; +} + +message ResourceListResponse { + google.protobuf.Struct data = 1; +} diff --git a/src/proto/resource/v1/resource_grpc.pb.go b/src/proto/resource/v1/resource_grpc.pb.go new file mode 100644 index 00000000..57ca7b83 --- /dev/null +++ b/src/proto/resource/v1/resource_grpc.pb.go @@ -0,0 +1,1034 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v5.29.3 +// source: proto/resource/v1/resource.proto + +package resourcev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ResourceService_Ping_FullMethodName = "/resource.v1.ResourceService/Ping" + ResourceService_ListProjects_FullMethodName = "/resource.v1.ResourceService/ListProjects" + ResourceService_GetProject_FullMethodName = "/resource.v1.ResourceService/GetProject" + ResourceService_ListContainers_FullMethodName = "/resource.v1.ResourceService/ListContainers" + ResourceService_GetContainer_FullMethodName = "/resource.v1.ResourceService/GetContainer" + ResourceService_ListDatasets_FullMethodName = "/resource.v1.ResourceService/ListDatasets" + ResourceService_GetDataset_FullMethodName = "/resource.v1.ResourceService/GetDataset" + ResourceService_CreateLabel_FullMethodName = "/resource.v1.ResourceService/CreateLabel" + ResourceService_GetLabel_FullMethodName = "/resource.v1.ResourceService/GetLabel" + ResourceService_ListLabels_FullMethodName = "/resource.v1.ResourceService/ListLabels" + ResourceService_UpdateLabel_FullMethodName = "/resource.v1.ResourceService/UpdateLabel" + ResourceService_DeleteLabel_FullMethodName = "/resource.v1.ResourceService/DeleteLabel" + ResourceService_BatchDeleteLabels_FullMethodName = "/resource.v1.ResourceService/BatchDeleteLabels" + ResourceService_ListChaosSystems_FullMethodName = "/resource.v1.ResourceService/ListChaosSystems" + ResourceService_GetChaosSystem_FullMethodName = "/resource.v1.ResourceService/GetChaosSystem" + ResourceService_CreateChaosSystem_FullMethodName = "/resource.v1.ResourceService/CreateChaosSystem" + ResourceService_UpdateChaosSystem_FullMethodName = "/resource.v1.ResourceService/UpdateChaosSystem" + ResourceService_DeleteChaosSystem_FullMethodName = "/resource.v1.ResourceService/DeleteChaosSystem" + ResourceService_UpsertChaosSystemMetadata_FullMethodName = "/resource.v1.ResourceService/UpsertChaosSystemMetadata" + ResourceService_ListChaosSystemMetadata_FullMethodName = "/resource.v1.ResourceService/ListChaosSystemMetadata" + ResourceService_ListDatapackEvaluationResults_FullMethodName = "/resource.v1.ResourceService/ListDatapackEvaluationResults" + ResourceService_ListDatasetEvaluationResults_FullMethodName = "/resource.v1.ResourceService/ListDatasetEvaluationResults" + ResourceService_ListEvaluations_FullMethodName = "/resource.v1.ResourceService/ListEvaluations" + ResourceService_GetEvaluation_FullMethodName = "/resource.v1.ResourceService/GetEvaluation" + ResourceService_DeleteEvaluation_FullMethodName = "/resource.v1.ResourceService/DeleteEvaluation" +) + +// ResourceServiceClient is the client API for ResourceService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ResourceServiceClient interface { + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + ListProjects(ctx context.Context, in *ListProjectsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetProject(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetContainer(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListDatasets(ctx context.Context, in *ListDatasetsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetDataset(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + CreateLabel(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + GetLabel(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListLabels(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + UpdateLabel(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + DeleteLabel(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + BatchDeleteLabels(ctx context.Context, in *BatchDeleteRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListChaosSystems(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetChaosSystem(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + CreateChaosSystem(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + UpdateChaosSystem(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + DeleteChaosSystem(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + UpsertChaosSystemMetadata(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + ListChaosSystemMetadata(ctx context.Context, in *IDQueryRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListDatapackEvaluationResults(ctx context.Context, in *ListDatapackEvaluationsRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListDatasetEvaluationResults(ctx context.Context, in *ListDatasetEvaluationsRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListEvaluations(ctx context.Context, in *ListEvaluationsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetEvaluation(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + DeleteEvaluation(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type resourceServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewResourceServiceClient(cc grpc.ClientConnInterface) ResourceServiceClient { + return &resourceServiceClient{cc} +} + +func (c *resourceServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PingResponse) + err := c.cc.Invoke(ctx, ResourceService_Ping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListProjects(ctx context.Context, in *ListProjectsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListProjects_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetProject(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListContainers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetContainer(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetContainer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListDatasets(ctx context.Context, in *ListDatasetsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListDatasets_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetDataset(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetDataset_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) CreateLabel(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_CreateLabel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetLabel(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetLabel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListLabels(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListLabels_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) UpdateLabel(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_UpdateLabel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) DeleteLabel(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResourceService_DeleteLabel_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) BatchDeleteLabels(ctx context.Context, in *BatchDeleteRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResourceService_BatchDeleteLabels_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListChaosSystems(ctx context.Context, in *QueryRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListChaosSystems_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetChaosSystem(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetChaosSystem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) CreateChaosSystem(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_CreateChaosSystem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) UpdateChaosSystem(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_UpdateChaosSystem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) DeleteChaosSystem(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResourceService_DeleteChaosSystem_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) UpsertChaosSystemMetadata(ctx context.Context, in *UpdateByIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResourceService_UpsertChaosSystemMetadata_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListChaosSystemMetadata(ctx context.Context, in *IDQueryRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_ListChaosSystemMetadata_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListDatapackEvaluationResults(ctx context.Context, in *ListDatapackEvaluationsRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_ListDatapackEvaluationResults_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListDatasetEvaluationResults(ctx context.Context, in *ListDatasetEvaluationsRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_ListDatasetEvaluationResults_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) ListEvaluations(ctx context.Context, in *ListEvaluationsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, ResourceService_ListEvaluations_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) GetEvaluation(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, ResourceService_GetEvaluation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *resourceServiceClient) DeleteEvaluation(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ResourceService_DeleteEvaluation_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ResourceServiceServer is the server API for ResourceService service. +// All implementations must embed UnimplementedResourceServiceServer +// for forward compatibility. +type ResourceServiceServer interface { + Ping(context.Context, *PingRequest) (*PingResponse, error) + ListProjects(context.Context, *ListProjectsRequest) (*ResourceListResponse, error) + GetProject(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + ListContainers(context.Context, *ListContainersRequest) (*ResourceListResponse, error) + GetContainer(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + ListDatasets(context.Context, *ListDatasetsRequest) (*ResourceListResponse, error) + GetDataset(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + CreateLabel(context.Context, *MutationRequest) (*ResourceItemResponse, error) + GetLabel(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + ListLabels(context.Context, *QueryRequest) (*ResourceListResponse, error) + UpdateLabel(context.Context, *UpdateByIDRequest) (*ResourceItemResponse, error) + DeleteLabel(context.Context, *GetResourceRequest) (*emptypb.Empty, error) + BatchDeleteLabels(context.Context, *BatchDeleteRequest) (*emptypb.Empty, error) + ListChaosSystems(context.Context, *QueryRequest) (*ResourceListResponse, error) + GetChaosSystem(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + CreateChaosSystem(context.Context, *MutationRequest) (*ResourceItemResponse, error) + UpdateChaosSystem(context.Context, *UpdateByIDRequest) (*ResourceItemResponse, error) + DeleteChaosSystem(context.Context, *GetResourceRequest) (*emptypb.Empty, error) + UpsertChaosSystemMetadata(context.Context, *UpdateByIDRequest) (*emptypb.Empty, error) + ListChaosSystemMetadata(context.Context, *IDQueryRequest) (*ResourceItemResponse, error) + ListDatapackEvaluationResults(context.Context, *ListDatapackEvaluationsRequest) (*ResourceItemResponse, error) + ListDatasetEvaluationResults(context.Context, *ListDatasetEvaluationsRequest) (*ResourceItemResponse, error) + ListEvaluations(context.Context, *ListEvaluationsRequest) (*ResourceListResponse, error) + GetEvaluation(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + DeleteEvaluation(context.Context, *GetResourceRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedResourceServiceServer() +} + +// UnimplementedResourceServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedResourceServiceServer struct{} + +func (UnimplementedResourceServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedResourceServiceServer) ListProjects(context.Context, *ListProjectsRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListProjects not implemented") +} +func (UnimplementedResourceServiceServer) GetProject(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProject not implemented") +} +func (UnimplementedResourceServiceServer) ListContainers(context.Context, *ListContainersRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListContainers not implemented") +} +func (UnimplementedResourceServiceServer) GetContainer(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetContainer not implemented") +} +func (UnimplementedResourceServiceServer) ListDatasets(context.Context, *ListDatasetsRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDatasets not implemented") +} +func (UnimplementedResourceServiceServer) GetDataset(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetDataset not implemented") +} +func (UnimplementedResourceServiceServer) CreateLabel(context.Context, *MutationRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateLabel not implemented") +} +func (UnimplementedResourceServiceServer) GetLabel(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetLabel not implemented") +} +func (UnimplementedResourceServiceServer) ListLabels(context.Context, *QueryRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListLabels not implemented") +} +func (UnimplementedResourceServiceServer) UpdateLabel(context.Context, *UpdateByIDRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateLabel not implemented") +} +func (UnimplementedResourceServiceServer) DeleteLabel(context.Context, *GetResourceRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteLabel not implemented") +} +func (UnimplementedResourceServiceServer) BatchDeleteLabels(context.Context, *BatchDeleteRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method BatchDeleteLabels not implemented") +} +func (UnimplementedResourceServiceServer) ListChaosSystems(context.Context, *QueryRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListChaosSystems not implemented") +} +func (UnimplementedResourceServiceServer) GetChaosSystem(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetChaosSystem not implemented") +} +func (UnimplementedResourceServiceServer) CreateChaosSystem(context.Context, *MutationRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateChaosSystem not implemented") +} +func (UnimplementedResourceServiceServer) UpdateChaosSystem(context.Context, *UpdateByIDRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateChaosSystem not implemented") +} +func (UnimplementedResourceServiceServer) DeleteChaosSystem(context.Context, *GetResourceRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteChaosSystem not implemented") +} +func (UnimplementedResourceServiceServer) UpsertChaosSystemMetadata(context.Context, *UpdateByIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method UpsertChaosSystemMetadata not implemented") +} +func (UnimplementedResourceServiceServer) ListChaosSystemMetadata(context.Context, *IDQueryRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListChaosSystemMetadata not implemented") +} +func (UnimplementedResourceServiceServer) ListDatapackEvaluationResults(context.Context, *ListDatapackEvaluationsRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDatapackEvaluationResults not implemented") +} +func (UnimplementedResourceServiceServer) ListDatasetEvaluationResults(context.Context, *ListDatasetEvaluationsRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDatasetEvaluationResults not implemented") +} +func (UnimplementedResourceServiceServer) ListEvaluations(context.Context, *ListEvaluationsRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListEvaluations not implemented") +} +func (UnimplementedResourceServiceServer) GetEvaluation(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetEvaluation not implemented") +} +func (UnimplementedResourceServiceServer) DeleteEvaluation(context.Context, *GetResourceRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteEvaluation not implemented") +} +func (UnimplementedResourceServiceServer) mustEmbedUnimplementedResourceServiceServer() {} +func (UnimplementedResourceServiceServer) testEmbeddedByValue() {} + +// UnsafeResourceServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ResourceServiceServer will +// result in compilation errors. +type UnsafeResourceServiceServer interface { + mustEmbedUnimplementedResourceServiceServer() +} + +func RegisterResourceServiceServer(s grpc.ServiceRegistrar, srv ResourceServiceServer) { + // If the following call panics, it indicates UnimplementedResourceServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ResourceService_ServiceDesc, srv) +} + +func _ResourceService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).Ping(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListProjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListProjectsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListProjects(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListProjects_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListProjects(ctx, req.(*ListProjectsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetProject(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListContainers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListContainersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListContainers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListContainers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListContainers(ctx, req.(*ListContainersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetContainer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetContainer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetContainer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetContainer(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListDatasets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDatasetsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListDatasets(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListDatasets_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListDatasets(ctx, req.(*ListDatasetsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetDataset_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetDataset(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetDataset_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetDataset(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_CreateLabel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).CreateLabel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_CreateLabel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).CreateLabel(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetLabel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetLabel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetLabel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetLabel(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListLabels_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListLabels(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListLabels_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListLabels(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_UpdateLabel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateByIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).UpdateLabel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_UpdateLabel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).UpdateLabel(ctx, req.(*UpdateByIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_DeleteLabel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).DeleteLabel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_DeleteLabel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).DeleteLabel(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_BatchDeleteLabels_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchDeleteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).BatchDeleteLabels(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_BatchDeleteLabels_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).BatchDeleteLabels(ctx, req.(*BatchDeleteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListChaosSystems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListChaosSystems(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListChaosSystems_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListChaosSystems(ctx, req.(*QueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetChaosSystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetChaosSystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetChaosSystem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetChaosSystem(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_CreateChaosSystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MutationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).CreateChaosSystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_CreateChaosSystem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).CreateChaosSystem(ctx, req.(*MutationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_UpdateChaosSystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateByIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).UpdateChaosSystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_UpdateChaosSystem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).UpdateChaosSystem(ctx, req.(*UpdateByIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_DeleteChaosSystem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).DeleteChaosSystem(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_DeleteChaosSystem_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).DeleteChaosSystem(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_UpsertChaosSystemMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateByIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).UpsertChaosSystemMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_UpsertChaosSystemMetadata_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).UpsertChaosSystemMetadata(ctx, req.(*UpdateByIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListChaosSystemMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IDQueryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListChaosSystemMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListChaosSystemMetadata_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListChaosSystemMetadata(ctx, req.(*IDQueryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListDatapackEvaluationResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDatapackEvaluationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListDatapackEvaluationResults(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListDatapackEvaluationResults_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListDatapackEvaluationResults(ctx, req.(*ListDatapackEvaluationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListDatasetEvaluationResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListDatasetEvaluationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListDatasetEvaluationResults(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListDatasetEvaluationResults_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListDatasetEvaluationResults(ctx, req.(*ListDatasetEvaluationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_ListEvaluations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListEvaluationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).ListEvaluations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_ListEvaluations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).ListEvaluations(ctx, req.(*ListEvaluationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_GetEvaluation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).GetEvaluation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_GetEvaluation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).GetEvaluation(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ResourceService_DeleteEvaluation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceServiceServer).DeleteEvaluation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceService_DeleteEvaluation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceServiceServer).DeleteEvaluation(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ResourceService_ServiceDesc is the grpc.ServiceDesc for ResourceService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ResourceService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "resource.v1.ResourceService", + HandlerType: (*ResourceServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _ResourceService_Ping_Handler, + }, + { + MethodName: "ListProjects", + Handler: _ResourceService_ListProjects_Handler, + }, + { + MethodName: "GetProject", + Handler: _ResourceService_GetProject_Handler, + }, + { + MethodName: "ListContainers", + Handler: _ResourceService_ListContainers_Handler, + }, + { + MethodName: "GetContainer", + Handler: _ResourceService_GetContainer_Handler, + }, + { + MethodName: "ListDatasets", + Handler: _ResourceService_ListDatasets_Handler, + }, + { + MethodName: "GetDataset", + Handler: _ResourceService_GetDataset_Handler, + }, + { + MethodName: "CreateLabel", + Handler: _ResourceService_CreateLabel_Handler, + }, + { + MethodName: "GetLabel", + Handler: _ResourceService_GetLabel_Handler, + }, + { + MethodName: "ListLabels", + Handler: _ResourceService_ListLabels_Handler, + }, + { + MethodName: "UpdateLabel", + Handler: _ResourceService_UpdateLabel_Handler, + }, + { + MethodName: "DeleteLabel", + Handler: _ResourceService_DeleteLabel_Handler, + }, + { + MethodName: "BatchDeleteLabels", + Handler: _ResourceService_BatchDeleteLabels_Handler, + }, + { + MethodName: "ListChaosSystems", + Handler: _ResourceService_ListChaosSystems_Handler, + }, + { + MethodName: "GetChaosSystem", + Handler: _ResourceService_GetChaosSystem_Handler, + }, + { + MethodName: "CreateChaosSystem", + Handler: _ResourceService_CreateChaosSystem_Handler, + }, + { + MethodName: "UpdateChaosSystem", + Handler: _ResourceService_UpdateChaosSystem_Handler, + }, + { + MethodName: "DeleteChaosSystem", + Handler: _ResourceService_DeleteChaosSystem_Handler, + }, + { + MethodName: "UpsertChaosSystemMetadata", + Handler: _ResourceService_UpsertChaosSystemMetadata_Handler, + }, + { + MethodName: "ListChaosSystemMetadata", + Handler: _ResourceService_ListChaosSystemMetadata_Handler, + }, + { + MethodName: "ListDatapackEvaluationResults", + Handler: _ResourceService_ListDatapackEvaluationResults_Handler, + }, + { + MethodName: "ListDatasetEvaluationResults", + Handler: _ResourceService_ListDatasetEvaluationResults_Handler, + }, + { + MethodName: "ListEvaluations", + Handler: _ResourceService_ListEvaluations_Handler, + }, + { + MethodName: "GetEvaluation", + Handler: _ResourceService_GetEvaluation_Handler, + }, + { + MethodName: "DeleteEvaluation", + Handler: _ResourceService_DeleteEvaluation_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/resource/v1/resource.proto", +} diff --git a/src/proto/runtime/v1/runtime.pb.go b/src/proto/runtime/v1/runtime.pb.go new file mode 100644 index 00000000..a8df3539 --- /dev/null +++ b/src/proto/runtime/v1/runtime.pb.go @@ -0,0 +1,821 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/runtime/v1/runtime.proto + +package runtimev1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{0} +} + +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + AppId string `protobuf:"bytes,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + TimestampUnix int64 `protobuf:"varint,4,opt,name=timestamp_unix,json=timestampUnix,proto3" json:"timestamp_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *PingResponse) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *PingResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PingResponse) GetTimestampUnix() int64 { + if x != nil { + return x.TimestampUnix + } + return 0 +} + +type RuntimeStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuntimeStatusRequest) Reset() { + *x = RuntimeStatusRequest{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuntimeStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuntimeStatusRequest) ProtoMessage() {} + +func (x *RuntimeStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuntimeStatusRequest.ProtoReflect.Descriptor instead. +func (*RuntimeStatusRequest) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{2} +} + +type RuntimeStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + Mode string `protobuf:"bytes,2,opt,name=mode,proto3" json:"mode,omitempty"` + AppId string `protobuf:"bytes,3,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + StartedAtUnix int64 `protobuf:"varint,4,opt,name=started_at_unix,json=startedAtUnix,proto3" json:"started_at_unix,omitempty"` + UptimeSeconds int64 `protobuf:"varint,5,opt,name=uptime_seconds,json=uptimeSeconds,proto3" json:"uptime_seconds,omitempty"` + DbAvailable bool `protobuf:"varint,10,opt,name=db_available,json=dbAvailable,proto3" json:"db_available,omitempty"` + DbHealthy bool `protobuf:"varint,11,opt,name=db_healthy,json=dbHealthy,proto3" json:"db_healthy,omitempty"` + DbError string `protobuf:"bytes,12,opt,name=db_error,json=dbError,proto3" json:"db_error,omitempty"` + RedisAvailable bool `protobuf:"varint,20,opt,name=redis_available,json=redisAvailable,proto3" json:"redis_available,omitempty"` + RedisHealthy bool `protobuf:"varint,21,opt,name=redis_healthy,json=redisHealthy,proto3" json:"redis_healthy,omitempty"` + RedisError string `protobuf:"bytes,22,opt,name=redis_error,json=redisError,proto3" json:"redis_error,omitempty"` + K8SAvailable bool `protobuf:"varint,30,opt,name=k8s_available,json=k8sAvailable,proto3" json:"k8s_available,omitempty"` + K8SHealthy bool `protobuf:"varint,31,opt,name=k8s_healthy,json=k8sHealthy,proto3" json:"k8s_healthy,omitempty"` + K8SError string `protobuf:"bytes,32,opt,name=k8s_error,json=k8sError,proto3" json:"k8s_error,omitempty"` + BuildkitAvailable bool `protobuf:"varint,40,opt,name=buildkit_available,json=buildkitAvailable,proto3" json:"buildkit_available,omitempty"` + BuildkitHealthy bool `protobuf:"varint,41,opt,name=buildkit_healthy,json=buildkitHealthy,proto3" json:"buildkit_healthy,omitempty"` + BuildkitError string `protobuf:"bytes,42,opt,name=buildkit_error,json=buildkitError,proto3" json:"buildkit_error,omitempty"` + HelmAvailable bool `protobuf:"varint,50,opt,name=helm_available,json=helmAvailable,proto3" json:"helm_available,omitempty"` + HelmHealthy bool `protobuf:"varint,51,opt,name=helm_healthy,json=helmHealthy,proto3" json:"helm_healthy,omitempty"` + HelmError string `protobuf:"bytes,52,opt,name=helm_error,json=helmError,proto3" json:"helm_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RuntimeStatusResponse) Reset() { + *x = RuntimeStatusResponse{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RuntimeStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RuntimeStatusResponse) ProtoMessage() {} + +func (x *RuntimeStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RuntimeStatusResponse.ProtoReflect.Descriptor instead. +func (*RuntimeStatusResponse) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{3} +} + +func (x *RuntimeStatusResponse) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *RuntimeStatusResponse) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *RuntimeStatusResponse) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *RuntimeStatusResponse) GetStartedAtUnix() int64 { + if x != nil { + return x.StartedAtUnix + } + return 0 +} + +func (x *RuntimeStatusResponse) GetUptimeSeconds() int64 { + if x != nil { + return x.UptimeSeconds + } + return 0 +} + +func (x *RuntimeStatusResponse) GetDbAvailable() bool { + if x != nil { + return x.DbAvailable + } + return false +} + +func (x *RuntimeStatusResponse) GetDbHealthy() bool { + if x != nil { + return x.DbHealthy + } + return false +} + +func (x *RuntimeStatusResponse) GetDbError() string { + if x != nil { + return x.DbError + } + return "" +} + +func (x *RuntimeStatusResponse) GetRedisAvailable() bool { + if x != nil { + return x.RedisAvailable + } + return false +} + +func (x *RuntimeStatusResponse) GetRedisHealthy() bool { + if x != nil { + return x.RedisHealthy + } + return false +} + +func (x *RuntimeStatusResponse) GetRedisError() string { + if x != nil { + return x.RedisError + } + return "" +} + +func (x *RuntimeStatusResponse) GetK8SAvailable() bool { + if x != nil { + return x.K8SAvailable + } + return false +} + +func (x *RuntimeStatusResponse) GetK8SHealthy() bool { + if x != nil { + return x.K8SHealthy + } + return false +} + +func (x *RuntimeStatusResponse) GetK8SError() string { + if x != nil { + return x.K8SError + } + return "" +} + +func (x *RuntimeStatusResponse) GetBuildkitAvailable() bool { + if x != nil { + return x.BuildkitAvailable + } + return false +} + +func (x *RuntimeStatusResponse) GetBuildkitHealthy() bool { + if x != nil { + return x.BuildkitHealthy + } + return false +} + +func (x *RuntimeStatusResponse) GetBuildkitError() string { + if x != nil { + return x.BuildkitError + } + return "" +} + +func (x *RuntimeStatusResponse) GetHelmAvailable() bool { + if x != nil { + return x.HelmAvailable + } + return false +} + +func (x *RuntimeStatusResponse) GetHelmHealthy() bool { + if x != nil { + return x.HelmHealthy + } + return false +} + +func (x *RuntimeStatusResponse) GetHelmError() string { + if x != nil { + return x.HelmError + } + return "" +} + +type QueueStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueueStatusRequest) Reset() { + *x = QueueStatusRequest{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueueStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueueStatusRequest) ProtoMessage() {} + +func (x *QueueStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueueStatusRequest.ProtoReflect.Descriptor instead. +func (*QueueStatusRequest) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{4} +} + +type QueueStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ReadyCount int64 `protobuf:"varint,1,opt,name=ready_count,json=readyCount,proto3" json:"ready_count,omitempty"` + DelayedCount int64 `protobuf:"varint,2,opt,name=delayed_count,json=delayedCount,proto3" json:"delayed_count,omitempty"` + DeadCount int64 `protobuf:"varint,3,opt,name=dead_count,json=deadCount,proto3" json:"dead_count,omitempty"` + IndexedCount int64 `protobuf:"varint,4,opt,name=indexed_count,json=indexedCount,proto3" json:"indexed_count,omitempty"` + ConcurrencyCount int64 `protobuf:"varint,5,opt,name=concurrency_count,json=concurrencyCount,proto3" json:"concurrency_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QueueStatusResponse) Reset() { + *x = QueueStatusResponse{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QueueStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueueStatusResponse) ProtoMessage() {} + +func (x *QueueStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QueueStatusResponse.ProtoReflect.Descriptor instead. +func (*QueueStatusResponse) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{5} +} + +func (x *QueueStatusResponse) GetReadyCount() int64 { + if x != nil { + return x.ReadyCount + } + return 0 +} + +func (x *QueueStatusResponse) GetDelayedCount() int64 { + if x != nil { + return x.DelayedCount + } + return 0 +} + +func (x *QueueStatusResponse) GetDeadCount() int64 { + if x != nil { + return x.DeadCount + } + return 0 +} + +func (x *QueueStatusResponse) GetIndexedCount() int64 { + if x != nil { + return x.IndexedCount + } + return 0 +} + +func (x *QueueStatusResponse) GetConcurrencyCount() int64 { + if x != nil { + return x.ConcurrencyCount + } + return 0 +} + +type LimiterStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LimiterStatusRequest) Reset() { + *x = LimiterStatusRequest{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LimiterStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LimiterStatusRequest) ProtoMessage() {} + +func (x *LimiterStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LimiterStatusRequest.ProtoReflect.Descriptor instead. +func (*LimiterStatusRequest) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{6} +} + +type LimiterStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*LimiterStatus `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LimiterStatusResponse) Reset() { + *x = LimiterStatusResponse{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LimiterStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LimiterStatusResponse) ProtoMessage() {} + +func (x *LimiterStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LimiterStatusResponse.ProtoReflect.Descriptor instead. +func (*LimiterStatusResponse) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{7} +} + +func (x *LimiterStatusResponse) GetItems() []*LimiterStatus { + if x != nil { + return x.Items + } + return nil +} + +type LimiterStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + BucketKey string `protobuf:"bytes,2,opt,name=bucket_key,json=bucketKey,proto3" json:"bucket_key,omitempty"` + MaxTokens int64 `protobuf:"varint,3,opt,name=max_tokens,json=maxTokens,proto3" json:"max_tokens,omitempty"` + WaitTimeoutSeconds int64 `protobuf:"varint,4,opt,name=wait_timeout_seconds,json=waitTimeoutSeconds,proto3" json:"wait_timeout_seconds,omitempty"` + InUseTokens int64 `protobuf:"varint,5,opt,name=in_use_tokens,json=inUseTokens,proto3" json:"in_use_tokens,omitempty"` + Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LimiterStatus) Reset() { + *x = LimiterStatus{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LimiterStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LimiterStatus) ProtoMessage() {} + +func (x *LimiterStatus) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LimiterStatus.ProtoReflect.Descriptor instead. +func (*LimiterStatus) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{8} +} + +func (x *LimiterStatus) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *LimiterStatus) GetBucketKey() string { + if x != nil { + return x.BucketKey + } + return "" +} + +func (x *LimiterStatus) GetMaxTokens() int64 { + if x != nil { + return x.MaxTokens + } + return 0 +} + +func (x *LimiterStatus) GetWaitTimeoutSeconds() int64 { + if x != nil { + return x.WaitTimeoutSeconds + } + return 0 +} + +func (x *LimiterStatus) GetInUseTokens() int64 { + if x != nil { + return x.InUseTokens + } + return 0 +} + +func (x *LimiterStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +type StructResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StructResponse) Reset() { + *x = StructResponse{} + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StructResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StructResponse) ProtoMessage() {} + +func (x *StructResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_runtime_v1_runtime_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StructResponse.ProtoReflect.Descriptor instead. +func (*StructResponse) Descriptor() ([]byte, []int) { + return file_proto_runtime_v1_runtime_proto_rawDescGZIP(), []int{9} +} + +func (x *StructResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +var File_proto_runtime_v1_runtime_proto protoreflect.FileDescriptor + +const file_proto_runtime_v1_runtime_proto_rawDesc = "" + + "\n" + + "\x1eproto/runtime/v1/runtime.proto\x12\n" + + "runtime.v1\x1a\x1cgoogle/protobuf/struct.proto\"\r\n" + + "\vPingRequest\"~\n" + + "\fPingResponse\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x15\n" + + "\x06app_id\x18\x02 \x01(\tR\x05appId\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12%\n" + + "\x0etimestamp_unix\x18\x04 \x01(\x03R\rtimestampUnix\"\x16\n" + + "\x14RuntimeStatusRequest\"\xc4\x05\n" + + "\x15RuntimeStatusResponse\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x12\n" + + "\x04mode\x18\x02 \x01(\tR\x04mode\x12\x15\n" + + "\x06app_id\x18\x03 \x01(\tR\x05appId\x12&\n" + + "\x0fstarted_at_unix\x18\x04 \x01(\x03R\rstartedAtUnix\x12%\n" + + "\x0euptime_seconds\x18\x05 \x01(\x03R\ruptimeSeconds\x12!\n" + + "\fdb_available\x18\n" + + " \x01(\bR\vdbAvailable\x12\x1d\n" + + "\n" + + "db_healthy\x18\v \x01(\bR\tdbHealthy\x12\x19\n" + + "\bdb_error\x18\f \x01(\tR\adbError\x12'\n" + + "\x0fredis_available\x18\x14 \x01(\bR\x0eredisAvailable\x12#\n" + + "\rredis_healthy\x18\x15 \x01(\bR\fredisHealthy\x12\x1f\n" + + "\vredis_error\x18\x16 \x01(\tR\n" + + "redisError\x12#\n" + + "\rk8s_available\x18\x1e \x01(\bR\fk8sAvailable\x12\x1f\n" + + "\vk8s_healthy\x18\x1f \x01(\bR\n" + + "k8sHealthy\x12\x1b\n" + + "\tk8s_error\x18 \x01(\tR\bk8sError\x12-\n" + + "\x12buildkit_available\x18( \x01(\bR\x11buildkitAvailable\x12)\n" + + "\x10buildkit_healthy\x18) \x01(\bR\x0fbuildkitHealthy\x12%\n" + + "\x0ebuildkit_error\x18* \x01(\tR\rbuildkitError\x12%\n" + + "\x0ehelm_available\x182 \x01(\bR\rhelmAvailable\x12!\n" + + "\fhelm_healthy\x183 \x01(\bR\vhelmHealthy\x12\x1d\n" + + "\n" + + "helm_error\x184 \x01(\tR\thelmError\"\x14\n" + + "\x12QueueStatusRequest\"\xcc\x01\n" + + "\x13QueueStatusResponse\x12\x1f\n" + + "\vready_count\x18\x01 \x01(\x03R\n" + + "readyCount\x12#\n" + + "\rdelayed_count\x18\x02 \x01(\x03R\fdelayedCount\x12\x1d\n" + + "\n" + + "dead_count\x18\x03 \x01(\x03R\tdeadCount\x12#\n" + + "\rindexed_count\x18\x04 \x01(\x03R\findexedCount\x12+\n" + + "\x11concurrency_count\x18\x05 \x01(\x03R\x10concurrencyCount\"\x16\n" + + "\x14LimiterStatusRequest\"H\n" + + "\x15LimiterStatusResponse\x12/\n" + + "\x05items\x18\x01 \x03(\v2\x19.runtime.v1.LimiterStatusR\x05items\"\xdc\x01\n" + + "\rLimiterStatus\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1d\n" + + "\n" + + "bucket_key\x18\x02 \x01(\tR\tbucketKey\x12\x1d\n" + + "\n" + + "max_tokens\x18\x03 \x01(\x03R\tmaxTokens\x120\n" + + "\x14wait_timeout_seconds\x18\x04 \x01(\x03R\x12waitTimeoutSeconds\x12\"\n" + + "\rin_use_tokens\x18\x05 \x01(\x03R\vinUseTokens\x12\x14\n" + + "\x05error\x18\x06 \x01(\tR\x05error\"=\n" + + "\x0eStructResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xe1\x03\n" + + "\x0eRuntimeService\x129\n" + + "\x04Ping\x12\x17.runtime.v1.PingRequest\x1a\x18.runtime.v1.PingResponse\x12W\n" + + "\x10GetRuntimeStatus\x12 .runtime.v1.RuntimeStatusRequest\x1a!.runtime.v1.RuntimeStatusResponse\x12Q\n" + + "\x0eGetQueueStatus\x12\x1e.runtime.v1.QueueStatusRequest\x1a\x1f.runtime.v1.QueueStatusResponse\x12W\n" + + "\x10GetLimiterStatus\x12 .runtime.v1.LimiterStatusRequest\x1a!.runtime.v1.LimiterStatusResponse\x12H\n" + + "\x11GetNamespaceLocks\x12\x17.runtime.v1.PingRequest\x1a\x1a.runtime.v1.StructResponse\x12E\n" + + "\x0eGetQueuedTasks\x12\x17.runtime.v1.PingRequest\x1a\x1a.runtime.v1.StructResponseB\"Z aegis/proto/runtime/v1;runtimev1b\x06proto3" + +var ( + file_proto_runtime_v1_runtime_proto_rawDescOnce sync.Once + file_proto_runtime_v1_runtime_proto_rawDescData []byte +) + +func file_proto_runtime_v1_runtime_proto_rawDescGZIP() []byte { + file_proto_runtime_v1_runtime_proto_rawDescOnce.Do(func() { + file_proto_runtime_v1_runtime_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_runtime_v1_runtime_proto_rawDesc), len(file_proto_runtime_v1_runtime_proto_rawDesc))) + }) + return file_proto_runtime_v1_runtime_proto_rawDescData +} + +var file_proto_runtime_v1_runtime_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_proto_runtime_v1_runtime_proto_goTypes = []any{ + (*PingRequest)(nil), // 0: runtime.v1.PingRequest + (*PingResponse)(nil), // 1: runtime.v1.PingResponse + (*RuntimeStatusRequest)(nil), // 2: runtime.v1.RuntimeStatusRequest + (*RuntimeStatusResponse)(nil), // 3: runtime.v1.RuntimeStatusResponse + (*QueueStatusRequest)(nil), // 4: runtime.v1.QueueStatusRequest + (*QueueStatusResponse)(nil), // 5: runtime.v1.QueueStatusResponse + (*LimiterStatusRequest)(nil), // 6: runtime.v1.LimiterStatusRequest + (*LimiterStatusResponse)(nil), // 7: runtime.v1.LimiterStatusResponse + (*LimiterStatus)(nil), // 8: runtime.v1.LimiterStatus + (*StructResponse)(nil), // 9: runtime.v1.StructResponse + (*structpb.Struct)(nil), // 10: google.protobuf.Struct +} +var file_proto_runtime_v1_runtime_proto_depIdxs = []int32{ + 8, // 0: runtime.v1.LimiterStatusResponse.items:type_name -> runtime.v1.LimiterStatus + 10, // 1: runtime.v1.StructResponse.data:type_name -> google.protobuf.Struct + 0, // 2: runtime.v1.RuntimeService.Ping:input_type -> runtime.v1.PingRequest + 2, // 3: runtime.v1.RuntimeService.GetRuntimeStatus:input_type -> runtime.v1.RuntimeStatusRequest + 4, // 4: runtime.v1.RuntimeService.GetQueueStatus:input_type -> runtime.v1.QueueStatusRequest + 6, // 5: runtime.v1.RuntimeService.GetLimiterStatus:input_type -> runtime.v1.LimiterStatusRequest + 0, // 6: runtime.v1.RuntimeService.GetNamespaceLocks:input_type -> runtime.v1.PingRequest + 0, // 7: runtime.v1.RuntimeService.GetQueuedTasks:input_type -> runtime.v1.PingRequest + 1, // 8: runtime.v1.RuntimeService.Ping:output_type -> runtime.v1.PingResponse + 3, // 9: runtime.v1.RuntimeService.GetRuntimeStatus:output_type -> runtime.v1.RuntimeStatusResponse + 5, // 10: runtime.v1.RuntimeService.GetQueueStatus:output_type -> runtime.v1.QueueStatusResponse + 7, // 11: runtime.v1.RuntimeService.GetLimiterStatus:output_type -> runtime.v1.LimiterStatusResponse + 9, // 12: runtime.v1.RuntimeService.GetNamespaceLocks:output_type -> runtime.v1.StructResponse + 9, // 13: runtime.v1.RuntimeService.GetQueuedTasks:output_type -> runtime.v1.StructResponse + 8, // [8:14] is the sub-list for method output_type + 2, // [2:8] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proto_runtime_v1_runtime_proto_init() } +func file_proto_runtime_v1_runtime_proto_init() { + if File_proto_runtime_v1_runtime_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runtime_v1_runtime_proto_rawDesc), len(file_proto_runtime_v1_runtime_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_runtime_v1_runtime_proto_goTypes, + DependencyIndexes: file_proto_runtime_v1_runtime_proto_depIdxs, + MessageInfos: file_proto_runtime_v1_runtime_proto_msgTypes, + }.Build() + File_proto_runtime_v1_runtime_proto = out.File + file_proto_runtime_v1_runtime_proto_goTypes = nil + file_proto_runtime_v1_runtime_proto_depIdxs = nil +} diff --git a/src/proto/runtime/v1/runtime.proto b/src/proto/runtime/v1/runtime.proto new file mode 100644 index 00000000..2df0eb9e --- /dev/null +++ b/src/proto/runtime/v1/runtime.proto @@ -0,0 +1,84 @@ +syntax = "proto3"; + +package runtime.v1; + +option go_package = "aegis/proto/runtime/v1;runtimev1"; + +import "google/protobuf/struct.proto"; + +service RuntimeService { + rpc Ping(PingRequest) returns (PingResponse); + rpc GetRuntimeStatus(RuntimeStatusRequest) returns (RuntimeStatusResponse); + rpc GetQueueStatus(QueueStatusRequest) returns (QueueStatusResponse); + rpc GetLimiterStatus(LimiterStatusRequest) returns (LimiterStatusResponse); + rpc GetNamespaceLocks(PingRequest) returns (StructResponse); + rpc GetQueuedTasks(PingRequest) returns (StructResponse); +} + +message PingRequest {} + +message PingResponse { + string service = 1; + string app_id = 2; + string status = 3; + int64 timestamp_unix = 4; +} + +message RuntimeStatusRequest {} + +message RuntimeStatusResponse { + string service = 1; + string mode = 2; + string app_id = 3; + int64 started_at_unix = 4; + int64 uptime_seconds = 5; + + bool db_available = 10; + bool db_healthy = 11; + string db_error = 12; + + bool redis_available = 20; + bool redis_healthy = 21; + string redis_error = 22; + + bool k8s_available = 30; + bool k8s_healthy = 31; + string k8s_error = 32; + + bool buildkit_available = 40; + bool buildkit_healthy = 41; + string buildkit_error = 42; + + bool helm_available = 50; + bool helm_healthy = 51; + string helm_error = 52; +} + +message QueueStatusRequest {} + +message QueueStatusResponse { + int64 ready_count = 1; + int64 delayed_count = 2; + int64 dead_count = 3; + int64 indexed_count = 4; + int64 concurrency_count = 5; +} + +message LimiterStatusRequest {} + +message LimiterStatusResponse { + repeated LimiterStatus items = 1; +} + +message LimiterStatus { + string service_name = 1; + string bucket_key = 2; + int64 max_tokens = 3; + int64 wait_timeout_seconds = 4; + int64 in_use_tokens = 5; + string error = 6; +} + +message StructResponse { + google.protobuf.Struct data = 1; +} diff --git a/src/proto/runtime/v1/runtime_grpc.pb.go b/src/proto/runtime/v1/runtime_grpc.pb.go new file mode 100644 index 00000000..91960de3 --- /dev/null +++ b/src/proto/runtime/v1/runtime_grpc.pb.go @@ -0,0 +1,311 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v5.29.3 +// source: proto/runtime/v1/runtime.proto + +package runtimev1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RuntimeService_Ping_FullMethodName = "/runtime.v1.RuntimeService/Ping" + RuntimeService_GetRuntimeStatus_FullMethodName = "/runtime.v1.RuntimeService/GetRuntimeStatus" + RuntimeService_GetQueueStatus_FullMethodName = "/runtime.v1.RuntimeService/GetQueueStatus" + RuntimeService_GetLimiterStatus_FullMethodName = "/runtime.v1.RuntimeService/GetLimiterStatus" + RuntimeService_GetNamespaceLocks_FullMethodName = "/runtime.v1.RuntimeService/GetNamespaceLocks" + RuntimeService_GetQueuedTasks_FullMethodName = "/runtime.v1.RuntimeService/GetQueuedTasks" +) + +// RuntimeServiceClient is the client API for RuntimeService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RuntimeServiceClient interface { + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + GetRuntimeStatus(ctx context.Context, in *RuntimeStatusRequest, opts ...grpc.CallOption) (*RuntimeStatusResponse, error) + GetQueueStatus(ctx context.Context, in *QueueStatusRequest, opts ...grpc.CallOption) (*QueueStatusResponse, error) + GetLimiterStatus(ctx context.Context, in *LimiterStatusRequest, opts ...grpc.CallOption) (*LimiterStatusResponse, error) + GetNamespaceLocks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetQueuedTasks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*StructResponse, error) +} + +type runtimeServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRuntimeServiceClient(cc grpc.ClientConnInterface) RuntimeServiceClient { + return &runtimeServiceClient{cc} +} + +func (c *runtimeServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PingResponse) + err := c.cc.Invoke(ctx, RuntimeService_Ping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeServiceClient) GetRuntimeStatus(ctx context.Context, in *RuntimeStatusRequest, opts ...grpc.CallOption) (*RuntimeStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RuntimeStatusResponse) + err := c.cc.Invoke(ctx, RuntimeService_GetRuntimeStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeServiceClient) GetQueueStatus(ctx context.Context, in *QueueStatusRequest, opts ...grpc.CallOption) (*QueueStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(QueueStatusResponse) + err := c.cc.Invoke(ctx, RuntimeService_GetQueueStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeServiceClient) GetLimiterStatus(ctx context.Context, in *LimiterStatusRequest, opts ...grpc.CallOption) (*LimiterStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LimiterStatusResponse) + err := c.cc.Invoke(ctx, RuntimeService_GetLimiterStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeServiceClient) GetNamespaceLocks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, RuntimeService_GetNamespaceLocks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *runtimeServiceClient) GetQueuedTasks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*StructResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StructResponse) + err := c.cc.Invoke(ctx, RuntimeService_GetQueuedTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RuntimeServiceServer is the server API for RuntimeService service. +// All implementations must embed UnimplementedRuntimeServiceServer +// for forward compatibility. +type RuntimeServiceServer interface { + Ping(context.Context, *PingRequest) (*PingResponse, error) + GetRuntimeStatus(context.Context, *RuntimeStatusRequest) (*RuntimeStatusResponse, error) + GetQueueStatus(context.Context, *QueueStatusRequest) (*QueueStatusResponse, error) + GetLimiterStatus(context.Context, *LimiterStatusRequest) (*LimiterStatusResponse, error) + GetNamespaceLocks(context.Context, *PingRequest) (*StructResponse, error) + GetQueuedTasks(context.Context, *PingRequest) (*StructResponse, error) + mustEmbedUnimplementedRuntimeServiceServer() +} + +// UnimplementedRuntimeServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRuntimeServiceServer struct{} + +func (UnimplementedRuntimeServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedRuntimeServiceServer) GetRuntimeStatus(context.Context, *RuntimeStatusRequest) (*RuntimeStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRuntimeStatus not implemented") +} +func (UnimplementedRuntimeServiceServer) GetQueueStatus(context.Context, *QueueStatusRequest) (*QueueStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetQueueStatus not implemented") +} +func (UnimplementedRuntimeServiceServer) GetLimiterStatus(context.Context, *LimiterStatusRequest) (*LimiterStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetLimiterStatus not implemented") +} +func (UnimplementedRuntimeServiceServer) GetNamespaceLocks(context.Context, *PingRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetNamespaceLocks not implemented") +} +func (UnimplementedRuntimeServiceServer) GetQueuedTasks(context.Context, *PingRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetQueuedTasks not implemented") +} +func (UnimplementedRuntimeServiceServer) mustEmbedUnimplementedRuntimeServiceServer() {} +func (UnimplementedRuntimeServiceServer) testEmbeddedByValue() {} + +// UnsafeRuntimeServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RuntimeServiceServer will +// result in compilation errors. +type UnsafeRuntimeServiceServer interface { + mustEmbedUnimplementedRuntimeServiceServer() +} + +func RegisterRuntimeServiceServer(s grpc.ServiceRegistrar, srv RuntimeServiceServer) { + // If the following call panics, it indicates UnimplementedRuntimeServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RuntimeService_ServiceDesc, srv) +} + +func _RuntimeService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).Ping(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_GetRuntimeStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RuntimeStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).GetRuntimeStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_GetRuntimeStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).GetRuntimeStatus(ctx, req.(*RuntimeStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_GetQueueStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueueStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).GetQueueStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_GetQueueStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).GetQueueStatus(ctx, req.(*QueueStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_GetLimiterStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LimiterStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).GetLimiterStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_GetLimiterStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).GetLimiterStatus(ctx, req.(*LimiterStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_GetNamespaceLocks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).GetNamespaceLocks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_GetNamespaceLocks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).GetNamespaceLocks(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RuntimeService_GetQueuedTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RuntimeServiceServer).GetQueuedTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RuntimeService_GetQueuedTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RuntimeServiceServer).GetQueuedTasks(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RuntimeService_ServiceDesc is the grpc.ServiceDesc for RuntimeService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RuntimeService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "runtime.v1.RuntimeService", + HandlerType: (*RuntimeServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _RuntimeService_Ping_Handler, + }, + { + MethodName: "GetRuntimeStatus", + Handler: _RuntimeService_GetRuntimeStatus_Handler, + }, + { + MethodName: "GetQueueStatus", + Handler: _RuntimeService_GetQueueStatus_Handler, + }, + { + MethodName: "GetLimiterStatus", + Handler: _RuntimeService_GetLimiterStatus_Handler, + }, + { + MethodName: "GetNamespaceLocks", + Handler: _RuntimeService_GetNamespaceLocks_Handler, + }, + { + MethodName: "GetQueuedTasks", + Handler: _RuntimeService_GetQueuedTasks_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/runtime/v1/runtime.proto", +} diff --git a/src/proto/system/v1/system.pb.go b/src/proto/system/v1/system.pb.go new file mode 100644 index 00000000..e72870f3 --- /dev/null +++ b/src/proto/system/v1/system.pb.go @@ -0,0 +1,466 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: proto/system/v1/system.proto + +package systemv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + mi := &file_proto_system_v1_system_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{0} +} + +type PingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Service string `protobuf:"bytes,1,opt,name=service,proto3" json:"service,omitempty"` + AppId string `protobuf:"bytes,2,opt,name=app_id,json=appId,proto3" json:"app_id,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + TimestampUnix int64 `protobuf:"varint,4,opt,name=timestamp_unix,json=timestampUnix,proto3" json:"timestamp_unix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + mi := &file_proto_system_v1_system_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *PingResponse) GetAppId() string { + if x != nil { + return x.AppId + } + return "" +} + +func (x *PingResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PingResponse) GetTimestampUnix() int64 { + if x != nil { + return x.TimestampUnix + } + return 0 +} + +type ListConfigsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListConfigsRequest) Reset() { + *x = ListConfigsRequest{} + mi := &file_proto_system_v1_system_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListConfigsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListConfigsRequest) ProtoMessage() {} + +func (x *ListConfigsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListConfigsRequest.ProtoReflect.Descriptor instead. +func (*ListConfigsRequest) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{2} +} + +func (x *ListConfigsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type ListAuditLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Query *structpb.Struct `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAuditLogsRequest) Reset() { + *x = ListAuditLogsRequest{} + mi := &file_proto_system_v1_system_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAuditLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAuditLogsRequest) ProtoMessage() {} + +func (x *ListAuditLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAuditLogsRequest.ProtoReflect.Descriptor instead. +func (*ListAuditLogsRequest) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{3} +} + +func (x *ListAuditLogsRequest) GetQuery() *structpb.Struct { + if x != nil { + return x.Query + } + return nil +} + +type GetResourceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceRequest) Reset() { + *x = GetResourceRequest{} + mi := &file_proto_system_v1_system_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceRequest) ProtoMessage() {} + +func (x *GetResourceRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceRequest.ProtoReflect.Descriptor instead. +func (*GetResourceRequest) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{4} +} + +func (x *GetResourceRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type ResourceItemResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceItemResponse) Reset() { + *x = ResourceItemResponse{} + mi := &file_proto_system_v1_system_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceItemResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceItemResponse) ProtoMessage() {} + +func (x *ResourceItemResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceItemResponse.ProtoReflect.Descriptor instead. +func (*ResourceItemResponse) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{5} +} + +func (x *ResourceItemResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +type ResourceListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *structpb.Struct `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceListResponse) Reset() { + *x = ResourceListResponse{} + mi := &file_proto_system_v1_system_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceListResponse) ProtoMessage() {} + +func (x *ResourceListResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_system_v1_system_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceListResponse.ProtoReflect.Descriptor instead. +func (*ResourceListResponse) Descriptor() ([]byte, []int) { + return file_proto_system_v1_system_proto_rawDescGZIP(), []int{6} +} + +func (x *ResourceListResponse) GetData() *structpb.Struct { + if x != nil { + return x.Data + } + return nil +} + +var File_proto_system_v1_system_proto protoreflect.FileDescriptor + +const file_proto_system_v1_system_proto_rawDesc = "" + + "\n" + + "\x1cproto/system/v1/system.proto\x12\tsystem.v1\x1a\x1cgoogle/protobuf/struct.proto\"\r\n" + + "\vPingRequest\"~\n" + + "\fPingResponse\x12\x18\n" + + "\aservice\x18\x01 \x01(\tR\aservice\x12\x15\n" + + "\x06app_id\x18\x02 \x01(\tR\x05appId\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12%\n" + + "\x0etimestamp_unix\x18\x04 \x01(\x03R\rtimestampUnix\"C\n" + + "\x12ListConfigsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"E\n" + + "\x14ListAuditLogsRequest\x12-\n" + + "\x05query\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x05query\"$\n" + + "\x12GetResourceRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"C\n" + + "\x14ResourceItemResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data\"C\n" + + "\x14ResourceListResponse\x12+\n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\x99\a\n" + + "\rSystemService\x127\n" + + "\x04Ping\x12\x16.system.v1.PingRequest\x1a\x17.system.v1.PingResponse\x12D\n" + + "\tGetHealth\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12E\n" + + "\n" + + "GetMetrics\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12H\n" + + "\rGetSystemInfo\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12M\n" + + "\vListConfigs\x12\x1d.system.v1.ListConfigsRequest\x1a\x1f.system.v1.ResourceListResponse\x12K\n" + + "\tGetConfig\x12\x1d.system.v1.GetResourceRequest\x1a\x1f.system.v1.ResourceItemResponse\x12Q\n" + + "\rListAuditLogs\x12\x1f.system.v1.ListAuditLogsRequest\x1a\x1f.system.v1.ResourceListResponse\x12M\n" + + "\vGetAuditLog\x12\x1d.system.v1.GetResourceRequest\x1a\x1f.system.v1.ResourceItemResponse\x12M\n" + + "\x12ListNamespaceLocks\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12J\n" + + "\x0fListQueuedTasks\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12K\n" + + "\x10GetSystemMetrics\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponse\x12R\n" + + "\x17GetSystemMetricsHistory\x12\x16.system.v1.PingRequest\x1a\x1f.system.v1.ResourceItemResponseB Z\x1eaegis/proto/system/v1;systemv1b\x06proto3" + +var ( + file_proto_system_v1_system_proto_rawDescOnce sync.Once + file_proto_system_v1_system_proto_rawDescData []byte +) + +func file_proto_system_v1_system_proto_rawDescGZIP() []byte { + file_proto_system_v1_system_proto_rawDescOnce.Do(func() { + file_proto_system_v1_system_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_system_v1_system_proto_rawDesc), len(file_proto_system_v1_system_proto_rawDesc))) + }) + return file_proto_system_v1_system_proto_rawDescData +} + +var file_proto_system_v1_system_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_proto_system_v1_system_proto_goTypes = []any{ + (*PingRequest)(nil), // 0: system.v1.PingRequest + (*PingResponse)(nil), // 1: system.v1.PingResponse + (*ListConfigsRequest)(nil), // 2: system.v1.ListConfigsRequest + (*ListAuditLogsRequest)(nil), // 3: system.v1.ListAuditLogsRequest + (*GetResourceRequest)(nil), // 4: system.v1.GetResourceRequest + (*ResourceItemResponse)(nil), // 5: system.v1.ResourceItemResponse + (*ResourceListResponse)(nil), // 6: system.v1.ResourceListResponse + (*structpb.Struct)(nil), // 7: google.protobuf.Struct +} +var file_proto_system_v1_system_proto_depIdxs = []int32{ + 7, // 0: system.v1.ListConfigsRequest.query:type_name -> google.protobuf.Struct + 7, // 1: system.v1.ListAuditLogsRequest.query:type_name -> google.protobuf.Struct + 7, // 2: system.v1.ResourceItemResponse.data:type_name -> google.protobuf.Struct + 7, // 3: system.v1.ResourceListResponse.data:type_name -> google.protobuf.Struct + 0, // 4: system.v1.SystemService.Ping:input_type -> system.v1.PingRequest + 0, // 5: system.v1.SystemService.GetHealth:input_type -> system.v1.PingRequest + 0, // 6: system.v1.SystemService.GetMetrics:input_type -> system.v1.PingRequest + 0, // 7: system.v1.SystemService.GetSystemInfo:input_type -> system.v1.PingRequest + 2, // 8: system.v1.SystemService.ListConfigs:input_type -> system.v1.ListConfigsRequest + 4, // 9: system.v1.SystemService.GetConfig:input_type -> system.v1.GetResourceRequest + 3, // 10: system.v1.SystemService.ListAuditLogs:input_type -> system.v1.ListAuditLogsRequest + 4, // 11: system.v1.SystemService.GetAuditLog:input_type -> system.v1.GetResourceRequest + 0, // 12: system.v1.SystemService.ListNamespaceLocks:input_type -> system.v1.PingRequest + 0, // 13: system.v1.SystemService.ListQueuedTasks:input_type -> system.v1.PingRequest + 0, // 14: system.v1.SystemService.GetSystemMetrics:input_type -> system.v1.PingRequest + 0, // 15: system.v1.SystemService.GetSystemMetricsHistory:input_type -> system.v1.PingRequest + 1, // 16: system.v1.SystemService.Ping:output_type -> system.v1.PingResponse + 5, // 17: system.v1.SystemService.GetHealth:output_type -> system.v1.ResourceItemResponse + 5, // 18: system.v1.SystemService.GetMetrics:output_type -> system.v1.ResourceItemResponse + 5, // 19: system.v1.SystemService.GetSystemInfo:output_type -> system.v1.ResourceItemResponse + 6, // 20: system.v1.SystemService.ListConfigs:output_type -> system.v1.ResourceListResponse + 5, // 21: system.v1.SystemService.GetConfig:output_type -> system.v1.ResourceItemResponse + 6, // 22: system.v1.SystemService.ListAuditLogs:output_type -> system.v1.ResourceListResponse + 5, // 23: system.v1.SystemService.GetAuditLog:output_type -> system.v1.ResourceItemResponse + 5, // 24: system.v1.SystemService.ListNamespaceLocks:output_type -> system.v1.ResourceItemResponse + 5, // 25: system.v1.SystemService.ListQueuedTasks:output_type -> system.v1.ResourceItemResponse + 5, // 26: system.v1.SystemService.GetSystemMetrics:output_type -> system.v1.ResourceItemResponse + 5, // 27: system.v1.SystemService.GetSystemMetricsHistory:output_type -> system.v1.ResourceItemResponse + 16, // [16:28] is the sub-list for method output_type + 4, // [4:16] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_proto_system_v1_system_proto_init() } +func file_proto_system_v1_system_proto_init() { + if File_proto_system_v1_system_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_system_v1_system_proto_rawDesc), len(file_proto_system_v1_system_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_system_v1_system_proto_goTypes, + DependencyIndexes: file_proto_system_v1_system_proto_depIdxs, + MessageInfos: file_proto_system_v1_system_proto_msgTypes, + }.Build() + File_proto_system_v1_system_proto = out.File + file_proto_system_v1_system_proto_goTypes = nil + file_proto_system_v1_system_proto_depIdxs = nil +} diff --git a/src/proto/system/v1/system.proto b/src/proto/system/v1/system.proto new file mode 100644 index 00000000..2918a6cb --- /dev/null +++ b/src/proto/system/v1/system.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +package system.v1; + +option go_package = "aegis/proto/system/v1;systemv1"; + +import "google/protobuf/struct.proto"; + +service SystemService { + rpc Ping(PingRequest) returns (PingResponse); + rpc GetHealth(PingRequest) returns (ResourceItemResponse); + rpc GetMetrics(PingRequest) returns (ResourceItemResponse); + rpc GetSystemInfo(PingRequest) returns (ResourceItemResponse); + rpc ListConfigs(ListConfigsRequest) returns (ResourceListResponse); + rpc GetConfig(GetResourceRequest) returns (ResourceItemResponse); + rpc ListAuditLogs(ListAuditLogsRequest) returns (ResourceListResponse); + rpc GetAuditLog(GetResourceRequest) returns (ResourceItemResponse); + rpc ListNamespaceLocks(PingRequest) returns (ResourceItemResponse); + rpc ListQueuedTasks(PingRequest) returns (ResourceItemResponse); + rpc GetSystemMetrics(PingRequest) returns (ResourceItemResponse); + rpc GetSystemMetricsHistory(PingRequest) returns (ResourceItemResponse); +} + +message PingRequest {} + +message PingResponse { + string service = 1; + string app_id = 2; + string status = 3; + int64 timestamp_unix = 4; +} + +message ListConfigsRequest { + google.protobuf.Struct query = 1; +} + +message ListAuditLogsRequest { + google.protobuf.Struct query = 1; +} + +message GetResourceRequest { + int64 id = 1; +} + +message ResourceItemResponse { + google.protobuf.Struct data = 1; +} + +message ResourceListResponse { + google.protobuf.Struct data = 1; +} diff --git a/src/proto/system/v1/system_grpc.pb.go b/src/proto/system/v1/system_grpc.pb.go new file mode 100644 index 00000000..f132bc37 --- /dev/null +++ b/src/proto/system/v1/system_grpc.pb.go @@ -0,0 +1,539 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v5.29.3 +// source: proto/system/v1/system.proto + +package systemv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SystemService_Ping_FullMethodName = "/system.v1.SystemService/Ping" + SystemService_GetHealth_FullMethodName = "/system.v1.SystemService/GetHealth" + SystemService_GetMetrics_FullMethodName = "/system.v1.SystemService/GetMetrics" + SystemService_GetSystemInfo_FullMethodName = "/system.v1.SystemService/GetSystemInfo" + SystemService_ListConfigs_FullMethodName = "/system.v1.SystemService/ListConfigs" + SystemService_GetConfig_FullMethodName = "/system.v1.SystemService/GetConfig" + SystemService_ListAuditLogs_FullMethodName = "/system.v1.SystemService/ListAuditLogs" + SystemService_GetAuditLog_FullMethodName = "/system.v1.SystemService/GetAuditLog" + SystemService_ListNamespaceLocks_FullMethodName = "/system.v1.SystemService/ListNamespaceLocks" + SystemService_ListQueuedTasks_FullMethodName = "/system.v1.SystemService/ListQueuedTasks" + SystemService_GetSystemMetrics_FullMethodName = "/system.v1.SystemService/GetSystemMetrics" + SystemService_GetSystemMetricsHistory_FullMethodName = "/system.v1.SystemService/GetSystemMetricsHistory" +) + +// SystemServiceClient is the client API for SystemService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type SystemServiceClient interface { + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + GetHealth(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + GetMetrics(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + GetSystemInfo(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListConfigs(ctx context.Context, in *ListConfigsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetConfig(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListAuditLogs(ctx context.Context, in *ListAuditLogsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) + GetAuditLog(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListNamespaceLocks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + ListQueuedTasks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + GetSystemMetrics(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) + GetSystemMetricsHistory(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) +} + +type systemServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSystemServiceClient(cc grpc.ClientConnInterface) SystemServiceClient { + return &systemServiceClient{cc} +} + +func (c *systemServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PingResponse) + err := c.cc.Invoke(ctx, SystemService_Ping_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetHealth(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetHealth_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetMetrics(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetMetrics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetSystemInfo(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetSystemInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListConfigs(ctx context.Context, in *ListConfigsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, SystemService_ListConfigs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetConfig(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListAuditLogs(ctx context.Context, in *ListAuditLogsRequest, opts ...grpc.CallOption) (*ResourceListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceListResponse) + err := c.cc.Invoke(ctx, SystemService_ListAuditLogs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetAuditLog(ctx context.Context, in *GetResourceRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetAuditLog_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListNamespaceLocks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_ListNamespaceLocks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) ListQueuedTasks(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_ListQueuedTasks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetSystemMetrics(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetSystemMetrics_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *systemServiceClient) GetSystemMetricsHistory(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*ResourceItemResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ResourceItemResponse) + err := c.cc.Invoke(ctx, SystemService_GetSystemMetricsHistory_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SystemServiceServer is the server API for SystemService service. +// All implementations must embed UnimplementedSystemServiceServer +// for forward compatibility. +type SystemServiceServer interface { + Ping(context.Context, *PingRequest) (*PingResponse, error) + GetHealth(context.Context, *PingRequest) (*ResourceItemResponse, error) + GetMetrics(context.Context, *PingRequest) (*ResourceItemResponse, error) + GetSystemInfo(context.Context, *PingRequest) (*ResourceItemResponse, error) + ListConfigs(context.Context, *ListConfigsRequest) (*ResourceListResponse, error) + GetConfig(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + ListAuditLogs(context.Context, *ListAuditLogsRequest) (*ResourceListResponse, error) + GetAuditLog(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) + ListNamespaceLocks(context.Context, *PingRequest) (*ResourceItemResponse, error) + ListQueuedTasks(context.Context, *PingRequest) (*ResourceItemResponse, error) + GetSystemMetrics(context.Context, *PingRequest) (*ResourceItemResponse, error) + GetSystemMetricsHistory(context.Context, *PingRequest) (*ResourceItemResponse, error) + mustEmbedUnimplementedSystemServiceServer() +} + +// UnimplementedSystemServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSystemServiceServer struct{} + +func (UnimplementedSystemServiceServer) Ping(context.Context, *PingRequest) (*PingResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedSystemServiceServer) GetHealth(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetHealth not implemented") +} +func (UnimplementedSystemServiceServer) GetMetrics(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMetrics not implemented") +} +func (UnimplementedSystemServiceServer) GetSystemInfo(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemInfo not implemented") +} +func (UnimplementedSystemServiceServer) ListConfigs(context.Context, *ListConfigsRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListConfigs not implemented") +} +func (UnimplementedSystemServiceServer) GetConfig(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetConfig not implemented") +} +func (UnimplementedSystemServiceServer) ListAuditLogs(context.Context, *ListAuditLogsRequest) (*ResourceListResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAuditLogs not implemented") +} +func (UnimplementedSystemServiceServer) GetAuditLog(context.Context, *GetResourceRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAuditLog not implemented") +} +func (UnimplementedSystemServiceServer) ListNamespaceLocks(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListNamespaceLocks not implemented") +} +func (UnimplementedSystemServiceServer) ListQueuedTasks(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListQueuedTasks not implemented") +} +func (UnimplementedSystemServiceServer) GetSystemMetrics(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemMetrics not implemented") +} +func (UnimplementedSystemServiceServer) GetSystemMetricsHistory(context.Context, *PingRequest) (*ResourceItemResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemMetricsHistory not implemented") +} +func (UnimplementedSystemServiceServer) mustEmbedUnimplementedSystemServiceServer() {} +func (UnimplementedSystemServiceServer) testEmbeddedByValue() {} + +// UnsafeSystemServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SystemServiceServer will +// result in compilation errors. +type UnsafeSystemServiceServer interface { + mustEmbedUnimplementedSystemServiceServer() +} + +func RegisterSystemServiceServer(s grpc.ServiceRegistrar, srv SystemServiceServer) { + // If the following call panics, it indicates UnimplementedSystemServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SystemService_ServiceDesc, srv) +} + +func _SystemService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_Ping_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).Ping(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetHealth_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetHealth(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetHealth_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetHealth(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetMetrics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetMetrics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetMetrics(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetSystemInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetSystemInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetSystemInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetSystemInfo(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListConfigsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListConfigs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListConfigs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListConfigs(ctx, req.(*ListConfigsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetConfig(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListAuditLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAuditLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListAuditLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListAuditLogs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListAuditLogs(ctx, req.(*ListAuditLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetAuditLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetAuditLog(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetAuditLog_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetAuditLog(ctx, req.(*GetResourceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListNamespaceLocks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListNamespaceLocks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListNamespaceLocks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListNamespaceLocks(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_ListQueuedTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).ListQueuedTasks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_ListQueuedTasks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).ListQueuedTasks(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetSystemMetrics_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetSystemMetrics(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetSystemMetrics_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetSystemMetrics(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SystemService_GetSystemMetricsHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SystemServiceServer).GetSystemMetricsHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SystemService_GetSystemMetricsHistory_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SystemServiceServer).GetSystemMetricsHistory(ctx, req.(*PingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SystemService_ServiceDesc is the grpc.ServiceDesc for SystemService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SystemService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "system.v1.SystemService", + HandlerType: (*SystemServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _SystemService_Ping_Handler, + }, + { + MethodName: "GetHealth", + Handler: _SystemService_GetHealth_Handler, + }, + { + MethodName: "GetMetrics", + Handler: _SystemService_GetMetrics_Handler, + }, + { + MethodName: "GetSystemInfo", + Handler: _SystemService_GetSystemInfo_Handler, + }, + { + MethodName: "ListConfigs", + Handler: _SystemService_ListConfigs_Handler, + }, + { + MethodName: "GetConfig", + Handler: _SystemService_GetConfig_Handler, + }, + { + MethodName: "ListAuditLogs", + Handler: _SystemService_ListAuditLogs_Handler, + }, + { + MethodName: "GetAuditLog", + Handler: _SystemService_GetAuditLog_Handler, + }, + { + MethodName: "ListNamespaceLocks", + Handler: _SystemService_ListNamespaceLocks_Handler, + }, + { + MethodName: "ListQueuedTasks", + Handler: _SystemService_ListQueuedTasks_Handler, + }, + { + MethodName: "GetSystemMetrics", + Handler: _SystemService_GetSystemMetrics_Handler, + }, + { + MethodName: "GetSystemMetricsHistory", + Handler: _SystemService_GetSystemMetricsHistory_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/system/v1/system.proto", +} diff --git a/src/repository/container.go b/src/repository/container.go deleted file mode 100644 index 2f42d623..00000000 --- a/src/repository/container.go +++ /dev/null @@ -1,581 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/model" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -const ( - containerActiveNameOmitFields = "active_name" - containerOmitFields = "Versions" - containerVersionOmitFields = "active_version_key,HelmConfig,EnvVars" - helmConfigOmitFields = "Values" -) - -type ParameterConfigFetcher func(db *gorm.DB, keys []string, resourceID int) ([]model.ParameterConfig, error) - -// ===================================================================== -// Container Repository Functions -// ===================================================================== - -// CreateContainer creates a new container record -func CreateContainer(db *gorm.DB, container *model.Container) error { - if err := db.Omit(containerActiveNameOmitFields, containerOmitFields).Create(container).Error; err != nil { - return fmt.Errorf("failed to create container: %w", err) - } - return nil -} - -// DeleteContainer soft deletes a container by setting its status to deleted -func DeleteContainer(db *gorm.DB, containerID int) (int64, error) { - result := db.Model(&model.Container{}). - Where("id = ? AND status != ?", containerID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete container %d: %w", containerID, err) - } - return result.RowsAffected, nil -} - -// GetContainerByID retrieves a container by its ID -func GetContainerByID(db *gorm.DB, id int) (*model.Container, error) { - var container model.Container - if err := db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&container).Error; err != nil { - return nil, fmt.Errorf("failed to find container with id %d: %w", id, err) - } - return &container, nil -} - -// GetContainerStatistics returns statistics about containers -func GetContainerStatistics(db *gorm.DB) (map[string]int64, error) { - stats := make(map[string]int64) - - // Total containers - var total int64 - if err := db.Model(&model.Container{}).Count(&total).Error; err != nil { - return nil, fmt.Errorf("failed to count total containers: %v", err) - } - stats["total"] = total - - // Active containers - var active int64 - if err := db.Model(&model.Container{}).Where("status = 1").Count(&active).Error; err != nil { - return nil, fmt.Errorf("failed to count active containers: %v", err) - } - stats["active"] = active - - // Disabled containers - var disabled int64 - if err := db.Model(&model.Container{}).Where("status = 0").Count(&disabled).Error; err != nil { - return nil, fmt.Errorf("failed to count disabled containers: %v", err) - } - stats["disabled"] = disabled - - // Deleted containers - var deleted int64 - if err := db.Model(&model.Container{}).Where("status = -1").Count(&deleted).Error; err != nil { - return nil, fmt.Errorf("failed to count deleted containers: %v", err) - } - stats["deleted"] = deleted - - return stats, nil -} - -// ListContainers lists containers based on filter options -func ListContainers(db *gorm.DB, limit, offset int, containerType *consts.ContainerType, isPublic *bool, status *consts.StatusType) ([]model.Container, int64, error) { - var containers []model.Container - var total int64 - - query := db.Model(&model.Container{}) - if containerType != nil { - query = query.Where("type = ?", *containerType) - } - if isPublic != nil { - query = query.Where("is_public = ?", *isPublic) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count containers: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&containers).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list containers: %w", err) - } - - return containers, total, nil -} - -// ListContainersByID retrieves multiple containers by their IDs -func ListContainersByID(tx *gorm.DB, containerIDs []int) ([]model.Container, error) { - if len(containerIDs) == 0 { - return []model.Container{}, nil - } - - var containers []model.Container - if err := tx. - Where("id IN (?) AND status != ?", containerIDs, consts.CommonDeleted). - Find(&containers).Error; err != nil { - return nil, fmt.Errorf("failed to query containers: %w", err) - } - return containers, nil -} - -// UpdateContainer updates a container -func UpdateContainer(db *gorm.DB, container *model.Container) error { - if err := db.Omit(containerActiveNameOmitFields).Save(container).Error; err != nil { - return fmt.Errorf("failed to update container: %w", err) - } - return nil -} - -// ===================================================================== -// ContainerVersion Repository Functions -// ===================================================================== - -// BatchCreateContainerVersions creates multiple container versions -func BatchCreateContainerVersions(db *gorm.DB, versions []model.ContainerVersion) error { - if len(versions) == 0 { - return fmt.Errorf("no container versions to create") - } - - if err := db.Omit(containerVersionOmitFields).Create(&versions).Error; err != nil { - return fmt.Errorf("failed to batch create container versions: %w", err) - } - - return nil -} - -// BatchDeleteContainerVersions soft deletes all versions of a specific container -func BatchDeleteContainerVersions(db *gorm.DB, containerID int) (int64, error) { - result := db.Model(&model.ContainerVersion{}). - Where("container_id = ? AND status != ?", containerID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to batch soft delete container versions for container %d: %w", containerID, result.Error) - } - return result.RowsAffected, nil -} - -// BatchGetContainerVersions retrieves container versions for multiple container names -func BatchGetContainerVersions(db *gorm.DB, containerType consts.ContainerType, containerNames []string, userID int) ([]model.ContainerVersion, error) { - if len(containerNames) == 0 { - return []model.ContainerVersion{}, nil - } - - var versions []model.ContainerVersion - - query := db.Table("container_versions cv"). - Preload("Container"). - Where("cv.status = ?", consts.CommonEnabled). - Order("cv.container_id DESC, cv.name_major DESC, cv.name_minor DESC, cv.name_patch DESC") - - query = query.Joins("INNER JOIN containers c ON c.id = cv.container_id"). - Where("c.type = ? AND c.name IN (?) AND c.status = ?", containerType, containerNames, consts.CommonEnabled) - - if userID > 0 { - query = query.Joins( - "LEFT JOIN user_containers uc ON uc.container_id = c.id AND uc.user_id = ? AND uc.status = ?", - userID, consts.CommonEnabled, - ).Where( - db.Where("c.is_public = ?", true). - Or("uc.container_id IS NOT NULL"), - ) - } - - if err := query.Find(&versions).Error; err != nil { - return nil, fmt.Errorf("failed to query container versions: %w", err) - } - - return versions, nil -} - -// CheckContainerExistsWithDifferentType checks if a container exists with a different type -func CheckContainerExistsWithDifferentType(db *gorm.DB, containerName string, requestedType consts.ContainerType, userID int) (bool, consts.ContainerType, error) { - var container model.Container - - query := db.Table("containers"). - Where("name = ? AND type != ? AND status = ?", containerName, requestedType, consts.CommonEnabled) - - if userID > 0 { - query = query.Joins( - "LEFT JOIN user_containers uc ON uc.container_id = containers.id AND uc.user_id = ? AND uc.status = ?", - userID, consts.CommonEnabled, - ).Where( - db.Where("containers.is_public = ?", true). - Or("uc.container_id IS NOT NULL"), - ) - } - - if err := query.First(&container).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return false, 0, nil - } - return false, 0, fmt.Errorf("failed to check container existence: %w", err) - } - - return true, container.Type, nil -} - -// DeleteContainerVersion soft deletes a container version -func DeleteContainerVersion(db *gorm.DB, versionID int) (int64, error) { - result := db.Model(&model.ContainerVersion{}). - Where("id = ? AND status != ?", versionID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to soft delete container version %d: %w", versionID, result.Error) - } - return result.RowsAffected, nil -} - -// GetContainerVersionByID retrieves a ContainerVersion by its ID -func GetContainerVersionByID(db *gorm.DB, versionID int) (*model.ContainerVersion, error) { - var version model.ContainerVersion - if err := db. - Preload("Container"). - Preload("HelmConfig"). - Where("id = ?", versionID).First(&version).Error; err != nil { - return nil, fmt.Errorf("failed to find container version with id %d: %w", versionID, err) - } - return &version, nil -} - -// ListContainerVersions lists container versions with pagination and optional status filtering -func ListContainerVersions(db *gorm.DB, limit, offset int, containerID int, status *consts.StatusType) ([]model.ContainerVersion, int64, error) { - var versions []model.ContainerVersion - var total int64 - - query := db.Model(&model.ContainerVersion{}).Where("container_id = ?", containerID) - if status != nil { - query = query.Where("status = ?", *status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count container versions: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&versions).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list container versions: %v", err) - } - - return versions, total, nil -} - -// ListContainerVersions lists all versions of a specific container -func ListContainerVersionsByContainerID(db *gorm.DB, containerID int) ([]model.ContainerVersion, error) { - var versions []model.ContainerVersion - if err := db. - Preload("Container"). - Preload("HelmConfig"). - Where("container_id = ?", containerID). - Find(&versions).Error; err != nil { - return nil, fmt.Errorf("failed to list container versions for container %d: %w", containerID, err) - } - return versions, nil -} - -// UpdateContainerVersion updates a container version -func UpdateContainerVersion(db *gorm.DB, version *model.ContainerVersion) error { - if err := db.Omit(containerVersionOmitFields).Save(version).Error; err != nil { - return fmt.Errorf("failed to update container version: %w", err) - } - return nil -} - -// ===================================================================== -// HelmConfig Repository Functions -// ===================================================================== - -// BatchCreateHelmConfigs creates multiple helm configs -func BatchCreateHelmConfigs(db *gorm.DB, helmConfigs []*model.HelmConfig) error { - if len(helmConfigs) == 0 { - return fmt.Errorf("no helm configs to create") - } - - if err := db.Omit(helmConfigOmitFields).Create(helmConfigs).Error; err != nil { - return fmt.Errorf("failed to batch create helm configs: %v", err) - } - - return nil -} - -// GetHelmConfigByContainerVersionID retrieves the HelmConfig associated with a specific ContainerVersion ID -func GetHelmConfigByContainerVersionID(db *gorm.DB, versionID int) (*model.HelmConfig, error) { - var helmConfig model.HelmConfig - if err := db.Preload("ContainerVersion"). - Where("container_version_id = ?", versionID). - First(&helmConfig).Error; err != nil { - return nil, fmt.Errorf("failed to find helm config for version id %d: %w", versionID, err) - } - return &helmConfig, nil -} - -// UpdateHelmConfig updates a helm config -func UpdateHelmConfig(db *gorm.DB, helmConfig *model.HelmConfig) error { - if err := db.Save(helmConfig).Error; err != nil { - return fmt.Errorf("failed to update helm config: %w", err) - } - return nil -} - -// ===================================================================== -// ParameterConfig Repository Functions -// ===================================================================== - -// BatchCreateOrFindParameterConfigs creates multiple parameter configs or finds existing ones using upsert -func BatchCreateOrFindParameterConfigs(db *gorm.DB, params []model.ParameterConfig) error { - if len(params) == 0 { - return nil - } - - if err := db.Clauses(clause.OnConflict{ - OnConstraint: "idx_unique_config", - DoNothing: true, - }).Create(¶ms).Error; err != nil { - return fmt.Errorf("failed to batch create parameter configs: %w", err) - } - return nil -} - -// ListParameterConfigsByKeys retrieves ParameterConfigs by their keys, type and category -func ListParameterConfigsByKeys(db *gorm.DB, configs []model.ParameterConfig) ([]model.ParameterConfig, error) { - if len(configs) == 0 { - return []model.ParameterConfig{}, nil - } - - // Build query conditions for batch lookup - var results []model.ParameterConfig - query := db.Model(&model.ParameterConfig{}) - - // Build OR conditions for each config - conditions := db.Where("1 = 0") // Start with false condition - for _, cfg := range configs { - conditions = conditions.Or( - db.Where("config_key = ? AND type = ? AND category = ?", cfg.Key, cfg.Type, cfg.Category), - ) - } - - if err := query.Where(conditions).Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to list parameter configs by keys: %w", err) - } - - return results, nil -} - -// ===================================================================== -// ContainerLabel Repository Functions -// ===================================================================== - -// AddContainerLabels adds multiple container-label associations in a batch -func AddContainerLabels(db *gorm.DB, containerLabels []model.ContainerLabel) error { - if len(containerLabels) == 0 { - return nil - } - if err := db.Create(&containerLabels).Error; err != nil { - return fmt.Errorf("failed to add container-label associations: %w", err) - } - return nil -} - -// ClearContainerLabels removes label associations from specified containers -func ClearContainerLabels(db *gorm.DB, containerIDs []int, labelIDs []int) error { - if len(containerIDs) == 0 { - return nil - } - - query := db.Table("container_labels"). - Where("container_id IN (?)", containerIDs) - if len(labelIDs) > 0 { - query = query.Where("label_id IN (?)", labelIDs) - } - - if err := query.Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear container-label associations: %w", err) - } - return nil -} - -// RemoveContainersFromLabel removes all container associations from a specific label -func RemoveContainersFromLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Where("label_id = ?", labelID). - Delete(&model.ContainerLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to remove all containers from label %d: %w", labelID, err) - } - return result.RowsAffected, nil -} - -// RemoveContainersFromLabels removes all container associations from multiple labels -func RemoveContainersFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { - if len(labelIDs) == 0 { - return 0, nil - } - - result := db.Where("label_id IN (?)", labelIDs). - Delete(&model.ContainerLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to remove all containers from labels %v: %w", labelIDs, err) - } - return result.RowsAffected, nil -} - -// ListContainerLabels gets labels for multiple containers in batch -func ListContainerLabels(db *gorm.DB, containerIDs []int) (map[int][]model.Label, error) { - if len(containerIDs) == 0 { - return nil, nil - } - - type containerLabelResult struct { - model.Label - containerID int `gorm:"column:container_id"` - } - - var flatResults []containerLabelResult - if err := db.Model(&model.Label{}). - Joins("JOIN container_labels cl ON cl.label_id = labels.id"). - Where("cl.container_id IN (?)", containerIDs). - Select("labels.*, cl.container_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query container labels: %w", err) - } - - labelsMap := make(map[int][]model.Label) - for _, id := range containerIDs { - labelsMap[id] = []model.Label{} - } - - for _, res := range flatResults { - label := res.Label - labelsMap[res.containerID] = append(labelsMap[res.containerID], label) - } - - return labelsMap, nil -} - -// ListContainerLabelCounts retrieves the count of containers associated with each label ID -func ListContainerLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - if len(labelIDs) == 0 { - return make(map[int]int64), nil - } - - type containerLabelResult struct { - labelID int `gorm:"column:label_id"` - count int64 - } - - var results []containerLabelResult - if err := db.Model(&model.ContainerLabel{}). - Select("label_id, count(label_id) as count"). - Where("label_id IN (?)", labelIDs). - Group("label_id"). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to count associations: %w", err) - } - - countMap := make(map[int]int64, len(results)) - for _, result := range results { - countMap[result.labelID] = result.count - } - - return countMap, nil -} - -// ListLabelsByContainerID lists all labels associated with a specific container -func ListLabelsByContainerID(db *gorm.DB, containerID int) ([]model.Label, error) { - var labels []model.Label - if err := db.Model(&model.Label{}). - Joins("JOIN container_labels cl ON cl.label_id = labels.id"). - Where("cl.container_id = ?", containerID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels for container %d: %w", containerID, err) - } - return labels, nil -} - -// ListLabelIDsByKeyAndContainerID finds label IDs by keys associated with a specific container -func ListLabelIDsByKeyAndContainerID(db *gorm.DB, containerID int, keys []string) ([]int, error) { - var labelIDs []int - - err := db.Table("labels l"). - Select("l.id"). - Joins("JOIN container_labels cl ON cl.label_id = l.id"). - Where("cl.container_id = ? AND l.label_key IN (?)", containerID, keys). - Pluck("l.id", &labelIDs).Error - if err != nil { - return nil, fmt.Errorf("failed to find label IDs by keys for container %d: %w", containerID, err) - } - - return labelIDs, nil -} - -// ===================================================================== -// ContainerVersionEnvVar Repository Functions -// ===================================================================== - -// AddContainerVersionEnvVars adds multiple environment variable parameters for a specific container version -func AddContainerVersionEnvVars(db *gorm.DB, envVars []model.ContainerVersionEnvVar) error { - if len(envVars) == 0 { - return nil - } - if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&envVars).Error; err != nil { - return fmt.Errorf("failed to add container version env vars: %w", err) - } - return nil -} - -// ListContainerEnvVars lists environment variable parameters for a specific container version -func ListContainerVersionEnvVars(db *gorm.DB, keys []string, containerVersionID int) ([]model.ParameterConfig, error) { - query := db.Model(&model.ParameterConfig{}). - Joins("JOIN container_version_env_vars cvev ON cvev.parameter_config_id = parameter_configs.id"). - Where("cvev.container_version_id = ?", containerVersionID). - Where("parameter_configs.category = ?", consts.ParameterCategoryEnvVars) - - if len(keys) > 0 { - query = query.Where("parameter_configs.config_key IN (?)", keys) - } - - var params []model.ParameterConfig - if err := query.Find(¶ms).Error; err != nil { - return nil, fmt.Errorf("failed to list container env vars: %w", err) - } - return params, nil -} - -// ===================================================================== -// HelmConfigValues Repository Functions -// ===================================================================== - -// AddHelmConfigValues adds multiple helm value parameters for a specific helm config -func AddHelmConfigValues(db *gorm.DB, helmValues []model.HelmConfigValue) error { - if len(helmValues) == 0 { - return nil - } - if err := db.Clauses(clause.OnConflict{DoNothing: true}).Create(&helmValues).Error; err != nil { - return fmt.Errorf("failed to add helm config values: %w", err) - } - return nil -} - -// ListHelmConfigValues lists helm value parameters for a specific helm config -func ListHelmConfigValues(db *gorm.DB, keys []string, helmConfigID int) ([]model.ParameterConfig, error) { - query := db.Model(&model.ParameterConfig{}). - Joins("JOIN helm_config_values hcv ON hcv.parameter_config_id = parameter_configs.id"). - Where("hcv.helm_config_id = ?", helmConfigID) - - if len(keys) > 0 { - query = query.Where("parameter_configs.config_key IN (?)", keys) - } - - var params []model.ParameterConfig - if err := query.Find(¶ms).Error; err != nil { - return nil, fmt.Errorf("failed to list helm values: %w", err) - } - return params, nil -} diff --git a/src/repository/dataset.go b/src/repository/dataset.go deleted file mode 100644 index 3f1c4a54..00000000 --- a/src/repository/dataset.go +++ /dev/null @@ -1,485 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/consts" - "aegis/model" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -const ( - datasetActiveNameOmitFields = "active_name" - datasetVersionOmitFields = "active_version_key" -) - -// ===================================================================== -// Dataset Repository Functions -// ===================================================================== - -// CreateDataset creates a new dataset record -func CreateDataset(db *gorm.DB, dataset *model.Dataset) error { - if err := db.Omit(datasetActiveNameOmitFields).Create(dataset).Error; err != nil { - return fmt.Errorf("failed to create dataset: %v", err) - } - return nil -} - -// DeleteDataset soft deletes a dataset by setting its status to deleted -func DeleteDataset(db *gorm.DB, id int) (int64, error) { - result := db.Model(&model.Dataset{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete dataset: %v", err) - } - return result.RowsAffected, nil -} - -// GetDatasetByID gets dataset by ID -func GetDatasetByID(db *gorm.DB, id int) (*model.Dataset, error) { - var dataset model.Dataset - if err := db.Where("id = ? AND status != ?", id, consts.CommonDeleted).First(&dataset).Error; err != nil { - return nil, fmt.Errorf("failed to get dataset: %v", err) - } - return &dataset, nil -} - -// ListDatasets gets dataset list -func ListDatasets(db *gorm.DB, limit, offset int, datasetType string, isPublic *bool, status *consts.StatusType) ([]model.Dataset, int64, error) { - var datasets []model.Dataset - var total int64 - - query := db.Model(&model.Dataset{}) - if datasetType != "" { - query = query.Where("type = ?", datasetType) - } - if isPublic != nil { - query = query.Where("is_public = ?", *isPublic) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - // Get total count - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count datasets: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&datasets).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list datasets: %v", err) - } - - return datasets, total, nil -} - -// ListDatasetsByID retrieves multiple datasets by their IDs -func ListDatasetsByID(db *gorm.DB, datasetIDs []int) ([]model.Dataset, error) { - if len(datasetIDs) == 0 { - return []model.Dataset{}, nil - } - - var datasets []model.Dataset - if err := db. - Where("id IN (?) AND status != ?", datasetIDs, consts.CommonDeleted). - Find(&datasets).Error; err != nil { - return nil, fmt.Errorf("failed to query datasets: %w", err) - } - - return datasets, nil -} - -// UpdateDataset updates dataset information -func UpdateDataset(db *gorm.DB, dataset *model.Dataset) error { - if err := db.Omit(datasetActiveNameOmitFields).Save(dataset).Error; err != nil { - return fmt.Errorf("failed to update dataset: %v", err) - } - return nil -} - -// GetDatasetStatistics returns statistics about datasets -func GetDatasetStatistics(db *gorm.DB) (map[string]int64, error) { - stats := make(map[string]int64) - - // Total datasets - var total int64 - if err := db.Model(&model.Dataset{}).Count(&total).Error; err != nil { - return nil, fmt.Errorf("failed to count total datasets: %v", err) - } - stats["total"] = total - - // Active datasets - var active int64 - if err := db.Model(&model.Dataset{}).Where("status = ?", consts.DatapackInjectSuccess).Count(&active).Error; err != nil { - return nil, fmt.Errorf("failed to count active datasets: %v", err) - } - stats["active"] = active - - // Disabled datasets - var disabled int64 - if err := db.Model(&model.Dataset{}).Where("status = ?", consts.DatapackInitial).Count(&disabled).Error; err != nil { - return nil, fmt.Errorf("failed to count disabled datasets: %v", err) - } - stats["disabled"] = disabled - - // Deleted datasets - var deleted int64 - if err := db.Model(&model.Dataset{}).Where("status = ?", consts.CommonDeleted).Count(&deleted).Error; err != nil { - return nil, fmt.Errorf("failed to count deleted datasets: %v", err) - } - stats["deleted"] = deleted - - return stats, nil -} - -// ===================================================================== -// DatasetVersion Repository Functions -// ===================================================================== - -// BatchCreateDatasetVersions creates multiple dataset versions -func BatchCreateDatasetVersions(db *gorm.DB, versions []model.DatasetVersion) error { - if len(versions) == 0 { - return fmt.Errorf("no dataset versions to create") - } - - if err := db.Omit(datasetVersionOmitFields).Create(&versions).Error; err != nil { - return fmt.Errorf("failed to batch create dataset versions: %w", err) - } - - return nil -} - -// BatchDeleteDatasetVersions soft deletes all versions of a specific dataset -func BatchDeleteDatasetVersions(db *gorm.DB, datasetID int) (int64, error) { - result := db.Model(&model.DatasetVersion{}). - Where("dataset_id = ? AND status != ?", datasetID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to batch soft delete dataset versions for dataset %d: %w", datasetID, result.Error) - } - return result.RowsAffected, nil -} - -// BatchGetDatasetVersions retrieves dataset versions for multiple dataset names -func BatchGetDatasetVersions(db *gorm.DB, datasetNames []string, userID int) ([]model.DatasetVersion, error) { - if len(datasetNames) == 0 { - return []model.DatasetVersion{}, nil - } - - var versions []model.DatasetVersion - - query := db.Table("dataset_versions dv"). - Preload("Dataset"). - Where("dv.status = ?", consts.CommonEnabled). - Order("dv.dataset_id DESC, dv.name_major DESC, dv.name_minor DESC, dv.name_patch DESC") - - query = query.Joins("INNER JOIN datasets d ON d.id = dv.dataset_id"). - Where("d.name IN (?) AND d.status = ?", datasetNames, consts.CommonEnabled) - - if userID > 0 { - query = query.Joins( - "LEFT JOIN user_datasets ud ON ud.dataset_id = d.id AND ud.user_id = ? AND ud.status = ?", - userID, consts.CommonEnabled, - ).Where( - db.Where("d.is_public = ?", true). - Or("ud.dataset_id IS NOT NULL"), - ) - } - - if err := query.Find(&versions).Error; err != nil { - return nil, fmt.Errorf("failed to query dataset versions: %w", err) - } - - return versions, nil -} - -// DeleteDatasetVersion performs a soft delete on the dataset version by setting its status to deleted -func DeleteDatasetVersion(db *gorm.DB, versionID int) (int64, error) { - result := db.Model(&model.DatasetVersion{}). - Where("id = ? AND status != ?", versionID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to soft delete dataset version %d: %w", versionID, result.Error) - } - return result.RowsAffected, nil -} - -// GetDatasetVersionByID retrieves a dataset version by its ID -func GetDatasetVersionByID(db *gorm.DB, id int) (*model.DatasetVersion, error) { - var version model.DatasetVersion - if err := db.Preload("Datapacks").Where("id = ?", id).First(&version).Error; err != nil { - return nil, fmt.Errorf("failed to get dataset version: %v", err) - } - return &version, nil -} - -// ListDatasetVersions lists dataset versions with pagination and optional status filtering -func ListDatasetVersions(db *gorm.DB, limit, offset int, datasetID int, status *consts.StatusType) ([]model.DatasetVersion, int64, error) { - var versions []model.DatasetVersion - var total int64 - - query := db.Model(&model.DatasetVersion{}).Where("dataset_id = ?", datasetID) - if status != nil { - query = query.Where("status = ?", *status) - } - - // Get total count - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count dataset versions: %v", err) - } - - if err := query.Limit(limit).Offset(offset).Order("created_at DESC").Find(&versions).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list dataset versions: %v", err) - } - - return versions, total, nil -} - -// ListDatasetVersions lists all versions of a specific dataset -func ListDatasetVersionsByDatasetID(db *gorm.DB, datasetID int) ([]model.DatasetVersion, error) { - var versions []model.DatasetVersion - if err := db.Where("dataset_id = ?", datasetID).Find(&versions).Error; err != nil { - return nil, fmt.Errorf("failed to list dataset versions for dataset %d: %w", datasetID, err) - } - return versions, nil -} - -// UpdateDatasetVersion updates a dataset version -func UpdateDatasetVersion(db *gorm.DB, version *model.DatasetVersion) error { - if err := db.Omit(datasetVersionOmitFields).Save(version).Error; err != nil { - return fmt.Errorf("failed to update dataset version: %w", err) - } - return nil -} - -// ===================================================================== -// DatasetLabel Repository Functions -// ===================================================================== - -// AddDatasetLabels adds multiple dataset-label associations in a batch -func AddDatasetLabels(db *gorm.DB, datasetLabels []model.DatasetLabel) error { - if len(datasetLabels) == 0 { - return nil - } - if err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "dataset_id"}, {Name: "label_id"}}, - DoNothing: true, - }).Create(&datasetLabels).Error; err != nil { - return fmt.Errorf("failed to add dataset-label associations: %w", err) - } - return nil -} - -// ClearDatasetLabels removes label associations from specified datasets -func ClearDatasetLabels(db *gorm.DB, datasetIDs []int, labelIDs []int) error { - if len(datasetIDs) == 0 { - return nil - } - - query := db.Table("dataset_labels"). - Where("dataset_id IN (?)", datasetIDs) - if len(labelIDs) > 0 { - query = query.Where("label_id IN (?)", labelIDs) - } - - if err := query.Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear dataset-label associations: %w", err) - } - return nil -} - -// RemoveLabelsFromDataset removes all label associations from a specific dataset -func RemoveLabelsFromDataset(db *gorm.DB, datasetID int) error { - if err := db.Where("dataset_id = ?", datasetID). - Delete(&model.DatasetLabel{}).Error; err != nil { - return fmt.Errorf("failed to delete all labels from dataset %d: %w", datasetID, err) - } - return nil -} - -// RemoveDatasetsFromLabel removes all dataset associations from a specific label -func RemoveDatasetsFromLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Where("label_id = ?", labelID). - Delete(&model.DatasetLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all datasets from label %d: %w", labelID, err) - } - return result.RowsAffected, nil -} - -// RemoveDatasetsFromLabels removes all dataset associations from multiple labels -func RemoveDatasetsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { - if len(labelIDs) == 0 { - return 0, nil - } - - result := db.Where("label_id IN (?)", labelIDs). - Delete(&model.DatasetLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete all datasets from labels %v: %w", labelIDs, err) - } - return result.RowsAffected, nil -} - -// ListDatasetLabelCounts retrieves the count of datasets associated with each label ID -func ListDatasetLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - if len(labelIDs) == 0 { - return make(map[int]int64), nil - } - - type datasetLabelResult struct { - labelID int `gorm:"column:label_id"` - count int64 - } - - var results []datasetLabelResult - if err := db.Model(&model.DatasetLabel{}). - Select("label_id, count(label_id) as count"). - Where("label_id IN (?)", labelIDs). - Group("label_id"). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to count dataset-label associations: %w", err) - } - - countMap := make(map[int]int64, len(results)) - for _, result := range results { - countMap[result.labelID] = result.count - } - - return countMap, nil -} - -// ListDatasetLabels lists all labels associated with multiple datasets -func ListDatasetLabels(db *gorm.DB, datasetIDs []int) (map[int][]model.Label, error) { - if len(datasetIDs) == 0 { - return nil, nil - } - - type datasetLabelResult struct { - model.Label - datasetID int `gorm:"column:dataset_id"` - } - - var flatResults []datasetLabelResult - if err := db.Model(&model.Label{}). - Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). - Where("dl.dataset_id IN (?)", datasetIDs). - Select("labels.*, dl.dataset_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query dataset labels: %w", err) - } - - labelsMap := make(map[int][]model.Label) - for _, id := range datasetIDs { - labelsMap[id] = []model.Label{} - } - - for _, res := range flatResults { - label := res.Label - labelsMap[res.datasetID] = append(labelsMap[res.datasetID], label) - } - - return labelsMap, nil -} - -// ListLabelsByDatasetID lists all labels associated with a specific dataset -func ListLabelsByDatasetID(db *gorm.DB, datasetID int) ([]model.Label, error) { - var labels []model.Label - if err := db.Model(&model.Label{}). - Joins("JOIN dataset_labels dl ON dl.label_id = labels.id"). - Where("dl.dataset_id = ?", datasetID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels for dataset %d: %w", datasetID, err) - } - return labels, nil -} - -// ListLabelIDsByKeyAndInjectionID finds label IDs by keys associated with a specific injection -func ListLabelIDsByKeyAndDatasetID(db *gorm.DB, datasetID int, keys []string) ([]int, error) { - var labelIDs []int - - err := db.Table("labels l"). - Select("l.id"). - Joins("JOIN dataset_labels dl ON dl.label_id = l.id"). - Where("dl.dataset_id = ? AND l.label_key IN (?)", datasetID, keys). - Pluck("l.id", &labelIDs).Error - if err != nil { - return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) - } - - return labelIDs, nil -} - -// ===================================================================== -// DatasetVersionInjection Repository Functions -// ===================================================================== - -// AddDatasetVersionInjections adds multiple dataset-version-injection associations in a batch -func AddDatasetVersionInjections(db *gorm.DB, datasetVersionInjections []model.DatasetVersionInjection) error { - if len(datasetVersionInjections) == 0 { - return nil - } - if err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "dataset_version_id"}, {Name: "injection_id"}}, - DoNothing: true, - }).Create(&datasetVersionInjections).Error; err != nil { - return fmt.Errorf("failed to add dataset-version-injection associations: %w", err) - } - return nil -} - -// ClearDatasetVersionInjections removes fault injection associations from specified dataset versions -func ClearDatasetVersionInjections(db *gorm.DB, datasetVersionIDs []int, injectionIDs []int) error { - if len(datasetVersionIDs) == 0 { - return nil - } - - query := db.Table("dataset_version_injections"). - Where("dataset_version_id IN (?)", datasetVersionIDs) - if len(injectionIDs) > 0 { - query = query.Where("injection_id IN (?)", injectionIDs) - } - - if err := query.Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear dataset-version-injection associations: %w", err) - } - return nil -} - -// RemoveInjectionsFromDatasetVersion deletes all injection associations for a given dataset version -func RemoveInjectionsFromDatasetVersion(db *gorm.DB, datasetVersionID int) error { - if err := db.Where("dataset_version_id = ?", datasetVersionID). - Delete(&model.DatasetVersionInjection{}).Error; err != nil { - return fmt.Errorf("failed to delete all injections from dataset version %d: %w", datasetVersionID, err) - } - return nil -} - -// RemoveDatasetVersionsFromInjection deletes all dataset version associations for a given fault injection -func RemoveDatasetVersionsFromInjection(db *gorm.DB, faultInjectionID int) error { - if err := db.Where("injection_id = ?", faultInjectionID). - Delete(&model.DatasetVersionInjection{}).Error; err != nil { - return fmt.Errorf("failed to delete all dataset versions from fault injection %d: %w", faultInjectionID, err) - } - return nil -} - -// ListInjectionsByDatasetVersionID lists all fault injections associated with a specific dataset version -func ListInjectionsByDatasetVersionID(db *gorm.DB, datasetVersionID int, includeLabels bool) ([]model.FaultInjection, error) { - query := db.Model(&model.FaultInjection{}) - if includeLabels { - query = query.Preload("Labels") - } - - var injections []model.FaultInjection - if err := query. - Joins("JOIN dataset_version_injections dvi ON dvi.injection_id = id"). - Where("state = ? AND status != ?", consts.DatapackBuildSuccess, consts.CommonDeleted). - Where("dvi.dataset_version_id = ?", datasetVersionID). - Find(&injections).Error; err != nil { - return nil, fmt.Errorf("failed to list fault injections for dataset version %d: %w", datasetVersionID, err) - } - return injections, nil -} diff --git a/src/repository/detector.go b/src/repository/detector.go deleted file mode 100644 index 290ba786..00000000 --- a/src/repository/detector.go +++ /dev/null @@ -1,33 +0,0 @@ -package repository - -import ( - "fmt" - - "aegis/model" - - "gorm.io/gorm" -) - -// ListDetectorResultsByExecutionID lists detector results for a specific execution ID -func ListDetectorResultsByExecutionID(db *gorm.DB, executionID int) ([]model.DetectorResult, error) { - var results []model.DetectorResult - if err := db. - Where("execution_id = ?", executionID). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to list detectors for execution %d: %w", executionID, err) - } - return results, nil -} - -// SaveDetectorResults saves multiple detector results -func SaveDetectorResults(db *gorm.DB, results []model.DetectorResult) error { - if len(results) == 0 { - return fmt.Errorf("no detector results to save") - } - - if err := db.Create(&results).Error; err != nil { - return fmt.Errorf("failed to save detector results: %w", err) - } - - return nil -} diff --git a/src/repository/execution.go b/src/repository/execution.go deleted file mode 100644 index f2299ac9..00000000 --- a/src/repository/execution.go +++ /dev/null @@ -1,522 +0,0 @@ -package repository - -import ( - "fmt" - "strings" - - "gorm.io/gorm" - "gorm.io/gorm/clause" - - "aegis/consts" - "aegis/model" -) - -const BATCH_SIZE = 500 - -// ===================================================================== -// Execution Repository Functions -// ===================================================================== - -// BatchDeleteExecutions marks multiple executions as deleted in batch -func BatchDeleteExecutions(db *gorm.DB, executions []int) error { - if len(executions) == 0 { - return nil - } - - if err := db.Model(&model.Execution{}). - Where("id IN (?) AND status != ?", executions, consts.CommonDeleted). - Update("status", consts.CommonDeleted).Error; err != nil { - return fmt.Errorf("failed to batch delete executions: %w", err) - } - - return nil -} - -// CreateExecution creates a new execution result record -func CreateExecution(db *gorm.DB, execution *model.Execution) error { - if err := db.Create(execution).Error; err != nil { - return fmt.Errorf("failed to create execution result: %w", err) - } - return nil -} - -// GetExecutionByID retrieves an execution result by its ID with preloaded associations -func GetExecutionByID(db *gorm.DB, id int) (*model.Execution, error) { - var result model.Execution - if err := db. - Preload("AlgorithmVersion.Container"). - Preload("Datapack.Benchmark.Container"). - Preload("Datapack.Pedestal.Container"). - Preload("DatasetVersion"). - Preload("Task.Trace.Project"). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - First(&result).Error; err != nil { - return nil, fmt.Errorf("failed to find execution result with id %d: %w", id, err) - } - return &result, nil -} - -// ListExecutions lists executions based on filters and pagination -func ListExecutions(db *gorm.DB, limit, offset int, event *consts.ExecutionState, status *consts.StatusType, labelConditions []map[string]string) ([]model.Execution, int64, error) { - var executions []model.Execution - var total int64 - - query := db.Model(&model.Execution{}). - Preload("AlgorithmVersion.Container"). - Preload("Datapack.Benchmark.Container"). - Preload("Datapack.Pedestal.Container"). - Preload("DatasetVersion"). - Preload("Task.Trace.Project") - if event != nil { - query = query.Where("event = ?", *event) - } - if status != nil { - query = query.Where("status = ?", *status) - } - - if len(labelConditions) > 0 { - for _, condition := range labelConditions { - subQuery := db.Table("execution_injection_labels eil"). - Select("eil.execution_id"). - Joins("JOIN labels ON labels.id = eil.label_id"). - Where("labels.label_key = ? AND labels.label_value = ?", condition["key"], condition["value"]) - - query = query.Where("executions.id IN (?)", subQuery) - } - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count executions: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("updated_at DESC").Find(&executions).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list executions: %w", err) - } - - return executions, total, nil -} - -func ListExecutionsByDatapackIDs(db *gorm.DB, datapackIDs []int) ([]model.Execution, error) { - if len(datapackIDs) == 0 { - return make([]model.Execution, 0), nil - } - - var results []model.Execution - - query := db. - Preload("AlgorithmVersion.Container"). - Preload("Datapack.Benchmark.Container"). - Preload("Datapack.Pedestal.Container"). - Preload("DatasetVersion"). - Preload("Task.Trace.Project"). - Where("datapack_id IN (?) AND status != ?", datapackIDs, consts.CommonDeleted) - if err := query.Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to list executions by datapack IDs: %w", err) - } - - return results, nil -} - -// UpdateExecution updates fields of an execution record -func UpdateExecution(db *gorm.DB, id int, updates map[string]any) error { - result := db.Model(&model.Execution{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Updates(updates) - if err := result.Error; err != nil { - return result.Error - } - if result.RowsAffected == 0 { - return fmt.Errorf("execution not found or no changes made") - } - return nil -} - -// ===================================================================== -// ExecutionLabel Repository Functions -// ===================================================================== - -// AddExecutionLabels adds multiple execution-label associations -func AddExecutionLabels(db *gorm.DB, executionID int, labelIDs []int) error { - if len(labelIDs) == 0 { - return nil - } - - // Create ExecutionInjectionLabel associations - executionLabels := make([]model.ExecutionInjectionLabel, 0, len(labelIDs)) - for _, labelID := range labelIDs { - executionLabels = append(executionLabels, model.ExecutionInjectionLabel{ - ExecutionID: executionID, - LabelID: labelID, - }) - } - - if err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "execution_id"}, {Name: "label_id"}}, - DoNothing: true, - }).Create(&executionLabels).Error; err != nil { - return fmt.Errorf("failed to add execution-label associatons: %w", err) - } - - return nil -} - -// ClearExecutionLabels removes label associations from specified executions -func ClearExecutionLabels(db *gorm.DB, executionIDs []int, labelIDs []int) error { - if len(executionIDs) == 0 { - return nil - } - - query := db.Table("execution_injection_labels"). - Where("execution_id IN (?)", executionIDs) - if len(labelIDs) > 0 { - query = query.Where("label_id IN (?)", labelIDs) - } - - if err := query.Delete(nil).Error; err != nil { - return fmt.Errorf("failed to clear execution labels: %w", err) - } - return nil -} - -// RemoveLabelsFromExecution removes all label associations from a specific execution -func RemoveLabelsFromExecution(db *gorm.DB, executionID int) error { - if err := db.Where("execution_id = ?", executionID). - Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { - return fmt.Errorf("failed to remove all labels from execution %d: %w", executionID, err) - } - return nil -} - -// RemoveLabelsFromExecutions removes all label associations from multiple executions -func RemoveLabelsFromExecutions(db *gorm.DB, executionIDs []int) error { - if len(executionIDs) == 0 { - return nil - } - - if err := db.Where("execution_id IN (?)", executionIDs). - Delete(&model.ExecutionInjectionLabel{}).Error; err != nil { - return fmt.Errorf("failed to remove all labels from executions %v: %w", executionIDs, err) - } - return nil -} - -// RemoveExecutionsFromLabel deletes all execution-label associations for a specific label -func RemoveExecutionsFromLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Where("label_id = ?", labelID). - Delete(&model.ExecutionInjectionLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete execution-label associations for label %d: %w", labelID, err) - } - - return result.RowsAffected, nil -} - -// RemoveExecutionsFromLabels removes all execution-label associations for multiple labels -func RemoveExecutionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { - if len(labelIDs) == 0 { - return 0, nil - } - - result := db.Where("label_id IN (?)", labelIDs). - Delete(&model.ExecutionInjectionLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to delete execution-label associations for labels %v: %w", labelIDs, err) - } - - return result.RowsAffected, nil -} - -// ListExecutionsByDatapackFilter lists executions for a specific algorithm version and datapack name, with optional label filtering -func ListExecutionsByDatapackFilter(db *gorm.DB, algorithmVersionID int, datapackName string, labelConditions []map[string]string) ([]model.Execution, error) { - var executions []model.Execution - - query := db.Model(&model.Execution{}). - Preload("DetectorResults"). - Preload("GranularityResults"). - Preload("AlgorithmVersion.Container"). - Preload("Datapack"). - Joins("JOIN fault_injections fi ON executions.datapack_id = fi.id"). - Where("executions.algorithm_version_id = ? AND fi.name = ? AND executions.status != ?", - algorithmVersionID, datapackName, consts.CommonDeleted) - - if len(labelConditions) > 0 { - query = query. - Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). - Joins("JOIN labels l ON l.id = eil.label_id") - - var whereConditions *gorm.DB - for _, condition := range labelConditions { - if whereConditions == nil { - whereConditions = db.Where("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) - } else { - whereConditions = whereConditions.Or("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) - } - } - - if whereConditions != nil { - query = query.Where(whereConditions) - } - - query = query. - Group("executions.id"). - Having("COUNT(executions.id) = ?", len(labelConditions)) - } - - if err := query.Order("executions.updated_at DESC").Find(&executions).Error; err != nil { - return nil, fmt.Errorf("failed to list executions for algorithm %d and datapack %s: %w", - algorithmVersionID, datapackName, err) - } - - return executions, nil -} - -// ListExecutionsByDatasetFilter lists executions for a specific algorithm version and dataset version, with optional label filtering -func ListExecutionsByDatasetFilter(db *gorm.DB, algorithmVersionID, datasetVersionID int, labelConditions []map[string]string) ([]model.Execution, error) { - var executions []model.Execution - - query := db.Model(&model.Execution{}). - Preload("DetectorResults"). - Preload("GranularityResults"). - Preload("AlgorithmVersion.Container"). - Preload("Datapack"). - Preload("DatasetVersion"). - Preload("DatasetVersion.Injections"). - Where("executions.algorithm_version_id = ? AND executions.dataset_version_id = ? AND executions.status != ?", - algorithmVersionID, datasetVersionID, consts.CommonDeleted) - - if len(labelConditions) > 0 { - query = query. - Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). - Joins("JOIN labels l ON l.id = eil.label_id") - - var whereConditions *gorm.DB - for _, condition := range labelConditions { - if whereConditions == nil { - whereConditions = db.Where("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) - } else { - whereConditions = whereConditions.Or("l.label_key = ? AND l.label_value = ?", condition["key"], condition["value"]) - } - } - - if whereConditions != nil { - query = query.Where(whereConditions) - } - - query = query. - Group("executions.id"). - Having("COUNT(executions.id) = ?", len(labelConditions)) - } - - if err := query.Order("executions.updated_at DESC").Find(&executions).Error; err != nil { - return nil, fmt.Errorf("failed to list executions for algorithm %d and dataset version %d: %w", - algorithmVersionID, datasetVersionID, err) - } - - return executions, nil -} - -// ListExecutionIDsByLabels gets execution IDs associated with all specified labels -func ListExecutionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) ([]int, error) { - var executionIDs []int - query := db.Model(&model.Execution{}). - Select("DISTINCT executions.id"). - Joins("JOIN execution_injection_labels eil ON eil.execution_id = executions.id"). - Joins("JOIN labels ON labels.id = eil.label_id"). - Where("executions.status != ?", consts.CommonDeleted) - - var whereClauses []string - var whereArgs []any - - for _, condition := range labelConditions { - whereClauses = append(whereClauses, "(labels.label_key = ? AND labels.label_value = ?)") - whereArgs = append(whereArgs, condition["key"], condition["value"]) - } - - if len(whereClauses) > 0 { - whereClause := strings.Join(whereClauses, " OR ") - query = query.Where(whereClause, whereArgs...) - } - - if err := query.Pluck("executions.id", &executionIDs).Error; err != nil { - return nil, fmt.Errorf("failed to list execution IDs by labels: %w", err) - } - - return executionIDs, nil -} - -// ListExecutionLabels gets labels for multiple executions in batch -func ListExecutionLabels(db *gorm.DB, executionIDs []int) (map[int][]model.Label, error) { - if len(executionIDs) == 0 { - return nil, nil - } - - type executionLabelResult struct { - model.Label - executionID int `gorm:"column:execution_id"` - } - - var flatResults []executionLabelResult - if err := db.Model(&model.Label{}). - Joins("JOIN execution_injection_labels eil ON eil.label_id = labels.id"). - Where("eil.execution_id IN (?)", executionIDs). - Select("labels.*, eil.execution_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query execution labels: %w", err) - } - - labelsMap := make(map[int][]model.Label) - for _, id := range executionIDs { - labelsMap[id] = []model.Label{} - } - - for _, res := range flatResults { - label := res.Label - labelsMap[res.executionID] = append(labelsMap[res.executionID], label) - } - - return labelsMap, nil -} - -// ListExecutionLabelCounts retrieves the count of executions associated with each label ID -func ListExecutionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - if len(labelIDs) == 0 { - return make(map[int]int64), nil - } - - type executionLabelResult struct { - labelID int `gorm:"column:label_id"` - count int64 - } - - var results []executionLabelResult - if err := db.Table("execution_injection_labels eil"). - Select("eil.label_id, count(DISTINCT eil.execution_id) as count"). - Where("eil.label_id IN (?)", labelIDs). - Group("eil.label_id"). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to count execution-label associations: %w", err) - } - - countMap := make(map[int]int64, len(results)) - for _, result := range results { - countMap[result.labelID] = result.count - } - - return countMap, nil -} - -// ListLabelsByExecutionID retrieves all labels associated with a specific execution -func ListLabelsByExecutionID(db *gorm.DB, executionID int) ([]model.Label, error) { - var labels []model.Label - if err := db.Table("labels"). - Joins("JOIN execution_injection_labels eil ON labels.id = eil.label_id"). - Where("eil.execution_id = ?", executionID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to get execution labels: %v", err) - } - return labels, nil -} - -// ListLabelIDsByKeyAndExecutionID retrieves label IDs for a specific execution based on label keys -func ListLabelIDsByKeyAndExecutionID(db *gorm.DB, executionID int, keys []string) ([]int, error) { - var labelIDs []int - - err := db.Table("labels l"). - Select("l.id"). - Joins("JOIN execution_injection_labels eil ON eil.label_id = l.id"). - Where("eil.execution_id = ? AND l.label_key IN (?)", executionID, keys). - Pluck("l.id", &labelIDs).Error - if err != nil { - return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) - } - - return labelIDs, nil -} - -// GetExecutionStatistics returns statistics about executions -func GetExecutionStatistics(db *gorm.DB) (map[string]int64, error) { - stats := make(map[string]int64) - - // Total executions - var total int64 - if err := db.Model(&model.Execution{}).Count(&total).Error; err != nil { - return nil, fmt.Errorf("failed to count total executions: %w", err) - } - stats["total"] = total - - // Executions by status - type StatusCount struct { - Status string `json:"status"` - Count int64 `json:"count"` - } - - var statusCounts []StatusCount - err := db.Model(&model.Execution{}). - Select("status, COUNT(*) as count"). - Group("status"). - Find(&statusCounts).Error - - if err != nil { - return nil, fmt.Errorf("failed to count executions by status: %w", err) - } - - // Set status counts - for _, sc := range statusCounts { - switch sc.Status { - case "pending": - stats["pending"] = sc.Count - case "running": - stats["running"] = sc.Count - case "completed": - stats["completed"] = sc.Count - case "failed": - stats["failed"] = sc.Count - case "cancelled": - stats["cancelled"] = sc.Count - default: - stats[sc.Status] = sc.Count - } - } - - // Initialize missing statuses with 0 - statuses := []string{"pending", "running", "completed", "failed", "cancelled"} - for _, status := range statuses { - if _, exists := stats[status]; !exists { - stats[status] = 0 - } - } - - return stats, nil -} - -// ListExecutionsByProjectID retrieves executions for a specific project with pagination -func ListExecutionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]model.Execution, int64, error) { - var executions []model.Execution - var total int64 - - // Base query with JOIN and WHERE conditions - baseQuery := db.Model(&model.Execution{}). - Joins("JOIN tasks ON tasks.id = executions.task_id"). - Joins("JOIN traces on traces.id = tasks.trace_id"). - Where("traces.project_id = ? AND executions.status != ?", projectID, consts.CommonDeleted) - - // Count without Preload - if err := baseQuery.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count executions for project %d: %w", projectID, err) - } - - // Find with Preload - if err := baseQuery. - Preload("AlgorithmVersion.Container"). - Preload("Datapack.Benchmark.Container"). - Preload("Datapack.Pedestal.Container"). - Preload("DatasetVersion"). - Limit(limit). - Offset(offset). - Order("executions.updated_at DESC"). - Find(&executions).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list executions for project %d: %w", projectID, err) - } - - return executions, total, nil -} diff --git a/src/repository/granularity.go b/src/repository/granularity.go deleted file mode 100644 index 63e34d63..00000000 --- a/src/repository/granularity.go +++ /dev/null @@ -1,42 +0,0 @@ -package repository - -import ( - "errors" - "fmt" - - "aegis/consts" - "aegis/model" - - "gorm.io/gorm" -) - -// ListGranularityResultsByExecutionID lists granularity results for a specific execution ID -func ListGranularityResultsByExecutionID(db *gorm.DB, executionID int) ([]model.GranularityResult, error) { - var results []model.GranularityResult - if err := db. - Where("execution_id = ?", executionID). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to list granularity results for execution %d: %w", executionID, err) - } - return results, nil -} - -// SaveGranularityResults saves multiple granularity results -func SaveGranularityResults(db *gorm.DB, results []model.GranularityResult) error { - if len(results) == 0 { - return fmt.Errorf("no granularity results to create") - } - - for i := range results { - resultPtr := &results[i] - err := db.Omit(containerVersionOmitFields).Create(resultPtr).Error - if err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: index %d", consts.ErrAlreadyExists, i) - } - return fmt.Errorf("failed to create record index %d: %w", i, err) - } - } - - return nil -} diff --git a/src/repository/injection.go b/src/repository/injection.go deleted file mode 100644 index fea20477..00000000 --- a/src/repository/injection.go +++ /dev/null @@ -1,533 +0,0 @@ -package repository - -import ( - "encoding/json" - "fmt" - "strings" - "time" - - "aegis/consts" - "aegis/model" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -// ===================================================================== -// Injection Repository Functions -// ===================================================================== - -const injectionActiveNameOmitFields = "active_name" - -// BatchDeleteInjections marks multiple injections as deleted in batch -func BatchDeleteInjections(db *gorm.DB, injectionIDs []int) error { - if len(injectionIDs) == 0 { - return nil - } - - if err := db.Model(&model.FaultInjection{}). - Where("id IN (?) AND status != ?", injectionIDs, consts.CommonDeleted). - Update("status", consts.CommonDeleted).Error; err != nil { - return fmt.Errorf("failed to batch delete injections: %w", err) - } - - return nil -} - -// CreateInjection creates a fault injection record -func CreateInjection(db *gorm.DB, injection *model.FaultInjection) error { - if err := db.Omit(injectionActiveNameOmitFields).Create(injection).Error; err != nil { - return fmt.Errorf("failed to create injection: %w", err) - } - return nil -} - -// GetInjectionByID gets injection by ID with preloaded associations -func GetInjectionByID(db *gorm.DB, id int) (*model.FaultInjection, error) { - var injection model.FaultInjection - if err := db. - Preload("Task"). - Preload("Task.Trace"). - Preload("Benchmark.Container"). - Preload("Pedestal.Container"). - Where("id = ?", id).First(&injection).Error; err != nil { - return nil, fmt.Errorf("failed to find injection with id %d: %w", id, err) - } - return &injection, nil -} - -// GetInjectionByName gets injection by name with preloaded associations -func GetInjectionByName(db *gorm.DB, name string, includeLabels bool) (*model.FaultInjection, error) { - query := db - if includeLabels { - query = query.Preload("Labels") - } - - var injection model.FaultInjection - if err := query. - Where("name = ? AND status != ?", name, consts.CommonDeleted).First(&injection).Error; err != nil { - return nil, fmt.Errorf("failed to find injection with name %s: %w", name, err) - } - return &injection, nil -} - -// ListFaultInjectionsByID retrieves multiple fault injections by their IDs with preloaded associations -func ListFaultInjectionsByID(db *gorm.DB, injectionIDs []int) ([]model.FaultInjection, error) { - if len(injectionIDs) == 0 { - return []model.FaultInjection{}, nil - } - - var injections []model.FaultInjection - if err := db. - Preload("Benchmark.Container"). - Preload("Pedestal.Container"). - Preload("Task.Trace.Project"). - Preload("Labels"). - Where("id IN (?) AND status != ?", injectionIDs, consts.CommonDeleted). - Find(&injections).Error; err != nil { - return nil, fmt.Errorf("failed to query fault injections: %w", err) - } - return injections, nil -} - -// ListExistingEngineConfigs lists engine_config strings that already exist in DB and are considered completed builds. -// This is used to de-duplicate incoming injection requests by their engine configuration. -// Excludes records that have the "invalid" label. -func ListExistingEngineConfigs(db *gorm.DB, configs []string) ([]string, error) { - if len(configs) == 0 { - return []string{}, nil - } - - query := db. - Model(&model.FaultInjection{}). - Select("engine_config"). - Where("engine_config in (?) AND state >= ? AND status = ?", configs, consts.DatapackInjectSuccess, consts.CommonEnabled) - - invalidLabelSubQuery := db.Table("fault_injection_labels fil"). - Select("fil.fault_injection_id"). - Joins("JOIN labels ON labels.id = fil.label_id"). - Where("labels.label_key = ? AND labels.label_value = ?", consts.LabelKeyTag, "invalid") - - query = query.Where("fault_injections.id NOT IN (?)", invalidLabelSubQuery) - - var existingEngineConfigs []string - if err := query.Pluck("engine_config", &existingEngineConfigs).Error; err != nil { - return nil, err - } - - return existingEngineConfigs, nil -} - -// ListEngineConfigByNames retrieves engine configurations by injection names -func ListEngineConfigByNames(db *gorm.DB, names []string) (map[string]string, error) { - var records []struct { - Name string `gorm:"column:name"` - EngineConfig string `gorm:"column:engine_config"` - } - - if err := db. - Model(&model.FaultInjection{}). - Select("name, engine_config"). - Where("name IN (?)", names). - Find(&records).Error; err != nil { - return nil, fmt.Errorf("failed to query engine configs: %v", err) - } - - result := make(map[string]string, len(records)) - for _, record := range records { - result[record.Name] = record.EngineConfig - } - - return result, nil -} - -// ListInjectionIDsByNames retrieves injection IDs by their names -func ListInjectionIDsByNames(db *gorm.DB, names []string) (map[string]int, error) { - if len(names) == 0 { - return map[string]int{}, nil - } - - var records []struct { - Name string `gorm:"column:name"` - ID int `gorm:"column:id"` - } - - if err := db.Model(&model.FaultInjection{}). - Select("name, id"). - Where("state = ? AND status = ?", consts.DatapackBuildSuccess, consts.CommonEnabled). - Where("name IN (?)", names). - Find(&records).Error; err != nil { - return nil, fmt.Errorf("failed to query injection IDs: %w", err) - } - - result := make(map[string]int, len(records)) - for _, record := range records { - result[record.Name] = record.ID - } - - return result, nil -} - -// UpdateGroundtruth updates ground truth and its source for an injection -func UpdateGroundtruth(db *gorm.DB, id int, groundtruths []model.Groundtruth, source string) error { - gtJSON, err := json.Marshal(groundtruths) - if err != nil { - return fmt.Errorf("failed to marshal groundtruths: %w", err) - } - result := db.Model(&model.FaultInjection{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Updates(map[string]interface{}{ - "groundtruths": string(gtJSON), - "groundtruth_source": source, - }) - if result.Error != nil { - return fmt.Errorf("failed to update groundtruth for injection %d: %w", id, result.Error) - } - if result.RowsAffected == 0 { - return fmt.Errorf("injection with id %d: %w", id, consts.ErrNotFound) - } - return nil -} - -// UpdateInjection updates fields of a fault injection record -func UpdateInjection(db *gorm.DB, id int, updates map[string]any) error { - result := db.Model(&model.FaultInjection{}). - Where("id = ? AND status != ?", id, consts.CommonDeleted). - Updates(updates) - if err := result.Error; err != nil { - return result.Error - } - if result.RowsAffected == 0 { - return fmt.Errorf("injection not found or no changes made") - } - return nil -} - -// ListInjectionsNoIssues lists fault injections without issues based on label conditions and time range -func ListInjectionsNoIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionNoIssues, error) { - query := db.Model(&model.FaultInjectionNoIssues{}).Scopes(Sort("dataset_id desc")) - if startTime != nil { - query = query.Where("created_at >= ?", *startTime) - } - if endTime != nil { - query = query.Where("created_at <= ?", *endTime) - } - - // Filter by project_id if provided - if projectID != nil { - query = query.Where("project_id = ?", *projectID) - } - - if len(labelConditions) > 0 { - var whereConditions *gorm.DB - for _, condition := range labelConditions { - if whereConditions == nil { - whereConditions = db.Where("label_key = ? AND label_value = ?", condition["key"], condition["value"]) - } else { - whereConditions = whereConditions.Or("label_key = ? AND label_value = ?", condition["key"], condition["value"]) - } - } - - if whereConditions != nil { - query = query.Where(whereConditions) - } - - query = query. - Group("id"). - Having("COUNT(id) = ?", len(labelConditions)) - } - - var records []model.FaultInjectionNoIssues - if err := query.Find(&records).Error; err != nil { - return nil, fmt.Errorf("failed to query fault injections without issues: %v", err) - } - - return records, nil -} - -// ListInjectionsWithIssues lists fault injections with issues based on label conditions and time range -func ListInjectionsWithIssues(db *gorm.DB, labelConditions []map[string]string, startTime, endTime *time.Time, projectID *int) ([]model.FaultInjectionWithIssues, error) { - query := db.Model(&model.FaultInjectionNoIssues{}).Scopes(Sort("dataset_id desc")) - if startTime != nil { - query = query.Where("created_at >= ?", *startTime) - } - if endTime != nil { - query = query.Where("created_at <= ?", *endTime) - } - - // Filter by project_id if provided - if projectID != nil { - query = query.Where("project_id = ?", *projectID) - } - - if len(labelConditions) > 0 { - var whereConditions *gorm.DB - for _, condition := range labelConditions { - if whereConditions == nil { - whereConditions = db.Where("label_key = ? AND label_value = ?", condition["key"], condition["value"]) - } else { - whereConditions = whereConditions.Or("label_key = ? AND label_value = ?", condition["key"], condition["value"]) - } - } - - if whereConditions != nil { - query = query.Where(whereConditions) - } - - query = query. - Group("id"). - Having("COUNT(id) = ?", len(labelConditions)) - } - - var records []model.FaultInjectionWithIssues - if err := query.Find(&records).Error; err != nil { - return nil, fmt.Errorf("failed to query fault injections without issues: %v", err) - } - - return records, nil -} - -// ===================================================================== -// InjectionLabel Repository Functions -// ===================================================================== - -// Business layer: Injection labels are stored as FaultInjectionLabel in database - -// AddInjectionLabels adds multiple injection-label associations via FaultInjectionLabel -func AddInjectionLabels(db *gorm.DB, injectionID int, labelIDs []int) error { - if len(labelIDs) == 0 { - return nil - } - - // Create FaultInjectionLabel associations - injectionLabels := make([]model.FaultInjectionLabel, 0, len(labelIDs)) - for _, labelID := range labelIDs { - injectionLabels = append(injectionLabels, model.FaultInjectionLabel{ - FaultInjectionID: injectionID, - LabelID: labelID, - }) - } - - if err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "fault_injection_id"}, {Name: "label_id"}}, - DoNothing: true, - }).Create(&injectionLabels).Error; err != nil { - return fmt.Errorf("failed to add injection-label associations: %w", err) - } - - return nil -} - -// ClearInjectionLabels removes label associations from specified fault injections via FaultInjectionLabel -func ClearInjectionLabels(db *gorm.DB, injectionIDs []int, labelIDs []int) error { - if len(injectionIDs) == 0 { - return nil - } - - query := db.Table("fault_injection_labels"). - Where("fault_injection_id IN (?)", injectionIDs) - if len(labelIDs) > 0 { - query = query.Where("label_id IN (?)", labelIDs) - } - - if err := query.Delete(&model.FaultInjectionLabel{}).Error; err != nil { - return fmt.Errorf("failed to clear injection labels: %w", err) - } - return nil -} - -// RemoveInjectionsFromLabel removes all injection-label associations for a specific label -func RemoveInjectionsFromLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Where("label_id = ?", labelID). - Delete(&model.FaultInjectionLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to remove injection-label associations for label %d: %w", labelID, err) - } - - return result.RowsAffected, nil -} - -// RemoveInjectionsFromLabels removes all injection-label associations for multiple labels -func RemoveInjectionsFromLabels(db *gorm.DB, labelIDs []int) (int64, error) { - if len(labelIDs) == 0 { - return 0, nil - } - - result := db.Where("label_id IN (?)", labelIDs). - Delete(&model.FaultInjectionLabel{}) - if err := result.Error; err != nil { - return 0, fmt.Errorf("failed to remove injection-label associations for labels %v: %w", labelIDs, err) - } - - return result.RowsAffected, nil -} - -// RemoveLabelsFromInjection removes all label associations from a specific injection -func RemoveLabelsFromInjection(db *gorm.DB, injectionID int) error { - if err := db.Where("fault_injection_id = ?", injectionID). - Delete(&model.FaultInjectionLabel{}).Error; err != nil { - return fmt.Errorf("failed to remove all labels from injection %d: %w", injectionID, err) - } - return nil -} - -// RemoveLabelsFromInjections removes all label associations from multiple injections -func RemoveLabelsFromInjections(db *gorm.DB, injectionIDs []int) error { - if len(injectionIDs) == 0 { - return nil - } - - if err := db.Where("fault_injection_id IN (?)", injectionIDs). - Delete(&model.FaultInjectionLabel{}).Error; err != nil { - return fmt.Errorf("failed to remove all labels from injections %v: %w", injectionIDs, err) - } - return nil -} - -// ListInjectionIDsByLabels gets injection IDs associated with all specified labels -func ListInjectionIDsByLabels(db *gorm.DB, labelConditions []map[string]string) ([]int, error) { - var injectionIDs []int - query := db.Model(&model.FaultInjection{}). - Select("DISTINCT fault_injections.id"). - Joins("JOIN fault_injection_labels fil ON fil.fault_injection_id = fault_injections.id"). - Joins("JOIN labels ON labels.id = fil.label_id"). - Where("fault_injections.status != ?", consts.CommonDeleted) - - var whereClauses []string - var whereArgs []any - - for _, condition := range labelConditions { - whereClauses = append(whereClauses, "(labels.label_key = ? AND labels.label_value = ?)") - whereArgs = append(whereArgs, condition["key"], condition["value"]) - } - - if len(whereClauses) > 0 { - whereClause := strings.Join(whereClauses, " OR ") - query = query.Where(whereClause, whereArgs...) - } - - if err := query.Pluck("fault_injections.id", &injectionIDs).Error; err != nil { - return nil, fmt.Errorf("failed to list injection IDs by labels: %v", err) - } - - return injectionIDs, nil -} - -// ListInjectionLabels gets labels for multiple injections in batch -func ListInjectionLabels(db *gorm.DB, injectionIDs []int) (map[int][]model.Label, error) { - if len(injectionIDs) == 0 { - return nil, nil - } - - type injectionLabelResult struct { - model.Label - InjectionID int `gorm:"column:injection_id"` - } - - var flatResults []injectionLabelResult - if err := db.Model(&model.Label{}). - Joins("JOIN fault_injection_labels fil ON fil.label_id = labels.id"). - Where("fil.fault_injection_id IN (?)", injectionIDs). - Select("labels.*, fil.fault_injection_id as injection_id"). - Find(&flatResults).Error; err != nil { - return nil, fmt.Errorf("failed to batch query fault injection labels: %w", err) - } - - labelsMap := make(map[int][]model.Label) - for _, id := range injectionIDs { - labelsMap[id] = []model.Label{} - } - - for _, res := range flatResults { - label := res.Label - labelsMap[res.InjectionID] = append(labelsMap[res.InjectionID], label) - } - - return labelsMap, nil -} - -// ListInjectionLabelCounts retrieves the count of injections associated with each label ID -func ListInjectionLabelCounts(db *gorm.DB, labelIDs []int) (map[int]int64, error) { - if len(labelIDs) == 0 { - return make(map[int]int64), nil - } - - type injectionLabelResult struct { - labelID int `gorm:"column:label_id"` - count int64 - } - - var results []injectionLabelResult - if err := db.Table("fault_injection_labels fil"). - Select("fil.label_id, count(DISTINCT fil.fault_injection_id) as count"). - Where("fil.label_id IN (?)", labelIDs). - Group("fil.label_id"). - Find(&results).Error; err != nil { - return nil, fmt.Errorf("failed to count injection-label associations: %w", err) - } - - countMap := make(map[int]int64, len(results)) - for _, result := range results { - countMap[result.labelID] = result.count - } - - return countMap, nil -} - -// ListInjectionLabelsByInjectionID gets labels for a specific injection -func ListLabelsByInjectionID(db *gorm.DB, injectionID int) ([]model.Label, error) { - var labels []model.Label - if err := db.Table("labels"). - Joins("JOIN fault_injection_labels fil ON labels.id = fil.label_id"). - Where("fil.fault_injection_id = ?", injectionID). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to get injection labels: %v", err) - } - return labels, nil -} - -// ListLabelIDsByKeyAndInjectionID finds label IDs by keys associated with a specific injection via TaskLabel -func ListLabelIDsByKeyAndInjectionID(db *gorm.DB, injectionID int, keys []string) ([]int, error) { - var labelIDs []int - - err := db.Table("labels l"). - Select("l.id"). - Joins("JOIN fault_injection_labels fil ON fil.label_id = l.id"). - Where("fil.fault_injection_id = ? AND l.label_key IN (?)", injectionID, keys). - Pluck("l.id", &labelIDs).Error - if err != nil { - return nil, fmt.Errorf("failed to find label IDs by key '%s': %w", keys, err) - } - - return labelIDs, nil -} - -// ListInjectionsByProjectID retrieves fault injections for a specific project with pagination -func ListInjectionsByProjectID(db *gorm.DB, projectID int, limit, offset int) ([]model.FaultInjection, int64, error) { - var injections []model.FaultInjection - var total int64 - - // Base query with JOIN and WHERE conditions - baseQuery := db.Model(&model.FaultInjection{}). - Joins("JOIN tasks ON tasks.id = fault_injections.task_id"). - Joins("JOIN traces on traces.id = tasks.trace_id"). - Where("traces.project_id = ? AND fault_injections.status != ?", projectID, consts.CommonDeleted) - - // Count without Preload - if err := baseQuery.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count injections for project %d: %w", projectID, err) - } - - // Find with Preload - if err := baseQuery. - Preload("Benchmark.Container"). - Preload("Pedestal.Container"). - Limit(limit). - Offset(offset). - Order("fault_injections.updated_at DESC"). - Find(&injections).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list injections for project %d: %w", projectID, err) - } - - return injections, total, nil -} diff --git a/src/repository/label.go b/src/repository/label.go deleted file mode 100644 index 7e28b169..00000000 --- a/src/repository/label.go +++ /dev/null @@ -1,303 +0,0 @@ -package repository - -import ( - "errors" - "fmt" - - "aegis/consts" - "aegis/model" - labelmodule "aegis/module/label" - - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -const ( - labelKeyOmitFields = "active_key_value" -) - -// ===================================================================== -// Label Repository Functions -// ===================================================================== - -// BatchCreateLabels inserts multiple labels -func BatchCreateLabels(db *gorm.DB, labels []model.Label) error { - if len(labels) == 0 { - return nil - } - - if err := db.Omit(labelKeyOmitFields).Create(&labels).Error; err != nil { - return fmt.Errorf("failed to batch upsert labels: %w", err) - } - - return nil -} - -// BatchDeleteLabels marks multiple labels as deleted in batch -func BatchDeleteLabels(db *gorm.DB, labelIDs []int) error { - if len(labelIDs) == 0 { - return nil - } - - if err := db.Model(&model.Label{}). - Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). - Update("status", consts.CommonDeleted).Error; err != nil { - return fmt.Errorf("failed to batch delete labels: %w", err) - } - return nil -} - -// BatchIncreaseLabelUsages increases the usage counts of multiple labels -func BatchIncreaseLabelUsages(db *gorm.DB, labelIDs []int, increament int) error { - if len(labelIDs) == 0 { - return nil - } - - expr := gorm.Expr("usage_count + ?", increament) - if err := db.Model(&model.Label{}). - Where("id IN (?)", labelIDs). - UpdateColumn("usage_count", expr).Error; err != nil { - return fmt.Errorf("failed to batch increase label usages: %w", err) - } - - return nil -} - -// BatchDecreaseLabelUsages decreases the usage counts of multiple labels -func BatchDecreaseLabelUsages(db *gorm.DB, labelIDs []int, decrement int) error { - if len(labelIDs) == 0 { - return nil - } - - expr := gorm.Expr("GREATEST(0, usage_count - ?)", decrement) - if err := db.Model(&model.Label{}). - Where("id IN (?)", labelIDs). - Clauses(clause.Returning{}). - UpdateColumn("usage_count", expr).Error; err != nil { - return fmt.Errorf("failed to batch decrease label usages: %w", err) - } - return nil -} - -// BatchUpdateLabels updates multiple labels -func BatchUpdateLabels(db *gorm.DB, labels []model.Label) error { - if len(labels) == 0 { - return fmt.Errorf("no labels to update") - } - - if err := db.Omit(labelKeyOmitFields).Save(&labels).Error; err != nil { - return fmt.Errorf("failed to batch update labels: %w", err) - } - - return nil -} - -// CreateLabel creates a label -func CreateLabel(db *gorm.DB, label *model.Label) error { - if err := db.Omit(labelKeyOmitFields).Create(label).Error; err != nil { - return fmt.Errorf("failed to create label: %w", err) - } - return nil -} - -// DeleteLabel soft deletes a label by setting its status to deleted -func DeleteLabel(db *gorm.DB, labelID int) (int64, error) { - result := db.Model(&model.Label{}). - Where("id = ? AND status != ?", labelID, consts.CommonDeleted). - Update("status", consts.CommonDeleted) - if result.Error != nil { - return 0, fmt.Errorf("failed to soft delete project %d: %w", labelID, result.Error) - } - return result.RowsAffected, nil -} - -// GetLabelByID gets label by ID -func GetLabelByID(db *gorm.DB, id int) (*model.Label, error) { - var label model.Label - if err := db.First(&label, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("label with id %d not found", id) - } - return nil, fmt.Errorf("failed to get label: %w", err) - } - return &label, nil -} - -// GetLabelByKeyAndValue gets label by key and value -func GetLabelByKeyAndValue(db *gorm.DB, key, value string, status ...consts.StatusType) (*model.Label, error) { - query := db.Where("label_key = ? AND label_value = ?", key, value) - - if len(status) == 0 { - query = query.Where("status != ?", consts.CommonDeleted) - } else if len(status) == 1 { - query = query.Where("status = ?", status[0]) - } else { - query = query.Where("status IN (?)", status) - } - - var label model.Label - if err := query.First(&label).Error; err != nil { - return nil, fmt.Errorf("failed to get label: %w", err) - } - - return &label, nil -} - -// ListLabels gets the label list -func ListLabels(db *gorm.DB, limit, offset int, filterOptions *labelmodule.ListLabelFilters) ([]model.Label, int64, error) { - var labels []model.Label - var total int64 - - query := db.Model(&model.Label{}) - if filterOptions.Key != "" { - query = query.Where("label_key = ?", filterOptions.Key) - } - if filterOptions.Value != "" { - query = query.Where("label_value = ?", filterOptions.Value) - } - if filterOptions.Category != nil { - query = query.Where("category = ?", *filterOptions.Category) - } - if filterOptions.IsSystem != nil { - query = query.Where("is_system = ?", *filterOptions.IsSystem) - } - if filterOptions.Status != nil { - query = query.Where("status = ?", *filterOptions.Status) - } - - if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count labels: %w", err) - } - - if err := query.Limit(limit).Offset(offset).Order("usage_count DESC, created_at DESC").Find(&labels).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list labels: %w", err) - } - - return labels, total, nil -} - -// ListLabelsByConditions lists labels based on key-value conditions -func ListLabelsByConditions(db *gorm.DB, conditions []map[string]string) ([]model.Label, error) { - if len(conditions) == 0 { - return []model.Label{}, nil - } - - query := db.Model(&model.Label{}).Where("status != ?", consts.CommonDeleted) - orBuilder := db.Where("1 = 0") - - for _, condition := range conditions { - andBuilder := db.Where("1 = 1") - - if key, ok := condition["key"]; ok { - andBuilder = andBuilder.Where("label_key = ?", key) - } - if value, ok := condition["value"]; ok { - andBuilder = andBuilder.Where("label_value = ?", value) - } - - orBuilder = orBuilder.Or(andBuilder) - } - - var labels []model.Label - if err := query.Where(orBuilder).Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels by conditions: %w", err) - } - return labels, nil -} - -// ListLabelIDsByConditions lists label IDs based on key-value conditions and category -func ListLabelIDsByConditions(db *gorm.DB, conditions []map[string]string, category consts.LabelCategory) ([]int, error) { - if len(conditions) == 0 { - return []int{}, nil - } - - query := db.Model(&model.Label{}). - Where("status != ? AND category = ?", consts.CommonDeleted, category) - - orBuilder := db.Where("1 = 0") - - for _, condition := range conditions { - andBuilder := db.Where("1 = 1") - - if key, ok := condition["key"]; ok { - andBuilder = andBuilder.Where("label_key = ?", key) - } - if value, ok := condition["value"]; ok { - andBuilder = andBuilder.Where("label_value = ?", value) - } - - orBuilder = orBuilder.Or(andBuilder) - } - - var labelIDs []int - if err := query.Where(orBuilder).Pluck("id", &labelIDs).Error; err != nil { - return nil, fmt.Errorf("failed to list label IDs by conditions: %w", err) - } - return labelIDs, nil -} - -// ListLabelsByID lists labels by their IDs -func ListLabelsByID(db *gorm.DB, labelIDs []int) ([]model.Label, error) { - if len(labelIDs) == 0 { - return []model.Label{}, nil - } - - var labels []model.Label - if err := db. - Where("id IN (?) AND status != ?", labelIDs, consts.CommonDeleted). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels by IDs: %w", err) - } - return labels, nil -} - -// ListLabelsGroupByCategory lists labels grouped by their categories -func ListLabelsGroupByCategory(db *gorm.DB) (map[consts.LabelCategory][]model.Label, error) { - var labels []model.Label - if err := db. - Where("status != ?", consts.CommonDeleted). - Order("usage_count DESC, created_at DESC"). - Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to list labels: %w", err) - } - - groupedLabels := make(map[consts.LabelCategory][]model.Label) - for _, label := range labels { - groupedLabels[label.Category] = append(groupedLabels[label.Category], label) - } - - return groupedLabels, nil -} - -// SearchLabels searches for labels -func SearchLabels(db *gorm.DB, keyword string, category string, limit int) ([]model.Label, error) { - var labels []model.Label - - query := db.Model(&model.Label{}) - - if keyword != "" { - query = query.Where("key ILIKE ? OR value ILIKE ? OR description ILIKE ?", - "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") - } - - if category != "" { - query = query.Where("category = ?", category) - } - - if limit > 0 { - query = query.Limit(limit) - } - - if err := query.Order("usage_count DESC, created_at DESC").Find(&labels).Error; err != nil { - return nil, fmt.Errorf("failed to search labels: %w", err) - } - - return labels, nil -} - -func UpdateLabel(db *gorm.DB, label *model.Label) error { - if err := db.Omit(labelKeyOmitFields).Save(label).Error; err != nil { - return fmt.Errorf("failed to update label: %w", err) - } - return nil -} diff --git a/src/repository/scope.go b/src/repository/scope.go deleted file mode 100644 index 0d57073a..00000000 --- a/src/repository/scope.go +++ /dev/null @@ -1,51 +0,0 @@ -package repository - -import ( - "fmt" - - "gorm.io/gorm" -) - -// KeywordSearch applies a fuzzy keyword search across the provided fields. -func KeywordSearch(keyword string, fields ...string) func(db *gorm.DB) *gorm.DB { - return func(db *gorm.DB) *gorm.DB { - if keyword == "" { - return db - } - query := "" - for i, field := range fields { - if i > 0 { - query += " OR " - } - query += fmt.Sprintf("%s LIKE ?", field) - } - return db.Where(query, "%"+keyword+"%") - } -} - -func CursorPaginate(lastID uint, size int) func(db *gorm.DB) *gorm.DB { - return func(db *gorm.DB) *gorm.DB { - if lastID > 0 { - db = db.Where("id > ?", lastID) - } - return db.Limit(size) - } -} - -// Paginate applies offset/limit pagination. -func Paginate(pageNum, pageSize int) func(db *gorm.DB) *gorm.DB { - return func(db *gorm.DB) *gorm.DB { - offset := (pageNum - 1) * pageSize - return db.Offset(offset).Limit(pageSize) - } -} - -// Sort applies ordering with a default fallback. -func Sort(sort string) func(db *gorm.DB) *gorm.DB { - return func(db *gorm.DB) *gorm.DB { - if sort == "" { - sort = "id desc" - } - return db.Order(sort) - } -} diff --git a/src/router/router.go b/src/router/router.go index ebb3645a..d7cb5cb9 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -20,14 +20,15 @@ func New(handlers *Handlers, services ...middleware.Service) *gin.Engine { // CORS configuration config := cors.DefaultConfig() config.AllowAllOrigins = true - config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "Cache-Control", "X-Requested-With"} + config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "Cache-Control", "X-Request-Id"} config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"} config.AllowCredentials = true - config.ExposeHeaders = []string{"Content-Length", "Content-Type"} + config.ExposeHeaders = []string{"Content-Length", "Content-Type", "X-Request-Id"} // Middleware setup router.Use( middleware.InjectService(middlewareService), + middleware.RequestID(), middleware.GroupID(), middleware.SSEPath(), cors.New(config), diff --git a/src/repository/query_builder.go b/src/searchx/query_builder.go similarity index 52% rename from src/repository/query_builder.go rename to src/searchx/query_builder.go index 7165465e..63c5baef 100644 --- a/src/repository/query_builder.go +++ b/src/searchx/query_builder.go @@ -1,4 +1,4 @@ -package repository +package searchx import ( "encoding/json" @@ -11,75 +11,44 @@ import ( "gorm.io/gorm" ) -// SearchQueryBuilder provides methods to build complex database queries from SearchRequest -type SearchQueryBuilder[F ~string] struct { +// QueryBuilder provides methods to build complex database queries from SearchRequest. +type QueryBuilder[F ~string] struct { db *gorm.DB query *gorm.DB - allowedSortFields map[F]string // user field name -> DB column name (whitelist for sort/group) + allowedSortFields map[F]string } -// NewSearchQueryBuilder creates a new search query builder. -// allowedSortFields is a whitelist mapping user-facing field names to DB column names -// for sort and group_by operations. If nil, sorting defaults to "id DESC" only. -func NewSearchQueryBuilder[F ~string](db *gorm.DB, allowedSortFields map[F]string) *SearchQueryBuilder[F] { - return &SearchQueryBuilder[F]{ +func NewQueryBuilder[F ~string](db *gorm.DB, allowedSortFields map[F]string) *QueryBuilder[F] { + return &QueryBuilder[F]{ db: db, query: db, allowedSortFields: allowedSortFields, } } -// ApplySearchReq applies filters, sorting, and pagination from SearchRequest. -func (qb *SearchQueryBuilder[F]) ApplySearchReq(filters []dto.SearchFilter, keyword string, sortOptions []dto.TypedSortOption[F], groupBy []F, modelType interface{}) *gorm.DB { - // Start with the base query +func (qb *QueryBuilder[F]) ApplySearchReq(filters []dto.SearchFilter, keyword string, sortOptions []dto.TypedSortOption[F], groupBy []F, modelType any) *gorm.DB { qb.query = qb.db.Model(modelType) - - // Apply filters qb.applyFilters(filters) - - // Apply keyword search if provided if keyword != "" { qb.applyKeywordSearch(keyword, modelType) } - - // Apply sorting (group_by fields first, then user sort) qb.applySorting(sortOptions, groupBy) - return qb.query } -// applyFilters applies all filters to the query -func (qb *SearchQueryBuilder[F]) applyFilters(filters []dto.SearchFilter) { - for _, filter := range filters { - qb.applySingleFilter(filter) - } -} - -// applyInclude applies include options to the query -func (qb *SearchQueryBuilder[F]) applyIncludes(includes []string) { +func (qb *QueryBuilder[F]) ApplyIncludes(includes []string) { for _, include := range includes { qb.query = qb.query.Preload(include) } } -func (qb *SearchQueryBuilder[F]) ApplyIncludes(includes []string) { - qb.applyIncludes(includes) -} - -// applyIncludeFields includes specified fields in the query -func (qb *SearchQueryBuilder[F]) applyIncludeFields(includeFields []string) { +func (qb *QueryBuilder[F]) ApplyIncludeFields(includeFields []string) { for _, field := range includeFields { qb.query = qb.query.Select(field) } } -func (qb *SearchQueryBuilder[F]) ApplyIncludeFields(includeFields []string) { - qb.applyIncludeFields(includeFields) -} - -// applyExcludeFields excludes specified fields from the query -func (qb *SearchQueryBuilder[F]) applyExcludeFields(excludeFields []string, modelType interface{}) { - // Get all fields from model type +func (qb *QueryBuilder[F]) ApplyExcludeFields(excludeFields []string, modelType any) { t := reflect.TypeOf(modelType) if t.Kind() == reflect.Ptr { t = t.Elem() @@ -92,57 +61,59 @@ func (qb *SearchQueryBuilder[F]) applyExcludeFields(excludeFields []string, mode if dbTag != "" { dbField := strings.Split(dbTag, ";")[0] allFields = append(allFields, dbField) - } else { - allFields = append(allFields, field.Name) + continue } + allFields = append(allFields, field.Name) } - // Determine fields to select fieldsToSelect := make([]string, 0, len(allFields)) - excludeMap := make(map[string]struct{}) + excludeMap := make(map[string]struct{}, len(excludeFields)) for _, field := range excludeFields { excludeMap[field] = struct{}{} } - for _, field := range allFields { if _, excluded := excludeMap[field]; !excluded { fieldsToSelect = append(fieldsToSelect, field) } } - if len(fieldsToSelect) > 0 { qb.query = qb.query.Select(strings.Join(fieldsToSelect, ", ")) } } -func (qb *SearchQueryBuilder[F]) ApplyExcludeFields(excludeFields []string, modelType interface{}) { - qb.applyExcludeFields(excludeFields, modelType) +func (qb *QueryBuilder[F]) GetCount() (int64, error) { + var count int64 + err := qb.query.Count(&count).Error + return count, err } -// applyKeywordSearch applies general keyword search across searchable fields -func (qb *SearchQueryBuilder[F]) applyKeywordSearch(keyword string, modelType interface{}) { - // Get searchable fields from model type - searchableFields := qb.getSearchableFields(modelType) +func (qb *QueryBuilder[F]) Query() *gorm.DB { + return qb.query +} +func (qb *QueryBuilder[F]) applyFilters(filters []dto.SearchFilter) { + for _, filter := range filters { + qb.applySingleFilter(filter) + } +} + +func (qb *QueryBuilder[F]) applyKeywordSearch(keyword string, modelType any) { + searchableFields := qb.getSearchableFields(modelType) if len(searchableFields) == 0 { return } - // Build OR conditions for keyword search var conditions []string var values []any - for _, field := range searchableFields { conditions = append(conditions, fmt.Sprintf("%s LIKE ?", field)) values = append(values, "%"+keyword+"%") } - whereClause := strings.Join(conditions, " OR ") - qb.query = qb.query.Where(whereClause, values...) + qb.query = qb.query.Where(strings.Join(conditions, " OR "), values...) } -// applySingleFilter applies a single filter to the query -func (qb *SearchQueryBuilder[F]) applySingleFilter(filter dto.SearchFilter) { +func (qb *QueryBuilder[F]) applySingleFilter(filter dto.SearchFilter) { field := qb.sanitizeFieldName(filter.Field) if field == "" { return @@ -151,59 +122,42 @@ func (qb *SearchQueryBuilder[F]) applySingleFilter(filter dto.SearchFilter) { switch filter.Operator { case dto.OpEqual: qb.query = qb.query.Where(fmt.Sprintf("%s = ?", field), filter.Value) - case dto.OpNotEqual: qb.query = qb.query.Where(fmt.Sprintf("%s != ?", field), filter.Value) - case dto.OpGreater: qb.query = qb.query.Where(fmt.Sprintf("%s > ?", field), filter.Value) - case dto.OpGreaterEq: qb.query = qb.query.Where(fmt.Sprintf("%s >= ?", field), filter.Value) - case dto.OpLess: qb.query = qb.query.Where(fmt.Sprintf("%s < ?", field), filter.Value) - case dto.OpLessEq: qb.query = qb.query.Where(fmt.Sprintf("%s <= ?", field), filter.Value) - case dto.OpLike: qb.query = qb.query.Where(fmt.Sprintf("%s LIKE ?", field), "%"+fmt.Sprintf("%v", filter.Value)+"%") - case dto.OpStartsWith: qb.query = qb.query.Where(fmt.Sprintf("%s LIKE ?", field), fmt.Sprintf("%v", filter.Value)+"%") - case dto.OpEndsWith: qb.query = qb.query.Where(fmt.Sprintf("%s LIKE ?", field), "%"+fmt.Sprintf("%v", filter.Value)) - case dto.OpNotLike: qb.query = qb.query.Where(fmt.Sprintf("%s NOT LIKE ?", field), "%"+fmt.Sprintf("%v", filter.Value)+"%") - case dto.OpIn: if values := resolveMultiValues(filter); len(values) > 0 { qb.query = qb.query.Where(fmt.Sprintf("%s IN (?)", field), values) } - case dto.OpNotIn: if values := resolveMultiValues(filter); len(values) > 0 { qb.query = qb.query.Where(fmt.Sprintf("%s NOT IN (?)", field), values) } - case dto.OpIsNull: qb.query = qb.query.Where(fmt.Sprintf("%s IS NULL", field)) - case dto.OpIsNotNull: qb.query = qb.query.Where(fmt.Sprintf("%s IS NOT NULL", field)) - case dto.OpDateEqual: qb.query = qb.query.Where(fmt.Sprintf("DATE(%s) = DATE(?)", field), filter.Value) - case dto.OpDateAfter: qb.query = qb.query.Where(fmt.Sprintf("DATE(%s) > DATE(?)", field), filter.Value) - case dto.OpDateBefore: qb.query = qb.query.Where(fmt.Sprintf("DATE(%s) < DATE(?)", field), filter.Value) - case dto.OpDateBetween: if len(filter.Values) == 2 { qb.query = qb.query.Where(fmt.Sprintf("DATE(%s) BETWEEN DATE(?) AND DATE(?)", field), filter.Values[0], filter.Values[1]) @@ -211,19 +165,9 @@ func (qb *SearchQueryBuilder[F]) applySingleFilter(filter dto.SearchFilter) { } } -// applyPagination applies pagination to the query -func (qb *SearchQueryBuilder[F]) applyPagination(pagination *dto.PaginationReq) *gorm.DB { - offset := (pagination.Page - 1) * int(pagination.Size) - return qb.query.Offset(offset).Limit(int(pagination.Size)) -} - -// applySorting applies sorting to the query using a whitelist approach. -// GroupBy fields are applied first (ASC) to ensure items in the same group are adjacent, -// then user sort options are applied within each group. -func (qb *SearchQueryBuilder[F]) applySorting(sortOptions []dto.TypedSortOption[F], groupBy []F) { +func (qb *QueryBuilder[F]) applySorting(sortOptions []dto.TypedSortOption[F], groupBy []F) { applied := false - // Apply group_by fields first for consistent grouping order for _, field := range groupBy { if dbField, ok := qb.allowedSortFields[field]; ok { qb.query = qb.query.Order(dbField + " ASC") @@ -231,11 +175,10 @@ func (qb *SearchQueryBuilder[F]) applySorting(sortOptions []dto.TypedSortOption[ } } - // Apply user sort options (whitelist validated via typed key lookup) for _, sort := range sortOptions { dbField, ok := qb.allowedSortFields[sort.Field] if !ok { - continue // skip fields not in whitelist + continue } direction := "ASC" if strings.ToUpper(string(sort.Direction)) == "DESC" { @@ -250,27 +193,7 @@ func (qb *SearchQueryBuilder[F]) applySorting(sortOptions []dto.TypedSortOption[ } } -// GetCount gets the total count before pagination -func (qb *SearchQueryBuilder[F]) getCount() (int64, error) { - var count int64 - err := qb.query.Count(&count).Error - return count, err -} - -func (qb *SearchQueryBuilder[F]) GetCount() (int64, error) { - return qb.getCount() -} - -func (qb *SearchQueryBuilder[F]) Query() *gorm.DB { - return qb.query -} - -// getSearchableFields returns fields that can be searched with keywords -func (qb *SearchQueryBuilder[F]) getSearchableFields(modelType interface{}) []string { - // This is a simplified implementation - // In a real application, you might want to use struct tags or configuration - // to mark fields as searchable - +func (qb *QueryBuilder[F]) getSearchableFields(modelType any) []string { searchableFields := map[string][]string{ "User": {"username", "email", "full_name"}, "Role": {"name", "display_name", "description"}, @@ -285,12 +208,10 @@ func (qb *SearchQueryBuilder[F]) getSearchableFields(modelType interface{}) []st if fields, exists := searchableFields[typeName]; exists { return fields } - return []string{} } -// getTypeName gets the type name from interface -func (qb *SearchQueryBuilder[F]) getTypeName(modelType interface{}) string { +func (qb *QueryBuilder[F]) getTypeName(modelType any) string { t := reflect.TypeOf(modelType) if t.Kind() == reflect.Ptr { t = t.Elem() @@ -298,10 +219,7 @@ func (qb *SearchQueryBuilder[F]) getTypeName(modelType interface{}) string { return t.Name() } -// sanitizeFieldName validates that a field name contains only safe characters -// (alphanumeric, underscore, dot for table.column notation). -// Returns empty string if any unsafe character is detected. -func (qb *SearchQueryBuilder[F]) sanitizeFieldName(field string) string { +func (qb *QueryBuilder[F]) sanitizeFieldName(field string) string { if field == "" { return "" } @@ -313,26 +231,18 @@ func (qb *SearchQueryBuilder[F]) sanitizeFieldName(field string) string { return field } -// resolveMultiValues returns the effective []string for IN/NOT IN operators. -// It prefers filter.Values when populated; otherwise it tries to parse filter.Value -// as a JSON array (e.g. "[\"a\",\"b\"]" or "[1,2]"). -// A bare non-JSON single value is wrapped in a one-element slice. func resolveMultiValues(filter dto.SearchFilter) []string { if len(filter.Values) > 0 { return filter.Values } - if filter.Value == "" { + + if strings.TrimSpace(filter.Value) == "" { return nil } - // Try JSON array parse - var parsed []any - if err := json.Unmarshal([]byte(filter.Value), &parsed); err == nil { - result := make([]string, len(parsed)) - for i, v := range parsed { - result[i] = fmt.Sprintf("%v", v) - } - return result + + var items []string + if strings.HasPrefix(strings.TrimSpace(filter.Value), "[") && json.Unmarshal([]byte(filter.Value), &items) == nil { + return items } - // Fallback: treat the whole value as a single element return []string{filter.Value} } diff --git a/src/service/common/label.go b/src/service/common/label.go deleted file mode 100644 index 2c396819..00000000 --- a/src/service/common/label.go +++ /dev/null @@ -1,110 +0,0 @@ -package common - -import ( - "aegis/consts" - "aegis/dto" - "aegis/model" - "aegis/repository" - "aegis/utils" - "fmt" - "sort" - - "gorm.io/gorm" -) - -// ConvertLabelFiltersToConditions converts a slice of LabelFilter to a slice of map conditions -func ConvertLabelFiltersToConditions(labelItems []dto.LabelItem) []map[string]string { - if len(labelItems) == 0 { - return []map[string]string{} - } - - labelConditions := make([]map[string]string, 0, len(labelItems)) - for _, label := range labelItems { - labelConditions = append(labelConditions, map[string]string{ - "key": label.Key, - "value": label.Value, - }) - } - - return labelConditions -} - -// CreateOrUpdateLabelsFromItems creates or updates labels based on the provided label items -// Returns labels with correct IDs and updates usage_count for existing labels -func CreateOrUpdateLabelsFromItems(db *gorm.DB, labelItems []dto.LabelItem, category consts.LabelCategory) ([]model.Label, error) { - if len(labelItems) == 0 { - return []model.Label{}, nil - } - - // Build key -> value map for quick lookup - kvMap := make(map[string]dto.LabelItem, len(labelItems)) - for _, item := range labelItems { - kvMap[item.Key] = item - } - - // Find existing labels using slice conditions for repository - labelConditions := dto.ConvertLabelItemsToConditions(labelItems) - existingLabels, err := repository.ListLabelsByConditions(db, labelConditions) - if err != nil { - return nil, fmt.Errorf("failed to find existing labels: %w", err) - } - - // Separate existing and new labels - result := make([]model.Label, 0, len(labelItems)) - existingIDs := make([]int, 0, len(existingLabels)) - for _, existing := range existingLabels { - if item, ok := kvMap[existing.Key]; ok && item.Value == existing.Value { - result = append(result, existing) - existingIDs = append(existingIDs, existing.ID) - delete(kvMap, existing.Key) - } - } - - // Increase usage count for existing labels - if len(existingIDs) > 0 { - if err := repository.BatchIncreaseLabelUsages(db, existingIDs, 1); err != nil { - return nil, fmt.Errorf("failed to increase usage for existing labels: %w", err) - } - } - - // Create new labels (only those not found in existing) - if len(kvMap) > 0 { - newLabels := make([]model.Label, 0, len(kvMap)) - - for key, item := range kvMap { - newLabels = append(newLabels, model.Label{ - Key: key, - Value: item.Value, - Category: category, - Description: fmt.Sprintf(consts.CustomLabelDescriptionTemplate, key, consts.GetLabelCategoryName(category)), - Color: utils.GenerateColorFromKey(key), - Usage: consts.DefaultLabelUsage, - IsSystem: item.IsSystem, - Status: consts.CommonEnabled, - }) - } - - if err := repository.BatchCreateLabels(db, newLabels); err != nil { - return nil, fmt.Errorf("failed to create new labels: %w", err) - } - - result = append(result, newLabels...) - } - - // Sort by ID ascending - sort.Slice(result, func(i, j int) bool { - return result[i].ID < result[j].ID - }) - return result, nil -} - -func GetLabelConditionsByItems(labelItems []dto.LabelItem) []map[string]string { - labelConditions := make([]map[string]string, 0, len(labelItems)) - for _, item := range labelItems { - labelConditions = append(labelConditions, map[string]string{ - "key": item.Key, - "value": item.Value, - }) - } - return labelConditions -} diff --git a/src/service/common/template.go b/src/service/common/template.go deleted file mode 100644 index 1b71d18e..00000000 --- a/src/service/common/template.go +++ /dev/null @@ -1,62 +0,0 @@ -package common - -import ( - "aegis/utils" - "fmt" - "reflect" - "regexp" - "strings" -) - -var templateVarRegex = regexp.MustCompile(`{{\s*\.([a-zA-Z0-9_]+)\s*}}`) - -// extractTemplateVars extracts all variable names used in the template string -func extractTemplateVars(templateString string) []string { - matches := templateVarRegex.FindAllStringSubmatch(templateString, -1) - if matches == nil { - return nil - } - - variables := make([]string, 0, len(matches)) - for _, match := range matches { - if len(match) > 1 { - variables = append(variables, match[1]) - } - } - - return variables -} - -// renderTemplate renders the template string by replacing variables with values from the context structure -func renderTemplate(templateStr string, vars []string, context any) (string, error) { - contextValue := reflect.ValueOf(context) - if contextValue.Kind() == reflect.Ptr { - contextValue = contextValue.Elem() - } - - renderedString := templateStr - contextType := contextValue.Type() - - for _, varName := range vars { - fieldValue := contextValue.FieldByName(varName) - - if !fieldValue.IsValid() { - return "", fmt.Errorf("variable '%s' not found in context structure", varName) - } - - fieldType, found := contextType.FieldByName(varName) - if !found || fieldType.PkgPath != "" { - return "", fmt.Errorf("variable '%s' is not an exported field in context", varName) - } - - strValue, err := utils.ConvertSimpleTypeToString(fieldValue.Interface()) - if err != nil { - return "", fmt.Errorf("failed to convert context value for %s: %w", varName, err) - } - - renderedString = strings.ReplaceAll(renderedString, fmt.Sprintf("{{ .%s }}", varName), strValue) - renderedString = strings.ReplaceAll(renderedString, fmt.Sprintf("{{.%s}}", varName), strValue) - } - - return renderedString, nil -} diff --git a/src/service/consumer/algo_execution.go b/src/service/consumer/algo_execution.go index ef548b51..63baac35 100644 --- a/src/service/consumer/algo_execution.go +++ b/src/service/consumer/algo_execution.go @@ -3,7 +3,6 @@ package consumer import ( "context" "encoding/json" - "errors" "fmt" "math/rand" "path/filepath" @@ -16,8 +15,7 @@ import ( "aegis/dto" k8sinfra "aegis/infra/k8s" redisinfra "aegis/infra/redis" - "aegis/model" - "aegis/repository" + executionmodule "aegis/module/execution" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -108,7 +106,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDe return handleExecutionError(span, logEntry, "failed to parse execution payload", err) } - executionID, err := createExecution(deps.DB, task.TaskID, payload.algorithm.ID, payload.datapack.ID, payload.datasetVersionID, payload.labels) + executionID, err := createExecution(childCtx, deps, task.TaskID, payload.algorithm.ID, payload.datapack.ID, payload.datasetVersionID, payload.labels) if err != nil { return handleExecutionError(span, logEntry, "failed to create execution result", err) } @@ -343,51 +341,15 @@ func getAlgoJobEnvVars(taskID string, executionID int, datapackPathPrefix, expPa } // createExecution creates a new execution record with associated labels -func createExecution(db *gorm.DB, taskID string, algorithmVersionID, datapackID int, datasetVersionID *int, labelItems []dto.LabelItem) (int, error) { - var createdExecutionID int - if db == nil { - return 0, fmt.Errorf("consumer runtime db is nil") +func createExecution(ctx context.Context, deps RuntimeDeps, taskID string, algorithmVersionID, datapackID int, datasetVersionID *int, labelItems []dto.LabelItem) (int, error) { + if deps.ExecutionOwner == nil { + return 0, fmt.Errorf("execution owner service is nil") } - - err := db.Transaction(func(tx *gorm.DB) error { - execution := &model.Execution{ - TaskID: &taskID, - AlgorithmVersionID: algorithmVersionID, - DatapackID: datapackID, - DatasetVersionID: datasetVersionID, - State: consts.ExecutionInitial, - Status: consts.CommonEnabled, - } - - if err := repository.CreateExecution(tx, execution); err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return fmt.Errorf("%w: execution with algorithm_version_id %d and datapack_id %d already exists", consts.ErrAlreadyExists, algorithmVersionID, datapackID) - } - return fmt.Errorf("failed to create execution: %w", err) - } - - if len(labelItems) > 0 { - labels, err := common.CreateOrUpdateLabelsFromItems(tx, labelItems, consts.ExecutionCategory) - if err != nil { - return fmt.Errorf("failed to create or update labels: %w", err) - } - - labelIDs := make([]int, 0, len(labels)) - for _, label := range labels { - labelIDs = append(labelIDs, label.ID) - } - - if err := repository.AddExecutionLabels(tx, execution.ID, labelIDs); err != nil { - return fmt.Errorf("failed to add execution labels: %w", err) - } - } - - createdExecutionID = execution.ID - return nil + return deps.ExecutionOwner.CreateExecution(ctx, &executionmodule.RuntimeCreateExecutionReq{ + TaskID: taskID, + AlgorithmVersionID: algorithmVersionID, + DatapackID: datapackID, + DatasetVersionID: datasetVersionID, + Labels: labelItems, }) - if err != nil { - return 0, err - } - - return createdExecutionID, nil } diff --git a/src/service/consumer/build_datapack.go b/src/service/consumer/build_datapack.go index db8fa38f..e73534d4 100644 --- a/src/service/consumer/build_datapack.go +++ b/src/service/consumer/build_datapack.go @@ -51,11 +51,6 @@ func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volu } } -// executeBuildDatapack handles the execution of a datapack building task -func executeBuildDatapack(ctx context.Context, task *dto.UnifiedTask) error { - return executeBuildDatapackWithDeps(ctx, task, RuntimeDeps{}) -} - func executeBuildDatapackWithDeps(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) diff --git a/src/service/consumer/collect_result.go b/src/service/consumer/collect_result.go index cc0ae82a..c189d3c8 100644 --- a/src/service/consumer/collect_result.go +++ b/src/service/consumer/collect_result.go @@ -8,7 +8,7 @@ import ( "aegis/consts" "aegis/dto" redisinfra "aegis/infra/redis" - "aegis/repository" + executionmodule "aegis/module/execution" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -47,7 +47,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask, deps Runti } if collectPayload.algorithm.ContainerName == config.GetDetectorName() { - results, err := repository.ListDetectorResultsByExecutionID(db, collectPayload.executionID) + results, err := loadDetectorResults(childCtx, deps, db, collectPayload.executionID) if err != nil { logEntry.Errorf("failed to get detector results by execution ID: %v", err) span.AddEvent("failed to get detector results by execution ID") @@ -115,7 +115,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask, deps Runti return nil } - results, err := repository.ListGranularityResultsByExecutionID(db, collectPayload.executionID) + results, err := loadGranularityResults(childCtx, deps, db, collectPayload.executionID) if err != nil { span.AddEvent("failed to get detector results by execution ID") span.RecordError(err) @@ -137,6 +137,28 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask, deps Runti }) } +func loadDetectorResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]executionmodule.DetectorResultItem, error) { + if deps.ExecutionOwner == nil { + return nil, fmt.Errorf("execution owner service is nil") + } + resp, err := deps.ExecutionOwner.GetExecution(ctx, executionID) + if err != nil { + return nil, err + } + return resp.DetectorResults, nil +} + +func loadGranularityResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]executionmodule.GranularityResultItem, error) { + if deps.ExecutionOwner == nil { + return nil, fmt.Errorf("execution owner service is nil") + } + resp, err := deps.ExecutionOwner.GetExecution(ctx, executionID) + if err != nil { + return nil, err + } + return resp.GranularityResults, nil +} + // parseCollectPayload parses the payload for collect result tasks func parseCollectPayload(payload map[string]any) (*collectionPayload, error) { algorithm, err := utils.ConvertToType[dto.ContainerVersionItem](payload[consts.CollectAlgorithm]) diff --git a/src/service/consumer/fault_injection.go b/src/service/consumer/fault_injection.go index 588d1677..4fc2e629 100644 --- a/src/service/consumer/fault_injection.go +++ b/src/service/consumer/fault_injection.go @@ -11,15 +11,13 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - "aegis/repository" - "aegis/service/common" + injectionmodule "aegis/module/injection" "aegis/tracing" "aegis/utils" chaos "github.com/OperationsPAI/chaos-experiment/handler" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" - "gorm.io/gorm" ) // injectionPayload contains all necessary data for executing a fault injection batch @@ -97,10 +95,6 @@ func (bm *FaultBatchManager) setBatchInjections(batchID string, injectionNames [ // - display_config: JSON array of display maps for each fault func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDeps) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { - db := deps.DB - if db == nil { - return fmt.Errorf("consumer runtime db is nil") - } batchManager := deps.FaultBatchManager if batchManager == nil { return fmt.Errorf("fault batch manager is nil") @@ -215,44 +209,30 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask, deps Runt faultType = chaos.ChaosType(payload.nodes[0].Value) } - return db.Transaction(func(tx *gorm.DB) error { - injection := &model.FaultInjection{ - Name: name, - FaultType: faultType, - Category: payload.pedestal, - Description: fmt.Sprintf("Fault batch for task %s (%d faults)", task.TaskID, len(payload.nodes)), - DisplayConfig: utils.StringPtr(string(displayData)), - EngineConfig: string(engineData), - Groundtruths: groundtruths, - GroundtruthSource: consts.GroundtruthSourceAuto, - PreDuration: payload.preDuration, - State: consts.DatapackInitial, - Status: consts.CommonEnabled, - TaskID: &task.TaskID, - BenchmarkID: utils.IntPtr(payload.benchmark.ID), - PedestalID: utils.IntPtr(payload.pedestalID), - } - - if err = repository.CreateInjection(tx, injection); err != nil { - return handleExecutionError(span, logEntry, "failed to write fault injection schedule to database", err) - } - - labels, err := common.CreateOrUpdateLabelsFromItems(tx, payload.labels, consts.InjectionCategory) - if err != nil { - return handleExecutionError(span, logEntry, "failed to create or update labels", err) - } - - labelIDs := make([]int, 0, len(labels)) - for _, label := range labels { - labelIDs = append(labelIDs, label.ID) - } - - if err := repository.AddInjectionLabels(tx, injection.ID, labelIDs); err != nil { - return handleExecutionError(span, logEntry, "failed to associate labels with injection", err) - } + if deps.InjectionOwner == nil { + return handleExecutionError(span, logEntry, "injection owner service is nil", fmt.Errorf("missing injection owner service")) + } - return nil + _, err = deps.InjectionOwner.CreateInjection(childCtx, &injectionmodule.RuntimeCreateInjectionReq{ + Name: name, + FaultType: faultType, + Category: payload.pedestal, + Description: fmt.Sprintf("Fault batch for task %s (%d faults)", task.TaskID, len(payload.nodes)), + DisplayConfig: string(displayData), + EngineConfig: string(engineData), + Groundtruths: groundtruths, + GroundtruthSource: consts.GroundtruthSourceAuto, + PreDuration: payload.preDuration, + TaskID: task.TaskID, + BenchmarkID: utils.IntPtr(payload.benchmark.ID), + PedestalID: utils.IntPtr(payload.pedestalID), + Labels: payload.labels, + State: consts.DatapackInitial, }) + if err != nil { + return handleExecutionError(span, logEntry, "failed to write fault injection schedule to owner service", err) + } + return nil }) } diff --git a/src/service/consumer/k8s_handler.go b/src/service/consumer/k8s_handler.go index 3e12bd13..372f48de 100644 --- a/src/service/consumer/k8s_handler.go +++ b/src/service/consumer/k8s_handler.go @@ -12,6 +12,7 @@ import ( "aegis/dto" k8sinfra "aegis/infra/k8s" redisinfra "aegis/infra/redis" + containermodule "aegis/module/container" "aegis/service/common" "aegis/utils" @@ -135,10 +136,10 @@ type k8sHandler struct { batchManager *FaultBatchManager } -func NewHandler(db *gorm.DB, monitor NamespaceMonitor, algoLimiter *TokenBucketRateLimiter, k8sGateway *k8sinfra.Gateway, redisGateway *redisinfra.Gateway, batchManager *FaultBatchManager) *k8sHandler { +func NewHandler(db *gorm.DB, monitor NamespaceMonitor, algoLimiter *TokenBucketRateLimiter, k8sGateway *k8sinfra.Gateway, redisGateway *redisinfra.Gateway, batchManager *FaultBatchManager, execution ExecutionOwner, injection InjectionOwner) *k8sHandler { return &k8sHandler{ db: db, - store: newStateStore(db), + store: newStateStore(execution, injection), monitor: monitor, algoLimiter: algoLimiter, k8sGateway: k8sGateway, @@ -229,7 +230,7 @@ func (h *k8sHandler) HandleCRDFailed(name string, annotations map[string]string, errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) postprocess := func(injectionName string) { - if err := h.store.updateInjectionState(injectionName, consts.DatapackInjectFailed); err != nil { + if err := h.store.updateInjectionState(taskCtx, injectionName, consts.DatapackInjectFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } } @@ -290,11 +291,11 @@ func (h *k8sHandler) HandleCRDSucceeded(namespace, pod, name string, startTime, errCtx := NewErrorContext(taskCtx, h.db, h.redisGateway, taskSpan, &parsedLabels.taskIdentifiers) postProcess := func(injectionName string) { - if err := h.store.updateInjectionState(injectionName, consts.DatapackInjectSuccess); err != nil { + if err := h.store.updateInjectionState(taskCtx, injectionName, consts.DatapackInjectSuccess); err != nil { errCtx.Warn(nil, "update injection state failed", err) } - datapack, err := h.store.updateInjectionTimestamp(injectionName, startTime, endTime) + datapack, err := h.store.updateInjectionTimestamp(taskCtx, injectionName, startTime, endTime) if err != nil { errCtx.Warn(nil, "update injection timestamps failed", err) return @@ -463,7 +464,7 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st JobName: job.Name, } - if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildFailed); err != nil { + if err := h.store.updateInjectionState(taskCtx, parsedAnnotations.datapack.Name, consts.DatapackBuildFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } @@ -499,12 +500,12 @@ func (h *k8sHandler) HandleJobFailed(job *batchv1.Job, annotations map[string]st } if parsedAnnotations.algorithm.ContainerName == config.GetDetectorName() { - if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorFailed); err != nil { + if err := h.store.updateInjectionState(taskCtx, parsedAnnotations.datapack.Name, consts.DatapackDetectorFailed); err != nil { errCtx.Warn(nil, "update injection state failed", err) } } - if err := h.store.updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionFailed); err != nil { + if err := h.store.updateExecutionState(taskCtx, *parsedLabels.ExecutionID, consts.ExecutionFailed); err != nil { errCtx.Fatal(nil, "update execution state failed", err) return } @@ -567,7 +568,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string logEntry.Info("datapack build successfully") taskSpan.AddEvent("datapack build successfully") - if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackBuildSuccess); err != nil { + if err := h.store.updateInjectionState(taskCtx, parsedAnnotations.datapack.Name, consts.DatapackBuildSuccess); err != nil { errCtx.Fatal(nil, "update injection state failed", err) return } @@ -592,7 +593,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string Name: config.GetDetectorName(), } - algorithmVersionResults, err := common.MapRefsToContainerVersionsWithDB(h.db, []*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) + algorithmVersionResults, err := containermodule.NewRepository(h.db).ResolveContainerVersions([]*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) if err != nil { errCtx.Fatal(nil, "failed to map container refs to versions", err) return @@ -657,13 +658,13 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string taskSpan.AddEvent("algorithm execute successfully") if parsedAnnotations.algorithm.ContainerName == config.GetDetectorName() { - if err := h.store.updateInjectionState(parsedAnnotations.datapack.Name, consts.DatapackDetectorSuccess); err != nil { + if err := h.store.updateInjectionState(taskCtx, parsedAnnotations.datapack.Name, consts.DatapackDetectorSuccess); err != nil { errCtx.Fatal(nil, "update injection state failed", err) return } } - if err := h.store.updateExecutionState(*parsedLabels.ExecutionID, consts.ExecutionSuccess); err != nil { + if err := h.store.updateExecutionState(taskCtx, *parsedLabels.ExecutionID, consts.ExecutionSuccess); err != nil { errCtx.Fatal(nil, "update execution state failed", err) return } diff --git a/src/service/consumer/owner_adapter.go b/src/service/consumer/owner_adapter.go new file mode 100644 index 00000000..66f61e0f --- /dev/null +++ b/src/service/consumer/owner_adapter.go @@ -0,0 +1,171 @@ +package consumer + +import ( + "context" + "fmt" + + "aegis/dto" + "aegis/internalclient/orchestratorclient" + executionmodule "aegis/module/execution" + injectionmodule "aegis/module/injection" + + "go.uber.org/fx" +) + +// ExecutionOwner captures the execution owner operations used by runtime code. +type ExecutionOwner interface { + CreateExecution(context.Context, *executionmodule.RuntimeCreateExecutionReq) (int, error) + GetExecution(context.Context, int) (*executionmodule.ExecutionDetailResp, error) + UpdateExecutionState(context.Context, *executionmodule.RuntimeUpdateExecutionStateReq) error +} + +// InjectionOwner captures the injection owner operations used by runtime code. +type InjectionOwner interface { + CreateInjection(context.Context, *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) + UpdateInjectionState(context.Context, *injectionmodule.RuntimeUpdateInjectionStateReq) error + UpdateInjectionTimestamps(context.Context, *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) +} + +type executionOwnerAdapter struct { + orchestrator *orchestratorclient.Client + local *executionmodule.Service + requireRemote bool +} + +type executionOwnerParams struct { + fx.In + + Orchestrator *orchestratorclient.Client + Local *executionmodule.Service `optional:"true"` +} + +type injectionOwnerParams struct { + fx.In + + Orchestrator *orchestratorclient.Client + Local *injectionmodule.Service `optional:"true"` +} + +func NewExecutionOwner(params executionOwnerParams) ExecutionOwner { + return executionOwnerAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: false, + } +} + +func NewInjectionOwner(params injectionOwnerParams) InjectionOwner { + return injectionOwnerAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: false, + } +} + +func newRemoteExecutionOwner(params executionOwnerParams) ExecutionOwner { + return executionOwnerAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: true, + } +} + +func newRemoteInjectionOwner(params injectionOwnerParams) InjectionOwner { + return injectionOwnerAdapter{ + orchestrator: params.Orchestrator, + local: params.Local, + requireRemote: true, + } +} + +func (a executionOwnerAdapter) CreateExecution(ctx context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.CreateExecution(ctx, req) + } + if a.requireRemote { + return 0, fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return 0, fmt.Errorf("missing execution owner service") + } + return a.local.CreateExecutionRecord(ctx, req) +} + +func (a executionOwnerAdapter) GetExecution(ctx context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.GetExecution(ctx, executionID) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return nil, fmt.Errorf("missing execution owner service") + } + return a.local.GetExecution(ctx, executionID) +} + +func (a executionOwnerAdapter) UpdateExecutionState(ctx context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.UpdateExecutionState(ctx, req) + } + if a.requireRemote { + return fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return fmt.Errorf("missing execution owner service") + } + return a.local.UpdateExecutionState(ctx, req) +} + +type injectionOwnerAdapter struct { + orchestrator *orchestratorclient.Client + local *injectionmodule.Service + requireRemote bool +} + +func (a injectionOwnerAdapter) CreateInjection(ctx context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.CreateInjection(ctx, req) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return nil, fmt.Errorf("missing injection owner service") + } + return a.local.CreateInjectionRecord(ctx, req) +} + +func (a injectionOwnerAdapter) UpdateInjectionState(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.UpdateInjectionState(ctx, req) + } + if a.requireRemote { + return fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return fmt.Errorf("missing injection owner service") + } + return a.local.UpdateInjectionState(ctx, req) +} + +func (a injectionOwnerAdapter) UpdateInjectionTimestamps(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { + if a.orchestrator != nil && a.orchestrator.Enabled() { + return a.orchestrator.UpdateInjectionTimestamps(ctx, req) + } + if a.requireRemote { + return nil, fmt.Errorf("orchestrator-service owner is not configured") + } + if a.local == nil { + return nil, fmt.Errorf("missing injection owner service") + } + return a.local.UpdateInjectionTimestamps(ctx, req) +} + +// RemoteOwnerOptions forces the dedicated runtime-worker-service path to use orchestrator RPC only. +func RemoteOwnerOptions() fx.Option { + return fx.Options( + fx.Decorate(newRemoteExecutionOwner), + fx.Decorate(newRemoteInjectionOwner), + ) +} diff --git a/src/service/consumer/rate_limiter.go b/src/service/consumer/rate_limiter.go index ff620b71..093c65c1 100644 --- a/src/service/consumer/rate_limiter.go +++ b/src/service/consumer/rate_limiter.go @@ -32,6 +32,15 @@ type TokenBucketRateLimiter struct { serviceName string } +type RateLimiterSnapshot struct { + ServiceName string + BucketKey string + MaxTokens int + WaitTimeout time.Duration + InUseTokens int64 + InUseTokensLoadErr error +} + // GetConfig returns the current configuration func (r *TokenBucketRateLimiter) GetConfig() (maxTokens int, waitTimeout time.Duration) { r.mu.RLock() @@ -39,6 +48,20 @@ func (r *TokenBucketRateLimiter) GetConfig() (maxTokens int, waitTimeout time.Du return r.maxTokens, r.waitTimeout } +func (r *TokenBucketRateLimiter) Snapshot(ctx context.Context) RateLimiterSnapshot { + maxTokens, waitTimeout := r.GetConfig() + inUseTokens, err := r.store.inUse(ctx) + + return RateLimiterSnapshot{ + ServiceName: r.serviceName, + BucketKey: r.bucketKey, + MaxTokens: maxTokens, + WaitTimeout: waitTimeout, + InUseTokens: inUseTokens, + InUseTokensLoadErr: err, + } +} + // UpdateConfig dynamically updates the rate limiter configuration func (r *TokenBucketRateLimiter) UpdateConfig(maxTokens int, waitTimeout time.Duration) { r.mu.Lock() diff --git a/src/service/consumer/rate_limiter_store.go b/src/service/consumer/rate_limiter_store.go index a1ec5e85..4802e2ab 100644 --- a/src/service/consumer/rate_limiter_store.go +++ b/src/service/consumer/rate_limiter_store.go @@ -52,3 +52,11 @@ func (s tokenBucketStore) release(ctx context.Context, taskID string) (int64, er } return result, nil } + +func (s tokenBucketStore) inUse(ctx context.Context) (int64, error) { + result, err := s.client.SetCard(ctx, s.bucketKey) + if err != nil { + return 0, fmt.Errorf("failed to get token usage: %v", err) + } + return result, nil +} diff --git a/src/service/consumer/runtime_deps.go b/src/service/consumer/runtime_deps.go index 1694d052..ed8cd3e2 100644 --- a/src/service/consumer/runtime_deps.go +++ b/src/service/consumer/runtime_deps.go @@ -20,4 +20,6 @@ type RuntimeDeps struct { BuildKitGateway *buildkitinfra.Gateway HelmGateway *helminfra.Gateway FaultBatchManager *FaultBatchManager + ExecutionOwner ExecutionOwner + InjectionOwner InjectionOwner } diff --git a/src/service/consumer/runtime_snapshot.go b/src/service/consumer/runtime_snapshot.go new file mode 100644 index 00000000..de997cb8 --- /dev/null +++ b/src/service/consumer/runtime_snapshot.go @@ -0,0 +1,175 @@ +package consumer + +import ( + "context" + "fmt" + "time" + + "aegis/consts" + buildkitinfra "aegis/infra/buildkit" + helminfra "aegis/infra/helm" + k8sinfra "aegis/infra/k8s" + redisinfra "aegis/infra/redis" + + "gorm.io/gorm" +) + +const ( + RuntimeServiceName = "runtime-worker-service" + healthCheckTimeout = 2 * time.Second + runtimeModeWorker = "runtime-worker" +) + +type DependencyStatus struct { + Available bool + Healthy bool + Error string +} + +type RuntimeStatusSnapshot struct { + ServiceName string + Mode string + AppID string + StartedAt time.Time + UptimeSeconds int64 + DB DependencyStatus + Redis DependencyStatus + K8s DependencyStatus + BuildKit DependencyStatus + Helm DependencyStatus +} + +type RuntimeSnapshotService struct { + db *gorm.DB + redis *redisinfra.Gateway + k8s *k8sinfra.Gateway + buildkit *buildkitinfra.Gateway + helm *helminfra.Gateway + restart *TokenBucketRateLimiter + build *TokenBucketRateLimiter + algorithm *TokenBucketRateLimiter +} + +func NewRuntimeSnapshotService( + db *gorm.DB, + redis *redisinfra.Gateway, + k8s *k8sinfra.Gateway, + buildkit *buildkitinfra.Gateway, + helm *helminfra.Gateway, + restart *TokenBucketRateLimiter, + build *TokenBucketRateLimiter, + algorithm *TokenBucketRateLimiter, +) *RuntimeSnapshotService { + return &RuntimeSnapshotService{ + db: db, + redis: redis, + k8s: k8s, + buildkit: buildkit, + helm: helm, + restart: restart, + build: build, + algorithm: algorithm, + } +} + +func (s *RuntimeSnapshotService) RuntimeStatus(ctx context.Context) RuntimeStatusSnapshot { + startedAt := time.Now() + if consts.InitialTime != nil { + startedAt = *consts.InitialTime + } + + return RuntimeStatusSnapshot{ + ServiceName: RuntimeServiceName, + Mode: runtimeModeWorker, + AppID: consts.AppID, + StartedAt: startedAt, + UptimeSeconds: int64(time.Since(startedAt).Seconds()), + DB: s.dbStatus(ctx), + Redis: s.redisStatus(ctx), + K8s: s.k8sStatus(ctx), + BuildKit: s.buildkitStatus(ctx), + Helm: s.helmStatus(), + } +} + +func (s *RuntimeSnapshotService) QueueStatus(ctx context.Context) (redisinfra.TaskQueueStats, error) { + if s.redis == nil { + return redisinfra.TaskQueueStats{}, fmt.Errorf("redis gateway is nil") + } + return s.redis.GetTaskQueueStats(ctx) +} + +func (s *RuntimeSnapshotService) LimiterStatus(ctx context.Context) []RateLimiterSnapshot { + limiters := make([]RateLimiterSnapshot, 0, 3) + for _, limiter := range []*TokenBucketRateLimiter{s.restart, s.build, s.algorithm} { + if limiter == nil { + continue + } + limiters = append(limiters, limiter.Snapshot(ctx)) + } + return limiters +} + +func (s *RuntimeSnapshotService) dbStatus(ctx context.Context) DependencyStatus { + if s.db == nil { + return DependencyStatus{Available: false} + } + + sqlDB, err := s.db.DB() + if err != nil { + return DependencyStatus{Available: true, Healthy: false, Error: err.Error()} + } + + checkCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), healthCheckTimeout) + defer cancel() + if err := sqlDB.PingContext(checkCtx); err != nil { + return DependencyStatus{Available: true, Healthy: false, Error: err.Error()} + } + return DependencyStatus{Available: true, Healthy: true} +} + +func (s *RuntimeSnapshotService) redisStatus(ctx context.Context) DependencyStatus { + if s.redis == nil { + return DependencyStatus{Available: false} + } + + checkCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), healthCheckTimeout) + defer cancel() + if err := s.redis.Ping(checkCtx); err != nil { + return DependencyStatus{Available: true, Healthy: false, Error: err.Error()} + } + return DependencyStatus{Available: true, Healthy: true} +} + +func (s *RuntimeSnapshotService) k8sStatus(ctx context.Context) DependencyStatus { + if s.k8s == nil { + return DependencyStatus{Available: false} + } + + checkCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), healthCheckTimeout) + defer cancel() + if err := s.k8s.CheckHealth(checkCtx); err != nil { + return DependencyStatus{Available: true, Healthy: false, Error: err.Error()} + } + return DependencyStatus{Available: true, Healthy: true} +} + +func (s *RuntimeSnapshotService) buildkitStatus(ctx context.Context) DependencyStatus { + if s.buildkit == nil { + return DependencyStatus{Available: false} + } + + checkCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), healthCheckTimeout) + defer cancel() + if err := s.buildkit.CheckHealth(checkCtx, healthCheckTimeout); err != nil { + return DependencyStatus{Available: true, Healthy: false, Error: err.Error()} + } + return DependencyStatus{Available: true, Healthy: true} +} + +func (s *RuntimeSnapshotService) helmStatus() DependencyStatus { + if s.helm == nil { + return DependencyStatus{Available: false} + } + return DependencyStatus{Available: true, Healthy: true} +} diff --git a/src/service/consumer/state_store.go b/src/service/consumer/state_store.go index fc8cfd1f..52b0772a 100644 --- a/src/service/consumer/state_store.go +++ b/src/service/consumer/state_store.go @@ -1,100 +1,55 @@ package consumer import ( - "aegis/consts" - "aegis/dto" - "aegis/model" - "aegis/repository" - "errors" + "context" "fmt" "time" - "gorm.io/gorm" + "aegis/consts" + "aegis/dto" + executionmodule "aegis/module/execution" + injectionmodule "aegis/module/injection" ) type stateStore struct { - db *gorm.DB + execution ExecutionOwner + injection InjectionOwner } -func newStateStore(db *gorm.DB) *stateStore { - return &stateStore{db: db} +func newStateStore(execution ExecutionOwner, injection InjectionOwner) *stateStore { + return &stateStore{ + execution: execution, + injection: injection, + } } -func (s *stateStore) updateExecutionState(executionID int, newState consts.ExecutionState) error { - return s.db.Transaction(func(tx *gorm.DB) error { - execution, err := repository.GetExecutionByID(tx, executionID) - if err != nil { - if errorsIsRecordNotFound(err) { - return fmt.Errorf("%w: execution %d not found", consts.ErrNotFound, executionID) - } - return fmt.Errorf("execution %d not found: %w", executionID, err) - } - - if execution.State != consts.ExecutionInitial { - return fmt.Errorf("cannot change state of execution %d from %s to %s", executionID, consts.GetExecutionStateName(execution.State), consts.GetExecutionStateName(newState)) - } - - if err := repository.UpdateExecution(tx, executionID, map[string]any{ - "state": newState, - }); err != nil { - return fmt.Errorf("failed to update execution %d duration: %w", executionID, err) - } - - return nil +func (s *stateStore) updateExecutionState(ctx context.Context, executionID int, newState consts.ExecutionState) error { + if s.execution == nil { + return fmt.Errorf("execution owner service is nil") + } + return s.execution.UpdateExecutionState(ctx, &executionmodule.RuntimeUpdateExecutionStateReq{ + ExecutionID: executionID, + State: newState, }) } -func (s *stateStore) updateInjectionState(injectionName string, newState consts.DatapackState) error { - return s.db.Transaction(func(tx *gorm.DB) error { - injection, err := repository.GetInjectionByName(tx, injectionName, false) - if err != nil { - return fmt.Errorf("failed to get injection %s: %w", injectionName, err) - } - - if err := repository.UpdateInjection(tx, injection.ID, map[string]any{ - "state": newState, - }); err != nil { - return fmt.Errorf("failed to update injection %s state: %w", injectionName, err) - } - - return nil +func (s *stateStore) updateInjectionState(ctx context.Context, injectionName string, newState consts.DatapackState) error { + if s.injection == nil { + return fmt.Errorf("injection owner service is nil") + } + return s.injection.UpdateInjectionState(ctx, &injectionmodule.RuntimeUpdateInjectionStateReq{ + Name: injectionName, + State: newState, }) } -func (s *stateStore) updateInjectionTimestamp(injectionName string, startTime time.Time, endTime time.Time) (*dto.InjectionItem, error) { - var updatedInjection *model.FaultInjection - err := s.db.Transaction(func(tx *gorm.DB) error { - injection, err := repository.GetInjectionByName(tx, injectionName, false) - if err != nil { - if errorsIsRecordNotFound(err) { - return fmt.Errorf("injection %s not found", injectionName) - } - return fmt.Errorf("failed to get injection %s: %w", injectionName, err) - } - - if err = repository.UpdateInjection(tx, injection.ID, map[string]any{ - "start_time": startTime, - "end_time": endTime, - }); err != nil { - return fmt.Errorf("update injection timestamps failed: %w", err) - } - - reloadedInjection, err := repository.GetInjectionByID(tx, injection.ID) - if err != nil { - return fmt.Errorf("failed to reload injection %d after update: %w", injection.ID, err) - } - - updatedInjection = reloadedInjection - return nil - }) - if err != nil { - return nil, err +func (s *stateStore) updateInjectionTimestamp(ctx context.Context, injectionName string, startTime time.Time, endTime time.Time) (*dto.InjectionItem, error) { + if s.injection == nil { + return nil, fmt.Errorf("injection owner service is nil") } - - injectionItem := dto.NewInjectionItem(updatedInjection) - return &injectionItem, nil -} - -func errorsIsRecordNotFound(err error) bool { - return errors.Is(err, gorm.ErrRecordNotFound) + return s.injection.UpdateInjectionTimestamps(ctx, &injectionmodule.RuntimeUpdateInjectionTimestampReq{ + Name: injectionName, + StartTime: startTime, + EndTime: endTime, + }) } diff --git a/src/service/initialization/producer.go b/src/service/initialization/producer.go index 4a12674c..dc6cafaf 100644 --- a/src/service/initialization/producer.go +++ b/src/service/initialization/producer.go @@ -389,14 +389,13 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { container.Versions = versions - createdContainer, err := containermodule.CreateContainerCore(tx, container, userID) + createdContainer, err := containermodule.NewRepository(tx).CreateContainerCore(container, userID) if err != nil { return fmt.Errorf("failed to create container %s: %w", containerData.Name, err) } if createdContainer.Type == consts.ContainerTypePedestal { - if err := containermodule.UploadHelmValueFileFromPath( - tx, + if err := containermodule.NewRepository(tx).UploadHelmValueFileFromPath( containerData.Name, container.Versions[0].HelmConfig, filepath.Join(dataPath, fmt.Sprintf("%s.yaml", createdContainer.Name)), @@ -419,7 +418,7 @@ func initializeDatasets(tx *gorm.DB, data *InitialData, userID int) error { versions = append(versions, *version) } - _, err := datasetmodule.CreateDatasetCore(tx, dataset, versions, userID) + _, err := datasetmodule.NewRepository(tx).CreateDatasetCore(dataset, versions, userID) if err != nil { return fmt.Errorf("failed to create dataset %s: %w", datasetData.Name, err) } @@ -438,7 +437,7 @@ func initializeExecutionLabels(tx *gorm.DB) error { } for _, labelInfo := range sourceLabels { - _, err := labelmodule.CreateLabelCore(tx, &model.Label{ + _, err := labelmodule.NewRepository(tx).CreateLabelCore(tx, &model.Label{ Key: consts.ExecutionLabelSource, Value: labelInfo.value, Category: consts.ExecutionCategory, diff --git a/src/testutil/redisstub.go b/src/testutil/redisstub.go index 60fca6ad..0020e9b6 100644 --- a/src/testutil/redisstub.go +++ b/src/testutil/redisstub.go @@ -44,7 +44,9 @@ func StartRedisStub(tb testing.TB) (string, func()) { } func handleRedisStubConn(conn net.Conn) { - defer conn.Close() + defer func() { + _ = conn.Close() + }() reader := bufio.NewReader(conn) writer := bufio.NewWriter(conn) From 9442b42c5da691cd9d5067b3d0ab0590b5776952 Mon Sep 17 00:00:00 2001 From: rainystevn1 Date: Sat, 18 Apr 2026 21:43:48 +0800 Subject: [PATCH 3/4] feat(auth): add API key scope validation middleware - add api_key_scope.go with scope-to-target matching logic (e.g., "project:*" matches "project:123:containers") - introduce RequireAPIKeyScopesAny middleware for endpoint-level API key permission checks - add RequireHumanUserAuth to reject service tokens on self-service user endpoints - refactor permission middleware to extract and propagate API key auth type and scopes - rename /access-keys endpoint to /api-keys with human-user-only access guard --- README.md | 5 +- docs/todo.md | 60 ++- scripts/command/src/swagger/common.py | 1 + scripts/command/src/swagger/init.py | 4 + sdk/python/README.md | 119 ++--- sdk/python/src/rcabench/__init__.py | 4 + sdk/python/src/rcabench/client/__init__.py | 3 +- sdk/python/src/rcabench/client/base.py | 78 ++++ sdk/python/src/rcabench/client/http_client.py | 177 ++------ .../src/rcabench/client/runtime_client.py | 60 +++ sdk/python/uv.lock | 2 +- src/app/compat_options.go | 6 +- src/app/consumer.go | 1 + src/app/gateway/auth_services.go | 56 ++- src/cmd/aegisctl/client/auth.go | 130 +++--- src/cmd/aegisctl/client/auth_test.go | 51 ++- src/cmd/aegisctl/cmd/auth.go | 112 ++--- src/cmd/aegisctl/cmd/root.go | 8 +- src/cmd/aegisctl/config/config.go | 26 +- src/docs/docs_test.go | 7 +- src/infra/db/migration.go | 2 +- src/interface/grpciam/service.go | 57 ++- src/interface/grpciam/service_test.go | 22 + src/internalclient/iamclient/client.go | 78 ++-- src/middleware/api_key_scope.go | 97 ++++ src/middleware/api_key_scope_test.go | 153 +++++++ src/middleware/auth.go | 48 ++ src/middleware/permission.go | 91 +++- src/middleware/permission_test.go | 189 ++++++++ src/model/entity.go | 32 +- src/module/auth/api_types.go | 94 ++-- src/module/auth/handler.go | 216 +++++---- src/module/auth/handler_service.go | 17 +- src/module/auth/module.go | 2 +- src/module/auth/repository.go | 44 +- src/module/auth/service.go | 210 +++++---- src/module/auth/service_test.go | 78 +++- src/module/auth/token_store.go | 8 +- src/module/execution/handler.go | 4 +- src/module/sdk/handler.go | 8 +- src/proto/iam/v1/iam.pb.go | 420 +++++++++--------- src/proto/iam/v1/iam.proto | 28 +- src/proto/iam/v1/iam_grpc.pb.go | 250 ++++++----- src/router/admin.go | 36 +- src/router/portal.go | 21 +- src/router/public.go | 4 +- src/router/router.go | 13 +- src/router/router_test.go | 1 + src/router/runtime.go | 15 + src/router/sdk.go | 4 +- src/router/system.go | 2 +- src/router/v2.go | 364 --------------- src/service/initialization/consumer.go | 23 +- src/utils/access_key_crypto.go | 21 +- src/utils/jwt.go | 42 +- 55 files changed, 2101 insertions(+), 1503 deletions(-) create mode 100644 sdk/python/src/rcabench/client/base.py create mode 100644 sdk/python/src/rcabench/client/runtime_client.py create mode 100644 src/middleware/api_key_scope.go create mode 100644 src/middleware/api_key_scope_test.go create mode 100644 src/middleware/permission_test.go create mode 100644 src/router/runtime.go delete mode 100644 src/router/v2.go diff --git a/README.md b/README.md index d337563c..7d6e6d34 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The current backend architecture is a single repository with a single `go.mod`, The main service boundaries are: - **`api-gateway`**: external HTTP/OpenAPI entrypoint -- **`iam-service`**: auth, user, RBAC, team, access-key +- **`iam-service`**: auth, user, RBAC, team, api-key - **`resource-service`**: project, label, container, dataset, evaluation metadata/query - **`orchestrator-service`**: submit, task, trace, retry, dead-letter, workflow control-plane - **`runtime-worker-service`**: Redis async consumption, K8s/BuildKit/Helm/Chaos runtime execution @@ -70,7 +70,7 @@ It is the fastest option for local end-to-end debugging such as: | `consumer` | worker/controller/receiver side only | Yes, local runtime-side owners | Optional depending on config | queue/runtime/worker-only debugging | | `both` | HTTP + worker/controller/receiver | Yes, local owners for integrated debugging | Optional depending on config | full local async loop | | `api-gateway` | external HTTP gateway | No cross-owner local fallback as main path; service-specific remote wiring is expected | Yes | gateway boundary and remote-first debugging | -| `iam-service` | IAM gRPC service | Yes, IAM-local owners only | Only if a specific cross-service read path needs it | auth/user/rbac/team/access-key | +| `iam-service` | IAM gRPC service | Yes, IAM-local owners only | Only if a specific cross-service read path needs it | auth/user/rbac/team/api-key | | `resource-service` | Resource gRPC service | Yes, resource-local owners only | Yes for orchestrator-backed queries like some statistics/evaluation views | project/container/dataset/label/evaluation | | `orchestrator-service` | Orchestrator gRPC service | Yes, orchestrator-local owners only | Optional runtime/resource dependencies as needed | submit/task/trace/workflow | | `runtime-worker-service` | runtime worker + runtime gRPC | Yes, runtime-side execution infrastructure only | Yes, especially orchestrator target | Redis consumer, K8s/build/helm runtime | @@ -176,6 +176,7 @@ make logs - **[Report Index](docs/report-index.md)**: Consolidated backend refactor, runtime, governance, SDK/auth, and validation notes - **[Refactor TODO](docs/todo.md)**: Source-of-truth task list and final acceptance checklist +- **[API Key Auth TODO](docs/api-key-auth-execution-todo.md)**: Key ID / Key Secret auth execution checklist and signing contract - **[Frontend Redesign](docs/frontend-redesign.md)**: Frontend redesign plan and IA notes - **[Frontend UI Guidelines](docs/frontend-ui-guidelines.md)**: Frontend visual/system guidelines diff --git a/docs/todo.md b/docs/todo.md index 332852e5..eb0a2ee2 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -415,31 +415,49 @@ Task: - 同一个 operation 可以同时落入多个 audience。 - Python SDK 只消费 `sdk.json`;TypeScript SDK 不再消费共享并集视图,而是分别按 `portal.json` / `admin.json` 生成独立 Portal SDK 与 Admin SDK。 - SDK audience 改为显式白名单;默认不再把通用登录、Portal/Admin 控制面接口顺手放进 `sdk.json`。 -- SDK / CLI 认证主线改为 `AK/SK -> access token`;`username/password login` 仅保留给 Portal / Admin 等人类交互入口。 -- `AK/SK -> token` 进一步改为 header 签名模式:`X-Access-Key`、`X-Timestamp`、`X-Nonce`、`X-Signature`,业务接口仍继续走 Bearer token。 +- SDK / CLI 认证主线改为 `Key ID / Key Secret -> access token`;`username/password login` 仅保留给 Portal / Admin 等人类交互入口。 +- `Key ID / Key Secret -> token` 进一步改为 header 签名模式:`X-Key-Id`、`X-Timestamp`、`X-Nonce`、`X-Signature`,业务接口仍继续走 Bearer token。 本轮执行清单: - [x] 收缩 `sdk` audience 到最小可维护白名单 - - 已继续把剩余误标的 `sdk` audience 收回,只保留 `POST /api/v2/auth/access-key/token` 与 `src/router/sdk.go` 下 4 个 SDK 样例接口;当前 `sdk.json` 已收缩到 `5 paths / 5 operations`。 + - 已继续把剩余误标的 `sdk` audience 收回,只保留 `POST /api/v2/auth/api-key/token` 与 `src/router/sdk.go` 下 4 个 SDK 样例接口;当前 `sdk.json` 已收缩到 `5 paths / 5 operations`。 - [x] 从 Swagger audience 中移除 `POST /api/v2/auth/register` 的 `sdk` - [x] 从 Swagger audience 中移除 `POST /api/v2/auth/login` 的 `sdk` -- [x] 盘点并设计 AK/SK 数据模型 - - 已新增 `database.UserAccessKey`,覆盖 `owner`、`enabled/disabled/deleted`、`expires_at`、`last_used_at`、`name/description`、`secret_hash`,并纳入 `AutoMigrate`。 -- [x] 增加 AK/SK 管理接口 - - 已补 `portal` 路由:`GET/POST /api/v2/access-keys`、`GET/DELETE /api/v2/access-keys/{access_key_id}`、`POST /api/v2/access-keys/{access_key_id}/rotate|disable|enable`。 -- [x] 增加 `AK/SK -> token` 接口并标为 `sdk` - - 已补 `POST /api/v2/auth/access-key/token`,返回 Bearer token,并在 JWT claims 中标记 `auth_type=access_key` 与 `access_key_id`;当前入口改为 `X-Access-Key` / `X-Timestamp` / `X-Nonce` / `X-Signature` 头签名校验,服务端会校验 5 分钟时间窗并用 Redis 做 nonce 防重放。 -- [x] 将 Python SDK 的鉴权入口切到 AK/SK - - `sdk/python/src/rcabench/client/http_client.py` 已改为优先使用 `token` 或 `access_key + secret_key`;SDK 不再依赖 username/password login,环境变量同步切到 `RCABENCH_ACCESS_KEY` / `RCABENCH_SECRET_KEY`,并在换 token 时自动按 `METHOD\\nPATH\\nACCESS_KEY\\nTIMESTAMP\\nNONCE` 规范计算 HMAC-SHA256 签名头。 -- [x] 将 `aegisctl` 的鉴权入口切到 AK/SK - - `src/cmd/aegisctl/cmd/auth.go` / `src/cmd/aegisctl/client/auth.go` 已切到 `--access-key` + `--secret-key` 签名换取 `POST /api/v2/auth/access-key/token`;签名规范与 Python SDK 保持一致,登录结果继续只落盘 Bearer token,不保存 `secret_key`。 +- [x] 盘点并设计 API key / Key ID / Key Secret 数据模型 + - 已新增 `model.APIKey`,并把物理 schema 一并改到 `api_keys` / `key_id` / `key_secret_hash` / `key_secret_ciphertext` / `active_key_id`,覆盖 `owner`、`enabled/disabled/deleted`、`expires_at`、`last_used_at`、`name/description` 等字段,并纳入 `AutoMigrate`。 +- [x] 增加 API key 管理接口 + - 已补 `portal` 路由:`GET/POST /api/v2/api-keys`、`GET/DELETE /api/v2/api-keys/{id}`、`POST /api/v2/api-keys/{id}/rotate|disable|enable`。 +- [x] 增加 `Key ID / Key Secret -> token` 接口并标为 `sdk` + - 已补 `POST /api/v2/auth/api-key/token`,返回 Bearer token,并在 JWT claims 中标记 `auth_type=api_key` 与 `api_key_id`;当前入口统一使用 `X-Key-Id` / `X-Timestamp` / `X-Nonce` / `X-Signature` 头签名校验,canonical string 已收敛为 `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)`,服务端会校验 5 分钟时间窗并用 Redis 做 nonce 防重放;`iam.proto` / `src/interface/grpciam` / `src/internalclient/iamclient` 这条内部链也已统一到 `key_id` 字段名。 +- [x] 将 Python SDK 的鉴权入口切到 Key ID / Key Secret + - `sdk/python/src/rcabench/client/http_client.py` 已改为优先使用 `token` 或 `key_id + key_secret`;SDK 不再依赖 username/password login,环境变量主入口切到 `RCABENCH_KEY_ID` / `RCABENCH_KEY_SECRET`,并按 `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)` 规范计算 HMAC-SHA256 签名头;重生成后的 `sdk/python/src/rcabench/openapi/*` 也已切到 `X-Key-Id`、`key_id`、`key_secret` schema,不再暴露旧 `X-Access-Key` / `access_key` / `secret_key` 鉴权字段。 +- [x] 将 `aegisctl` 的鉴权入口切到 Key ID / Key Secret + - `src/cmd/aegisctl/cmd/auth.go` / `src/cmd/aegisctl/client/auth.go` 已切到 `--key-id` + `--key-secret` 签名换取 `POST /api/v2/auth/api-key/token`;环境变量主入口改为 `AEGIS_KEY_ID` / `AEGIS_KEY_SECRET`,登录结果继续只落盘 Bearer token 与 `key_id`,不保存 `key_secret`;旧 flag/env 与响应字段兼容读取已删除。 - [x] 给 `aegisctl` 增加本地签名排障命令 - - 已补 `aegisctl auth inspect` 与 `aegisctl auth sign-debug`;前者可检查当前 context 的 token / auth_type / access_key / expiry,后者可直接打印 canonical string、签名头与 curl 样例,并可通过 `--execute` 直接发起换 token 请求回显响应,或通过 `--save-context` 直接把成功返回的 Bearer token 落盘到当前 CLI context,便于排查 SDK / CLI / 服务端签名不一致问题。 -- [x] 补充 AK/SK 头签名规范文档 - - 相关说明现已并入 `docs/report-index.md`:明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 access key 的使用说明与 `aegisctl` 排障命令说明;同时已回填 `src/handlers/v2/access_keys.go` / `src/dto/auth.go` 的 Swagger/OpenAPI 注释与 schema example,前端与文档站可直接消费。 -- [x] 补 Portal access key 前端文案与表单提示 - - `../AegisLab-frontend/src/pages/settings/Settings.tsx` 已新增 Access Keys 管理页签,覆盖创建 / 轮换 / 启停 / 删除与一次性 secret 提示;`../AegisLab-frontend/src/api/auth.ts` 也已补齐 access key API 封装,页面文案与 OpenAPI 说明保持一致。 + - 已补 `aegisctl auth inspect` 与 `aegisctl auth sign-debug`;前者可检查当前 context 的 token / auth_type / key_id / expiry,后者可直接打印 canonical string、签名头与 curl 样例,并可通过 `--execute` 直接发起换 token 请求回显响应,或通过 `--save-context` 直接把成功返回的 Bearer token 落盘到当前 CLI context,便于排查 SDK / CLI / 服务端签名不一致问题。 +- [x] 补充 Key ID / Key Secret 头签名规范文档 + - 相关说明现已并入 `docs/report-index.md`:明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 API key 的使用说明与 `aegisctl` 排障命令说明;Swagger 注释与生成文档也已统一到 `X-Key-Id`、`key_id`、`key_secret` 口径,可直接供文档站与 SDK 生成消费。 +- [x] 收口 API key 命名与样例前缀 + - auth handler / service / gRPC / internal client / CLI / Swagger / Python SDK 现已统一使用 `API key`、`key_id`、`key_secret`;公开样例前缀也统一为 `pk_...` / `ks_...`。Go 存储模型与物理 schema 现都已统一到 `APIKey` / `api_keys` / `key_id` 口径,不再保留旧 `user_access_keys` / `access_key` 兼容层。 +- [x] 补 API key 的 `scopes` / `revoked_at` 语义 + - `model.APIKey` 已新增 `scopes` 与 `revoked_at`;创建接口支持提交 `scopes`,默认会归一化为 `["*"]`;列表/详情/创建/轮换响应会返回 scopes 与 revoked_at;同时新增 `POST /api/v2/api-keys/{id}/revoke`,被 revoke 的 API key 会被永久拒绝换 token,且不能再 re-enable / rotate。 +- [x] 把 API key scopes 继续推进到 bearer token / gRPC verify / middleware 上下文 + - `utils.Claims` 已补 `api_key_scopes`,`Key ID / Key Secret -> token` 成功后签发的 JWT 会携带 scopes;`iam.proto` / `src/interface/grpciam` / `src/internalclient/iamclient` 的 verify 响应链也已同步透传,HTTP middleware 现会把 `auth_type` / `api_key_id` / `api_key_scopes` 一并放入请求上下文,后续做 scope enforcement 不用再回查 API key 表。 +- [x] 给 API key scopes 接上首版运行时拦截 + - `src/middleware/permission.go` 现已在 permission middleware 里先按 `api_key_scopes` 做匹配,再落 DB 权限校验;当前支持 `*`、`resource`、`resource:action`、`resource:action:scope` 以及各段 `*` 通配,先覆盖所有基于 `RequirePermission/RequireAnyPermission/RequireAllPermissions` 的路由。 +- [x] 把 team/project 成员关系型中间件也接上 API key scopes 预过滤 + - `RequireTeamMemberAccess` / `RequireTeamAdminAccess` / `RequireProjectAccess(...)` 现在会先按 API key scope 做 read/manage 级别过滤,再执行成员/管理员关系判断,避免 API key bearer token 绕过非 permission 型访问守卫。 +- [x] 再扫一轮 JWTAuth-only 路由,把明显漏掉的敏感守卫补齐 + - 已补上 `team list/create`、`/api/v2/resources*`、`/api/v2/systems*`、`/api/v2/system/metrics*` 这批原先只有 `JWTAuth()` 的敏感入口;当前剩余仅 `auth profile/logout/change-password`、`/api/v2/api-keys/*` 自助凭证管理,以及 `sdk` 样例查询这几类刻意保留的 JWTAuth-only 路由,后两者若要继续收紧可再单独引入更明确的 API key/self-service scope 语义。 +- [x] 给 `sdk/*` 和 `/api/v2/api-keys/*` 落一版明确语义 + - `src/router/sdk.go` 已引入显式 API key scope gate:`/api/v2/sdk/evaluations*` 需要 `sdk:*` / `sdk:evaluations:*` / `sdk:evaluations:read`,`/api/v2/sdk/datasets` 需要 `sdk:*` / `sdk:datasets:*` / `sdk:datasets:read`;同时 `src/router/portal.go` 的 `/api/v2/api-keys/*` 已统一挂 `RequireHumanUserAuth()`,明确只允许人类用户 session 管理 API key,禁止“API key 再管理 API key”。 +- [x] 把 auth 自助接口也限制为 human session + - `src/router/public.go` 的 `/api/v2/auth/profile`、`/logout`、`/change-password` 现已统一挂 `RequireHumanUserAuth()`;当前 API key bearer token 只保留给显式允许的 SDK/业务 API,不再可进入用户账号自助管理接口。 +- [x] 启动 Python SDK / runtime wrapper 分层主线第一批落地 + - 已新增 `docs/python-runtime-wrapper-design.md` 与 `docs/python-runtime-wrapper-todo.md`;Swagger `x-api-type` 现支持 `runtime` audience,并新增 `src/docs/converted/runtime.json`;`module/execution` 的 detector/granularity upload 已标为 `runtime:"true"`,`src/router/runtime.go` 也已把这两条 `/api/v2/executions/{execution_id}/*_results` 路由挂到 `JWTAuth() + RequireServiceTokenAuth()`;同时 `sdk/python/src/rcabench/client/runtime_client.py` 已新增 `RCABenchRuntimeClient`,并从 Python 包根导出,作为后续 `rcabench-platform` wrapper 的 service-token-only 基础客户端。 +- [x] 收紧 hand-written Python client 边界:public client 只保留 API key,runtime client 只保留 service token + - `sdk/python/src/rcabench/client/base.py` 已新增 `BaseRCABenchClient` 抽出共享 session/api-client 生命周期;`RCABenchClient` 已删除直接 bearer token 模式,仅保留 `key_id + key_secret`;`RCABenchRuntimeClient` 现改成与 public client 同结构的 service-token-only connector,不再承载 detector/granularity upload 调度语义,后续 heartbeat / status / artifact/result 的调用时机统一留给外仓 `rcabench-platform` wrapper 控制。 - [x] 重新生成 `openapi3` / `sdk.json` 并回填最新统计 - 当前生成结果为:`openapi3/openapi.json` `138 paths / 173 operations`,`sdk.json` `5 / 5`,`portal.json` `31 / 43`,`admin.json` `48 / 58`;Python SDK 已按最新 `sdk.json` 重新生成,TypeScript 侧改为分别消费 `portal.json` 与 `admin.json`。 @@ -592,7 +610,7 @@ Task: - [x] 在 `src/app/` 建立第一批服务边界分组:`gateway / runtime / iam / resource / orchestrator / system` - [x] 新增第一批可运行服务入口:`src/cmd/api-gateway`、`src/cmd/runtime-worker-service`、`src/cmd/iam-service` - [x] Runtime Worker Service:补 `runtime.proto` 与 gRPC control-plane(`Ping / GetRuntimeStatus / GetQueueStatus / GetLimiterStatus`) -- [x] IAM Service:补 `iam.proto` 与 token verify / permission check / access-key exchange gRPC +- [x] IAM Service:补 `iam.proto` 与 token verify / permission check / API key exchange gRPC - [x] Orchestrator Service:补 `orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service` - [x] Orchestrator Service:首批 submit / cancel RPC 已收口(`Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask`) - [x] Gateway -> Orchestrator:execution / injection submit 主路径已支持通过 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 切到内部 gRPC @@ -626,7 +644,7 @@ Task: - 当前 `api-gateway` 语义上对应既有 producer HTTP 栈,`runtime-worker-service` 语义上对应既有 consumer 栈;其余服务的核心内部 RPC 与独立启动入口已落地,后续再按边界细化 owner 职责。 - 本轮已落地 `src/proto/runtime/v1/runtime.proto`、`src/interface/grpcruntime/*` 与 queue/limiter/runtime snapshot 聚合能力,并把 gRPC lifecycle 接入 `ConsumerOptions` / `BothOptions`;默认监听 `:9094`,可通过 `runtime_worker.grpc.addr` 覆盖。 - 本轮继续扩展 `runtime-worker-service` control-plane:当前额外提供 `GetNamespaceLocks / GetQueuedTasks`,用于承接 runtime Redis 运行态对内查询。 -- 本轮继续落地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/*` 与 `src/cmd/iam-service`,当前 IAM 内部 RPC 已覆盖鉴权、access key、team、user、rbac 五组主路径:除 `VerifyToken / CheckPermission / ExchangeAccessKeyToken` 与 team membership 判定外,也已补齐 `Login / Register / RefreshToken / Logout / ChangePassword / GetProfile / access key CRUD`、`Create/Get/List/Update/Delete user`、user role/permission/resource 绑定、`Create/Get/List/Update/Delete role`、role-permission 绑定以及 permission/resource 查询;默认监听 `:9091`,可通过 `iam.grpc.addr` 覆盖。 +- 本轮继续落地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/*` 与 `src/cmd/iam-service`,当前 IAM 内部 RPC 已覆盖鉴权、API key、team、user、rbac 五组主路径:除 `VerifyToken / CheckPermission / ExchangeAPIKeyToken` 与 team membership 判定外,也已补齐 `Login / Register / RefreshToken / Logout / ChangePassword / GetProfile / API key CRUD`、`Create/Get/List/Update/Delete user`、user role/permission/resource 绑定、`Create/Get/List/Update/Delete role`、role-permission 绑定以及 permission/resource 查询;默认监听 `:9091`,可通过 `iam.grpc.addr` 覆盖。 - 本轮继续补上 `src/proto/orchestrator/v1/orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service`,当前 Orchestrator 内部 RPC 已提供 `Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask` 五个入口;默认监听 `:9092`,可通过 `orchestrator.grpc.addr` 覆盖。 - 本轮继续扩展 `orchestrator-service` 控制面:当前额外已提供 `GetTask / ListTasks / GetTrace / ListTraces / ListDeadLetterTasks / RetryTask` 六个入口,用于 workflow state 查询、dead-letter 补偿与手动 retry;同时执行/消费异步仍保持 Redis queue/event 主链不变。 - 本轮继续扩展 `orchestrator-service` owner facade:当前又额外提供 `CreateExecution / CreateInjection / UpdateExecutionState / UpdateInjectionState / UpdateInjectionTimestamps / GetExecution / ListEvaluationExecutionsByDatapack / ListEvaluationExecutionsByDataset` 八个入口,分别承接 runtime 状态回写与 evaluation 执行结果查询。 @@ -656,7 +674,7 @@ Task: - 这一轮又继续把兼容入口装配层压实到单点:新增 `src/app/compat_options.go`,把 producer 侧 HTTP/K8s/chaos 与 producer init/http server 装配收成 `ProducerCompatibilityOptions / ProducerHTTPEntryOptions`,把 consumer/both 共享的本地 owner runtime 组合继续收成 `CompatibilityRuntimeOptions()`;`src/app/producer.go`、`consumer.go`、`both.go`、`gateway/options.go` 现在不再各自重复拼 `Base/Observe/Data/Coordination/Build + modules + init + http`,同时已删掉 `NormalizeAddr(...)` 与 gateway 专用 `NewProducerInitializerForGateway / RegisterProducerInitializationForGateway` 这类多余壳函数。 - 这一轮继续把 dedicated `api-gateway` 的 metrics 边界收紧:`src/module/metric` 已补 `HandlerService`,`src/app/gateway/metric_services.go` 新增 remote-aware metrics wrapper,gateway 上的 `/api/v2/metrics/injections|executions|algorithms` 不再直接落本地 `fault_injections / executions / containers` 表;其中 injection/execution metrics 已走新增的 orchestrator RPC `GetInjectionMetrics / GetExecutionMetrics`,algorithm metrics 则由 gateway 经 `resource-service` 拉 algorithm 列表后再按算法向 orchestrator 聚合执行指标,先把 dedicated gateway 这块跨 owner 直查面收掉。 - 这一轮继续把 dedicated `api-gateway` 的 team 主路径切到 IAM:`src/module/team` 已补 `HandlerService`,`src/app/gateway/team_services.go` 新增 remote-aware team wrapper,gateway 上的 `/api/v2/teams/*` 现在统一经 `iamclient` 转发 `Create/Get/List/Update/Delete`、member 管理、team project/member 列表,而不再直接吃本地 team owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 已补齐 team RPC 面。同时 `src/module/team/project_reader.go` 又新增 `RemoteProjectReaderOption()`,`src/app/iam/options.go` 已把 dedicated `iam-service` 上的 team->project 视图继续收成 resource RPC-only。 -- 这一轮再把 dedicated `api-gateway` 的 IAM 剩余主路径继续收口:`src/module/{auth,user,rbac}` 已补 `HandlerService`,`src/app/gateway/{auth,user,rbac}_services.go` 新增 remote-aware wrapper,gateway 上的 `/api/v2/auth/*`、`/api/v2/access-keys/*`、`/api/v2/users/*`、`/api/v2/roles|permissions|resources/*` 已统一经 `iamclient` 转发,不再在 dedicated `api-gateway` 入口直接吃本地 IAM owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 也已补齐 auth/user/rbac RPC 面。 +- 这一轮再把 dedicated `api-gateway` 的 IAM 剩余主路径继续收口:`src/module/{auth,user,rbac}` 已补 `HandlerService`,`src/app/gateway/{auth,user,rbac}_services.go` 新增 remote-aware wrapper,gateway 上的 `/api/v2/auth/*`、`/api/v2/api-keys/*`、`/api/v2/users/*`、`/api/v2/roles|permissions|resources/*` 已统一经 `iamclient` 转发,不再在 dedicated `api-gateway` 入口直接吃本地 IAM owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 也已补齐 auth/user/rbac RPC 面。 - `src/app/app.go` 已开始按服务边界拆装配层:当前新增 `BaseOptions / ObserveOptions / DataOptions / CoordinationOptions / BuildInfraOptions`,独立服务启动链不再统一吃满所有 infra。 - `src/app/resource/options.go` 这一轮继续把 standalone 边界推进到 `project / label / container / dataset / evaluation`;`resource-service` 已接入 `orchestratorclient.Module` 承接 evaluation -> orchestrator 的远程查询,gateway 对 `evaluation` handler 也已补上 remote-aware 装饰。 - 这一轮继续把 dedicated `api-gateway` 的 label 主路径切到 Resource:`src/module/label` 已补 `HandlerService`,`src/proto/resource/v1/resource.proto` / `src/interface/grpcresource/service.go` / `src/internalclient/resourceclient/client.go` 已补齐 `Create/Get/List/Update/Delete/BatchDelete label` 对内 RPC;同时 `src/app/gateway/resource_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/labels/*` 改成统一经 `resource-service` 转发,dedicated gateway 不再直接承载 label owner 读写。 diff --git a/scripts/command/src/swagger/common.py b/scripts/command/src/swagger/common.py index f1961737..d6b2a46e 100644 --- a/scripts/command/src/swagger/common.py +++ b/scripts/command/src/swagger/common.py @@ -12,6 +12,7 @@ class RunMode(str, Enum): CLIENT = "client" SDK = "sdk" + RUNTIME = "runtime" PORTAL = "portal" ADMIN = "admin" diff --git a/scripts/command/src/swagger/init.py b/scripts/command/src/swagger/init.py index 164cc218..5efac184 100644 --- a/scripts/command/src/swagger/init.py +++ b/scripts/command/src/swagger/init.py @@ -723,6 +723,7 @@ def _filter_apis_by_audience(self, category: RunMode) -> dict[str, Any] | None: """ audience_keys_by_mode = { RunMode.SDK: {"sdk"}, + RunMode.RUNTIME: {"runtime"}, RunMode.PORTAL: {"portal"}, RunMode.ADMIN: {"admin"}, } @@ -873,11 +874,13 @@ def init(version: str) -> None: post_input_file = OPENAPI3_DIR / "openapi.json" client_file = CONVERTED_DIR / "client.json" sdk_file = CONVERTED_DIR / "sdk.json" + runtime_file = CONVERTED_DIR / "runtime.json" portal_file = CONVERTED_DIR / "portal.json" admin_file = CONVERTED_DIR / "admin.json" shutil.copyfile(post_input_file, dst=client_file) shutil.copyfile(post_input_file, dst=sdk_file) + shutil.copyfile(post_input_file, dst=runtime_file) shutil.copyfile(post_input_file, dst=portal_file) shutil.copyfile(post_input_file, dst=admin_file) @@ -890,6 +893,7 @@ def init(version: str) -> None: processor.output(client_file, RunMode.CLIENT) processor.output(sdk_file, RunMode.SDK) + processor.output(runtime_file, RunMode.RUNTIME) processor.output(portal_file, RunMode.PORTAL) processor.output(admin_file, RunMode.ADMIN) diff --git a/sdk/python/README.md b/sdk/python/README.md index 9ad72885..a5303420 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -1,86 +1,93 @@ -# RCABench SDK +# RCABench Python SDK -A Python SDK for interacting with RCABench services. +The SDK exposes two handwritten entry clients on top of the generated OpenAPI package: -## Installation +- `RCABenchClient`: public/business API client authenticated by `Key ID` + `Key Secret` +- `RCABenchRuntimeClient`: runtime-only client authenticated by service token + +Generated OpenAPI code lives under `src/rcabench/openapi`. Handwritten auth/session logic lives under `src/rcabench/client`. -### From PyPI +## Installation ```bash pip install rcabench ``` -### From Source +For local development: ```bash -# Clone the repository -git clone https://github.com/your-username/rcabench.git -cd rcabench/sdk/python - -# Install the package +cd sdk/python pip install -e . ``` -## Building the Package +## Authentication Model -To build the package for distribution: +Secrets are never passed directly in code. The SDK reads credentials from environment variables only. + +### Public Client + +Required environment variables: ```bash -# Install build dependencies -pip install build +export RCABENCH_BASE_URL="http://localhost:8082" +export RCABENCH_KEY_ID="pk_xxx" +export RCABENCH_KEY_SECRET="sk_xxx" +``` -# Build the package -python -m build +`RCABenchClient` exchanges the key pair for a bearer token through the API-key token endpoint, then reuses the authenticated OpenAPI client. -# This will create distribution files in the dist/ directory +### Runtime Client + +Required environment variables: + +```bash +export RCABENCH_BASE_URL="http://localhost:8082" +export RCABENCH_SERVICE_TOKEN="runtime_token_xxx" ``` +`RCABenchRuntimeClient` is intended for managed runtime/wrapper usage. It injects the service token into the generated OpenAPI client directly. + ## Usage +### Public API Client + ```python -from rcabench import RCABenchSDK - -# Initialize the SDK -sdk = RCABenchSDK("http://localhost:8082") - -# Get available algorithms -algorithms = sdk.algorithm.list() -print(algorithms) - -# Submit an injection task -injection_payload = [{ - "duration": 1, - "faultType": 5, - "injectNamespace": "ts", - "injectPod": "ts-preserve-service", - "spec": {"CPULoad": 1, "CPUWorker": 3}, - "benchmark": "clickhouse", -}] -response = sdk.injection.execute(injection_payload) -print(response) - -# Run an algorithm -algorithm_payload = [{ - "benchmark": "clickhouse", - "algorithm": "e-diagnose", - "dataset": "dataset-name", -}] -response = sdk.algorithm.execute(algorithm_payload) -print(response) +from rcabench import RCABenchClient +from rcabench.openapi.api.datasets_api import DatasetsApi + +client = RCABenchClient() +api = DatasetsApi(client.get_client()) + +datasets = api.list_sdk_dataset_samples(page=1, size=10) +print(datasets) +``` + +You may still override `base_url` in code when needed: + +```python +client = RCABenchClient(base_url="http://localhost:8082") ``` -## API Reference +### Runtime API Client + +```python +from rcabench import RCABenchRuntimeClient + +runtime_client = RCABenchRuntimeClient() +api_client = runtime_client.get_client() + +print(api_client.configuration.host) +``` -The SDK provides the following main components: +Runtime-tagged API classes are generated from the current OpenAPI audience split. Use `runtime_client.get_client()` with the generated API module that corresponds to those runtime-only routes. -- `RCABenchSDK`: The main entry point for the SDK - - `algorithm`: For interacting with algorithm endpoints - - `evaluation`: For interacting with evaluation endpoints - - `injection`: For interacting with injection endpoints +## Development -For detailed API documentation, please refer to the code docstrings. +Run type checking only on handwritten SDK code: -## Requirements +```bash +cd sdk/python +uv run --with pyright pyright src/rcabench/client +``` -- Python 3.8 or higher -- `requests` and `aiohttp` libraries +The generated package under `src/rcabench/openapi` is excluded from Pyright. diff --git a/sdk/python/src/rcabench/__init__.py b/sdk/python/src/rcabench/__init__.py index a955fdae..429f2c71 100644 --- a/sdk/python/src/rcabench/__init__.py +++ b/sdk/python/src/rcabench/__init__.py @@ -1 +1,5 @@ __version__ = "1.2.1" + +from rcabench.client import RCABenchClient, RCABenchRuntimeClient + +__all__ = ["RCABenchClient", "RCABenchRuntimeClient", "__version__"] diff --git a/sdk/python/src/rcabench/client/__init__.py b/sdk/python/src/rcabench/client/__init__.py index 57ef9ed3..0c05f51e 100644 --- a/sdk/python/src/rcabench/client/__init__.py +++ b/sdk/python/src/rcabench/client/__init__.py @@ -1,3 +1,4 @@ from rcabench.client.http_client import RCABenchClient +from rcabench.client.runtime_client import RCABenchRuntimeClient -__all__ = ["RCABenchClient"] +__all__ = ["RCABenchClient", "RCABenchRuntimeClient"] diff --git a/sdk/python/src/rcabench/client/base.py b/sdk/python/src/rcabench/client/base.py new file mode 100644 index 00000000..a06c0eb2 --- /dev/null +++ b/sdk/python/src/rcabench/client/base.py @@ -0,0 +1,78 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import ClassVar + +from pydantic import StrictStr + +from rcabench.openapi.api_client import ApiClient +from rcabench.openapi.configuration import Configuration + + +@dataclass(kw_only=True) +class SessionData: + access_token: StrictStr | None = None + api_client: ApiClient | None = None + + +CacheKey = tuple[str, str, str | None] + + +class BaseRCABenchClient(ABC): + """ + Shared authenticated client lifecycle for hand-written RCABench clients. + + Subclasses own: + - auth input resolution + - instance/session cache keys + - _authenticate implementation + """ + + _instances: ClassVar[dict[CacheKey, "BaseRCABenchClient"]] = {} + _sessions: ClassVar[dict[CacheKey, SessionData]] = {} + base_url: str + instance_key: CacheKey + _initialized: bool + + def __enter__(self) -> ApiClient: + if self.instance_key not in self.__class__._sessions or not self._is_session_valid(): + self._authenticate() + return self._get_authenticated_client() + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + pass + + def _is_session_valid(self) -> bool: + session_data = self.__class__._sessions.get(self.instance_key) + if not session_data: + return False + return session_data.access_token is not None + + @abstractmethod + def _authenticate(self) -> None: + raise NotImplementedError + + def _get_authenticated_client(self) -> ApiClient: + if self.instance_key not in self.__class__._sessions or not self._is_session_valid(): + self._authenticate() + + session_data = self.__class__._sessions[self.instance_key] + bearer_token = session_data.access_token + assert bearer_token is not None, "Access token is missing in session data" + + if not session_data.api_client: + auth_config = Configuration( + host=self.base_url, + api_key={"BearerAuth": bearer_token}, + api_key_prefix={"BearerAuth": "Bearer"}, + ) + session_data.api_client = ApiClient(auth_config) + + return session_data.api_client + + def get_client(self) -> ApiClient: + return self._get_authenticated_client() + + @classmethod + def clear_sessions(cls) -> None: + cls._sessions.clear() + cls._instances.clear() diff --git a/sdk/python/src/rcabench/client/http_client.py b/sdk/python/src/rcabench/client/http_client.py index 0d2a355b..db7a82f7 100644 --- a/sdk/python/src/rcabench/client/http_client.py +++ b/sdk/python/src/rcabench/client/http_client.py @@ -1,66 +1,42 @@ import os import secrets import time -from dataclasses import dataclass from hashlib import sha256 from hmac import new as hmac_new +from typing import ClassVar -from pydantic import StrictStr - +from rcabench.client.base import BaseRCABenchClient, CacheKey, SessionData from rcabench.openapi.api.authentication_api import AuthenticationApi from rcabench.openapi.api_client import ApiClient from rcabench.openapi.configuration import Configuration -@dataclass(kw_only=True) -class SessionData: - access_token: StrictStr | None = None - api_client: ApiClient | None = None - - -class RCABenchClient: +class RCABenchClient(BaseRCABenchClient): """ - RCABench client supporting access-key and token-based authentication. - - - Token-based auth (for K8s jobs): - client = RCABenchClient(base_url="...", token="...") - or via environment variable RCABENCH_TOKEN + RCABench public client supporting API-key authentication. - - Access-key auth (recommended for SDK use): - client = RCABenchClient(base_url="...", access_key="...", secret_key="...") - or via environment variables RCABENCH_ACCESS_KEY, RCABENCH_SECRET_KEY + Auth credentials are loaded from environment variables only: + - RCABENCH_BASE_URL or `base_url=...` + - RCABENCH_KEY_ID + - RCABENCH_KEY_SECRET """ - _instances: dict[tuple[str, str, str | None], "RCABenchClient"] = {} - _sessions: dict[tuple[str, str, str | None], SessionData] = {} - _token_exchange_path = "/api/v2/auth/access-key/token" + _instances: ClassVar[dict[CacheKey, BaseRCABenchClient]] = {} + _sessions: ClassVar[dict[CacheKey, SessionData]] = {} + _token_exchange_path = "/api/v2/auth/api-key/token" def __new__( cls, base_url: str | None = None, - access_key: str | None = None, - secret_key: str | None = None, - token: str | None = None, ): - # Parse actual configuration values actual_base_url = base_url or os.getenv("RCABENCH_BASE_URL") - actual_token = token or os.getenv("RCABENCH_TOKEN") - actual_access_key = access_key or os.getenv("RCABENCH_ACCESS_KEY") - actual_secret_key = secret_key or os.getenv("RCABENCH_SECRET_KEY") + actual_key_id = os.getenv("RCABENCH_KEY_ID") + actual_key_secret = os.getenv("RCABENCH_KEY_SECRET") assert actual_base_url is not None, "base_url or RCABENCH_BASE_URL is not set" - - # Token auth takes precedence over access-key authentication - if actual_token: - instance_key = (actual_base_url, actual_token, None) - else: - assert actual_access_key is not None, ( - "access_key or RCABENCH_ACCESS_KEY is not set (or use token/RCABENCH_TOKEN)" - ) - assert actual_secret_key is not None, ( - "secret_key or RCABENCH_SECRET_KEY is not set (or use token/RCABENCH_TOKEN)" - ) - instance_key = (actual_base_url, actual_access_key, actual_secret_key) + assert actual_key_id is not None, "RCABENCH_KEY_ID is not set" + assert actual_key_secret is not None, "RCABENCH_KEY_SECRET is not set" + instance_key = (actual_base_url, actual_key_id, actual_key_secret) if instance_key not in cls._instances: instance = super().__new__(cls) @@ -72,138 +48,67 @@ def __new__( def __init__( self, base_url: str | None = None, - access_key: str | None = None, - secret_key: str | None = None, - token: str | None = None, ): - # Avoid duplicate initialization of the same instance if hasattr(self, "_initialized") and self._initialized: return - self.base_url = base_url or os.getenv("RCABENCH_BASE_URL") - self.token = token or os.getenv("RCABENCH_TOKEN") - self.access_key = access_key or os.getenv("RCABENCH_ACCESS_KEY") - self.secret_key = secret_key or os.getenv("RCABENCH_SECRET_KEY") - - assert self.base_url is not None, "base_url or RCABENCH_BASE_URL is not set" + actual_base_url = base_url or os.getenv("RCABENCH_BASE_URL") + actual_key_id = os.getenv("RCABENCH_KEY_ID") + actual_key_secret = os.getenv("RCABENCH_KEY_SECRET") - # Token auth takes precedence - if self.token: - self.instance_key = (self.base_url, self.token, None) - else: - assert self.access_key is not None, ( - "access_key or RCABENCH_ACCESS_KEY is not set (or use token/RCABENCH_TOKEN)" - ) - assert self.secret_key is not None, ( - "secret_key or RCABENCH_SECRET_KEY is not set (or use token/RCABENCH_TOKEN)" - ) - self.instance_key = (self.base_url, self.access_key, self.secret_key) + assert actual_base_url is not None, "base_url or RCABENCH_BASE_URL is not set" + assert actual_key_id is not None, "RCABENCH_KEY_ID is not set" + assert actual_key_secret is not None, "RCABENCH_KEY_SECRET is not set" + self.base_url = actual_base_url + self.key_id = actual_key_id + self.key_secret = actual_key_secret + self.instance_key = (self.base_url, self.key_id, self.key_secret) self._initialized = True - def __enter__(self): - # Check if there is already a valid session - if self.instance_key not in self._sessions or not self._is_session_valid(): - self._authenticate() - return self._get_authenticated_client() - - def __exit__(self, exc_type, exc_val, exc_tb): - # Do not close session, maintain singleton state - pass - - def _is_session_valid(self) -> bool: - """Check if the current session is valid""" - session_data = self._sessions.get(self.instance_key) - if not session_data: - return False - - # More complex session validity checks can be added here, such as checking if token is expired - # Currently simply check if access_token exists - return session_data.access_token is not None - def _authenticate(self) -> None: - """Authenticate using either token or access-key credentials.""" - if self.token: - # Direct token authentication. - self._sessions[self.instance_key] = SessionData( - access_token=self.token, - api_client=None, - ) - else: - self._exchange_access_key_token() + self._exchange_api_key_token() - def _exchange_access_key_token(self) -> None: - """Exchange access_key/secret_key for a bearer token.""" + def _exchange_api_key_token(self) -> None: config = Configuration(host=self.base_url) with ApiClient(config) as api_client: auth_api = AuthenticationApi(api_client) assert self.base_url is not None - assert self.access_key is not None - assert self.secret_key is not None + assert self.key_id is not None + assert self.key_secret is not None timestamp = str(int(time.time())) nonce = secrets.token_hex(16) - signature = self._sign_access_key_request( - secret_key=self.secret_key, + signature = self._sign_api_key_request( + key_secret=self.key_secret, method="POST", path=self._token_exchange_path, - access_key=self.access_key, timestamp=timestamp, nonce=nonce, ) - response = auth_api.exchange_access_key_token( - x_access_key=self.access_key, + response = auth_api.exchange_api_key_token( + x_key_id=self.key_id, x_timestamp=timestamp, x_nonce=nonce, x_signature=signature, ) assert response.data is not None - - # Store session information in class-level cache - self._sessions[self.instance_key] = SessionData( + self.__class__._sessions[self.instance_key] = SessionData( access_token=response.data.token, - api_client=None, # Will be created on demand - ) - - def _get_authenticated_client(self) -> ApiClient: - if self.instance_key not in self._sessions or not self._is_session_valid(): - self._authenticate() - - session_data = self._sessions[self.instance_key] - - # If api_client has not been created or needs to be updated, create a new one - bearer_token = session_data.access_token - assert bearer_token is not None, "Access token is missing in session data" - - if not session_data.api_client: - auth_config = Configuration( - host=self.base_url, - api_key={"BearerAuth": bearer_token}, - api_key_prefix={"BearerAuth": "Bearer"}, + api_client=None, ) - session_data.api_client = ApiClient(auth_config) - - return session_data.api_client - - def get_client(self) -> ApiClient: - return self._get_authenticated_client() @staticmethod - def _sign_access_key_request( - secret_key: str, + def _sign_api_key_request( + key_secret: str, method: str, path: str, - access_key: str, timestamp: str, nonce: str, ) -> str: - canonical = "\n".join([method.upper(), path, access_key, timestamp, nonce]) + body_hash = sha256(b"").hexdigest() + canonical = "\n".join([method.upper(), path, timestamp, nonce, body_hash]) return hmac_new( - secret_key.encode("utf-8"), + key_secret.encode("utf-8"), canonical.encode("utf-8"), sha256, ).hexdigest() - - @classmethod - def clear_sessions(cls): - cls._sessions.clear() - cls._instances.clear() diff --git a/sdk/python/src/rcabench/client/runtime_client.py b/sdk/python/src/rcabench/client/runtime_client.py new file mode 100644 index 00000000..bb7d69fa --- /dev/null +++ b/sdk/python/src/rcabench/client/runtime_client.py @@ -0,0 +1,60 @@ +import os +from typing import ClassVar + +from rcabench.client.base import BaseRCABenchClient, CacheKey, SessionData + + +class RCABenchRuntimeClient(BaseRCABenchClient): + """ + Runtime-only client for managed workloads. + + Auth credentials are loaded from environment variables only: + - RCABENCH_BASE_URL or `base_url=...` + - RCABENCH_SERVICE_TOKEN + """ + + _instances: ClassVar[dict[CacheKey, BaseRCABenchClient]] = {} + _sessions: ClassVar[dict[CacheKey, SessionData]] = {} + + def __new__( + cls, + base_url: str | None = None, + ): + actual_base_url = base_url or os.getenv("RCABENCH_BASE_URL") + actual_service_token = os.getenv("RCABENCH_SERVICE_TOKEN") + + assert actual_base_url is not None, "base_url or RCABENCH_BASE_URL is not set" + assert actual_service_token is not None, "RCABENCH_SERVICE_TOKEN is not set" + + instance_key = (actual_base_url, actual_service_token, None) + + if instance_key not in cls._instances: + instance = super().__new__(cls) + cls._instances[instance_key] = instance + instance._initialized = False + + return cls._instances[instance_key] + + def __init__( + self, + base_url: str | None = None, + ): + if hasattr(self, "_initialized") and self._initialized: + return + + actual_base_url = base_url or os.getenv("RCABENCH_BASE_URL") + actual_service_token = os.getenv("RCABENCH_SERVICE_TOKEN") + + assert actual_base_url is not None, "base_url or RCABENCH_BASE_URL is not set" + assert actual_service_token is not None, "RCABENCH_SERVICE_TOKEN is not set" + + self.base_url = actual_base_url + self.service_token = actual_service_token + self.instance_key = (self.base_url, self.service_token, None) + self._initialized = True + + def _authenticate(self) -> None: + self.__class__._sessions[self.instance_key] = SessionData( + access_token=self.service_token, + api_client=None, + ) diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 93576a6d..e2313272 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -519,7 +519,7 @@ wheels = [ [[package]] name = "rcabench" -version = "1.1.55" +version = "1.2.1" source = { editable = "." } dependencies = [ { name = "lazy-imports" }, diff --git a/src/app/compat_options.go b/src/app/compat_options.go index 75ab360a..e54d25d7 100644 --- a/src/app/compat_options.go +++ b/src/app/compat_options.go @@ -30,12 +30,12 @@ func ProducerHTTPEntryOptions(port string) fx.Option { ) } -// CompatibilityRuntimeOptions centralizes the legacy runtime stack that still -// needs local execution/injection owners for producer/consumer/both entrypoints. +// CompatibilityRuntimeOptions centralizes the legacy runtime stack shared by +// consumer/both entrypoints. Local execution/injection owner modules are added +// explicitly by the caller when that entrypoint needs local owner fallback. func CompatibilityRuntimeOptions() fx.Option { return fx.Options( RuntimeWorkerStackOptions(), - ExecutionInjectionOwnerModules(), ) } diff --git a/src/app/consumer.go b/src/app/consumer.go index 15177ea6..ad057a63 100644 --- a/src/app/consumer.go +++ b/src/app/consumer.go @@ -6,5 +6,6 @@ func ConsumerOptions(confPath string) fx.Option { return fx.Options( CommonOptions(confPath), CompatibilityRuntimeOptions(), + ExecutionInjectionOwnerModules(), ) } diff --git a/src/app/gateway/auth_services.go b/src/app/gateway/auth_services.go index 5dd82c7e..fd29720f 100644 --- a/src/app/gateway/auth_services.go +++ b/src/app/gateway/auth_services.go @@ -15,14 +15,15 @@ type authIAMClient interface { Logout(context.Context, *utils.Claims) error ChangePassword(context.Context, *authmodule.ChangePasswordReq, int) error GetProfile(context.Context, int) (*authmodule.UserProfileResp, error) - CreateAccessKey(context.Context, int, *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) - ListAccessKeys(context.Context, int, *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) - GetAccessKey(context.Context, int, int) (*authmodule.AccessKeyInfo, error) - DeleteAccessKey(context.Context, int, int) error - DisableAccessKey(context.Context, int, int) error - EnableAccessKey(context.Context, int, int) error - RotateAccessKey(context.Context, int, int) (*authmodule.AccessKeyWithSecretResp, error) - ExchangeAccessKeyToken(context.Context, *authmodule.AccessKeyTokenReq, string, string) (*authmodule.AccessKeyTokenResp, error) + CreateAPIKey(context.Context, int, *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) + ListAPIKeys(context.Context, int, *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) + GetAPIKey(context.Context, int, int) (*authmodule.APIKeyInfo, error) + DeleteAPIKey(context.Context, int, int) error + DisableAPIKey(context.Context, int, int) error + EnableAPIKey(context.Context, int, int) error + RevokeAPIKey(context.Context, int, int) error + RotateAPIKey(context.Context, int, int) (*authmodule.APIKeyWithSecretResp, error) + ExchangeAPIKeyToken(context.Context, *authmodule.APIKeyTokenReq, string, string) (*authmodule.APIKeyTokenResp, error) } type remoteAwareAuthService struct { @@ -72,58 +73,65 @@ func (s remoteAwareAuthService) GetProfile(ctx context.Context, userID int) (*au return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) CreateAccessKey(ctx context.Context, userID int, req *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) { +func (s remoteAwareAuthService) CreateAPIKey(ctx context.Context, userID int, req *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) { if s.iam != nil && s.iam.Enabled() { - return s.iam.CreateAccessKey(ctx, userID, req) + return s.iam.CreateAPIKey(ctx, userID, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) ListAccessKeys(ctx context.Context, userID int, req *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) { +func (s remoteAwareAuthService) ListAPIKeys(ctx context.Context, userID int, req *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) { if s.iam != nil && s.iam.Enabled() { - return s.iam.ListAccessKeys(ctx, userID, req) + return s.iam.ListAPIKeys(ctx, userID, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyInfo, error) { +func (s remoteAwareAuthService) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyInfo, error) { if s.iam != nil && s.iam.Enabled() { - return s.iam.GetAccessKey(ctx, userID, accessKeyID) + return s.iam.GetAPIKey(ctx, userID, accessKeyID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (s remoteAwareAuthService) DeleteAPIKey(ctx context.Context, userID, accessKeyID int) error { if s.iam != nil && s.iam.Enabled() { - return s.iam.DeleteAccessKey(ctx, userID, accessKeyID) + return s.iam.DeleteAPIKey(ctx, userID, accessKeyID) } return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (s remoteAwareAuthService) DisableAPIKey(ctx context.Context, userID, accessKeyID int) error { if s.iam != nil && s.iam.Enabled() { - return s.iam.DisableAccessKey(ctx, userID, accessKeyID) + return s.iam.DisableAPIKey(ctx, userID, accessKeyID) } return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (s remoteAwareAuthService) EnableAPIKey(ctx context.Context, userID, accessKeyID int) error { if s.iam != nil && s.iam.Enabled() { - return s.iam.EnableAccessKey(ctx, userID, accessKeyID) + return s.iam.EnableAPIKey(ctx, userID, accessKeyID) } return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyWithSecretResp, error) { +func (s remoteAwareAuthService) RevokeAPIKey(ctx context.Context, userID, accessKeyID int) error { if s.iam != nil && s.iam.Enabled() { - return s.iam.RotateAccessKey(ctx, userID, accessKeyID) + return s.iam.RevokeAPIKey(ctx, userID, accessKeyID) + } + return missingRemoteDependency("iam-service") +} + +func (s remoteAwareAuthService) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyWithSecretResp, error) { + if s.iam != nil && s.iam.Enabled() { + return s.iam.RotateAPIKey(ctx, userID, accessKeyID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) ExchangeAccessKeyToken(ctx context.Context, req *authmodule.AccessKeyTokenReq, method, path string) (*authmodule.AccessKeyTokenResp, error) { +func (s remoteAwareAuthService) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKeyTokenReq, method, path string) (*authmodule.APIKeyTokenResp, error) { if s.iam != nil && s.iam.Enabled() { - return s.iam.ExchangeAccessKeyToken(ctx, req, method, path) + return s.iam.ExchangeAPIKeyToken(ctx, req, method, path) } return nil, missingRemoteDependency("iam-service") } diff --git a/src/cmd/aegisctl/client/auth.go b/src/cmd/aegisctl/client/auth.go index f5b4d80f..fe09527e 100644 --- a/src/cmd/aegisctl/client/auth.go +++ b/src/cmd/aegisctl/client/auth.go @@ -11,36 +11,37 @@ import ( "time" ) -const accessKeyTokenPath = "/api/v2/auth/access-key/token" +const apiKeyTokenPath = "/api/v2/auth/api-key/token" -// AccessKeyTokenDebug contains the fully materialized signed request data for -// POST /api/v2/auth/access-key/token. -type AccessKeyTokenDebug struct { +// APIKeyTokenDebug contains the fully materialized signed request data for +// POST /api/v2/auth/api-key/token. +type APIKeyTokenDebug struct { Method string Path string - AccessKey string + KeyID string Timestamp string Nonce string + BodySHA256 string CanonicalString string Signature string } -func (d *AccessKeyTokenDebug) Headers() map[string]string { +func (d *APIKeyTokenDebug) Headers() map[string]string { return map[string]string{ - "X-Access-Key": d.AccessKey, - "X-Timestamp": d.Timestamp, - "X-Nonce": d.Nonce, - "X-Signature": d.Signature, + "X-Key-Id": d.KeyID, + "X-Timestamp": d.Timestamp, + "X-Nonce": d.Nonce, + "X-Signature": d.Signature, } } -// accessKeyTokenResponseData matches dto.AccessKeyTokenResp. -type accessKeyTokenResponseData struct { +// apiKeyTokenResponseData matches dto.APIKeyTokenResp. +type apiKeyTokenResponseData struct { Token string `json:"token"` TokenType string `json:"token_type"` ExpiresAt time.Time `json:"expires_at"` AuthType string `json:"auth_type"` - AccessKey string `json:"access_key"` + KeyID string `json:"key_id"` } // tokenRefreshRequest matches dto.TokenRefreshReq. @@ -59,36 +60,36 @@ type LoginResult struct { Token string ExpiresAt time.Time AuthType string - AccessKey string + KeyID string } -// LoginWithAccessKey exchanges an access key signature for a bearer token. -func LoginWithAccessKey(server, accessKey, secretKey string) (*LoginResult, error) { - accessKey = strings.TrimSpace(accessKey) - secretKey = strings.TrimSpace(secretKey) - if accessKey == "" { - return nil, fmt.Errorf("access key is required") +// LoginWithAPIKey exchanges a Key ID / Key Secret signature for a bearer token. +func LoginWithAPIKey(server, keyID, keySecret string) (*LoginResult, error) { + keyID = strings.TrimSpace(keyID) + keySecret = strings.TrimSpace(keySecret) + if keyID == "" { + return nil, fmt.Errorf("key id is required") } - if secretKey == "" { - return nil, fmt.Errorf("secret key is required") + if keySecret == "" { + return nil, fmt.Errorf("key secret is required") } c := NewClient(server, "", 30*time.Second) - debugInfo, err := PrepareAccessKeyTokenDebug(accessKey, secretKey, time.Now().UTC(), "") + debugInfo, err := PrepareAPIKeyTokenDebug(keyID, keySecret, time.Now().UTC(), "") if err != nil { return nil, fmt.Errorf("prepare signed headers: %w", err) } - var resp APIResponse[accessKeyTokenResponseData] - if err := c.PostWithHeaders(accessKeyTokenPath, debugInfo.Headers(), &resp); err != nil { - return nil, fmt.Errorf("exchange access key token failed: %w", err) + var resp APIResponse[apiKeyTokenResponseData] + if err := c.PostWithHeaders(apiKeyTokenPath, debugInfo.Headers(), &resp); err != nil { + return nil, fmt.Errorf("exchange api key token failed: %w", err) } return &LoginResult{ Token: resp.Data.Token, ExpiresAt: resp.Data.ExpiresAt, AuthType: resp.Data.AuthType, - AccessKey: resp.Data.AccessKey, + KeyID: resp.Data.KeyID, }, nil } @@ -135,100 +136,109 @@ func IsTokenExpired(expiry time.Time) bool { return time.Now().After(expiry) } -// PrepareAccessKeyTokenDebug builds the canonical string, signature, and -// headers for the access-key token exchange request. -func PrepareAccessKeyTokenDebug(accessKey, secretKey string, now time.Time, nonce string) (*AccessKeyTokenDebug, error) { - accessKey = strings.TrimSpace(accessKey) - secretKey = strings.TrimSpace(secretKey) +// PrepareAPIKeyTokenDebug builds the canonical string, signature, and +// headers for the token exchange request. +func PrepareAPIKeyTokenDebug(keyID, keySecret string, now time.Time, nonce string) (*APIKeyTokenDebug, error) { + keyID = strings.TrimSpace(keyID) + keySecret = strings.TrimSpace(keySecret) nonce = strings.TrimSpace(nonce) - if accessKey == "" { - return nil, fmt.Errorf("access key is required") + if keyID == "" { + return nil, fmt.Errorf("key id is required") } - if secretKey == "" { - return nil, fmt.Errorf("secret key is required") + if keySecret == "" { + return nil, fmt.Errorf("key secret is required") } var err error if nonce == "" { - nonce, err = newAccessKeyNonce() + nonce, err = newAPIKeyNonce() if err != nil { return nil, err } } timestamp := strconv.FormatInt(now.Unix(), 10) - canonical := canonicalAccessKeyString("POST", accessKeyTokenPath, accessKey, timestamp, nonce) + bodySHA256 := sha256Hex("") + canonical := canonicalAPIKeyString("POST", apiKeyTokenPath, timestamp, nonce, bodySHA256) - return &AccessKeyTokenDebug{ + return &APIKeyTokenDebug{ Method: "POST", - Path: accessKeyTokenPath, - AccessKey: accessKey, + Path: apiKeyTokenPath, + KeyID: keyID, Timestamp: timestamp, Nonce: nonce, + BodySHA256: bodySHA256, CanonicalString: canonical, - Signature: signAccessKeyRequest(secretKey, canonical), + Signature: signAPIKeyRequest(keySecret, canonical), }, nil } -func buildAccessKeyHeaders(accessKey, secretKey string, now time.Time, path string) (map[string]string, error) { - debugInfo, err := prepareAccessKeyDebug(accessKey, secretKey, now, path, "") +func buildAPIKeyHeaders(keyID, keySecret string, now time.Time, path string) (map[string]string, error) { + debugInfo, err := prepareAPIKeyDebug(keyID, keySecret, now, path, "") if err != nil { return nil, err } return debugInfo.Headers(), nil } -func prepareAccessKeyDebug(accessKey, secretKey string, now time.Time, path, nonce string) (*AccessKeyTokenDebug, error) { - accessKey = strings.TrimSpace(accessKey) - secretKey = strings.TrimSpace(secretKey) +func prepareAPIKeyDebug(keyID, keySecret string, now time.Time, path, nonce string) (*APIKeyTokenDebug, error) { + keyID = strings.TrimSpace(keyID) + keySecret = strings.TrimSpace(keySecret) nonce = strings.TrimSpace(nonce) - if accessKey == "" { - return nil, fmt.Errorf("access key is required") + if keyID == "" { + return nil, fmt.Errorf("key id is required") } - if secretKey == "" { - return nil, fmt.Errorf("secret key is required") + if keySecret == "" { + return nil, fmt.Errorf("key secret is required") } var err error if nonce == "" { - nonce, err = newAccessKeyNonce() + nonce, err = newAPIKeyNonce() if err != nil { return nil, err } } timestamp := strconv.FormatInt(now.Unix(), 10) - canonical := canonicalAccessKeyString("POST", path, accessKey, timestamp, nonce) + bodySHA256 := sha256Hex("") + canonical := canonicalAPIKeyString("POST", path, timestamp, nonce, bodySHA256) - return &AccessKeyTokenDebug{ + return &APIKeyTokenDebug{ Method: "POST", Path: path, - AccessKey: accessKey, + KeyID: keyID, Timestamp: timestamp, Nonce: nonce, + BodySHA256: bodySHA256, CanonicalString: canonical, - Signature: signAccessKeyRequest(secretKey, canonical), + Signature: signAPIKeyRequest(keySecret, canonical), }, nil } -func canonicalAccessKeyString(method, path, accessKey, timestamp, nonce string) string { +func canonicalAPIKeyString(method, path, timestamp, nonce, bodySHA256 string) string { return strings.Join([]string{ strings.ToUpper(method), path, - accessKey, timestamp, nonce, + bodySHA256, }, "\n") } -func signAccessKeyRequest(secretKey, payload string) string { +func signAPIKeyRequest(secretKey, payload string) string { mac := hmac.New(sha256.New, []byte(secretKey)) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } -func newAccessKeyNonce() (string, error) { +func newAPIKeyNonce() (string, error) { nonce := make([]byte, 16) if _, err := rand.Read(nonce); err != nil { return "", fmt.Errorf("generate nonce: %w", err) } return hex.EncodeToString(nonce), nil } + +func sha256Hex(payload string) string { + sum := sha256.Sum256([]byte(payload)) + return hex.EncodeToString(sum[:]) +} diff --git a/src/cmd/aegisctl/client/auth_test.go b/src/cmd/aegisctl/client/auth_test.go index e5e11feb..e728ae62 100644 --- a/src/cmd/aegisctl/client/auth_test.go +++ b/src/cmd/aegisctl/client/auth_test.go @@ -7,34 +7,34 @@ import ( "time" ) -func TestCanonicalAccessKeyString(t *testing.T) { - got := canonicalAccessKeyString( +func TestCanonicalAPIKeyString(t *testing.T) { + got := canonicalAPIKeyString( "post", - "/api/v2/auth/access-key/token", - "ak_demo", + "/api/v2/auth/api-key/token", "1713333333", "abc123", + "body_hash", ) - want := "POST\n/api/v2/auth/access-key/token\nak_demo\n1713333333\nabc123" + want := "POST\n/api/v2/auth/api-key/token\n1713333333\nabc123\nbody_hash" if got != want { t.Fatalf("canonical string mismatch:\nwant: %q\ngot: %q", want, got) } } -func TestBuildAccessKeyHeaders(t *testing.T) { - headers, err := buildAccessKeyHeaders( - "ak_demo", - "sk_demo", +func TestBuildAPIKeyHeaders(t *testing.T) { + headers, err := buildAPIKeyHeaders( + "pk_demo", + "ks_demo", time.Unix(1713333333, 0).UTC(), - "/api/v2/auth/access-key/token", + "/api/v2/auth/api-key/token", ) if err != nil { - t.Fatalf("buildAccessKeyHeaders returned error: %v", err) + t.Fatalf("buildAPIKeyHeaders returned error: %v", err) } - if headers["X-Access-Key"] != "ak_demo" { - t.Fatalf("unexpected access key header: %q", headers["X-Access-Key"]) + if headers["X-Key-Id"] != "pk_demo" { + t.Fatalf("unexpected key id header: %q", headers["X-Key-Id"]) } if headers["X-Timestamp"] != "1713333333" { t.Fatalf("unexpected timestamp header: %q", headers["X-Timestamp"]) @@ -47,26 +47,29 @@ func TestBuildAccessKeyHeaders(t *testing.T) { } } -func TestPrepareAccessKeyTokenDebug(t *testing.T) { - debugInfo, err := PrepareAccessKeyTokenDebug( - "ak_demo", - "sk_demo", +func TestPrepareAPIKeyTokenDebug(t *testing.T) { + debugInfo, err := PrepareAPIKeyTokenDebug( + "pk_demo", + "ks_demo", time.Unix(1713333333, 0).UTC(), "abc123", ) if err != nil { - t.Fatalf("PrepareAccessKeyTokenDebug returned error: %v", err) + t.Fatalf("PrepareAPIKeyTokenDebug returned error: %v", err) } if debugInfo.Method != "POST" { t.Fatalf("unexpected method: %q", debugInfo.Method) } - if debugInfo.Path != "/api/v2/auth/access-key/token" { + if debugInfo.Path != "/api/v2/auth/api-key/token" { t.Fatalf("unexpected path: %q", debugInfo.Path) } - if debugInfo.CanonicalString != "POST\n/api/v2/auth/access-key/token\nak_demo\n1713333333\nabc123" { + if debugInfo.CanonicalString != "POST\n/api/v2/auth/api-key/token\n1713333333\nabc123\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { t.Fatalf("unexpected canonical string: %q", debugInfo.CanonicalString) } + if debugInfo.BodySHA256 != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" { + t.Fatalf("unexpected body hash: %q", debugInfo.BodySHA256) + } if debugInfo.Headers()["X-Signature"] != debugInfo.Signature { t.Fatal("signature header mismatch") } @@ -74,8 +77,8 @@ func TestPrepareAccessKeyTokenDebug(t *testing.T) { func TestPostWithHeaders(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if got := r.Header.Get("X-Access-Key"); got != "ak_demo" { - t.Fatalf("unexpected X-Access-Key header: %q", got) + if got := r.Header.Get("X-Key-Id"); got != "pk_demo" { + t.Fatalf("unexpected X-Key-Id header: %q", got) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -85,8 +88,8 @@ func TestPostWithHeaders(t *testing.T) { c := NewClient(server.URL, "", 5*time.Second) var resp APIResponse[map[string]any] - if err := c.PostWithHeaders("/api/v2/auth/access-key/token", map[string]string{ - "X-Access-Key": "ak_demo", + if err := c.PostWithHeaders("/api/v2/auth/api-key/token", map[string]string{ + "X-Key-Id": "pk_demo", }, &resp); err != nil { t.Fatalf("PostWithHeaders returned error: %v", err) } diff --git a/src/cmd/aegisctl/cmd/auth.go b/src/cmd/aegisctl/cmd/auth.go index 73da9ba2..212b8530 100644 --- a/src/cmd/aegisctl/cmd/auth.go +++ b/src/cmd/aegisctl/cmd/auth.go @@ -21,13 +21,13 @@ var authCmd = &cobra.Command{ // --- auth login --- var authLoginServer string -var authLoginAccessKey string -var authLoginSecretKey string +var authLoginKeyID string +var authLoginKeySecret string var authLoginContext string var authLoginCmd = &cobra.Command{ Use: "login", - Short: "Exchange AK/SK for a bearer token", + Short: "Exchange Key ID / Key Secret for a bearer token", RunE: func(cmd *cobra.Command, args []string) error { server := authLoginServer if server == "" { @@ -37,25 +37,25 @@ var authLoginCmd = &cobra.Command{ return fmt.Errorf("--server is required for login") } - accessKey := authLoginAccessKey - if accessKey == "" { - accessKey = os.Getenv("AEGIS_ACCESS_KEY") + keyID := authLoginKeyID + if keyID == "" { + keyID = os.Getenv("AEGIS_KEY_ID") } - if accessKey == "" { - return fmt.Errorf("--access-key is required") + if keyID == "" { + return fmt.Errorf("--key-id is required") } - secretKey := authLoginSecretKey - if secretKey == "" { - secretKey = os.Getenv("AEGIS_SECRET_KEY") + keySecret := authLoginKeySecret + if keySecret == "" { + keySecret = os.Getenv("AEGIS_KEY_SECRET") } - if secretKey == "" { - return fmt.Errorf("--secret-key is required") + if keySecret == "" { + return fmt.Errorf("--key-secret is required") } - output.PrintInfo(fmt.Sprintf("Exchanging access key token with %s using %s...", server, accessKey)) + output.PrintInfo(fmt.Sprintf("Exchanging API key token with %s using %s...", server, keyID)) - result, err := client.LoginWithAccessKey(server, accessKey, secretKey) + result, err := client.LoginWithAPIKey(server, keyID, keySecret) if err != nil { return err } @@ -71,7 +71,7 @@ var authLoginCmd = &cobra.Command{ Server: server, Token: result.Token, AuthType: result.AuthType, - AccessKey: result.AccessKey, + KeyID: result.KeyID, TokenExpiry: result.ExpiresAt, } cfg.CurrentContext = ctxName @@ -85,11 +85,11 @@ var authLoginCmd = &cobra.Command{ "context": ctxName, "server": server, "auth_type": result.AuthType, - "access_key": result.AccessKey, + "key_id": result.KeyID, "expires_at": result.ExpiresAt.Format(time.RFC3339), }) } else { - output.PrintInfo(fmt.Sprintf("Token issued for access key %s (context: %s)", result.AccessKey, ctxName)) + output.PrintInfo(fmt.Sprintf("Token issued for key id %s (context: %s)", result.KeyID, ctxName)) output.PrintInfo(fmt.Sprintf("Token expires at %s", result.ExpiresAt.Format(time.RFC3339))) } return nil @@ -123,7 +123,7 @@ var authStatusCmd = &cobra.Command{ "server": ctx.Server, "status": status, "auth_type": ctx.AuthType, - "access_key": ctx.AccessKey, + "key_id": ctx.KeyID, "expires_at": ctx.TokenExpiry.Format(time.RFC3339), }) return nil @@ -151,8 +151,8 @@ var authStatusCmd = &cobra.Command{ } else { output.PrintInfo(fmt.Sprintf("Authenticated as: %s (id: %d)", profile.Username, profile.ID)) } - if ctx.AccessKey != "" { - output.PrintInfo(fmt.Sprintf("Issued via access key: %s", ctx.AccessKey)) + if ctx.KeyID != "" { + output.PrintInfo(fmt.Sprintf("Issued via key id: %s", ctx.KeyID)) } return nil @@ -191,7 +191,7 @@ var authInspectCmd = &cobra.Command{ "context": ctxName, "server": ctx.Server, "auth_type": ctx.AuthType, - "access_key": ctx.AccessKey, + "key_id": ctx.KeyID, "token_present": ctx.Token != "", "token_preview": tokenPreview, "token_expired": expired, @@ -201,12 +201,12 @@ var authInspectCmd = &cobra.Command{ } output.PrintTable( - []string{"Context", "Server", "AuthType", "AccessKey", "Token", "Expired", "Expires"}, + []string{"Context", "Server", "AuthType", "KeyID", "Token", "Expired", "Expires"}, [][]string{{ ctxName, ctx.Server, emptyOrValue(ctx.AuthType, "-"), - emptyOrValue(ctx.AccessKey, "-"), + emptyOrValue(ctx.KeyID, "-"), emptyOrValue(tokenPreview, "-"), fmt.Sprintf("%t", expired), emptyOrValue(expiresAt, "-"), @@ -218,8 +218,8 @@ var authInspectCmd = &cobra.Command{ // --- auth sign-debug --- -var authSignDebugAccessKey string -var authSignDebugSecretKey string +var authSignDebugKeyID string +var authSignDebugKeySecret string var authSignDebugTimestamp int64 var authSignDebugNonce string var authSignDebugExecute bool @@ -227,22 +227,22 @@ var authSignDebugSaveContext bool var authSignDebugCmd = &cobra.Command{ Use: "sign-debug", - Short: "Print canonical string and signature headers for AK/SK token exchange", + Short: "Print canonical string and signature headers for Key ID / Key Secret token exchange", RunE: func(cmd *cobra.Command, args []string) error { - accessKey := authSignDebugAccessKey - if accessKey == "" { - accessKey = os.Getenv("AEGIS_ACCESS_KEY") + keyID := authSignDebugKeyID + if keyID == "" { + keyID = os.Getenv("AEGIS_KEY_ID") } - if accessKey == "" { - return fmt.Errorf("--access-key is required") + if keyID == "" { + return fmt.Errorf("--key-id is required") } - secretKey := authSignDebugSecretKey - if secretKey == "" { - secretKey = os.Getenv("AEGIS_SECRET_KEY") + keySecret := authSignDebugKeySecret + if keySecret == "" { + keySecret = os.Getenv("AEGIS_KEY_SECRET") } - if secretKey == "" { - return fmt.Errorf("--secret-key is required") + if keySecret == "" { + return fmt.Errorf("--key-secret is required") } signTime := time.Now().UTC() @@ -250,7 +250,7 @@ var authSignDebugCmd = &cobra.Command{ signTime = time.Unix(authSignDebugTimestamp, 0).UTC() } - debugInfo, err := client.PrepareAccessKeyTokenDebug(accessKey, secretKey, signTime, authSignDebugNonce) + debugInfo, err := client.PrepareAPIKeyTokenDebug(keyID, keySecret, signTime, authSignDebugNonce) if err != nil { return err } @@ -259,15 +259,15 @@ var authSignDebugCmd = &cobra.Command{ if authSignDebugExecute && (server == "" || strings.Contains(server, "HOST:8082")) { return fmt.Errorf("--execute requires a real --server or configured AEGIS_SERVER/current context") } - curlCommand := buildAccessKeyCurl(server, debugInfo) + curlCommand := buildAPIKeyCurl(server, debugInfo) var executeResp map[string]any if authSignDebugExecute { - executeResp, err = executeAccessKeyTokenExchange(server, debugInfo) + executeResp, err = executeAPIKeyTokenExchange(server, debugInfo) if err != nil { return err } if authSignDebugSaveContext { - if err := saveAccessKeyContext(server, executeResp); err != nil { + if err := saveAPIKeyContext(server, executeResp); err != nil { return err } } @@ -280,9 +280,10 @@ var authSignDebugCmd = &cobra.Command{ "server": server, "method": debugInfo.Method, "path": debugInfo.Path, - "access_key": debugInfo.AccessKey, + "key_id": debugInfo.KeyID, "timestamp": debugInfo.Timestamp, "nonce": debugInfo.Nonce, + "body_sha256": debugInfo.BodySHA256, "canonical_string": debugInfo.CanonicalString, "signature": debugInfo.Signature, "headers": debugInfo.Headers(), @@ -300,9 +301,10 @@ var authSignDebugCmd = &cobra.Command{ fmt.Printf("Server: %s\n", server) fmt.Printf("Method: %s\n", debugInfo.Method) fmt.Printf("Path: %s\n", debugInfo.Path) - fmt.Printf("Access-Key: %s\n", debugInfo.AccessKey) + fmt.Printf("Key-Id: %s\n", debugInfo.KeyID) fmt.Printf("Timestamp: %s\n", debugInfo.Timestamp) fmt.Printf("Nonce: %s\n", debugInfo.Nonce) + fmt.Printf("Body-SHA256: %s\n", debugInfo.BodySHA256) fmt.Printf("Signature: %s\n\n", debugInfo.Signature) fmt.Println("Canonical String:") fmt.Println(debugInfo.CanonicalString) @@ -354,7 +356,7 @@ var authTokenCmd = &cobra.Command{ ctx := cfg.Contexts[ctxName] ctx.Token = authTokenSet ctx.AuthType = "token" - ctx.AccessKey = "" + ctx.KeyID = "" ctx.TokenExpiry = time.Time{} cfg.Contexts[ctxName] = ctx cfg.CurrentContext = ctxName @@ -370,11 +372,11 @@ var authTokenCmd = &cobra.Command{ func init() { authLoginCmd.Flags().StringVar(&authLoginServer, "server", "", "Server URL") - authLoginCmd.Flags().StringVar(&authLoginAccessKey, "access-key", "", "Access key (env: AEGIS_ACCESS_KEY)") - authLoginCmd.Flags().StringVar(&authLoginSecretKey, "secret-key", "", "Secret key (env: AEGIS_SECRET_KEY)") + authLoginCmd.Flags().StringVar(&authLoginKeyID, "key-id", "", "Key ID (env: AEGIS_KEY_ID)") + authLoginCmd.Flags().StringVar(&authLoginKeySecret, "key-secret", "", "Key secret (env: AEGIS_KEY_SECRET)") authLoginCmd.Flags().StringVar(&authLoginContext, "context", "", "Context name to save credentials under (default: \"default\")") - authSignDebugCmd.Flags().StringVar(&authSignDebugAccessKey, "access-key", "", "Access key (env: AEGIS_ACCESS_KEY)") - authSignDebugCmd.Flags().StringVar(&authSignDebugSecretKey, "secret-key", "", "Secret key (env: AEGIS_SECRET_KEY)") + authSignDebugCmd.Flags().StringVar(&authSignDebugKeyID, "key-id", "", "Key ID (env: AEGIS_KEY_ID)") + authSignDebugCmd.Flags().StringVar(&authSignDebugKeySecret, "key-secret", "", "Key secret (env: AEGIS_KEY_SECRET)") authSignDebugCmd.Flags().Int64Var(&authSignDebugTimestamp, "timestamp", 0, "Override unix timestamp in seconds") authSignDebugCmd.Flags().StringVar(&authSignDebugNonce, "nonce", "", "Override nonce for reproducible signature output") authSignDebugCmd.Flags().BoolVar(&authSignDebugExecute, "execute", false, "Execute the signed token exchange request and print the response") @@ -404,19 +406,19 @@ func resolveServerForAuthDebug() string { return "http://HOST:8082" } -func buildAccessKeyCurl(server string, debugInfo *client.AccessKeyTokenDebug) string { +func buildAPIKeyCurl(server string, debugInfo *client.APIKeyTokenDebug) string { return fmt.Sprintf( - "curl -X POST %s%s -H 'Accept: application/json' -H 'X-Access-Key: %s' -H 'X-Timestamp: %s' -H 'X-Nonce: %s' -H 'X-Signature: %s'", + "curl -X POST %s%s -H 'Accept: application/json' -H 'X-Key-Id: %s' -H 'X-Timestamp: %s' -H 'X-Nonce: %s' -H 'X-Signature: %s'", server, debugInfo.Path, - debugInfo.AccessKey, + debugInfo.KeyID, debugInfo.Timestamp, debugInfo.Nonce, debugInfo.Signature, ) } -func executeAccessKeyTokenExchange(server string, debugInfo *client.AccessKeyTokenDebug) (map[string]any, error) { +func executeAPIKeyTokenExchange(server string, debugInfo *client.APIKeyTokenDebug) (map[string]any, error) { httpClient := client.NewClient(server, "", 30*time.Second) var response map[string]any if err := httpClient.PostWithHeaders(debugInfo.Path, debugInfo.Headers(), &response); err != nil { @@ -425,7 +427,7 @@ func executeAccessKeyTokenExchange(server string, debugInfo *client.AccessKeyTok return response, nil } -func saveAccessKeyContext(server string, executeResp map[string]any) error { +func saveAPIKeyContext(server string, executeResp map[string]any) error { ctxName := resolveContextNameForSave() ctx := cfg.Contexts[ctxName] ctx.Server = server @@ -444,8 +446,8 @@ func saveAccessKeyContext(server string, executeResp map[string]any) error { if authType, _ := data["auth_type"].(string); strings.TrimSpace(authType) != "" { ctx.AuthType = authType } - if accessKey, _ := data["access_key"].(string); strings.TrimSpace(accessKey) != "" { - ctx.AccessKey = accessKey + if keyID, _ := data["key_id"].(string); strings.TrimSpace(keyID) != "" { + ctx.KeyID = keyID } if expiresAt, _ := data["expires_at"].(string); strings.TrimSpace(expiresAt) != "" { parsed, err := time.Parse(time.RFC3339, expiresAt) diff --git a/src/cmd/aegisctl/cmd/root.go b/src/cmd/aegisctl/cmd/root.go index 05e83533..3ca51bd6 100644 --- a/src/cmd/aegisctl/cmd/root.go +++ b/src/cmd/aegisctl/cmd/root.go @@ -33,8 +33,8 @@ var rootCmd = &cobra.Command{ fault-injection and root-cause-analysis benchmarking platform. QUICK START: - # 1. Exchange AK/SK for a token (saves token to ~/.aegisctl/config.yaml) - aegisctl auth login --server http://HOST:8082 --access-key ak_xxx --secret-key sk_xxx + # 1. Exchange Key ID / Key Secret for a token (saves token to ~/.aegisctl/config.yaml) + aegisctl auth login --server http://HOST:8082 --key-id pk_xxx --key-secret ks_xxx # 2. Set default project so you don't need --project every time aegisctl context set --name default --default-project pair_diagnosis @@ -70,8 +70,8 @@ OUTPUT: ENVIRONMENT VARIABLES: AEGIS_SERVER - Server URL (overridden by --server flag) AEGIS_TOKEN - Auth token (overridden by --token flag) - AEGIS_ACCESS_KEY - Access key for 'aegisctl auth login' - AEGIS_SECRET_KEY - Secret key for 'aegisctl auth login' + AEGIS_KEY_ID - API key ID for 'aegisctl auth login' + AEGIS_KEY_SECRET - API key secret for 'aegisctl auth login' AEGIS_PROJECT - Default project name (overridden by --project flag) AEGIS_OUTPUT - Output format: table|json (overridden by --output flag) AEGIS_TIMEOUT - Request timeout in seconds (overridden by --request-timeout flag) diff --git a/src/cmd/aegisctl/config/config.go b/src/cmd/aegisctl/config/config.go index 5aea9622..6313de22 100644 --- a/src/cmd/aegisctl/config/config.go +++ b/src/cmd/aegisctl/config/config.go @@ -21,11 +21,35 @@ type Context struct { Server string `yaml:"server"` Token string `yaml:"token,omitempty"` AuthType string `yaml:"auth-type,omitempty"` - AccessKey string `yaml:"access-key,omitempty"` + KeyID string `yaml:"key-id,omitempty"` DefaultProject string `yaml:"default-project,omitempty"` TokenExpiry time.Time `yaml:"token-expiry,omitempty"` } +func (c *Context) UnmarshalYAML(value *yaml.Node) error { + type rawContext struct { + Server string `yaml:"server"` + Token string `yaml:"token,omitempty"` + AuthType string `yaml:"auth-type,omitempty"` + KeyID string `yaml:"key-id,omitempty"` + DefaultProject string `yaml:"default-project,omitempty"` + TokenExpiry time.Time `yaml:"token-expiry,omitempty"` + } + + var raw rawContext + if err := value.Decode(&raw); err != nil { + return err + } + + c.Server = raw.Server + c.Token = raw.Token + c.AuthType = raw.AuthType + c.KeyID = raw.KeyID + c.DefaultProject = raw.DefaultProject + c.TokenExpiry = raw.TokenExpiry + return nil +} + // Preferences holds user-level defaults. type Preferences struct { Output string `yaml:"output,omitempty"` diff --git a/src/docs/docs_test.go b/src/docs/docs_test.go index 5f248442..e496af26 100644 --- a/src/docs/docs_test.go +++ b/src/docs/docs_test.go @@ -23,15 +23,20 @@ func TestGeneratedAPIDocsContainCorePaths(t *testing.T) { `"/api/v2/users"`, }) checkJSONContains(t, filepath.Join(baseDir, "converted", "sdk.json"), []string{ - `"/api/v2/auth/access-key/token"`, + `"/api/v2/auth/api-key/token"`, `"/api/v2/sdk/evaluations"`, }) + checkJSONContains(t, filepath.Join(baseDir, "converted", "runtime.json"), []string{ + `"/api/v2/executions/{execution_id}/detector_results"`, + `"/api/v2/executions/{execution_id}/granularity_results"`, + }) } func TestAudienceFilteredDocsMatchOpenAPI3Extensions(t *testing.T) { openapi := readJSON(t, filepath.Join(".", "openapi3", "openapi.json")) checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "sdk.json"), "sdk") + checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "runtime.json"), "runtime") checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "portal.json"), "portal") checkAudienceMatches(t, openapi, filepath.Join(".", "converted", "admin.json"), "admin") } diff --git a/src/infra/db/migration.go b/src/infra/db/migration.go index f8dc4da3..b0ee43e4 100644 --- a/src/infra/db/migration.go +++ b/src/infra/db/migration.go @@ -18,7 +18,7 @@ func migrate(db *gorm.DB) { &model.Project{}, &model.Label{}, &model.User{}, - &model.UserAccessKey{}, + &model.APIKey{}, &model.Role{}, &model.Permission{}, &model.Resource{}, diff --git a/src/interface/grpciam/service.go b/src/interface/grpciam/service.go index 4795ef58..c84c3124 100644 --- a/src/interface/grpciam/service.go +++ b/src/interface/grpciam/service.go @@ -69,7 +69,8 @@ func (s *iamServer) VerifyToken(ctx context.Context, req *iamv1.VerifyTokenReque Roles: claims.Roles, ExpiresAtUnix: claims.ExpiresAt.Unix(), AuthType: claims.AuthType, - AccessKeyId: int64(claims.AccessKeyID), + KeyId: int64(claims.APIKeyID), + ApiKeyScopes: append([]string(nil), claims.APIKeyScopes...), }, nil } @@ -202,88 +203,98 @@ func (s *iamServer) GetProfile(ctx context.Context, req *iamv1.UserIDRequest) (* return encodeStruct(resp) } -func (s *iamServer) CreateAccessKey(ctx context.Context, req *iamv1.UserBodyRequest) (*iamv1.StructResponse, error) { +func (s *iamServer) CreateAPIKey(ctx context.Context, req *iamv1.UserBodyRequest) (*iamv1.StructResponse, error) { if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[authmodule.CreateAccessKeyReq](req.GetBody()) + body, err := decodeBody[authmodule.CreateAPIKeyReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } if err := body.Validate(); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } - resp, err := s.authAPI.CreateAccessKey(ctx, int(req.GetUserId()), body) + resp, err := s.authAPI.CreateAPIKey(ctx, int(req.GetUserId()), body) if err != nil { return nil, mapIAMError(err) } return encodeStruct(resp) } -func (s *iamServer) ListAccessKeys(ctx context.Context, req *iamv1.UserQueryRequest) (*iamv1.StructResponse, error) { +func (s *iamServer) ListAPIKeys(ctx context.Context, req *iamv1.UserQueryRequest) (*iamv1.StructResponse, error) { if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - query, err := decodeQuery[authmodule.ListAccessKeyReq](req.GetQuery()) + query, err := decodeQuery[authmodule.ListAPIKeyReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } if err := query.Validate(); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } - resp, err := s.authAPI.ListAccessKeys(ctx, int(req.GetUserId()), query) + resp, err := s.authAPI.ListAPIKeys(ctx, int(req.GetUserId()), query) if err != nil { return nil, mapIAMError(err) } return encodeStruct(resp) } -func (s *iamServer) GetAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { +func (s *iamServer) GetAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { if req.GetUserId() <= 0 || req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id and id are required") } - resp, err := s.authAPI.GetAccessKey(ctx, int(req.GetUserId()), int(req.GetId())) + resp, err := s.authAPI.GetAPIKey(ctx, int(req.GetUserId()), int(req.GetId())) if err != nil { return nil, mapIAMError(err) } return encodeStruct(resp) } -func (s *iamServer) DeleteAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { +func (s *iamServer) DeleteAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { if req.GetUserId() <= 0 || req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id and id are required") } - if err := s.authAPI.DeleteAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + if err := s.authAPI.DeleteAPIKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { return nil, mapIAMError(err) } return &emptypb.Empty{}, nil } -func (s *iamServer) DisableAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { +func (s *iamServer) DisableAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { if req.GetUserId() <= 0 || req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id and id are required") } - if err := s.authAPI.DisableAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + if err := s.authAPI.DisableAPIKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { return nil, mapIAMError(err) } return &emptypb.Empty{}, nil } -func (s *iamServer) EnableAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { +func (s *iamServer) EnableAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { if req.GetUserId() <= 0 || req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id and id are required") } - if err := s.authAPI.EnableAccessKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + if err := s.authAPI.EnableAPIKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { return nil, mapIAMError(err) } return &emptypb.Empty{}, nil } -func (s *iamServer) RotateAccessKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { +func (s *iamServer) RevokeAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*emptypb.Empty, error) { if req.GetUserId() <= 0 || req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id and id are required") } - resp, err := s.authAPI.RotateAccessKey(ctx, int(req.GetUserId()), int(req.GetId())) + if err := s.authAPI.RevokeAPIKey(ctx, int(req.GetUserId()), int(req.GetId())); err != nil { + return nil, mapIAMError(err) + } + return &emptypb.Empty{}, nil +} + +func (s *iamServer) RotateAPIKey(ctx context.Context, req *iamv1.UserScopedIDRequest) (*iamv1.StructResponse, error) { + if req.GetUserId() <= 0 || req.GetId() <= 0 { + return nil, status.Error(codes.InvalidArgument, "user_id and id are required") + } + resp, err := s.authAPI.RotateAPIKey(ctx, int(req.GetUserId()), int(req.GetId())) if err != nil { return nil, mapIAMError(err) } @@ -710,9 +721,9 @@ func (s *iamServer) IsUserInProject(ctx context.Context, req *iamv1.UserProjectR return &iamv1.BoolResponse{Value: allowed}, nil } -func (s *iamServer) ExchangeAccessKeyToken(ctx context.Context, req *iamv1.ExchangeAccessKeyTokenRequest) (*iamv1.ExchangeAccessKeyTokenResponse, error) { - authReq := &authmodule.AccessKeyTokenReq{ - AccessKey: req.GetAccessKey(), +func (s *iamServer) ExchangeAPIKeyToken(ctx context.Context, req *iamv1.ExchangeAPIKeyTokenRequest) (*iamv1.ExchangeAPIKeyTokenResponse, error) { + authReq := &authmodule.APIKeyTokenReq{ + KeyID: req.GetKeyId(), Timestamp: req.GetTimestamp(), Nonce: req.GetNonce(), Signature: req.GetSignature(), @@ -724,16 +735,16 @@ func (s *iamServer) ExchangeAccessKeyToken(ctx context.Context, req *iamv1.Excha return nil, status.Error(codes.InvalidArgument, "method and path are required") } - resp, err := s.auth.ExchangeAccessKeyToken(ctx, authReq, req.GetMethod(), req.GetPath()) + resp, err := s.auth.ExchangeAPIKeyToken(ctx, authReq, req.GetMethod(), req.GetPath()) if err != nil { return nil, mapIAMError(err) } - return &iamv1.ExchangeAccessKeyTokenResponse{ + return &iamv1.ExchangeAPIKeyTokenResponse{ Token: resp.Token, TokenType: resp.TokenType, ExpiresAtUnix: resp.ExpiresAt.Unix(), AuthType: resp.AuthType, - AccessKey: resp.AccessKey, + KeyId: resp.KeyID, }, nil } diff --git a/src/interface/grpciam/service_test.go b/src/interface/grpciam/service_test.go index a7a8d496..0f6acaaa 100644 --- a/src/interface/grpciam/service_test.go +++ b/src/interface/grpciam/service_test.go @@ -2,6 +2,7 @@ package grpciaminterface import ( "context" + "reflect" "testing" "time" @@ -122,6 +123,27 @@ func TestIAMServerVerifyTokenUser(t *testing.T) { } } +func TestIAMServerVerifyTokenAPIKeyScopes(t *testing.T) { + token, _, err := utils.GenerateAPIKeyToken(7, "demo", "demo@example.com", true, false, []string{"user"}, 11, []string{"project:read", "execution:write"}) + if err != nil { + t.Fatalf("GenerateAPIKeyToken() error = %v", err) + } + + authSvc := authmodule.NewService(nil, nil, nil, nil) + server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) + resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) + if err != nil { + t.Fatalf("VerifyToken() error = %v", err) + } + + if resp.AuthType != "api_key" || resp.KeyId != 11 { + t.Fatalf("VerifyToken() unexpected api-key response: %+v", resp) + } + if !reflect.DeepEqual(resp.ApiKeyScopes, []string{"project:read", "execution:write"}) { + t.Fatalf("VerifyToken() api_key_scopes = %v", resp.ApiKeyScopes) + } +} + func TestIAMServerCheckPermission(t *testing.T) { authSvc := authmodule.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) diff --git a/src/internalclient/iamclient/client.go b/src/internalclient/iamclient/client.go index 28e4d423..03628fd3 100644 --- a/src/internalclient/iamclient/client.go +++ b/src/internalclient/iamclient/client.go @@ -83,14 +83,15 @@ func (c *Client) VerifyToken(ctx context.Context, token string) (*utils.Claims, return nil, fmt.Errorf("token is not a user token") } return &utils.Claims{ - UserID: int(resp.GetUserId()), - Username: resp.GetUsername(), - Email: resp.GetEmail(), - IsActive: resp.GetIsActive(), - IsAdmin: resp.GetIsAdmin(), - Roles: resp.GetRoles(), - AuthType: resp.GetAuthType(), - AccessKeyID: int(resp.GetAccessKeyId()), + UserID: int(resp.GetUserId()), + Username: resp.GetUsername(), + Email: resp.GetEmail(), + IsActive: resp.GetIsActive(), + IsAdmin: resp.GetIsAdmin(), + Roles: resp.GetRoles(), + AuthType: resp.GetAuthType(), + APIKeyID: int(resp.GetKeyId()), + APIKeyScopes: resp.GetApiKeyScopes(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Unix(resp.GetExpiresAtUnix(), 0)), }, @@ -309,109 +310,120 @@ func (c *Client) GetProfile(ctx context.Context, userID int) (*authmodule.UserPr return decodeStruct[authmodule.UserProfileResp](resp.GetData()) } -func (c *Client) CreateAccessKey(ctx context.Context, userID int, req *authmodule.CreateAccessKeyReq) (*authmodule.AccessKeyWithSecretResp, error) { +func (c *Client) CreateAPIKey(ctx context.Context, userID int, req *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } body, err := toStructPB(req) if err != nil { - return nil, fmt.Errorf("encode create access key request: %w", err) + return nil, fmt.Errorf("encode create api key request: %w", err) } - resp, err := c.rpc.CreateAccessKey(ctx, &iamv1.UserBodyRequest{ + resp, err := c.rpc.CreateAPIKey(ctx, &iamv1.UserBodyRequest{ UserId: int64(userID), Body: body, }) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.AccessKeyWithSecretResp](resp.GetData()) + return decodeStruct[authmodule.APIKeyWithSecretResp](resp.GetData()) } -func (c *Client) ListAccessKeys(ctx context.Context, userID int, req *authmodule.ListAccessKeyReq) (*authmodule.ListAccessKeyResp, error) { +func (c *Client) ListAPIKeys(ctx context.Context, userID int, req *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } query, err := toStructPB(req) if err != nil { - return nil, fmt.Errorf("encode list access keys request: %w", err) + return nil, fmt.Errorf("encode list api keys request: %w", err) } - resp, err := c.rpc.ListAccessKeys(ctx, &iamv1.UserQueryRequest{ + resp, err := c.rpc.ListAPIKeys(ctx, &iamv1.UserQueryRequest{ UserId: int64(userID), Query: query, }) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.ListAccessKeyResp](resp.GetData()) + return decodeStruct[authmodule.ListAPIKeyResp](resp.GetData()) } -func (c *Client) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyInfo, error) { +func (c *Client) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyInfo, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } - resp, err := c.rpc.GetAccessKey(ctx, &iamv1.UserScopedIDRequest{ + resp, err := c.rpc.GetAPIKey(ctx, &iamv1.UserScopedIDRequest{ UserId: int64(userID), Id: int64(accessKeyID), }) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.AccessKeyInfo](resp.GetData()) + return decodeStruct[authmodule.APIKeyInfo](resp.GetData()) } -func (c *Client) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (c *Client) DeleteAPIKey(ctx context.Context, userID, accessKeyID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } - _, err := c.rpc.DeleteAccessKey(ctx, &iamv1.UserScopedIDRequest{ + _, err := c.rpc.DeleteAPIKey(ctx, &iamv1.UserScopedIDRequest{ UserId: int64(userID), Id: int64(accessKeyID), }) return mapRPCError(err) } -func (c *Client) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (c *Client) DisableAPIKey(ctx context.Context, userID, accessKeyID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } - _, err := c.rpc.DisableAccessKey(ctx, &iamv1.UserScopedIDRequest{ + _, err := c.rpc.DisableAPIKey(ctx, &iamv1.UserScopedIDRequest{ UserId: int64(userID), Id: int64(accessKeyID), }) return mapRPCError(err) } -func (c *Client) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { +func (c *Client) EnableAPIKey(ctx context.Context, userID, accessKeyID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } - _, err := c.rpc.EnableAccessKey(ctx, &iamv1.UserScopedIDRequest{ + _, err := c.rpc.EnableAPIKey(ctx, &iamv1.UserScopedIDRequest{ UserId: int64(userID), Id: int64(accessKeyID), }) return mapRPCError(err) } -func (c *Client) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*authmodule.AccessKeyWithSecretResp, error) { +func (c *Client) RevokeAPIKey(ctx context.Context, userID, accessKeyID int) error { + if !c.Enabled() { + return fmt.Errorf("iam grpc client is not configured") + } + _, err := c.rpc.RevokeAPIKey(ctx, &iamv1.UserScopedIDRequest{ + UserId: int64(userID), + Id: int64(accessKeyID), + }) + return mapRPCError(err) +} + +func (c *Client) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyWithSecretResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } - resp, err := c.rpc.RotateAccessKey(ctx, &iamv1.UserScopedIDRequest{ + resp, err := c.rpc.RotateAPIKey(ctx, &iamv1.UserScopedIDRequest{ UserId: int64(userID), Id: int64(accessKeyID), }) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.AccessKeyWithSecretResp](resp.GetData()) + return decodeStruct[authmodule.APIKeyWithSecretResp](resp.GetData()) } -func (c *Client) ExchangeAccessKeyToken(ctx context.Context, req *authmodule.AccessKeyTokenReq, method, path string) (*authmodule.AccessKeyTokenResp, error) { +func (c *Client) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKeyTokenReq, method, path string) (*authmodule.APIKeyTokenResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } - resp, err := c.rpc.ExchangeAccessKeyToken(ctx, &iamv1.ExchangeAccessKeyTokenRequest{ - AccessKey: req.AccessKey, + resp, err := c.rpc.ExchangeAPIKeyToken(ctx, &iamv1.ExchangeAPIKeyTokenRequest{ + KeyId: req.KeyID, Timestamp: req.Timestamp, Nonce: req.Nonce, Signature: req.Signature, @@ -421,12 +433,12 @@ func (c *Client) ExchangeAccessKeyToken(ctx context.Context, req *authmodule.Acc if err != nil { return nil, mapRPCError(err) } - return &authmodule.AccessKeyTokenResp{ + return &authmodule.APIKeyTokenResp{ Token: resp.GetToken(), TokenType: resp.GetTokenType(), ExpiresAt: time.Unix(resp.GetExpiresAtUnix(), 0), AuthType: resp.GetAuthType(), - AccessKey: resp.GetAccessKey(), + KeyID: resp.GetKeyId(), }, nil } diff --git a/src/middleware/api_key_scope.go b/src/middleware/api_key_scope.go new file mode 100644 index 00000000..8433db07 --- /dev/null +++ b/src/middleware/api_key_scope.go @@ -0,0 +1,97 @@ +package middleware + +import ( + "net/http" + "strings" + + "aegis/dto" + + "github.com/gin-gonic/gin" +) + +func apiKeyScopeMatchesTarget(scope, target string) bool { + scope = strings.TrimSpace(scope) + target = strings.TrimSpace(target) + if scope == "" || target == "" { + return false + } + if scope == "*" { + return true + } + + targetParts := strings.Split(target, ":") + scopeParts := strings.Split(scope, ":") + if len(scopeParts) > len(targetParts) { + return false + } + for len(scopeParts) < len(targetParts) { + scopeParts = append(scopeParts, "*") + } + for i := range targetParts { + part := strings.TrimSpace(scopeParts[i]) + if part == "*" { + continue + } + if part != targetParts[i] { + return false + } + } + return true +} + +func apiKeyScopesAllowAnyTarget(scopes, targets []string) bool { + if len(scopes) == 0 || len(targets) == 0 { + return false + } + for _, scope := range scopes { + for _, target := range targets { + if apiKeyScopeMatchesTarget(scope, target) { + return true + } + } + } + return false +} + +// RequireHumanUserAuth rejects service tokens and API key bearer tokens. +// It is intended for self-service user/account endpoints. +func RequireHumanUserAuth() gin.HandlerFunc { + return func(c *gin.Context) { + if !RequireUserAuth(c) { + c.Abort() + return + } + if GetAuthType(c) == "api_key" { + dto.ErrorResponse(c, http.StatusForbidden, "User session required, API key token not allowed") + c.Abort() + return + } + c.Next() + } +} + +// RequireAPIKeyScopesAny applies explicit scope checks only to API key bearer tokens. +// Human user tokens continue through unchanged. +func RequireAPIKeyScopesAny(targets ...string) gin.HandlerFunc { + trimmed := make([]string, 0, len(targets)) + for _, target := range targets { + target = strings.TrimSpace(target) + if target != "" { + trimmed = append(trimmed, target) + } + } + + return func(c *gin.Context) { + if GetAuthType(c) != "api_key" { + c.Next() + return + } + scopes, ok := GetCurrentAPIKeyScopes(c) + if !ok || !apiKeyScopesAllowAnyTarget(scopes, trimmed) { + dto.ErrorResponse(c, http.StatusForbidden, "API key scope does not allow this endpoint") + c.Abort() + return + } + c.Next() + } +} diff --git a/src/middleware/api_key_scope_test.go b/src/middleware/api_key_scope_test.go new file mode 100644 index 00000000..c0a703d2 --- /dev/null +++ b/src/middleware/api_key_scope_test.go @@ -0,0 +1,153 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestAPIKeyScopeMatchesTarget(t *testing.T) { + tests := []struct { + name string + scope string + target string + want bool + }{ + {name: "global wildcard", scope: "*", target: "sdk:evaluations:read", want: true}, + {name: "sdk wildcard", scope: "sdk:*", target: "sdk:evaluations:read", want: true}, + {name: "sdk evaluations wildcard", scope: "sdk:evaluations:*", target: "sdk:evaluations:read", want: true}, + {name: "exact match", scope: "sdk:datasets:read", target: "sdk:datasets:read", want: true}, + {name: "resource only", scope: "sdk", target: "sdk:datasets:read", want: true}, + {name: "different family", scope: "project:read", target: "sdk:evaluations:read", want: false}, + {name: "different action", scope: "sdk:evaluations:write", target: "sdk:evaluations:read", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := apiKeyScopeMatchesTarget(tt.scope, tt.target); got != tt.want { + t.Fatalf("apiKeyScopeMatchesTarget(%q, %q) = %v, want %v", tt.scope, tt.target, got, tt.want) + } + }) + } +} + +func TestRequireAPIKeyScopesAny(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + setup func(*gin.Context) + wantStatus int + }{ + { + name: "user token bypasses explicit sdk scope gate", + setup: func(c *gin.Context) { + c.Set("user_id", 1) + c.Set("is_active", true) + c.Set("auth_type", "user") + }, + wantStatus: http.StatusNoContent, + }, + { + name: "api key with matching sdk scope passes", + setup: func(c *gin.Context) { + c.Set("user_id", 1) + c.Set("is_active", true) + c.Set("auth_type", "api_key") + c.Set("api_key_scopes", []string{"sdk:evaluations:read"}) + }, + wantStatus: http.StatusNoContent, + }, + { + name: "api key with non matching sdk scope denied", + setup: func(c *gin.Context) { + c.Set("user_id", 1) + c.Set("is_active", true) + c.Set("auth_type", "api_key") + c.Set("api_key_scopes", []string{"sdk:datasets:read"}) + }, + wantStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status := runMiddlewareChain(func(r *gin.Engine) { + r.GET("/", func(c *gin.Context) { + tt.setup(c) + c.Next() + }, RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read"), func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + }) + if status != tt.wantStatus { + t.Fatalf("status = %d, want %d", status, tt.wantStatus) + } + }) + } +} + +func TestRequireHumanUserAuth(t *testing.T) { + gin.SetMode(gin.TestMode) + + tests := []struct { + name string + setup func(*gin.Context) + wantStatus int + }{ + { + name: "human user token passes", + setup: func(c *gin.Context) { + c.Set("user_id", 1) + c.Set("is_active", true) + c.Set("auth_type", "user") + }, + wantStatus: http.StatusNoContent, + }, + { + name: "api key token denied", + setup: func(c *gin.Context) { + c.Set("user_id", 1) + c.Set("is_active", true) + c.Set("auth_type", "api_key") + }, + wantStatus: http.StatusForbidden, + }, + { + name: "service token denied", + setup: func(c *gin.Context) { + c.Set("is_service_token", true) + c.Set("task_id", "task-1") + }, + wantStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status := runMiddlewareChain(func(r *gin.Engine) { + r.GET("/", func(c *gin.Context) { + tt.setup(c) + c.Next() + }, RequireHumanUserAuth(), func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) + }) + if status != tt.wantStatus { + t.Fatalf("status = %d, want %d", status, tt.wantStatus) + } + }) + } +} + +func runMiddlewareChain(register func(*gin.Engine)) int { + engine := gin.New() + register(engine) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + engine.ServeHTTP(w, req) + return w.Code +} diff --git a/src/middleware/auth.go b/src/middleware/auth.go index d1752220..88cbf67c 100644 --- a/src/middleware/auth.go +++ b/src/middleware/auth.go @@ -38,6 +38,9 @@ func JWTAuth() gin.HandlerFunc { c.Set("is_active", claims.IsActive) c.Set("is_admin", claims.IsAdmin) c.Set("user_roles", claims.Roles) + c.Set("auth_type", claims.AuthType) + c.Set("api_key_id", claims.APIKeyID) + c.Set("api_key_scopes", append([]string(nil), claims.APIKeyScopes...)) c.Set("token_expires_at", claims.ExpiresAt.Time) c.Set("token_type", "user") c.Next() @@ -94,6 +97,9 @@ func OptionalJWTAuth() gin.HandlerFunc { c.Set("is_active", claims.IsActive) c.Set("is_admin", claims.IsAdmin) c.Set("user_roles", claims.Roles) + c.Set("auth_type", claims.AuthType) + c.Set("api_key_id", claims.APIKeyID) + c.Set("api_key_scopes", append([]string(nil), claims.APIKeyScopes...)) c.Set("token_expires_at", claims.ExpiresAt.Time) c.Set("token_type", "user") c.Next() @@ -208,6 +214,32 @@ func GetCurrentUserRoles(c *gin.Context) ([]string, bool) { return userRoles, ok } +// GetCurrentAPIKeyScopes returns API key scopes when the current bearer token +// was issued via Key ID / Key Secret exchange. +func GetCurrentAPIKeyScopes(c *gin.Context) ([]string, bool) { + scopes, exists := c.Get("api_key_scopes") + if !exists { + return nil, false + } + + apiKeyScopes, ok := scopes.([]string) + return apiKeyScopes, ok +} + +// GetAuthType returns the auth_type claim of the current bearer token when present. +func GetAuthType(c *gin.Context) string { + authType, exists := c.Get("auth_type") + if !exists { + return "" + } + + value, ok := authType.(string) + if !ok { + return "" + } + return value +} + // GetServiceTaskID extracts task ID from service token context func GetServiceTaskID(c *gin.Context) (string, bool) { taskID, exists := c.Get("task_id") @@ -252,6 +284,22 @@ func RequireUserAuth(c *gin.Context) bool { return RequireAuth(c) } +// RequireServiceTokenAuth is a helper that ensures the current request uses a service token. +func RequireServiceTokenAuth() gin.HandlerFunc { + return func(c *gin.Context) { + if !RequireAuth(c) { + c.Abort() + return + } + if !IsServiceToken(c) { + dto.ErrorResponse(c, http.StatusForbidden, "Service token required") + c.Abort() + return + } + c.Next() + } +} + // RequireActiveUser ensures the current user exists and is active func RequireActiveUser() gin.HandlerFunc { return func(c *gin.Context) { diff --git a/src/middleware/permission.go b/src/middleware/permission.go index e5a590d4..6fbe3106 100644 --- a/src/middleware/permission.go +++ b/src/middleware/permission.go @@ -14,15 +14,17 @@ import ( ) type permissionContext struct { - userID int - isAdmin bool - roles []string - checker permissionChecker - ctx context.Context - teamID *int - projectID *int - containerID *int - datasetID *int + userID int + isAdmin bool + roles []string + authType string + apiKeyScopes []string + checker permissionChecker + ctx context.Context + teamID *int + projectID *int + containerID *int + datasetID *int } // permissionCheckFunc is a function that checks permission given the context @@ -62,11 +64,15 @@ func extractPermissionContext(c *gin.Context) (*permissionContext, string) { } ctx := &permissionContext{ - userID: userID, - isAdmin: isAdmin, - roles: roles, - checker: permissionCheckerFromContext(c), - ctx: c.Request.Context(), + userID: userID, + isAdmin: isAdmin, + roles: roles, + checker: permissionCheckerFromContext(c), + ctx: c.Request.Context(), + authType: GetAuthType(c), + } + if scopes, ok := GetCurrentAPIKeyScopes(c); ok { + ctx.apiKeyScopes = append([]string(nil), scopes...) } // Extract optional IDs from URL parameters @@ -97,6 +103,38 @@ func extractPermissionContext(c *gin.Context) (*permissionContext, string) { return ctx, "" } +func (ctx *permissionContext) isAPIKeyAuth() bool { + return ctx != nil && ctx.authType == "api_key" +} + +func (ctx *permissionContext) scopeAllowsPermission(permission consts.PermissionRule) bool { + if !ctx.isAPIKeyAuth() { + return true + } + if len(ctx.apiKeyScopes) == 0 { + return false + } + for _, scope := range ctx.apiKeyScopes { + if apiKeyScopeMatchesPermission(scope, permission) { + return true + } + } + return false +} + +func (ctx *permissionContext) scopeAllowsAnyPermission(permissions []consts.PermissionRule) bool { + for _, permission := range permissions { + if ctx.scopeAllowsPermission(permission) { + return true + } + } + return false +} + +func apiKeyScopeMatchesPermission(scope string, permission consts.PermissionRule) bool { + return apiKeyScopeMatchesTarget(scope, permission.String()) +} + // withPermissionCheck creates a middleware decorator that wraps permission check logic // This is similar to Python's decorator pattern func withPermissionCheck(checkFunc permissionCheckFunc) gin.HandlerFunc { @@ -147,6 +185,9 @@ func withPermissionCheck(checkFunc permissionCheckFunc) gin.HandlerFunc { // singlePermission creates a check for a single permission func singlePermission(permission consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { + if !ctx.scopeAllowsPermission(permission) { + return false, nil + } return ctx.checker.CheckUserPermission(ctx.ctx, &dto.CheckPermissionParams{ UserID: ctx.userID, Action: permission.Action, @@ -165,6 +206,9 @@ func singlePermission(permission consts.PermissionRule) permissionCheckFunc { func anyPermission(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { + if !ctx.scopeAllowsPermission(perm) { + continue + } hasPermission, err := ctx.checker.CheckUserPermission( ctx.ctx, &dto.CheckPermissionParams{ @@ -194,6 +238,9 @@ func anyPermission(permissions []consts.PermissionRule) permissionCheckFunc { func allPermissions(permissions []consts.PermissionRule) permissionCheckFunc { return func(ctx *permissionContext) (bool, error) { for _, perm := range permissions { + if !ctx.scopeAllowsPermission(perm) { + return false, nil + } hasPermission, err := ctx.checker.CheckUserPermission( ctx.ctx, &dto.CheckPermissionParams{ @@ -266,6 +313,14 @@ func teamAccessCheck(requireAdmin bool) permissionCheckFunc { return false, fmt.Errorf("team_id is required") } + requiredScopes := []consts.PermissionRule{consts.PermTeamReadAll, consts.PermTeamManageAll} + if requireAdmin { + requiredScopes = []consts.PermissionRule{consts.PermTeamManageAll} + } + if !ctx.scopeAllowsAnyPermission(requiredScopes) { + return false, nil + } + // Check if system admin (from JWT token, no DB query) if ctx.isAdmin { return true, nil @@ -303,6 +358,14 @@ func projectAccessCheck(requireAdmin bool) permissionCheckFunc { return false, fmt.Errorf("project_id is required") } + requiredScopes := []consts.PermissionRule{consts.PermProjectReadAll, consts.PermProjectManageAll} + if requireAdmin { + requiredScopes = []consts.PermissionRule{consts.PermProjectManageAll} + } + if !ctx.scopeAllowsAnyPermission(requiredScopes) { + return false, nil + } + // Check if system admin (from JWT token, no DB query) if ctx.isAdmin { return true, nil diff --git a/src/middleware/permission_test.go b/src/middleware/permission_test.go new file mode 100644 index 00000000..d4d1c702 --- /dev/null +++ b/src/middleware/permission_test.go @@ -0,0 +1,189 @@ +package middleware + +import ( + "context" + "testing" + + "aegis/consts" + "aegis/dto" + "aegis/utils" +) + +type permissionCheckerStub struct{} + +func (permissionCheckerStub) VerifyToken(context.Context, string) (*utils.Claims, error) { + return nil, nil +} +func (permissionCheckerStub) VerifyServiceToken(context.Context, string) (*utils.ServiceClaims, error) { + return nil, nil +} +func (permissionCheckerStub) CheckUserPermission(context.Context, *dto.CheckPermissionParams) (bool, error) { + return false, nil +} +func (permissionCheckerStub) IsUserTeamAdmin(context.Context, int, int) (bool, error) { + return false, nil +} +func (permissionCheckerStub) IsUserInTeam(context.Context, int, int) (bool, error) { + return false, nil +} +func (permissionCheckerStub) IsTeamPublic(context.Context, int) (bool, error) { + return false, nil +} +func (permissionCheckerStub) IsUserProjectAdmin(context.Context, int, int) (bool, error) { + return false, nil +} +func (permissionCheckerStub) IsUserInProject(context.Context, int, int) (bool, error) { + return false, nil +} +func (permissionCheckerStub) LogFailedAction(string, string, string, string, int, int, consts.ResourceName) error { + return nil +} +func (permissionCheckerStub) LogUserAction(string, string, string, string, int, int, consts.ResourceName) error { + return nil +} + +func TestAPIKeyScopeMatchesPermission(t *testing.T) { + permission := consts.PermProjectReadAll + + tests := []struct { + name string + scope string + want bool + }{ + {name: "wildcard all", scope: "*", want: true}, + {name: "resource only", scope: "project", want: true}, + {name: "resource action", scope: "project:read", want: true}, + {name: "resource action scope", scope: "project:read:all", want: true}, + {name: "resource wildcard action", scope: "project:*", want: true}, + {name: "resource action wildcard scope", scope: "project:read:*", want: true}, + {name: "full wildcard segments", scope: "project:*:*", want: true}, + {name: "other action", scope: "project:update", want: false}, + {name: "other resource", scope: "dataset:read", want: false}, + {name: "too specific mismatch", scope: "project:read:team", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := apiKeyScopeMatchesPermission(tt.scope, permission); got != tt.want { + t.Fatalf("apiKeyScopeMatchesPermission(%q, %q) = %v, want %v", tt.scope, permission.String(), got, tt.want) + } + }) + } +} + +func TestPermissionContextScopeAllowsPermission(t *testing.T) { + ctx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"project:read", "execution:execute:project"}, + } + + if !ctx.scopeAllowsPermission(consts.PermProjectReadAll) { + t.Fatalf("scopeAllowsPermission(project read) = false, want true") + } + if ctx.scopeAllowsPermission(consts.PermProjectUpdateAll) { + t.Fatalf("scopeAllowsPermission(project update) = true, want false") + } + if !ctx.scopeAllowsPermission(consts.PermExecutionExecuteProject) { + t.Fatalf("scopeAllowsPermission(execution execute project) = false, want true") + } +} + +func TestPermissionContextScopeAllowsAnyPermission(t *testing.T) { + ctx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"team:read"}, + } + + if !ctx.scopeAllowsAnyPermission([]consts.PermissionRule{consts.PermTeamManageAll, consts.PermTeamReadAll}) { + t.Fatalf("scopeAllowsAnyPermission(team manage/read) = false, want true") + } + if ctx.scopeAllowsAnyPermission([]consts.PermissionRule{consts.PermProjectManageAll, consts.PermProjectReadAll}) { + t.Fatalf("scopeAllowsAnyPermission(project manage/read) = true, want false") + } +} + +func TestTeamAccessCheckScopes(t *testing.T) { + memberCheck := teamAccessCheck(false) + adminCheck := teamAccessCheck(true) + teamID := 9 + + memberCtx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"team:read"}, + teamID: &teamID, + checker: permissionCheckerStub{}, + } + allowed, err := memberCheck(memberCtx) + if err != nil { + t.Fatalf("memberCheck() error = %v", err) + } + if allowed { + t.Fatalf("memberCheck() = true, want false without membership backing") + } + + allowed, err = adminCheck(memberCtx) + if err != nil { + t.Fatalf("adminCheck() error = %v", err) + } + if allowed { + t.Fatalf("adminCheck() = true, want false for read-only scope") + } + + adminCtx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"team:manage"}, + teamID: &teamID, + isAdmin: true, + checker: permissionCheckerStub{}, + } + allowed, err = adminCheck(adminCtx) + if err != nil { + t.Fatalf("adminCheck(manage) error = %v", err) + } + if !allowed { + t.Fatalf("adminCheck(manage) = false, want true") + } +} + +func TestProjectAccessCheckScopes(t *testing.T) { + memberCheck := projectAccessCheck(false) + adminCheck := projectAccessCheck(true) + projectID := 7 + + memberCtx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"project:read"}, + projectID: &projectID, + checker: permissionCheckerStub{}, + } + allowed, err := memberCheck(memberCtx) + if err != nil { + t.Fatalf("memberCheck() error = %v", err) + } + if allowed { + t.Fatalf("memberCheck() = true, want false without project membership") + } + + allowed, err = adminCheck(memberCtx) + if err != nil { + t.Fatalf("adminCheck() error = %v", err) + } + if allowed { + t.Fatalf("adminCheck() = true, want false for read-only scope") + } + + adminCtx := &permissionContext{ + authType: "api_key", + apiKeyScopes: []string{"project:manage"}, + projectID: &projectID, + isAdmin: true, + checker: permissionCheckerStub{}, + } + allowed, err = adminCheck(adminCtx) + if err != nil { + t.Fatalf("adminCheck(manage) error = %v", err) + } + if !allowed { + t.Fatalf("adminCheck(manage) = false, want true") + } +} diff --git a/src/model/entity.go b/src/model/entity.go index 322b2e5c..e8b0a19c 100644 --- a/src/model/entity.go +++ b/src/model/entity.go @@ -412,21 +412,23 @@ func (u *User) BeforeCreate(tx *gorm.DB) error { return nil } -type UserAccessKey struct { - ID int `gorm:"primaryKey;autoIncrement"` - UserID int `gorm:"not null;index:idx_user_access_key_owner_status"` - Name string `gorm:"not null;size:128"` - Description string `gorm:"type:text"` - AccessKey string `gorm:"not null;size:64"` - SecretHash string `gorm:"not null;size:255"` - SecretCiphertext string `gorm:"not null;type:text"` - LastUsedAt *time.Time - ExpiresAt *time.Time - Status consts.StatusType `gorm:"not null;default:1;index:idx_user_access_key_owner_status"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - - ActiveAccessKey string `gorm:"type:varchar(64) GENERATED ALWAYS AS (CASE WHEN status >= 0 THEN access_key ELSE NULL END) VIRTUAL;uniqueIndex:idx_active_user_access_key"` +type APIKey struct { + ID int `gorm:"primaryKey;autoIncrement"` + UserID int `gorm:"not null;index:idx_api_key_owner_status"` + Name string `gorm:"not null;size:128"` + Description string `gorm:"type:text"` + KeyID string `gorm:"not null;size:64"` + KeySecretHash string `gorm:"not null;size:255"` + KeySecretCiphertext string `gorm:"not null;type:text"` + Scopes []string `gorm:"type:json;serializer:json"` + RevokedAt *time.Time + LastUsedAt *time.Time + ExpiresAt *time.Time + Status consts.StatusType `gorm:"not null;default:1;index:idx_api_key_owner_status"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + + ActiveKeyID string `gorm:"type:varchar(64) GENERATED ALWAYS AS (CASE WHEN status >= 0 THEN key_id ELSE NULL END) VIRTUAL;uniqueIndex:idx_active_api_key"` User *User `gorm:"foreignKey:UserID"` } diff --git a/src/module/auth/api_types.go b/src/module/auth/api_types.go index 11508040..76dc242c 100644 --- a/src/module/auth/api_types.go +++ b/src/module/auth/api_types.go @@ -11,6 +11,7 @@ import ( "aegis/dto" "aegis/model" usermodule "aegis/module/user" + "aegis/utils" ) const usernamePattern = `^[a-zA-Z0-9_]{3,20}$` @@ -83,13 +84,14 @@ func (req *ChangePasswordReq) Validate() error { return nil } -type CreateAccessKeyReq struct { +type CreateAPIKeyReq struct { Name string `json:"name" binding:"required" example:"ci-bot"` Description string `json:"description,omitempty" example:"SDK credential for CI pipeline"` + Scopes []string `json:"scopes,omitempty" example:"[\"*\"]"` ExpiresAt *time.Time `json:"expires_at,omitempty" example:"2026-12-31T23:59:59Z"` } -func (req *CreateAccessKeyReq) Validate() error { +func (req *CreateAPIKeyReq) Validate() error { if req == nil { return fmt.Errorf("request is required") } @@ -99,40 +101,45 @@ func (req *CreateAccessKeyReq) Validate() error { if len(req.Name) > 128 { return fmt.Errorf("name must be no more than 128 characters long") } + normalizedScopes, err := normalizeAPIKeyScopes(req.Scopes) + if err != nil { + return err + } + req.Scopes = normalizedScopes if req.ExpiresAt != nil && req.ExpiresAt.Before(time.Now()) { return fmt.Errorf("expires_at must be in the future") } return nil } -type ListAccessKeyReq struct { +type ListAPIKeyReq struct { dto.PaginationReq } -func (req *ListAccessKeyReq) Validate() error { +func (req *ListAPIKeyReq) Validate() error { if req == nil { return fmt.Errorf("request is required") } return req.PaginationReq.Validate() } -type AccessKeyTokenReq struct { - AccessKey string `header:"X-Access-Key" example:"ak_1234567890abcdef"` +type APIKeyTokenReq struct { + KeyID string `header:"X-Key-Id" example:"pk_1234567890abcdef"` Timestamp string `header:"X-Timestamp" example:"1713333333"` Nonce string `header:"X-Nonce" example:"abc123"` Signature string `header:"X-Signature" example:"4cf2f2cbb93d..."` } -func (req *AccessKeyTokenReq) Validate() error { +func (req *APIKeyTokenReq) Validate() error { if req == nil { return fmt.Errorf("request is required") } - req.AccessKey = strings.TrimSpace(req.AccessKey) + req.KeyID = strings.TrimSpace(req.KeyID) req.Timestamp = strings.TrimSpace(req.Timestamp) req.Nonce = strings.TrimSpace(req.Nonce) req.Signature = strings.ToLower(strings.TrimSpace(req.Signature)) - if req.AccessKey == "" || req.Timestamp == "" || req.Nonce == "" || req.Signature == "" { - return fmt.Errorf("X-Access-Key, X-Timestamp, X-Nonce and X-Signature are required") + if req.KeyID == "" || req.Timestamp == "" || req.Nonce == "" || req.Signature == "" { + return fmt.Errorf("X-Key-Id, X-Timestamp, X-Nonce and X-Signature are required") } if _, err := strconv.ParseInt(req.Timestamp, 10, 64); err != nil { return fmt.Errorf("X-Timestamp must be a unix timestamp in seconds") @@ -143,17 +150,17 @@ func (req *AccessKeyTokenReq) Validate() error { return nil } -func (req *AccessKeyTokenReq) TimestampUnix() (int64, error) { +func (req *APIKeyTokenReq) TimestampUnix() (int64, error) { return strconv.ParseInt(req.Timestamp, 10, 64) } -func (req *AccessKeyTokenReq) CanonicalString(method, path string) string { +func (req *APIKeyTokenReq) CanonicalString(method, path string) string { return strings.Join([]string{ strings.ToUpper(method), path, - req.AccessKey, req.Timestamp, req.Nonce, + utils.SHA256Hex(nil), }, "\n") } @@ -168,28 +175,32 @@ type TokenRefreshResp struct { ExpiresAt time.Time `json:"expires_at" example:"2024-12-31T23:59:59Z"` } -type AccessKeyInfo struct { +type APIKeyInfo struct { ID int `json:"id" example:"12"` Name string `json:"name" example:"ci-bot"` Description string `json:"description,omitempty" example:"SDK credential for CI pipeline"` - AccessKey string `json:"access_key" example:"ak_1234567890abcdef"` + KeyID string `json:"key_id" example:"pk_1234567890abcdef"` + Scopes []string `json:"scopes,omitempty" example:"[\"*\"]"` Status consts.StatusType `json:"status" example:"1"` + RevokedAt *time.Time `json:"revoked_at,omitempty" example:"2026-04-17T12:30:00Z"` LastUsedAt *time.Time `json:"last_used_at,omitempty" example:"2026-04-17T12:00:00Z"` ExpiresAt *time.Time `json:"expires_at,omitempty" example:"2026-12-31T23:59:59Z"` CreatedAt time.Time `json:"created_at" example:"2026-04-17T11:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2026-04-17T11:00:00Z"` } -func NewAccessKeyInfo(key *model.UserAccessKey) *AccessKeyInfo { +func NewAPIKeyInfo(key *model.APIKey) *APIKeyInfo { if key == nil { return nil } - return &AccessKeyInfo{ + return &APIKeyInfo{ ID: key.ID, Name: key.Name, Description: key.Description, - AccessKey: key.AccessKey, + KeyID: key.KeyID, + Scopes: append([]string(nil), key.Scopes...), Status: key.Status, + RevokedAt: key.RevokedAt, LastUsedAt: key.LastUsedAt, ExpiresAt: key.ExpiresAt, CreatedAt: key.CreatedAt, @@ -197,22 +208,51 @@ func NewAccessKeyInfo(key *model.UserAccessKey) *AccessKeyInfo { } } -type AccessKeyWithSecretResp struct { - AccessKeyInfo - SecretKey string `json:"secret_key" example:"sk_abcdefghijklmnopqrstuvwxyz123456"` +type APIKeyWithSecretResp struct { + APIKeyInfo + KeySecret string `json:"key_secret" example:"ks_abcdefghijklmnopqrstuvwxyz123456"` } -type ListAccessKeyResp struct { - Items []AccessKeyInfo `json:"items"` +type ListAPIKeyResp struct { + Items []APIKeyInfo `json:"items"` Pagination dto.PaginationInfo `json:"pagination"` } -type AccessKeyTokenResp struct { - Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.access_key.jwt"` +type APIKeyTokenResp struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.api_key.jwt"` TokenType string `json:"token_type" example:"Bearer"` ExpiresAt time.Time `json:"expires_at" example:"2026-04-17T12:00:00Z"` - AuthType string `json:"auth_type" example:"access_key"` - AccessKey string `json:"access_key" example:"ak_1234567890abcdef"` + AuthType string `json:"auth_type" example:"api_key"` + KeyID string `json:"key_id" example:"pk_1234567890abcdef"` +} + +const defaultAPIKeyScope = "*" + +func normalizeAPIKeyScopes(scopes []string) ([]string, error) { + if len(scopes) == 0 { + return []string{defaultAPIKeyScope}, nil + } + + normalized := make([]string, 0, len(scopes)) + seen := make(map[string]struct{}, len(scopes)) + for _, scope := range scopes { + scope = strings.TrimSpace(scope) + if scope == "" { + return nil, fmt.Errorf("scopes cannot contain empty items") + } + if len(scope) > 128 { + return nil, fmt.Errorf("scope %q must be no more than 128 characters long", scope) + } + if _, exists := seen[scope]; exists { + continue + } + seen[scope] = struct{}{} + normalized = append(normalized, scope) + } + if len(normalized) == 0 { + return []string{defaultAPIKeyScope}, nil + } + return normalized, nil } type UserProfileResp struct { diff --git a/src/module/auth/handler.go b/src/module/auth/handler.go index fbbc124c..1292ca48 100644 --- a/src/module/auth/handler.go +++ b/src/module/auth/handler.go @@ -230,30 +230,30 @@ func (h *Handler) GetProfile(c *gin.Context) { dto.JSONResponse(c, http.StatusOK, "Profile retrieved successfully", resp) } -// CreateAccessKey handles access key creation for the current user. +// CreateAPIKey handles API key creation for the current user. // -// @Summary Create access key -// @Description Create an AK/SK credential for the current authenticated user. This Portal response is the only time the `secret_key` is returned in plaintext, so callers must save it immediately. +// @Summary Create API key +// @Description Create a Key ID / Key Secret credential for the current authenticated user. This Portal response is the only time the `key_secret` is returned in plaintext, so callers must save it immediately. // @Tags Authentication -// @ID create_access_key +// @ID create_api_key // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateAccessKeyReq true "Access key create request" -// @Success 201 {object} dto.GenericResponse[AccessKeyWithSecretResp] "Access key created successfully" +// @Param request body CreateAPIKeyReq true "API key create request" +// @Success 201 {object} dto.GenericResponse[APIKeyWithSecretResp] "API key created successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys [post] +// @Router /api/v2/api-keys [post] // @x-api-type {"portal":"true"} -func (h *Handler) CreateAccessKey(c *gin.Context) { +func (h *Handler) CreateAPIKey(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req CreateAccessKeyReq + var req CreateAPIKeyReq if err := c.ShouldBindJSON(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -263,38 +263,38 @@ func (h *Handler) CreateAccessKey(c *gin.Context) { return } - resp, err := h.service.CreateAccessKey(c.Request.Context(), userID, &req) + resp, err := h.service.CreateAPIKey(c.Request.Context(), userID, &req) if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusCreated, "Access key created successfully", resp) + dto.JSONResponse(c, http.StatusCreated, "API key created successfully", resp) } -// ListAccessKeys lists access keys for the current user. +// ListAPIKeys lists API keys for the current user. // -// @Summary List access keys -// @Description List AK/SK credentials owned by the current authenticated user +// @Summary List API keys +// @Description List Key ID / Key Secret credentials owned by the current authenticated user // @Tags Authentication -// @ID list_access_keys +// @ID list_api_keys // @Produce json // @Security BearerAuth // @Param page query int false "Page number" // @Param size query int false "Page size" -// @Success 200 {object} dto.GenericResponse[ListAccessKeyResp] "Access keys listed successfully" +// @Success 200 {object} dto.GenericResponse[ListAPIKeyResp] "API keys listed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys [get] +// @Router /api/v2/api-keys [get] // @x-api-type {"portal":"true"} -func (h *Handler) ListAccessKeys(c *gin.Context) { +func (h *Handler) ListAPIKeys(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return } - var req ListAccessKeyReq + var req ListAPIKeyReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) return @@ -304,7 +304,7 @@ func (h *Handler) ListAccessKeys(c *gin.Context) { return } - resp, err := h.service.ListAccessKeys(c.Request.Context(), userID, &req) + resp, err := h.service.ListAPIKeys(c.Request.Context(), userID, &req) if httpx.HandleServiceError(c, err) { return } @@ -312,28 +312,28 @@ func (h *Handler) ListAccessKeys(c *gin.Context) { dto.SuccessResponse(c, resp) } -// GetAccessKey gets a single access key for the current user. +// GetAPIKey gets a single API key for the current user. // -// @Summary Get access key detail -// @Description Get metadata for an AK/SK credential owned by the current authenticated user +// @Summary Get API key detail +// @Description Get metadata for a Key ID / Key Secret credential owned by the current authenticated user // @Tags Authentication -// @ID get_access_key +// @ID get_api_key // @Produce json // @Security BearerAuth -// @Param access_key_id path int true "Access key ID" -// @Success 200 {object} dto.GenericResponse[AccessKeyInfo] "Access key detail retrieved successfully" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[APIKeyInfo] "API key detail retrieved successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys/{access_key_id} [get] +// @Router /api/v2/api-keys/{id} [get] // @x-api-type {"portal":"true"} -func (h *Handler) GetAccessKey(c *gin.Context) { - userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) +func (h *Handler) GetAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) if !ok { return } - resp, err := h.service.GetAccessKey(c.Request.Context(), userID, accessKeyID) + resp, err := h.service.GetAPIKey(c.Request.Context(), userID, accessKeyID) if httpx.HandleServiceError(c, err) { return } @@ -341,139 +341,167 @@ func (h *Handler) GetAccessKey(c *gin.Context) { dto.SuccessResponse(c, resp) } -// DeleteAccessKey deletes an access key for the current user. +// DeleteAPIKey deletes an API key for the current user. // -// @Summary Delete access key -// @Description Delete an AK/SK credential owned by the current authenticated user +// @Summary Delete API key +// @Description Delete a Key ID / Key Secret credential owned by the current authenticated user // @Tags Authentication -// @ID delete_access_key +// @ID delete_api_key // @Produce json // @Security BearerAuth -// @Param access_key_id path int true "Access key ID" -// @Success 204 {object} dto.GenericResponse[any] "Access key deleted successfully" +// @Param id path int true "API key record ID" +// @Success 204 {object} dto.GenericResponse[any] "API key deleted successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys/{access_key_id} [delete] +// @Router /api/v2/api-keys/{id} [delete] // @x-api-type {"portal":"true"} -func (h *Handler) DeleteAccessKey(c *gin.Context) { - userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) +func (h *Handler) DeleteAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) if !ok { return } - if httpx.HandleServiceError(c, h.service.DeleteAccessKey(c.Request.Context(), userID, accessKeyID)) { + if httpx.HandleServiceError(c, h.service.DeleteAPIKey(c.Request.Context(), userID, accessKeyID)) { return } - dto.JSONResponse[any](c, http.StatusNoContent, "Access key deleted successfully", nil) + dto.JSONResponse[any](c, http.StatusNoContent, "API key deleted successfully", nil) } -// DisableAccessKey disables an access key for the current user. +// DisableAPIKey disables an API key for the current user. // -// @Summary Disable access key -// @Description Disable an AK/SK credential owned by the current authenticated user +// @Summary Disable API key +// @Description Disable a Key ID / Key Secret credential owned by the current authenticated user // @Tags Authentication -// @ID disable_access_key +// @ID disable_api_key // @Produce json // @Security BearerAuth -// @Param access_key_id path int true "Access key ID" -// @Success 200 {object} dto.GenericResponse[any] "Access key disabled successfully" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[any] "API key disabled successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys/{access_key_id}/disable [post] +// @Router /api/v2/api-keys/{id}/disable [post] // @x-api-type {"portal":"true"} -func (h *Handler) DisableAccessKey(c *gin.Context) { - userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) +func (h *Handler) DisableAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) if !ok { return } - if httpx.HandleServiceError(c, h.service.DisableAccessKey(c.Request.Context(), userID, accessKeyID)) { + if httpx.HandleServiceError(c, h.service.DisableAPIKey(c.Request.Context(), userID, accessKeyID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Access key disabled successfully", nil) + dto.JSONResponse[any](c, http.StatusOK, "API key disabled successfully", nil) } -// EnableAccessKey enables an access key for the current user. +// EnableAPIKey enables an API key for the current user. // -// @Summary Enable access key -// @Description Enable an AK/SK credential owned by the current authenticated user +// @Summary Enable API key +// @Description Enable a Key ID / Key Secret credential owned by the current authenticated user // @Tags Authentication -// @ID enable_access_key +// @ID enable_api_key // @Produce json // @Security BearerAuth -// @Param access_key_id path int true "Access key ID" -// @Success 200 {object} dto.GenericResponse[any] "Access key enabled successfully" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[any] "API key enabled successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys/{access_key_id}/enable [post] +// @Router /api/v2/api-keys/{id}/enable [post] // @x-api-type {"portal":"true"} -func (h *Handler) EnableAccessKey(c *gin.Context) { - userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) +func (h *Handler) EnableAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) if !ok { return } - if httpx.HandleServiceError(c, h.service.EnableAccessKey(c.Request.Context(), userID, accessKeyID)) { + if httpx.HandleServiceError(c, h.service.EnableAPIKey(c.Request.Context(), userID, accessKeyID)) { return } - dto.JSONResponse[any](c, http.StatusOK, "Access key enabled successfully", nil) + dto.JSONResponse[any](c, http.StatusOK, "API key enabled successfully", nil) } -// RotateAccessKey rotates the secret key for an existing access key. +// RevokeAPIKey permanently revokes an API key for the current user. // -// @Summary Rotate access key secret -// @Description Rotate the secret half of an AK/SK credential owned by the current authenticated user +// @Summary Revoke API key +// @Description Permanently revoke a Key ID / Key Secret credential owned by the current authenticated user. Revoked API keys can no longer be re-enabled or used to exchange bearer tokens. // @Tags Authentication -// @ID rotate_access_key +// @ID revoke_api_key // @Produce json // @Security BearerAuth -// @Param access_key_id path int true "Access key ID" -// @Success 200 {object} dto.GenericResponse[AccessKeyWithSecretResp] "Access key rotated successfully" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[any] "API key revoked successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /api/v2/api-keys/{id}/revoke [post] +// @x-api-type {"portal":"true"} +func (h *Handler) RevokeAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) + if !ok { + return + } + + if httpx.HandleServiceError(c, h.service.RevokeAPIKey(c.Request.Context(), userID, accessKeyID)) { + return + } + + dto.JSONResponse[any](c, http.StatusOK, "API key revoked successfully", nil) +} + +// RotateAPIKey rotates the key secret for an existing API key. +// +// @Summary Rotate API key secret +// @Description Rotate the key secret half of a Key ID / Key Secret credential owned by the current authenticated user +// @Tags Authentication +// @ID rotate_api_key +// @Produce json +// @Security BearerAuth +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[APIKeyWithSecretResp] "API key rotated successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Access key not found" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/access-keys/{access_key_id}/rotate [post] +// @Router /api/v2/api-keys/{id}/rotate [post] // @x-api-type {"portal":"true"} -func (h *Handler) RotateAccessKey(c *gin.Context) { - userID, accessKeyID, ok := parseCurrentUserAndAccessKeyID(c) +func (h *Handler) RotateAPIKey(c *gin.Context) { + userID, accessKeyID, ok := parseCurrentUserAndAPIKeyID(c) if !ok { return } - resp, err := h.service.RotateAccessKey(c.Request.Context(), userID, accessKeyID) + resp, err := h.service.RotateAPIKey(c.Request.Context(), userID, accessKeyID) if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusOK, "Access key rotated successfully", resp) + dto.JSONResponse(c, http.StatusOK, "API key rotated successfully", resp) } -// ExchangeAccessKeyToken exchanges AK/SK for a bearer token. +// ExchangeAPIKeyToken exchanges a signed API key request for a bearer token. // -// @Summary Exchange access key for token -// @Description Exchange an AK/SK signed request for a short-lived bearer token. Access keys are created in Portal, while SDK and CLI callers use this endpoint with `X-Access-Key`, `X-Timestamp`, `X-Nonce`, and `X-Signature`. +// @Summary Exchange API key for token +// @Description Exchange a signed Key ID / Key Secret request for a short-lived bearer token. SDK and CLI callers sign `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)` with the key secret and send the result via `X-Key-Id`, `X-Timestamp`, `X-Nonce`, and `X-Signature`. // @Tags Authentication -// @ID exchange_access_key_token +// @ID exchange_api_key_token // @Produce json -// @Param X-Access-Key header string true "Access key ID" +// @Param X-Key-Id header string true "Public key identifier" // @Param X-Timestamp header string true "Unix timestamp in seconds" // @Param X-Nonce header string true "Unique request nonce" -// @Param X-Signature header string true "Hex encoded HMAC-SHA256 signature of METHOD\\nPATH\\nACCESS_KEY\\nTIMESTAMP\\nNONCE" -// @Success 200 {object} dto.GenericResponse[AccessKeyTokenResp] "Access key token issued successfully" +// @Param X-Signature header string true "Hex encoded HMAC-SHA256 signature of METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)" +// @Success 200 {object} dto.GenericResponse[APIKeyTokenResp] "API key token issued successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Invalid signature or replayed request" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/auth/access-key/token [post] +// @Router /api/v2/auth/api-key/token [post] // @x-api-type {"sdk":"true"} -func (h *Handler) ExchangeAccessKeyToken(c *gin.Context) { - var req AccessKeyTokenReq - req.AccessKey = c.GetHeader("X-Access-Key") +func (h *Handler) ExchangeAPIKeyToken(c *gin.Context) { + var req APIKeyTokenReq + req.KeyID = c.GetHeader("X-Key-Id") req.Timestamp = c.GetHeader("X-Timestamp") req.Nonce = c.GetHeader("X-Nonce") req.Signature = c.GetHeader("X-Signature") @@ -482,22 +510,22 @@ func (h *Handler) ExchangeAccessKeyToken(c *gin.Context) { return } - resp, err := h.service.ExchangeAccessKeyToken(c.Request.Context(), &req, c.Request.Method, c.Request.URL.Path) + resp, err := h.service.ExchangeAPIKeyToken(c.Request.Context(), &req, c.Request.Method, c.Request.URL.Path) if httpx.HandleServiceError(c, err) { return } - dto.JSONResponse(c, http.StatusOK, "Access key token issued successfully", resp) + dto.JSONResponse(c, http.StatusOK, "API key token issued successfully", resp) } -func parseCurrentUserAndAccessKeyID(c *gin.Context) (int, int, bool) { +func parseCurrentUserAndAPIKeyID(c *gin.Context) (int, int, bool) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { dto.ErrorResponse(c, http.StatusUnauthorized, "Authentication required") return 0, 0, false } - accessKeyID, ok := httpx.ParsePositiveID(c, c.Param("access_key_id"), consts.URLPathID) + accessKeyID, ok := httpx.ParsePositiveID(c, c.Param("id"), consts.URLPathID) if !ok { return 0, 0, false } diff --git a/src/module/auth/handler_service.go b/src/module/auth/handler_service.go index 5377ffb4..923930bc 100644 --- a/src/module/auth/handler_service.go +++ b/src/module/auth/handler_service.go @@ -14,14 +14,15 @@ type HandlerService interface { Logout(context.Context, *utils.Claims) error ChangePassword(context.Context, *ChangePasswordReq, int) error GetProfile(context.Context, int) (*UserProfileResp, error) - CreateAccessKey(context.Context, int, *CreateAccessKeyReq) (*AccessKeyWithSecretResp, error) - ListAccessKeys(context.Context, int, *ListAccessKeyReq) (*ListAccessKeyResp, error) - GetAccessKey(context.Context, int, int) (*AccessKeyInfo, error) - DeleteAccessKey(context.Context, int, int) error - DisableAccessKey(context.Context, int, int) error - EnableAccessKey(context.Context, int, int) error - RotateAccessKey(context.Context, int, int) (*AccessKeyWithSecretResp, error) - ExchangeAccessKeyToken(context.Context, *AccessKeyTokenReq, string, string) (*AccessKeyTokenResp, error) + CreateAPIKey(context.Context, int, *CreateAPIKeyReq) (*APIKeyWithSecretResp, error) + ListAPIKeys(context.Context, int, *ListAPIKeyReq) (*ListAPIKeyResp, error) + GetAPIKey(context.Context, int, int) (*APIKeyInfo, error) + DeleteAPIKey(context.Context, int, int) error + DisableAPIKey(context.Context, int, int) error + EnableAPIKey(context.Context, int, int) error + RevokeAPIKey(context.Context, int, int) error + RotateAPIKey(context.Context, int, int) (*APIKeyWithSecretResp, error) + ExchangeAPIKeyToken(context.Context, *APIKeyTokenReq, string, string) (*APIKeyTokenResp, error) } func AsHandlerService(service *Service) HandlerService { diff --git a/src/module/auth/module.go b/src/module/auth/module.go index 80009f3a..dfe1e4a1 100644 --- a/src/module/auth/module.go +++ b/src/module/auth/module.go @@ -7,7 +7,7 @@ import ( var Module = fx.Module("auth", fx.Provide(NewUserRepository), fx.Provide(NewRoleRepository), - fx.Provide(NewAccessKeyRepository), + fx.Provide(NewAPIKeyRepository), fx.Provide(NewTokenStore), fx.Provide(NewService), fx.Provide(AsHandlerService), diff --git a/src/module/auth/repository.go b/src/module/auth/repository.go index 6c1826b1..791e4b28 100644 --- a/src/module/auth/repository.go +++ b/src/module/auth/repository.go @@ -124,63 +124,63 @@ func (r *RoleRepository) ListByUserID(userID int) ([]model.Role, error) { return roles, nil } -type AccessKeyRepository struct { +type APIKeyRepository struct { db *gorm.DB } -func NewAccessKeyRepository(db *gorm.DB) *AccessKeyRepository { - return &AccessKeyRepository{db: db} +func NewAPIKeyRepository(db *gorm.DB) *APIKeyRepository { + return &APIKeyRepository{db: db} } -func (r *AccessKeyRepository) Create(key *model.UserAccessKey) error { +func (r *APIKeyRepository) Create(key *model.APIKey) error { if err := r.db.Create(key).Error; err != nil { - return fmt.Errorf("failed to create access key: %w", err) + return fmt.Errorf("failed to create api key: %w", err) } return nil } -func (r *AccessKeyRepository) ListByUserID(userID, limit, offset int) ([]model.UserAccessKey, int64, error) { - query := r.db.Model(&model.UserAccessKey{}). +func (r *APIKeyRepository) ListByUserID(userID, limit, offset int) ([]model.APIKey, int64, error) { + query := r.db.Model(&model.APIKey{}). Where("user_id = ? AND status != ?", userID, consts.CommonDeleted) var total int64 if err := query.Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count access keys: %w", err) + return nil, 0, fmt.Errorf("failed to count api keys: %w", err) } - var keys []model.UserAccessKey + var keys []model.APIKey if err := query.Order("id DESC").Limit(limit).Offset(offset).Find(&keys).Error; err != nil { - return nil, 0, fmt.Errorf("failed to list access keys: %w", err) + return nil, 0, fmt.Errorf("failed to list api keys: %w", err) } return keys, total, nil } -func (r *AccessKeyRepository) GetByIDForUser(id, userID int) (*model.UserAccessKey, error) { - var key model.UserAccessKey +func (r *APIKeyRepository) GetByIDForUser(id, userID int) (*model.APIKey, error) { + var key model.APIKey if err := r.db.Where("id = ? AND user_id = ? AND status != ?", id, userID, consts.CommonDeleted).First(&key).Error; err != nil { - return nil, fmt.Errorf("failed to find access key: %w", err) + return nil, fmt.Errorf("failed to find api key: %w", err) } return &key, nil } -func (r *AccessKeyRepository) GetByAccessKey(accessKey string) (*model.UserAccessKey, error) { - var key model.UserAccessKey - if err := r.db.Where("access_key = ? AND status != ?", accessKey, consts.CommonDeleted).First(&key).Error; err != nil { - return nil, fmt.Errorf("failed to find access key: %w", err) +func (r *APIKeyRepository) GetByKeyID(keyID string) (*model.APIKey, error) { + var key model.APIKey + if err := r.db.Where("key_id = ? AND status != ?", keyID, consts.CommonDeleted).First(&key).Error; err != nil { + return nil, fmt.Errorf("failed to find api key: %w", err) } return &key, nil } -func (r *AccessKeyRepository) Update(key *model.UserAccessKey) error { +func (r *APIKeyRepository) Update(key *model.APIKey) error { if err := r.db.Save(key).Error; err != nil { - return fmt.Errorf("failed to update access key: %w", err) + return fmt.Errorf("failed to update api key: %w", err) } return nil } -func (r *AccessKeyRepository) UpdateLastUsedAt(id int, usedAt time.Time) error { - if err := r.db.Model(&model.UserAccessKey{}).Where("id = ?", id).Update("last_used_at", usedAt).Error; err != nil { - return fmt.Errorf("failed to update access key last used time: %w", err) +func (r *APIKeyRepository) UpdateLastUsedAt(id int, usedAt time.Time) error { + if err := r.db.Model(&model.APIKey{}).Where("id = ?", id).Update("last_used_at", usedAt).Error; err != nil { + return fmt.Errorf("failed to update api key last used time: %w", err) } return nil } diff --git a/src/module/auth/service.go b/src/module/auth/service.go index 6664fb09..b73340ca 100644 --- a/src/module/auth/service.go +++ b/src/module/auth/service.go @@ -20,18 +20,18 @@ import ( const accessKeySignatureTTL = 5 * time.Minute type Service struct { - userRepo *UserRepository - roleRepo *RoleRepository - accessKeyRepo *AccessKeyRepository - tokenStore *TokenStore + userRepo *UserRepository + roleRepo *RoleRepository + apiKeyRepo *APIKeyRepository + tokenStore *TokenStore } -func NewService(userRepo *UserRepository, roleRepo *RoleRepository, accessKeyRepo *AccessKeyRepository, tokenStore *TokenStore) *Service { +func NewService(userRepo *UserRepository, roleRepo *RoleRepository, apiKeyRepo *APIKeyRepository, tokenStore *TokenStore) *Service { return &Service{ - userRepo: userRepo, - roleRepo: roleRepo, - accessKeyRepo: accessKeyRepo, - tokenStore: tokenStore, + userRepo: userRepo, + roleRepo: roleRepo, + apiKeyRepo: apiKeyRepo, + tokenStore: tokenStore, } } @@ -248,155 +248,184 @@ func (s *Service) GetProfile(ctx context.Context, userID int) (*UserProfileResp, return resp, nil } -func (s *Service) CreateAccessKey(ctx context.Context, userID int, req *CreateAccessKeyReq) (*AccessKeyWithSecretResp, error) { +func (s *Service) CreateAPIKey(ctx context.Context, userID int, req *CreateAPIKeyReq) (*APIKeyWithSecretResp, error) { if req == nil { - return nil, fmt.Errorf("access key create request is nil") + return nil, fmt.Errorf("api key create request is nil") + } + normalizedScopes, err := normalizeAPIKeyScopes(req.Scopes) + if err != nil { + return nil, err } - accessKeyValue, err := generateCredentialValue("ak_", 16) + accessKeyValue, err := generateCredentialValue("pk_", 16) if err != nil { - return nil, fmt.Errorf("failed to generate access key: %w", err) + return nil, fmt.Errorf("failed to generate api key id: %w", err) } - secretKeyValue, err := generateCredentialValue("sk_", 24) + secretKeyValue, err := generateCredentialValue("ks_", 24) if err != nil { - return nil, fmt.Errorf("failed to generate secret key: %w", err) + return nil, fmt.Errorf("failed to generate key secret: %w", err) } secretHash, err := utils.HashPassword(secretKeyValue) if err != nil { - return nil, fmt.Errorf("failed to hash secret key: %w", err) + return nil, fmt.Errorf("failed to hash key secret: %w", err) } - secretCiphertext, err := utils.EncryptAccessKeySecret(secretKeyValue) + secretCiphertext, err := utils.EncryptAPIKeySecret(secretKeyValue) if err != nil { - return nil, fmt.Errorf("failed to encrypt secret key: %w", err) + return nil, fmt.Errorf("failed to encrypt key secret: %w", err) } - key := &model.UserAccessKey{ - UserID: userID, - Name: req.Name, - Description: req.Description, - AccessKey: accessKeyValue, - SecretHash: secretHash, - SecretCiphertext: secretCiphertext, - ExpiresAt: req.ExpiresAt, - Status: consts.CommonEnabled, + key := &model.APIKey{ + UserID: userID, + Name: req.Name, + Description: req.Description, + KeyID: accessKeyValue, + KeySecretHash: secretHash, + KeySecretCiphertext: secretCiphertext, + Scopes: normalizedScopes, + ExpiresAt: req.ExpiresAt, + Status: consts.CommonEnabled, } - if err := s.accessKeyRepo.Create(key); err != nil { + if err := s.apiKeyRepo.Create(key); err != nil { return nil, err } - resp := &AccessKeyWithSecretResp{ - AccessKeyInfo: *NewAccessKeyInfo(key), - SecretKey: secretKeyValue, + resp := &APIKeyWithSecretResp{ + APIKeyInfo: *NewAPIKeyInfo(key), + KeySecret: secretKeyValue, } return resp, nil } -func (s *Service) ListAccessKeys(ctx context.Context, userID int, req *ListAccessKeyReq) (*ListAccessKeyResp, error) { +func (s *Service) ListAPIKeys(ctx context.Context, userID int, req *ListAPIKeyReq) (*ListAPIKeyResp, error) { if req == nil { - return nil, fmt.Errorf("access key list request is nil") + return nil, fmt.Errorf("api key list request is nil") } limit, offset := req.ToGormParams() - keys, total, err := s.accessKeyRepo.ListByUserID(userID, limit, offset) + keys, total, err := s.apiKeyRepo.ListByUserID(userID, limit, offset) if err != nil { return nil, err } - items := make([]AccessKeyInfo, 0, len(keys)) + items := make([]APIKeyInfo, 0, len(keys)) for i := range keys { - items = append(items, *NewAccessKeyInfo(&keys[i])) + items = append(items, *NewAPIKeyInfo(&keys[i])) } - return &ListAccessKeyResp{ + return &ListAPIKeyResp{ Items: items, Pagination: *req.ConvertToPaginationInfo(total), }, nil } -func (s *Service) GetAccessKey(ctx context.Context, userID, accessKeyID int) (*AccessKeyInfo, error) { - key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) +func (s *Service) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*APIKeyInfo, error) { + key, err := s.apiKeyRepo.GetByIDForUser(accessKeyID, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: access key not found", consts.ErrNotFound) + return nil, fmt.Errorf("%w: api key not found", consts.ErrNotFound) } return nil, err } - return NewAccessKeyInfo(key), nil + return NewAPIKeyInfo(key), nil } -func (s *Service) DeleteAccessKey(ctx context.Context, userID, accessKeyID int) error { - key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) +func (s *Service) DeleteAPIKey(ctx context.Context, userID, accessKeyID int) error { + key, err := s.apiKeyRepo.GetByIDForUser(accessKeyID, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: access key not found", consts.ErrNotFound) + return fmt.Errorf("%w: api key not found", consts.ErrNotFound) } return err } key.Status = consts.CommonDeleted - return s.accessKeyRepo.Update(key) + return s.apiKeyRepo.Update(key) } -func (s *Service) DisableAccessKey(ctx context.Context, userID, accessKeyID int) error { - return s.setAccessKeyStatus(userID, accessKeyID, consts.CommonDisabled) +func (s *Service) DisableAPIKey(ctx context.Context, userID, accessKeyID int) error { + return s.setAPIKeyStatus(userID, accessKeyID, consts.CommonDisabled) } -func (s *Service) EnableAccessKey(ctx context.Context, userID, accessKeyID int) error { - return s.setAccessKeyStatus(userID, accessKeyID, consts.CommonEnabled) +func (s *Service) EnableAPIKey(ctx context.Context, userID, accessKeyID int) error { + return s.setAPIKeyStatus(userID, accessKeyID, consts.CommonEnabled) } -func (s *Service) RotateAccessKey(ctx context.Context, userID, accessKeyID int) (*AccessKeyWithSecretResp, error) { - key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) +func (s *Service) RevokeAPIKey(ctx context.Context, userID, accessKeyID int) error { + key, err := s.apiKeyRepo.GetByIDForUser(accessKeyID, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: access key not found", consts.ErrNotFound) + return fmt.Errorf("%w: api key not found", consts.ErrNotFound) + } + return err + } + if key.RevokedAt != nil { + return nil + } + + now := time.Now() + key.RevokedAt = &now + key.Status = consts.CommonDisabled + return s.apiKeyRepo.Update(key) +} + +func (s *Service) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*APIKeyWithSecretResp, error) { + key, err := s.apiKeyRepo.GetByIDForUser(accessKeyID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("%w: api key not found", consts.ErrNotFound) } return nil, err } + if key.RevokedAt != nil { + return nil, fmt.Errorf("%w: revoked api key cannot be rotated", consts.ErrBadRequest) + } - secretKeyValue, err := generateCredentialValue("sk_", 24) + secretKeyValue, err := generateCredentialValue("ks_", 24) if err != nil { - return nil, fmt.Errorf("failed to generate secret key: %w", err) + return nil, fmt.Errorf("failed to generate key secret: %w", err) } secretHash, err := utils.HashPassword(secretKeyValue) if err != nil { - return nil, fmt.Errorf("failed to hash secret key: %w", err) + return nil, fmt.Errorf("failed to hash key secret: %w", err) } - secretCiphertext, err := utils.EncryptAccessKeySecret(secretKeyValue) + secretCiphertext, err := utils.EncryptAPIKeySecret(secretKeyValue) if err != nil { - return nil, fmt.Errorf("failed to encrypt secret key: %w", err) + return nil, fmt.Errorf("failed to encrypt key secret: %w", err) } - key.SecretHash = secretHash - key.SecretCiphertext = secretCiphertext - if err := s.accessKeyRepo.Update(key); err != nil { + key.KeySecretHash = secretHash + key.KeySecretCiphertext = secretCiphertext + if err := s.apiKeyRepo.Update(key); err != nil { return nil, err } - return &AccessKeyWithSecretResp{ - AccessKeyInfo: *NewAccessKeyInfo(key), - SecretKey: secretKeyValue, + return &APIKeyWithSecretResp{ + APIKeyInfo: *NewAPIKeyInfo(key), + KeySecret: secretKeyValue, }, nil } -func (s *Service) ExchangeAccessKeyToken(ctx context.Context, req *AccessKeyTokenReq, method, path string) (*AccessKeyTokenResp, error) { +func (s *Service) ExchangeAPIKeyToken(ctx context.Context, req *APIKeyTokenReq, method, path string) (*APIKeyTokenResp, error) { if req == nil { - return nil, fmt.Errorf("access key token request is nil") + return nil, fmt.Errorf("api key token request is nil") } - key, err := s.accessKeyRepo.GetByAccessKey(req.AccessKey) + key, err := s.apiKeyRepo.GetByKeyID(req.KeyID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: invalid access key or secret key", consts.ErrAuthenticationFailed) + return nil, fmt.Errorf("%w: invalid key id or key secret", consts.ErrAuthenticationFailed) } return nil, err } if key.Status != consts.CommonEnabled { - return nil, fmt.Errorf("%w: access key is disabled", consts.ErrAuthenticationFailed) + return nil, fmt.Errorf("%w: api key is disabled", consts.ErrAuthenticationFailed) + } + if key.RevokedAt != nil { + return nil, fmt.Errorf("%w: api key is revoked", consts.ErrAuthenticationFailed) } if key.ExpiresAt != nil && key.ExpiresAt.Before(time.Now()) { - return nil, fmt.Errorf("%w: access key is expired", consts.ErrAuthenticationFailed) + return nil, fmt.Errorf("%w: api key is expired", consts.ErrAuthenticationFailed) } timestampUnix, err := req.TimestampUnix() if err != nil { @@ -408,43 +437,43 @@ func (s *Service) ExchangeAccessKeyToken(ctx context.Context, req *AccessKeyToke return nil, fmt.Errorf("%w: request timestamp is outside the allowed window", consts.ErrAuthenticationFailed) } - secretKey, err := utils.DecryptAccessKeySecret(key.SecretCiphertext) + secretKey, err := utils.DecryptAPIKeySecret(key.KeySecretCiphertext) if err != nil { - return nil, fmt.Errorf("failed to decrypt access key secret: %w", err) + return nil, fmt.Errorf("failed to decrypt api key secret: %w", err) } - if !utils.VerifyAccessKeyRequestSignature(secretKey, req.CanonicalString(method, path), req.Signature) { - return nil, fmt.Errorf("%w: invalid access key signature", consts.ErrAuthenticationFailed) + if !utils.VerifyAPIKeyRequestSignature(secretKey, req.CanonicalString(method, path), req.Signature) { + return nil, fmt.Errorf("%w: invalid api key signature", consts.ErrAuthenticationFailed) } - if err := s.tokenStore.ReserveAccessKeyNonce(ctx, key.AccessKey, req.Nonce, accessKeySignatureTTL); err != nil { + if err := s.tokenStore.ReserveAPIKeyNonce(ctx, key.KeyID, req.Nonce, accessKeySignatureTTL); err != nil { return nil, err } user, err := s.userRepo.GetByID(key.UserID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("%w: access key owner not found", consts.ErrAuthenticationFailed) + return nil, fmt.Errorf("%w: api key owner not found", consts.ErrAuthenticationFailed) } return nil, err } if !user.IsActive || user.Status != consts.CommonEnabled { - return nil, fmt.Errorf("%w: access key owner is inactive", consts.ErrAuthenticationFailed) + return nil, fmt.Errorf("%w: api key owner is inactive", consts.ErrAuthenticationFailed) } - token, expiresAt, err := s.generateAccessKeyTokenWithRoles(s.roleRepo, user, key.ID) + token, expiresAt, err := s.generateAPIKeyTokenWithRoles(s.roleRepo, user, key.ID, key.Scopes) if err != nil { return nil, err } - if err := s.accessKeyRepo.UpdateLastUsedAt(key.ID, time.Now()); err != nil { - logrus.WithError(err).Warn("failed to update access key last used time") + if err := s.apiKeyRepo.UpdateLastUsedAt(key.ID, time.Now()); err != nil { + logrus.WithError(err).Warn("failed to update api key last used time") } - return &AccessKeyTokenResp{ + return &APIKeyTokenResp{ Token: token, TokenType: "Bearer", ExpiresAt: expiresAt, - AuthType: "access_key", - AccessKey: key.AccessKey, + AuthType: "api_key", + KeyID: key.KeyID, }, nil } @@ -471,7 +500,7 @@ func (s *Service) generateTokenWithRoles(roleRepo *RoleRepository, user *model.U return token, expiresAt, nil } -func (s *Service) generateAccessKeyTokenWithRoles(roleRepo *RoleRepository, user *model.User, accessKeyID int) (string, time.Time, error) { +func (s *Service) generateAPIKeyTokenWithRoles(roleRepo *RoleRepository, user *model.User, apiKeyID int, apiKeyScopes []string) (string, time.Time, error) { roles, err := roleRepo.ListByUserID(user.ID) if err != nil { return "", time.Time{}, fmt.Errorf("failed to get user roles: %w", err) @@ -486,9 +515,9 @@ func (s *Service) generateAccessKeyTokenWithRoles(roleRepo *RoleRepository, user } } - token, expiresAt, err := utils.GenerateAccessKeyToken(user.ID, user.Username, user.Email, user.IsActive, isAdmin, roleNames, accessKeyID) + token, expiresAt, err := utils.GenerateAPIKeyToken(user.ID, user.Username, user.Email, user.IsActive, isAdmin, roleNames, apiKeyID, apiKeyScopes) if err != nil { - return "", time.Time{}, fmt.Errorf("failed to generate access key token: %w", err) + return "", time.Time{}, fmt.Errorf("failed to generate api key token: %w", err) } return token, expiresAt, nil @@ -525,17 +554,20 @@ func (s *Service) getAllUserResourceRoles(userID int) ([]usermodule.UserContaine return containerRoles, datasetRoles, projectRoles, nil } -func (s *Service) setAccessKeyStatus(userID, accessKeyID int, status consts.StatusType) error { - key, err := s.accessKeyRepo.GetByIDForUser(accessKeyID, userID) +func (s *Service) setAPIKeyStatus(userID, accessKeyID int, status consts.StatusType) error { + key, err := s.apiKeyRepo.GetByIDForUser(accessKeyID, userID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fmt.Errorf("%w: access key not found", consts.ErrNotFound) + return fmt.Errorf("%w: api key not found", consts.ErrNotFound) } return err } + if status == consts.CommonEnabled && key.RevokedAt != nil { + return fmt.Errorf("%w: revoked api key cannot be re-enabled", consts.ErrBadRequest) + } key.Status = status - return s.accessKeyRepo.Update(key) + return s.apiKeyRepo.Update(key) } func generateCredentialValue(prefix string, randomBytes int) (string, error) { diff --git a/src/module/auth/service_test.go b/src/module/auth/service_test.go index 9bde36ef..17fb4aad 100644 --- a/src/module/auth/service_test.go +++ b/src/module/auth/service_test.go @@ -40,7 +40,7 @@ func newAuthService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { }), &gorm.Config{}) require.NoError(t, err) - service := NewService(NewUserRepository(db), NewRoleRepository(db), NewAccessKeyRepository(db), &TokenStore{}) + service := NewService(NewUserRepository(db), NewRoleRepository(db), NewAPIKeyRepository(db), &TokenStore{}) return service, mock, func() { _ = sqlDB.Close() } @@ -153,17 +153,17 @@ func TestAuthServiceRefreshTokenSuccess(t *testing.T) { require.NoError(t, mock.ExpectationsWereMet()) } -func TestAuthServiceCreateAccessKeySuccess(t *testing.T) { +func TestAuthServiceCreateAPIKeySuccess(t *testing.T) { service, mock, cleanup := newAuthService(t) defer cleanup() mock.ExpectBegin() - mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `user_access_keys` (`user_id`,`name`,`description`,`access_key`,`secret_hash`,`secret_ciphertext`,`last_used_at`,`expires_at`,`status`,`created_at`,`updated_at`,`active_access_key`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")). - WithArgs(7, "ci-bot", "SDK credential", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), nil, nil, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `api_keys` (`user_id`,`name`,`description`,`key_id`,`key_secret_hash`,`key_secret_ciphertext`,`scopes`,`revoked_at`,`last_used_at`,`expires_at`,`status`,`created_at`,`updated_at`,`active_key_id`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)")). + WithArgs(7, "ci-bot", "SDK credential", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), nil, nil, nil, consts.CommonEnabled, sqlmock.AnyArg(), sqlmock.AnyArg(), ""). WillReturnResult(sqlmock.NewResult(11, 1)) mock.ExpectCommit() - resp, err := service.CreateAccessKey(t.Context(), 7, &CreateAccessKeyReq{ + resp, err := service.CreateAPIKey(t.Context(), 7, &CreateAPIKeyReq{ Name: "ci-bot", Description: "SDK credential", }) @@ -171,33 +171,34 @@ func TestAuthServiceCreateAccessKeySuccess(t *testing.T) { require.NoError(t, err) require.Equal(t, 11, resp.ID) require.Equal(t, "ci-bot", resp.Name) - require.NotEmpty(t, resp.AccessKey) - require.NotEmpty(t, resp.SecretKey) + require.NotEmpty(t, resp.KeyID) + require.NotEmpty(t, resp.KeySecret) + require.Equal(t, []string{"*"}, resp.Scopes) require.NoError(t, mock.ExpectationsWereMet()) } -func TestAuthServiceExchangeAccessKeyTokenSuccess(t *testing.T) { +func TestAuthServiceExchangeAPIKeyTokenSuccess(t *testing.T) { service, mock, cleanup := newAuthService(t) defer cleanup() now := time.Now() - secret := "sk_test_secret_123456" + secret := "ks_test_secret_123456" secretHash, err := utils.HashPassword(secret) require.NoError(t, err) - secretCiphertext, err := utils.EncryptAccessKeySecret(secret) + secretCiphertext, err := utils.EncryptAPIKeySecret(secret) require.NoError(t, err) - req := &AccessKeyTokenReq{ - AccessKey: "ak_test_credential", + req := &APIKeyTokenReq{ + KeyID: "pk_test_credential", Timestamp: fmt.Sprintf("%d", now.Unix()), Nonce: "nonce_123", } - req.Signature = utils.SignAccessKeyRequest(secret, req.CanonicalString("POST", "/api/v2/auth/access-key/token")) + req.Signature = utils.SignAPIKeyRequest(secret, req.CanonicalString("POST", "/api/v2/auth/api-key/token")) - mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `user_access_keys` WHERE access_key = ? AND status != ? ORDER BY `user_access_keys`.`id` LIMIT ?")). - WithArgs("ak_test_credential", consts.CommonDeleted, 1). + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `api_keys` WHERE key_id = ? AND status != ? ORDER BY `api_keys`.`id` LIMIT ?")). + WithArgs("pk_test_credential", consts.CommonDeleted, 1). WillReturnRows(sqlmock.NewRows([]string{ - "id", "user_id", "name", "description", "access_key", "secret_hash", "secret_ciphertext", "last_used_at", "expires_at", "status", "created_at", "updated_at", - }).AddRow(5, 7, "ci-bot", "SDK credential", "ak_test_credential", secretHash, secretCiphertext, nil, nil, consts.CommonEnabled, now, now)) + "id", "user_id", "name", "description", "key_id", "key_secret_hash", "key_secret_ciphertext", "scopes", "revoked_at", "last_used_at", "expires_at", "status", "created_at", "updated_at", + }).AddRow(5, 7, "ci-bot", "SDK credential", "pk_test_credential", secretHash, secretCiphertext, []byte(`["*"]`), nil, nil, nil, consts.CommonEnabled, now, now)) mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE id = ? ORDER BY `users`.`id` LIMIT ?")). WithArgs(7, 1). WillReturnRows(sqlmock.NewRows([]string{ @@ -210,21 +211,54 @@ func TestAuthServiceExchangeAccessKeyTokenSuccess(t *testing.T) { "id", "name", "display_name", "description", "is_system", "status", "created_at", "updated_at", "active_name", }).AddRow(2, consts.RoleUser.String(), "User", "", true, consts.CommonEnabled, now, now, consts.RoleUser.String())) mock.ExpectBegin() - mock.ExpectExec(regexp.QuoteMeta("UPDATE `user_access_keys` SET `last_used_at`=?,`updated_at`=? WHERE id = ?")). + mock.ExpectExec(regexp.QuoteMeta("UPDATE `api_keys` SET `last_used_at`=?,`updated_at`=? WHERE id = ?")). WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), 5). WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectCommit() - resp, err := service.ExchangeAccessKeyToken(t.Context(), req, "POST", "/api/v2/auth/access-key/token") + resp, err := service.ExchangeAPIKeyToken(t.Context(), req, "POST", "/api/v2/auth/api-key/token") require.NoError(t, err) require.Equal(t, "Bearer", resp.TokenType) - require.Equal(t, "access_key", resp.AuthType) + require.Equal(t, "api_key", resp.AuthType) claims, err := utils.ValidateToken(resp.Token) require.NoError(t, err) require.Equal(t, 7, claims.UserID) - require.Equal(t, "access_key", claims.AuthType) - require.Equal(t, 5, claims.AccessKeyID) + require.Equal(t, "api_key", claims.AuthType) + require.Equal(t, 5, claims.APIKeyID) + require.Equal(t, []string{"*"}, claims.APIKeyScopes) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestAuthServiceExchangeAPIKeyTokenRevoked(t *testing.T) { + service, mock, cleanup := newAuthService(t) + defer cleanup() + + now := time.Now() + revokedAt := now.Add(-time.Minute) + secret := "ks_test_secret_123456" + secretHash, err := utils.HashPassword(secret) + require.NoError(t, err) + secretCiphertext, err := utils.EncryptAPIKeySecret(secret) + require.NoError(t, err) + req := &APIKeyTokenReq{ + KeyID: "pk_test_credential", + Timestamp: fmt.Sprintf("%d", now.Unix()), + Nonce: "nonce_123", + } + req.Signature = utils.SignAPIKeyRequest(secret, req.CanonicalString("POST", "/api/v2/auth/api-key/token")) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `api_keys` WHERE key_id = ? AND status != ? ORDER BY `api_keys`.`id` LIMIT ?")). + WithArgs("pk_test_credential", consts.CommonDeleted, 1). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "user_id", "name", "description", "key_id", "key_secret_hash", "key_secret_ciphertext", "scopes", "revoked_at", "last_used_at", "expires_at", "status", "created_at", "updated_at", + }).AddRow(5, 7, "ci-bot", "SDK credential", "pk_test_credential", secretHash, secretCiphertext, []byte(`["*"]`), revokedAt, nil, nil, consts.CommonEnabled, now, now)) + + resp, err := service.ExchangeAPIKeyToken(t.Context(), req, "POST", "/api/v2/auth/api-key/token") + + require.Nil(t, resp) + require.Error(t, err) + require.ErrorContains(t, err, "api key is revoked") require.NoError(t, mock.ExpectationsWereMet()) } diff --git a/src/module/auth/token_store.go b/src/module/auth/token_store.go index f4c4c6b0..f24f14ab 100644 --- a/src/module/auth/token_store.go +++ b/src/module/auth/token_store.go @@ -11,7 +11,7 @@ import ( ) const tokenBlacklistPrefix = "blacklist:token:%s" -const accessKeyNoncePrefix = "access_key:nonce:%s:%s" +const apiKeyNoncePrefix = "api_key:nonce:%s:%s" type TokenStore struct { redis *redisinfra.Gateway @@ -41,15 +41,15 @@ func (s *TokenStore) AddTokenToBlacklist(ctx context.Context, tokenID string, ex return nil } -func (s *TokenStore) ReserveAccessKeyNonce(ctx context.Context, accessKey, nonce string, ttl time.Duration) error { +func (s *TokenStore) ReserveAPIKeyNonce(ctx context.Context, keyID, nonce string, ttl time.Duration) error { if s == nil || s.redis == nil { return nil } - key := fmt.Sprintf(accessKeyNoncePrefix, accessKey, nonce) + key := fmt.Sprintf(apiKeyNoncePrefix, keyID, nonce) ok, err := s.redis.SetNX(ctx, key, "1", ttl) if err != nil { - return fmt.Errorf("failed to reserve access key nonce: %w", err) + return fmt.Errorf("failed to reserve api key nonce: %w", err) } if !ok { return fmt.Errorf("%w: request nonce has already been used", consts.ErrAuthenticationFailed) diff --git a/src/module/execution/handler.go b/src/module/execution/handler.go index 797f4346..7dae9053 100644 --- a/src/module/execution/handler.go +++ b/src/module/execution/handler.go @@ -304,7 +304,7 @@ func (h *Handler) BatchDeleteExecutions(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Execution not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/detector_results [post] -// @x-api-type {} +// @x-api-type {"runtime":"true"} func (h *Handler) UploadDetectorResults(c *gin.Context) { executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { @@ -344,7 +344,7 @@ func (h *Handler) UploadDetectorResults(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Execution not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/granularity_results [post] -// @x-api-type {} +// @x-api-type {"runtime":"true"} func (h *Handler) UploadGranularityResults(c *gin.Context) { executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { diff --git a/src/module/sdk/handler.go b/src/module/sdk/handler.go index a79f70d3..e6aa5f2a 100644 --- a/src/module/sdk/handler.go +++ b/src/module/sdk/handler.go @@ -22,7 +22,7 @@ func NewHandler(service *Service) *Handler { // // @Summary List SDK evaluation samples // @Description Get a paginated list of SDK evaluation samples, optionally filtered by exp_id and stage -// @Tags SDK Evaluations +// @Tags Evaluations // @ID list_sdk_evaluations // @Produce json // @Security BearerAuth @@ -56,7 +56,7 @@ func (h *Handler) ListEvaluations(c *gin.Context) { // // @Summary Get SDK evaluation sample by ID // @Description Get detailed information about a specific SDK evaluation sample -// @Tags SDK Evaluations +// @Tags Evaluations // @ID get_sdk_evaluation // @Produce json // @Security BearerAuth @@ -83,7 +83,7 @@ func (h *Handler) GetEvaluation(c *gin.Context) { // // @Summary List SDK experiment IDs // @Description Get all distinct experiment IDs from SDK evaluation data -// @Tags SDK Evaluations +// @Tags Evaluations // @ID list_sdk_experiments // @Produce json // @Security BearerAuth @@ -103,7 +103,7 @@ func (h *Handler) ListExperiments(c *gin.Context) { // // @Summary List SDK dataset samples // @Description Get a paginated list of SDK dataset samples, optionally filtered by dataset name -// @Tags SDK Datasets +// @Tags Datasets // @ID list_sdk_dataset_samples // @Produce json // @Security BearerAuth diff --git a/src/proto/iam/v1/iam.pb.go b/src/proto/iam/v1/iam.pb.go index f383ea53..40f21573 100644 --- a/src/proto/iam/v1/iam.pb.go +++ b/src/proto/iam/v1/iam.pb.go @@ -79,8 +79,9 @@ type VerifyTokenResponse struct { Roles []string `protobuf:"bytes,8,rep,name=roles,proto3" json:"roles,omitempty"` ExpiresAtUnix int64 `protobuf:"varint,9,opt,name=expires_at_unix,json=expiresAtUnix,proto3" json:"expires_at_unix,omitempty"` AuthType string `protobuf:"bytes,10,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` - AccessKeyId int64 `protobuf:"varint,11,opt,name=access_key_id,json=accessKeyId,proto3" json:"access_key_id,omitempty"` + KeyId int64 `protobuf:"varint,11,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` TaskId string `protobuf:"bytes,12,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"` + ApiKeyScopes []string `protobuf:"bytes,13,rep,name=api_key_scopes,json=apiKeyScopes,proto3" json:"api_key_scopes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -185,9 +186,9 @@ func (x *VerifyTokenResponse) GetAuthType() string { return "" } -func (x *VerifyTokenResponse) GetAccessKeyId() int64 { +func (x *VerifyTokenResponse) GetKeyId() int64 { if x != nil { - return x.AccessKeyId + return x.KeyId } return 0 } @@ -199,6 +200,13 @@ func (x *VerifyTokenResponse) GetTaskId() string { return "" } +func (x *VerifyTokenResponse) GetApiKeyScopes() []string { + if x != nil { + return x.ApiKeyScopes + } + return nil +} + type CheckPermissionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` @@ -535,9 +543,9 @@ func (x *BoolResponse) GetValue() bool { return false } -type ExchangeAccessKeyTokenRequest struct { +type ExchangeAPIKeyTokenRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + KeyId string `protobuf:"bytes,1,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` Timestamp string `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` Nonce string `protobuf:"bytes,3,opt,name=nonce,proto3" json:"nonce,omitempty"` Signature string `protobuf:"bytes,4,opt,name=signature,proto3" json:"signature,omitempty"` @@ -547,20 +555,20 @@ type ExchangeAccessKeyTokenRequest struct { sizeCache protoimpl.SizeCache } -func (x *ExchangeAccessKeyTokenRequest) Reset() { - *x = ExchangeAccessKeyTokenRequest{} +func (x *ExchangeAPIKeyTokenRequest) Reset() { + *x = ExchangeAPIKeyTokenRequest{} mi := &file_proto_iam_v1_iam_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExchangeAccessKeyTokenRequest) String() string { +func (x *ExchangeAPIKeyTokenRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExchangeAccessKeyTokenRequest) ProtoMessage() {} +func (*ExchangeAPIKeyTokenRequest) ProtoMessage() {} -func (x *ExchangeAccessKeyTokenRequest) ProtoReflect() protoreflect.Message { +func (x *ExchangeAPIKeyTokenRequest) ProtoReflect() protoreflect.Message { mi := &file_proto_iam_v1_iam_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -572,78 +580,78 @@ func (x *ExchangeAccessKeyTokenRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExchangeAccessKeyTokenRequest.ProtoReflect.Descriptor instead. -func (*ExchangeAccessKeyTokenRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use ExchangeAPIKeyTokenRequest.ProtoReflect.Descriptor instead. +func (*ExchangeAPIKeyTokenRequest) Descriptor() ([]byte, []int) { return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{8} } -func (x *ExchangeAccessKeyTokenRequest) GetAccessKey() string { +func (x *ExchangeAPIKeyTokenRequest) GetKeyId() string { if x != nil { - return x.AccessKey + return x.KeyId } return "" } -func (x *ExchangeAccessKeyTokenRequest) GetTimestamp() string { +func (x *ExchangeAPIKeyTokenRequest) GetTimestamp() string { if x != nil { return x.Timestamp } return "" } -func (x *ExchangeAccessKeyTokenRequest) GetNonce() string { +func (x *ExchangeAPIKeyTokenRequest) GetNonce() string { if x != nil { return x.Nonce } return "" } -func (x *ExchangeAccessKeyTokenRequest) GetSignature() string { +func (x *ExchangeAPIKeyTokenRequest) GetSignature() string { if x != nil { return x.Signature } return "" } -func (x *ExchangeAccessKeyTokenRequest) GetMethod() string { +func (x *ExchangeAPIKeyTokenRequest) GetMethod() string { if x != nil { return x.Method } return "" } -func (x *ExchangeAccessKeyTokenRequest) GetPath() string { +func (x *ExchangeAPIKeyTokenRequest) GetPath() string { if x != nil { return x.Path } return "" } -type ExchangeAccessKeyTokenResponse struct { +type ExchangeAPIKeyTokenResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` TokenType string `protobuf:"bytes,2,opt,name=token_type,json=tokenType,proto3" json:"token_type,omitempty"` ExpiresAtUnix int64 `protobuf:"varint,3,opt,name=expires_at_unix,json=expiresAtUnix,proto3" json:"expires_at_unix,omitempty"` AuthType string `protobuf:"bytes,4,opt,name=auth_type,json=authType,proto3" json:"auth_type,omitempty"` - AccessKey string `protobuf:"bytes,5,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"` + KeyId string `protobuf:"bytes,5,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *ExchangeAccessKeyTokenResponse) Reset() { - *x = ExchangeAccessKeyTokenResponse{} +func (x *ExchangeAPIKeyTokenResponse) Reset() { + *x = ExchangeAPIKeyTokenResponse{} mi := &file_proto_iam_v1_iam_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ExchangeAccessKeyTokenResponse) String() string { +func (x *ExchangeAPIKeyTokenResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ExchangeAccessKeyTokenResponse) ProtoMessage() {} +func (*ExchangeAPIKeyTokenResponse) ProtoMessage() {} -func (x *ExchangeAccessKeyTokenResponse) ProtoReflect() protoreflect.Message { +func (x *ExchangeAPIKeyTokenResponse) ProtoReflect() protoreflect.Message { mi := &file_proto_iam_v1_iam_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -655,42 +663,42 @@ func (x *ExchangeAccessKeyTokenResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ExchangeAccessKeyTokenResponse.ProtoReflect.Descriptor instead. -func (*ExchangeAccessKeyTokenResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ExchangeAPIKeyTokenResponse.ProtoReflect.Descriptor instead. +func (*ExchangeAPIKeyTokenResponse) Descriptor() ([]byte, []int) { return file_proto_iam_v1_iam_proto_rawDescGZIP(), []int{9} } -func (x *ExchangeAccessKeyTokenResponse) GetToken() string { +func (x *ExchangeAPIKeyTokenResponse) GetToken() string { if x != nil { return x.Token } return "" } -func (x *ExchangeAccessKeyTokenResponse) GetTokenType() string { +func (x *ExchangeAPIKeyTokenResponse) GetTokenType() string { if x != nil { return x.TokenType } return "" } -func (x *ExchangeAccessKeyTokenResponse) GetExpiresAtUnix() int64 { +func (x *ExchangeAPIKeyTokenResponse) GetExpiresAtUnix() int64 { if x != nil { return x.ExpiresAtUnix } return 0 } -func (x *ExchangeAccessKeyTokenResponse) GetAuthType() string { +func (x *ExchangeAPIKeyTokenResponse) GetAuthType() string { if x != nil { return x.AuthType } return "" } -func (x *ExchangeAccessKeyTokenResponse) GetAccessKey() string { +func (x *ExchangeAPIKeyTokenResponse) GetKeyId() string { if x != nil { - return x.AccessKey + return x.KeyId } return "" } @@ -1801,7 +1809,7 @@ const file_proto_iam_v1_iam_proto_rawDesc = "" + "\n" + "\x16proto/iam/v1/iam.proto\x12\x06iam.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\"*\n" + "\x12VerifyTokenRequest\x12\x14\n" + - "\x05token\x18\x01 \x01(\tR\x05token\"\xe5\x02\n" + + "\x05token\x18\x01 \x01(\tR\x05token\"\xfe\x02\n" + "\x13VerifyTokenResponse\x12\x14\n" + "\x05valid\x18\x01 \x01(\bR\x05valid\x12\x1d\n" + "\n" + @@ -1814,9 +1822,10 @@ const file_proto_iam_v1_iam_proto_rawDesc = "" + "\x05roles\x18\b \x03(\tR\x05roles\x12&\n" + "\x0fexpires_at_unix\x18\t \x01(\x03R\rexpiresAtUnix\x12\x1b\n" + "\tauth_type\x18\n" + - " \x01(\tR\bauthType\x12\"\n" + - "\raccess_key_id\x18\v \x01(\x03R\vaccessKeyId\x12\x17\n" + - "\atask_id\x18\f \x01(\tR\x06taskId\"\xfe\x01\n" + + " \x01(\tR\bauthType\x12\x15\n" + + "\x06key_id\x18\v \x01(\x03R\x05keyId\x12\x17\n" + + "\atask_id\x18\f \x01(\tR\x06taskId\x12$\n" + + "\x0eapi_key_scopes\x18\r \x03(\tR\fapiKeyScopes\"\xfe\x01\n" + "\x16CheckPermissionRequest\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x16\n" + "\x06action\x18\x02 \x01(\tR\x06action\x12\x14\n" + @@ -1840,23 +1849,21 @@ const file_proto_iam_v1_iam_proto_rawDesc = "" + "\n" + "project_id\x18\x02 \x01(\x03R\tprojectId\"$\n" + "\fBoolResponse\x12\x14\n" + - "\x05value\x18\x01 \x01(\bR\x05value\"\xbc\x01\n" + - "\x1dExchangeAccessKeyTokenRequest\x12\x1d\n" + - "\n" + - "access_key\x18\x01 \x01(\tR\taccessKey\x12\x1c\n" + + "\x05value\x18\x01 \x01(\bR\x05value\"\xb1\x01\n" + + "\x1aExchangeAPIKeyTokenRequest\x12\x15\n" + + "\x06key_id\x18\x01 \x01(\tR\x05keyId\x12\x1c\n" + "\ttimestamp\x18\x02 \x01(\tR\ttimestamp\x12\x14\n" + "\x05nonce\x18\x03 \x01(\tR\x05nonce\x12\x1c\n" + "\tsignature\x18\x04 \x01(\tR\tsignature\x12\x16\n" + "\x06method\x18\x05 \x01(\tR\x06method\x12\x12\n" + - "\x04path\x18\x06 \x01(\tR\x04path\"\xb9\x01\n" + - "\x1eExchangeAccessKeyTokenResponse\x12\x14\n" + + "\x04path\x18\x06 \x01(\tR\x04path\"\xae\x01\n" + + "\x1bExchangeAPIKeyTokenResponse\x12\x14\n" + "\x05token\x18\x01 \x01(\tR\x05token\x12\x1d\n" + "\n" + "token_type\x18\x02 \x01(\tR\ttokenType\x12&\n" + "\x0fexpires_at_unix\x18\x03 \x01(\x03R\rexpiresAtUnix\x12\x1b\n" + - "\tauth_type\x18\x04 \x01(\tR\bauthType\x12\x1d\n" + - "\n" + - "access_key\x18\x05 \x01(\tR\taccessKey\">\n" + + "\tauth_type\x18\x04 \x01(\tR\bauthType\x12\x15\n" + + "\x06key_id\x18\x05 \x01(\tR\x05keyId\">\n" + "\x0fMutationRequest\x12+\n" + "\x04body\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04body\"=\n" + "\fQueryRequest\x12-\n" + @@ -1921,7 +1928,7 @@ const file_proto_iam_v1_iam_proto_rawDesc = "" + "\ateam_id\x18\x01 \x01(\x03R\x06teamId\x12-\n" + "\x05query\x18\x02 \x01(\v2\x17.google.protobuf.StructR\x05query\"=\n" + "\x0eStructResponse\x12+\n" + - "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xa3 \n" + + "\x04data\x18\x01 \x01(\v2\x17.google.protobuf.StructR\x04data2\xca \n" + "\n" + "IAMService\x12F\n" + "\vVerifyToken\x12\x1a.iam.v1.VerifyTokenRequest\x1a\x1b.iam.v1.VerifyTokenResponse\x12R\n" + @@ -1932,20 +1939,21 @@ const file_proto_iam_v1_iam_proto_rawDesc = "" + "\x06Logout\x12\x15.iam.v1.LogoutRequest\x1a\x16.google.protobuf.Empty\x12A\n" + "\x0eChangePassword\x12\x17.iam.v1.UserBodyRequest\x1a\x16.google.protobuf.Empty\x12;\n" + "\n" + - "GetProfile\x12\x15.iam.v1.UserIDRequest\x1a\x16.iam.v1.StructResponse\x12B\n" + - "\x0fCreateAccessKey\x12\x17.iam.v1.UserBodyRequest\x1a\x16.iam.v1.StructResponse\x12B\n" + - "\x0eListAccessKeys\x12\x18.iam.v1.UserQueryRequest\x1a\x16.iam.v1.StructResponse\x12C\n" + - "\fGetAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12F\n" + - "\x0fDeleteAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12G\n" + - "\x10DisableAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12F\n" + - "\x0fEnableAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12F\n" + - "\x0fRotateAccessKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12@\n" + + "GetProfile\x12\x15.iam.v1.UserIDRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\fCreateAPIKey\x12\x17.iam.v1.UserBodyRequest\x1a\x16.iam.v1.StructResponse\x12?\n" + + "\vListAPIKeys\x12\x18.iam.v1.UserQueryRequest\x1a\x16.iam.v1.StructResponse\x12@\n" + + "\tGetAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12C\n" + + "\fDeleteAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12D\n" + + "\rDisableAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12C\n" + + "\fEnableAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12C\n" + + "\fRevokeAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.google.protobuf.Empty\x12C\n" + + "\fRotateAPIKey\x12\x1b.iam.v1.UserScopedIDRequest\x1a\x16.iam.v1.StructResponse\x12@\n" + "\x0fIsUserTeamAdmin\x12\x17.iam.v1.UserTeamRequest\x1a\x14.iam.v1.BoolResponse\x12=\n" + "\fIsUserInTeam\x12\x17.iam.v1.UserTeamRequest\x1a\x14.iam.v1.BoolResponse\x129\n" + "\fIsTeamPublic\x12\x13.iam.v1.TeamRequest\x1a\x14.iam.v1.BoolResponse\x12F\n" + "\x12IsUserProjectAdmin\x12\x1a.iam.v1.UserProjectRequest\x1a\x14.iam.v1.BoolResponse\x12C\n" + - "\x0fIsUserInProject\x12\x1a.iam.v1.UserProjectRequest\x1a\x14.iam.v1.BoolResponse\x12g\n" + - "\x16ExchangeAccessKeyToken\x12%.iam.v1.ExchangeAccessKeyTokenRequest\x1a&.iam.v1.ExchangeAccessKeyTokenResponse\x12=\n" + + "\x0fIsUserInProject\x12\x1a.iam.v1.UserProjectRequest\x1a\x14.iam.v1.BoolResponse\x12^\n" + + "\x13ExchangeAPIKeyToken\x12\".iam.v1.ExchangeAPIKeyTokenRequest\x1a#.iam.v1.ExchangeAPIKeyTokenResponse\x12=\n" + "\n" + "CreateUser\x12\x17.iam.v1.MutationRequest\x1a\x16.iam.v1.StructResponse\x127\n" + "\n" + @@ -2009,39 +2017,39 @@ func file_proto_iam_v1_iam_proto_rawDescGZIP() []byte { var file_proto_iam_v1_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 31) var file_proto_iam_v1_iam_proto_goTypes = []any{ - (*VerifyTokenRequest)(nil), // 0: iam.v1.VerifyTokenRequest - (*VerifyTokenResponse)(nil), // 1: iam.v1.VerifyTokenResponse - (*CheckPermissionRequest)(nil), // 2: iam.v1.CheckPermissionRequest - (*CheckPermissionResponse)(nil), // 3: iam.v1.CheckPermissionResponse - (*UserTeamRequest)(nil), // 4: iam.v1.UserTeamRequest - (*TeamRequest)(nil), // 5: iam.v1.TeamRequest - (*UserProjectRequest)(nil), // 6: iam.v1.UserProjectRequest - (*BoolResponse)(nil), // 7: iam.v1.BoolResponse - (*ExchangeAccessKeyTokenRequest)(nil), // 8: iam.v1.ExchangeAccessKeyTokenRequest - (*ExchangeAccessKeyTokenResponse)(nil), // 9: iam.v1.ExchangeAccessKeyTokenResponse - (*MutationRequest)(nil), // 10: iam.v1.MutationRequest - (*QueryRequest)(nil), // 11: iam.v1.QueryRequest - (*IDRequest)(nil), // 12: iam.v1.IDRequest - (*UpdateByIDRequest)(nil), // 13: iam.v1.UpdateByIDRequest - (*UserIDRequest)(nil), // 14: iam.v1.UserIDRequest - (*UserQueryRequest)(nil), // 15: iam.v1.UserQueryRequest - (*UserBodyRequest)(nil), // 16: iam.v1.UserBodyRequest - (*UserScopedIDRequest)(nil), // 17: iam.v1.UserScopedIDRequest - (*UserRoleBindingRequest)(nil), // 18: iam.v1.UserRoleBindingRequest - (*UserResourceBindingRequest)(nil), // 19: iam.v1.UserResourceBindingRequest - (*LogoutRequest)(nil), // 20: iam.v1.LogoutRequest - (*RolePermissionsRequest)(nil), // 21: iam.v1.RolePermissionsRequest - (*CreateTeamRequest)(nil), // 22: iam.v1.CreateTeamRequest - (*ListTeamsRequest)(nil), // 23: iam.v1.ListTeamsRequest - (*UpdateTeamRequest)(nil), // 24: iam.v1.UpdateTeamRequest - (*ListTeamProjectsRequest)(nil), // 25: iam.v1.ListTeamProjectsRequest - (*AddTeamMemberRequest)(nil), // 26: iam.v1.AddTeamMemberRequest - (*RemoveTeamMemberRequest)(nil), // 27: iam.v1.RemoveTeamMemberRequest - (*UpdateTeamMemberRoleRequest)(nil), // 28: iam.v1.UpdateTeamMemberRoleRequest - (*ListTeamMembersRequest)(nil), // 29: iam.v1.ListTeamMembersRequest - (*StructResponse)(nil), // 30: iam.v1.StructResponse - (*structpb.Struct)(nil), // 31: google.protobuf.Struct - (*emptypb.Empty)(nil), // 32: google.protobuf.Empty + (*VerifyTokenRequest)(nil), // 0: iam.v1.VerifyTokenRequest + (*VerifyTokenResponse)(nil), // 1: iam.v1.VerifyTokenResponse + (*CheckPermissionRequest)(nil), // 2: iam.v1.CheckPermissionRequest + (*CheckPermissionResponse)(nil), // 3: iam.v1.CheckPermissionResponse + (*UserTeamRequest)(nil), // 4: iam.v1.UserTeamRequest + (*TeamRequest)(nil), // 5: iam.v1.TeamRequest + (*UserProjectRequest)(nil), // 6: iam.v1.UserProjectRequest + (*BoolResponse)(nil), // 7: iam.v1.BoolResponse + (*ExchangeAPIKeyTokenRequest)(nil), // 8: iam.v1.ExchangeAPIKeyTokenRequest + (*ExchangeAPIKeyTokenResponse)(nil), // 9: iam.v1.ExchangeAPIKeyTokenResponse + (*MutationRequest)(nil), // 10: iam.v1.MutationRequest + (*QueryRequest)(nil), // 11: iam.v1.QueryRequest + (*IDRequest)(nil), // 12: iam.v1.IDRequest + (*UpdateByIDRequest)(nil), // 13: iam.v1.UpdateByIDRequest + (*UserIDRequest)(nil), // 14: iam.v1.UserIDRequest + (*UserQueryRequest)(nil), // 15: iam.v1.UserQueryRequest + (*UserBodyRequest)(nil), // 16: iam.v1.UserBodyRequest + (*UserScopedIDRequest)(nil), // 17: iam.v1.UserScopedIDRequest + (*UserRoleBindingRequest)(nil), // 18: iam.v1.UserRoleBindingRequest + (*UserResourceBindingRequest)(nil), // 19: iam.v1.UserResourceBindingRequest + (*LogoutRequest)(nil), // 20: iam.v1.LogoutRequest + (*RolePermissionsRequest)(nil), // 21: iam.v1.RolePermissionsRequest + (*CreateTeamRequest)(nil), // 22: iam.v1.CreateTeamRequest + (*ListTeamsRequest)(nil), // 23: iam.v1.ListTeamsRequest + (*UpdateTeamRequest)(nil), // 24: iam.v1.UpdateTeamRequest + (*ListTeamProjectsRequest)(nil), // 25: iam.v1.ListTeamProjectsRequest + (*AddTeamMemberRequest)(nil), // 26: iam.v1.AddTeamMemberRequest + (*RemoveTeamMemberRequest)(nil), // 27: iam.v1.RemoveTeamMemberRequest + (*UpdateTeamMemberRoleRequest)(nil), // 28: iam.v1.UpdateTeamMemberRoleRequest + (*ListTeamMembersRequest)(nil), // 29: iam.v1.ListTeamMembersRequest + (*StructResponse)(nil), // 30: iam.v1.StructResponse + (*structpb.Struct)(nil), // 31: google.protobuf.Struct + (*emptypb.Empty)(nil), // 32: google.protobuf.Empty } var file_proto_iam_v1_iam_proto_depIdxs = []int32{ 31, // 0: iam.v1.MutationRequest.body:type_name -> google.protobuf.Struct @@ -2065,120 +2073,122 @@ var file_proto_iam_v1_iam_proto_depIdxs = []int32{ 20, // 18: iam.v1.IAMService.Logout:input_type -> iam.v1.LogoutRequest 16, // 19: iam.v1.IAMService.ChangePassword:input_type -> iam.v1.UserBodyRequest 14, // 20: iam.v1.IAMService.GetProfile:input_type -> iam.v1.UserIDRequest - 16, // 21: iam.v1.IAMService.CreateAccessKey:input_type -> iam.v1.UserBodyRequest - 15, // 22: iam.v1.IAMService.ListAccessKeys:input_type -> iam.v1.UserQueryRequest - 17, // 23: iam.v1.IAMService.GetAccessKey:input_type -> iam.v1.UserScopedIDRequest - 17, // 24: iam.v1.IAMService.DeleteAccessKey:input_type -> iam.v1.UserScopedIDRequest - 17, // 25: iam.v1.IAMService.DisableAccessKey:input_type -> iam.v1.UserScopedIDRequest - 17, // 26: iam.v1.IAMService.EnableAccessKey:input_type -> iam.v1.UserScopedIDRequest - 17, // 27: iam.v1.IAMService.RotateAccessKey:input_type -> iam.v1.UserScopedIDRequest - 4, // 28: iam.v1.IAMService.IsUserTeamAdmin:input_type -> iam.v1.UserTeamRequest - 4, // 29: iam.v1.IAMService.IsUserInTeam:input_type -> iam.v1.UserTeamRequest - 5, // 30: iam.v1.IAMService.IsTeamPublic:input_type -> iam.v1.TeamRequest - 6, // 31: iam.v1.IAMService.IsUserProjectAdmin:input_type -> iam.v1.UserProjectRequest - 6, // 32: iam.v1.IAMService.IsUserInProject:input_type -> iam.v1.UserProjectRequest - 8, // 33: iam.v1.IAMService.ExchangeAccessKeyToken:input_type -> iam.v1.ExchangeAccessKeyTokenRequest - 10, // 34: iam.v1.IAMService.CreateUser:input_type -> iam.v1.MutationRequest - 12, // 35: iam.v1.IAMService.DeleteUser:input_type -> iam.v1.IDRequest - 12, // 36: iam.v1.IAMService.GetUser:input_type -> iam.v1.IDRequest - 11, // 37: iam.v1.IAMService.ListUsers:input_type -> iam.v1.QueryRequest - 13, // 38: iam.v1.IAMService.UpdateUser:input_type -> iam.v1.UpdateByIDRequest - 18, // 39: iam.v1.IAMService.AssignUserRole:input_type -> iam.v1.UserRoleBindingRequest - 18, // 40: iam.v1.IAMService.RemoveUserRole:input_type -> iam.v1.UserRoleBindingRequest - 16, // 41: iam.v1.IAMService.AssignUserPermissions:input_type -> iam.v1.UserBodyRequest - 16, // 42: iam.v1.IAMService.RemoveUserPermissions:input_type -> iam.v1.UserBodyRequest - 19, // 43: iam.v1.IAMService.AssignUserContainer:input_type -> iam.v1.UserResourceBindingRequest - 17, // 44: iam.v1.IAMService.RemoveUserContainer:input_type -> iam.v1.UserScopedIDRequest - 19, // 45: iam.v1.IAMService.AssignUserDataset:input_type -> iam.v1.UserResourceBindingRequest - 17, // 46: iam.v1.IAMService.RemoveUserDataset:input_type -> iam.v1.UserScopedIDRequest - 19, // 47: iam.v1.IAMService.AssignUserProject:input_type -> iam.v1.UserResourceBindingRequest - 17, // 48: iam.v1.IAMService.RemoveUserProject:input_type -> iam.v1.UserScopedIDRequest - 10, // 49: iam.v1.IAMService.CreateRole:input_type -> iam.v1.MutationRequest - 12, // 50: iam.v1.IAMService.DeleteRole:input_type -> iam.v1.IDRequest - 12, // 51: iam.v1.IAMService.GetRole:input_type -> iam.v1.IDRequest - 11, // 52: iam.v1.IAMService.ListRoles:input_type -> iam.v1.QueryRequest - 13, // 53: iam.v1.IAMService.UpdateRole:input_type -> iam.v1.UpdateByIDRequest - 21, // 54: iam.v1.IAMService.AssignRolePermissions:input_type -> iam.v1.RolePermissionsRequest - 21, // 55: iam.v1.IAMService.RemoveRolePermissions:input_type -> iam.v1.RolePermissionsRequest - 12, // 56: iam.v1.IAMService.ListUsersFromRole:input_type -> iam.v1.IDRequest - 12, // 57: iam.v1.IAMService.GetPermission:input_type -> iam.v1.IDRequest - 11, // 58: iam.v1.IAMService.ListPermissions:input_type -> iam.v1.QueryRequest - 12, // 59: iam.v1.IAMService.ListRolesFromPermission:input_type -> iam.v1.IDRequest - 12, // 60: iam.v1.IAMService.GetResource:input_type -> iam.v1.IDRequest - 11, // 61: iam.v1.IAMService.ListResources:input_type -> iam.v1.QueryRequest - 12, // 62: iam.v1.IAMService.ListResourcePermissions:input_type -> iam.v1.IDRequest - 22, // 63: iam.v1.IAMService.CreateTeam:input_type -> iam.v1.CreateTeamRequest - 5, // 64: iam.v1.IAMService.DeleteTeam:input_type -> iam.v1.TeamRequest - 5, // 65: iam.v1.IAMService.GetTeam:input_type -> iam.v1.TeamRequest - 23, // 66: iam.v1.IAMService.ListTeams:input_type -> iam.v1.ListTeamsRequest - 24, // 67: iam.v1.IAMService.UpdateTeam:input_type -> iam.v1.UpdateTeamRequest - 25, // 68: iam.v1.IAMService.ListTeamProjects:input_type -> iam.v1.ListTeamProjectsRequest - 26, // 69: iam.v1.IAMService.AddTeamMember:input_type -> iam.v1.AddTeamMemberRequest - 27, // 70: iam.v1.IAMService.RemoveTeamMember:input_type -> iam.v1.RemoveTeamMemberRequest - 28, // 71: iam.v1.IAMService.UpdateTeamMemberRole:input_type -> iam.v1.UpdateTeamMemberRoleRequest - 29, // 72: iam.v1.IAMService.ListTeamMembers:input_type -> iam.v1.ListTeamMembersRequest - 1, // 73: iam.v1.IAMService.VerifyToken:output_type -> iam.v1.VerifyTokenResponse - 3, // 74: iam.v1.IAMService.CheckPermission:output_type -> iam.v1.CheckPermissionResponse - 30, // 75: iam.v1.IAMService.Login:output_type -> iam.v1.StructResponse - 30, // 76: iam.v1.IAMService.Register:output_type -> iam.v1.StructResponse - 30, // 77: iam.v1.IAMService.RefreshToken:output_type -> iam.v1.StructResponse - 32, // 78: iam.v1.IAMService.Logout:output_type -> google.protobuf.Empty - 32, // 79: iam.v1.IAMService.ChangePassword:output_type -> google.protobuf.Empty - 30, // 80: iam.v1.IAMService.GetProfile:output_type -> iam.v1.StructResponse - 30, // 81: iam.v1.IAMService.CreateAccessKey:output_type -> iam.v1.StructResponse - 30, // 82: iam.v1.IAMService.ListAccessKeys:output_type -> iam.v1.StructResponse - 30, // 83: iam.v1.IAMService.GetAccessKey:output_type -> iam.v1.StructResponse - 32, // 84: iam.v1.IAMService.DeleteAccessKey:output_type -> google.protobuf.Empty - 32, // 85: iam.v1.IAMService.DisableAccessKey:output_type -> google.protobuf.Empty - 32, // 86: iam.v1.IAMService.EnableAccessKey:output_type -> google.protobuf.Empty - 30, // 87: iam.v1.IAMService.RotateAccessKey:output_type -> iam.v1.StructResponse - 7, // 88: iam.v1.IAMService.IsUserTeamAdmin:output_type -> iam.v1.BoolResponse - 7, // 89: iam.v1.IAMService.IsUserInTeam:output_type -> iam.v1.BoolResponse - 7, // 90: iam.v1.IAMService.IsTeamPublic:output_type -> iam.v1.BoolResponse - 7, // 91: iam.v1.IAMService.IsUserProjectAdmin:output_type -> iam.v1.BoolResponse - 7, // 92: iam.v1.IAMService.IsUserInProject:output_type -> iam.v1.BoolResponse - 9, // 93: iam.v1.IAMService.ExchangeAccessKeyToken:output_type -> iam.v1.ExchangeAccessKeyTokenResponse - 30, // 94: iam.v1.IAMService.CreateUser:output_type -> iam.v1.StructResponse - 32, // 95: iam.v1.IAMService.DeleteUser:output_type -> google.protobuf.Empty - 30, // 96: iam.v1.IAMService.GetUser:output_type -> iam.v1.StructResponse - 30, // 97: iam.v1.IAMService.ListUsers:output_type -> iam.v1.StructResponse - 30, // 98: iam.v1.IAMService.UpdateUser:output_type -> iam.v1.StructResponse - 32, // 99: iam.v1.IAMService.AssignUserRole:output_type -> google.protobuf.Empty - 32, // 100: iam.v1.IAMService.RemoveUserRole:output_type -> google.protobuf.Empty - 32, // 101: iam.v1.IAMService.AssignUserPermissions:output_type -> google.protobuf.Empty - 32, // 102: iam.v1.IAMService.RemoveUserPermissions:output_type -> google.protobuf.Empty - 32, // 103: iam.v1.IAMService.AssignUserContainer:output_type -> google.protobuf.Empty - 32, // 104: iam.v1.IAMService.RemoveUserContainer:output_type -> google.protobuf.Empty - 32, // 105: iam.v1.IAMService.AssignUserDataset:output_type -> google.protobuf.Empty - 32, // 106: iam.v1.IAMService.RemoveUserDataset:output_type -> google.protobuf.Empty - 32, // 107: iam.v1.IAMService.AssignUserProject:output_type -> google.protobuf.Empty - 32, // 108: iam.v1.IAMService.RemoveUserProject:output_type -> google.protobuf.Empty - 30, // 109: iam.v1.IAMService.CreateRole:output_type -> iam.v1.StructResponse - 32, // 110: iam.v1.IAMService.DeleteRole:output_type -> google.protobuf.Empty - 30, // 111: iam.v1.IAMService.GetRole:output_type -> iam.v1.StructResponse - 30, // 112: iam.v1.IAMService.ListRoles:output_type -> iam.v1.StructResponse - 30, // 113: iam.v1.IAMService.UpdateRole:output_type -> iam.v1.StructResponse - 32, // 114: iam.v1.IAMService.AssignRolePermissions:output_type -> google.protobuf.Empty - 32, // 115: iam.v1.IAMService.RemoveRolePermissions:output_type -> google.protobuf.Empty - 30, // 116: iam.v1.IAMService.ListUsersFromRole:output_type -> iam.v1.StructResponse - 30, // 117: iam.v1.IAMService.GetPermission:output_type -> iam.v1.StructResponse - 30, // 118: iam.v1.IAMService.ListPermissions:output_type -> iam.v1.StructResponse - 30, // 119: iam.v1.IAMService.ListRolesFromPermission:output_type -> iam.v1.StructResponse - 30, // 120: iam.v1.IAMService.GetResource:output_type -> iam.v1.StructResponse - 30, // 121: iam.v1.IAMService.ListResources:output_type -> iam.v1.StructResponse - 30, // 122: iam.v1.IAMService.ListResourcePermissions:output_type -> iam.v1.StructResponse - 30, // 123: iam.v1.IAMService.CreateTeam:output_type -> iam.v1.StructResponse - 32, // 124: iam.v1.IAMService.DeleteTeam:output_type -> google.protobuf.Empty - 30, // 125: iam.v1.IAMService.GetTeam:output_type -> iam.v1.StructResponse - 30, // 126: iam.v1.IAMService.ListTeams:output_type -> iam.v1.StructResponse - 30, // 127: iam.v1.IAMService.UpdateTeam:output_type -> iam.v1.StructResponse - 30, // 128: iam.v1.IAMService.ListTeamProjects:output_type -> iam.v1.StructResponse - 32, // 129: iam.v1.IAMService.AddTeamMember:output_type -> google.protobuf.Empty - 32, // 130: iam.v1.IAMService.RemoveTeamMember:output_type -> google.protobuf.Empty - 32, // 131: iam.v1.IAMService.UpdateTeamMemberRole:output_type -> google.protobuf.Empty - 30, // 132: iam.v1.IAMService.ListTeamMembers:output_type -> iam.v1.StructResponse - 73, // [73:133] is the sub-list for method output_type - 13, // [13:73] is the sub-list for method input_type + 16, // 21: iam.v1.IAMService.CreateAPIKey:input_type -> iam.v1.UserBodyRequest + 15, // 22: iam.v1.IAMService.ListAPIKeys:input_type -> iam.v1.UserQueryRequest + 17, // 23: iam.v1.IAMService.GetAPIKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 24: iam.v1.IAMService.DeleteAPIKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 25: iam.v1.IAMService.DisableAPIKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 26: iam.v1.IAMService.EnableAPIKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 27: iam.v1.IAMService.RevokeAPIKey:input_type -> iam.v1.UserScopedIDRequest + 17, // 28: iam.v1.IAMService.RotateAPIKey:input_type -> iam.v1.UserScopedIDRequest + 4, // 29: iam.v1.IAMService.IsUserTeamAdmin:input_type -> iam.v1.UserTeamRequest + 4, // 30: iam.v1.IAMService.IsUserInTeam:input_type -> iam.v1.UserTeamRequest + 5, // 31: iam.v1.IAMService.IsTeamPublic:input_type -> iam.v1.TeamRequest + 6, // 32: iam.v1.IAMService.IsUserProjectAdmin:input_type -> iam.v1.UserProjectRequest + 6, // 33: iam.v1.IAMService.IsUserInProject:input_type -> iam.v1.UserProjectRequest + 8, // 34: iam.v1.IAMService.ExchangeAPIKeyToken:input_type -> iam.v1.ExchangeAPIKeyTokenRequest + 10, // 35: iam.v1.IAMService.CreateUser:input_type -> iam.v1.MutationRequest + 12, // 36: iam.v1.IAMService.DeleteUser:input_type -> iam.v1.IDRequest + 12, // 37: iam.v1.IAMService.GetUser:input_type -> iam.v1.IDRequest + 11, // 38: iam.v1.IAMService.ListUsers:input_type -> iam.v1.QueryRequest + 13, // 39: iam.v1.IAMService.UpdateUser:input_type -> iam.v1.UpdateByIDRequest + 18, // 40: iam.v1.IAMService.AssignUserRole:input_type -> iam.v1.UserRoleBindingRequest + 18, // 41: iam.v1.IAMService.RemoveUserRole:input_type -> iam.v1.UserRoleBindingRequest + 16, // 42: iam.v1.IAMService.AssignUserPermissions:input_type -> iam.v1.UserBodyRequest + 16, // 43: iam.v1.IAMService.RemoveUserPermissions:input_type -> iam.v1.UserBodyRequest + 19, // 44: iam.v1.IAMService.AssignUserContainer:input_type -> iam.v1.UserResourceBindingRequest + 17, // 45: iam.v1.IAMService.RemoveUserContainer:input_type -> iam.v1.UserScopedIDRequest + 19, // 46: iam.v1.IAMService.AssignUserDataset:input_type -> iam.v1.UserResourceBindingRequest + 17, // 47: iam.v1.IAMService.RemoveUserDataset:input_type -> iam.v1.UserScopedIDRequest + 19, // 48: iam.v1.IAMService.AssignUserProject:input_type -> iam.v1.UserResourceBindingRequest + 17, // 49: iam.v1.IAMService.RemoveUserProject:input_type -> iam.v1.UserScopedIDRequest + 10, // 50: iam.v1.IAMService.CreateRole:input_type -> iam.v1.MutationRequest + 12, // 51: iam.v1.IAMService.DeleteRole:input_type -> iam.v1.IDRequest + 12, // 52: iam.v1.IAMService.GetRole:input_type -> iam.v1.IDRequest + 11, // 53: iam.v1.IAMService.ListRoles:input_type -> iam.v1.QueryRequest + 13, // 54: iam.v1.IAMService.UpdateRole:input_type -> iam.v1.UpdateByIDRequest + 21, // 55: iam.v1.IAMService.AssignRolePermissions:input_type -> iam.v1.RolePermissionsRequest + 21, // 56: iam.v1.IAMService.RemoveRolePermissions:input_type -> iam.v1.RolePermissionsRequest + 12, // 57: iam.v1.IAMService.ListUsersFromRole:input_type -> iam.v1.IDRequest + 12, // 58: iam.v1.IAMService.GetPermission:input_type -> iam.v1.IDRequest + 11, // 59: iam.v1.IAMService.ListPermissions:input_type -> iam.v1.QueryRequest + 12, // 60: iam.v1.IAMService.ListRolesFromPermission:input_type -> iam.v1.IDRequest + 12, // 61: iam.v1.IAMService.GetResource:input_type -> iam.v1.IDRequest + 11, // 62: iam.v1.IAMService.ListResources:input_type -> iam.v1.QueryRequest + 12, // 63: iam.v1.IAMService.ListResourcePermissions:input_type -> iam.v1.IDRequest + 22, // 64: iam.v1.IAMService.CreateTeam:input_type -> iam.v1.CreateTeamRequest + 5, // 65: iam.v1.IAMService.DeleteTeam:input_type -> iam.v1.TeamRequest + 5, // 66: iam.v1.IAMService.GetTeam:input_type -> iam.v1.TeamRequest + 23, // 67: iam.v1.IAMService.ListTeams:input_type -> iam.v1.ListTeamsRequest + 24, // 68: iam.v1.IAMService.UpdateTeam:input_type -> iam.v1.UpdateTeamRequest + 25, // 69: iam.v1.IAMService.ListTeamProjects:input_type -> iam.v1.ListTeamProjectsRequest + 26, // 70: iam.v1.IAMService.AddTeamMember:input_type -> iam.v1.AddTeamMemberRequest + 27, // 71: iam.v1.IAMService.RemoveTeamMember:input_type -> iam.v1.RemoveTeamMemberRequest + 28, // 72: iam.v1.IAMService.UpdateTeamMemberRole:input_type -> iam.v1.UpdateTeamMemberRoleRequest + 29, // 73: iam.v1.IAMService.ListTeamMembers:input_type -> iam.v1.ListTeamMembersRequest + 1, // 74: iam.v1.IAMService.VerifyToken:output_type -> iam.v1.VerifyTokenResponse + 3, // 75: iam.v1.IAMService.CheckPermission:output_type -> iam.v1.CheckPermissionResponse + 30, // 76: iam.v1.IAMService.Login:output_type -> iam.v1.StructResponse + 30, // 77: iam.v1.IAMService.Register:output_type -> iam.v1.StructResponse + 30, // 78: iam.v1.IAMService.RefreshToken:output_type -> iam.v1.StructResponse + 32, // 79: iam.v1.IAMService.Logout:output_type -> google.protobuf.Empty + 32, // 80: iam.v1.IAMService.ChangePassword:output_type -> google.protobuf.Empty + 30, // 81: iam.v1.IAMService.GetProfile:output_type -> iam.v1.StructResponse + 30, // 82: iam.v1.IAMService.CreateAPIKey:output_type -> iam.v1.StructResponse + 30, // 83: iam.v1.IAMService.ListAPIKeys:output_type -> iam.v1.StructResponse + 30, // 84: iam.v1.IAMService.GetAPIKey:output_type -> iam.v1.StructResponse + 32, // 85: iam.v1.IAMService.DeleteAPIKey:output_type -> google.protobuf.Empty + 32, // 86: iam.v1.IAMService.DisableAPIKey:output_type -> google.protobuf.Empty + 32, // 87: iam.v1.IAMService.EnableAPIKey:output_type -> google.protobuf.Empty + 32, // 88: iam.v1.IAMService.RevokeAPIKey:output_type -> google.protobuf.Empty + 30, // 89: iam.v1.IAMService.RotateAPIKey:output_type -> iam.v1.StructResponse + 7, // 90: iam.v1.IAMService.IsUserTeamAdmin:output_type -> iam.v1.BoolResponse + 7, // 91: iam.v1.IAMService.IsUserInTeam:output_type -> iam.v1.BoolResponse + 7, // 92: iam.v1.IAMService.IsTeamPublic:output_type -> iam.v1.BoolResponse + 7, // 93: iam.v1.IAMService.IsUserProjectAdmin:output_type -> iam.v1.BoolResponse + 7, // 94: iam.v1.IAMService.IsUserInProject:output_type -> iam.v1.BoolResponse + 9, // 95: iam.v1.IAMService.ExchangeAPIKeyToken:output_type -> iam.v1.ExchangeAPIKeyTokenResponse + 30, // 96: iam.v1.IAMService.CreateUser:output_type -> iam.v1.StructResponse + 32, // 97: iam.v1.IAMService.DeleteUser:output_type -> google.protobuf.Empty + 30, // 98: iam.v1.IAMService.GetUser:output_type -> iam.v1.StructResponse + 30, // 99: iam.v1.IAMService.ListUsers:output_type -> iam.v1.StructResponse + 30, // 100: iam.v1.IAMService.UpdateUser:output_type -> iam.v1.StructResponse + 32, // 101: iam.v1.IAMService.AssignUserRole:output_type -> google.protobuf.Empty + 32, // 102: iam.v1.IAMService.RemoveUserRole:output_type -> google.protobuf.Empty + 32, // 103: iam.v1.IAMService.AssignUserPermissions:output_type -> google.protobuf.Empty + 32, // 104: iam.v1.IAMService.RemoveUserPermissions:output_type -> google.protobuf.Empty + 32, // 105: iam.v1.IAMService.AssignUserContainer:output_type -> google.protobuf.Empty + 32, // 106: iam.v1.IAMService.RemoveUserContainer:output_type -> google.protobuf.Empty + 32, // 107: iam.v1.IAMService.AssignUserDataset:output_type -> google.protobuf.Empty + 32, // 108: iam.v1.IAMService.RemoveUserDataset:output_type -> google.protobuf.Empty + 32, // 109: iam.v1.IAMService.AssignUserProject:output_type -> google.protobuf.Empty + 32, // 110: iam.v1.IAMService.RemoveUserProject:output_type -> google.protobuf.Empty + 30, // 111: iam.v1.IAMService.CreateRole:output_type -> iam.v1.StructResponse + 32, // 112: iam.v1.IAMService.DeleteRole:output_type -> google.protobuf.Empty + 30, // 113: iam.v1.IAMService.GetRole:output_type -> iam.v1.StructResponse + 30, // 114: iam.v1.IAMService.ListRoles:output_type -> iam.v1.StructResponse + 30, // 115: iam.v1.IAMService.UpdateRole:output_type -> iam.v1.StructResponse + 32, // 116: iam.v1.IAMService.AssignRolePermissions:output_type -> google.protobuf.Empty + 32, // 117: iam.v1.IAMService.RemoveRolePermissions:output_type -> google.protobuf.Empty + 30, // 118: iam.v1.IAMService.ListUsersFromRole:output_type -> iam.v1.StructResponse + 30, // 119: iam.v1.IAMService.GetPermission:output_type -> iam.v1.StructResponse + 30, // 120: iam.v1.IAMService.ListPermissions:output_type -> iam.v1.StructResponse + 30, // 121: iam.v1.IAMService.ListRolesFromPermission:output_type -> iam.v1.StructResponse + 30, // 122: iam.v1.IAMService.GetResource:output_type -> iam.v1.StructResponse + 30, // 123: iam.v1.IAMService.ListResources:output_type -> iam.v1.StructResponse + 30, // 124: iam.v1.IAMService.ListResourcePermissions:output_type -> iam.v1.StructResponse + 30, // 125: iam.v1.IAMService.CreateTeam:output_type -> iam.v1.StructResponse + 32, // 126: iam.v1.IAMService.DeleteTeam:output_type -> google.protobuf.Empty + 30, // 127: iam.v1.IAMService.GetTeam:output_type -> iam.v1.StructResponse + 30, // 128: iam.v1.IAMService.ListTeams:output_type -> iam.v1.StructResponse + 30, // 129: iam.v1.IAMService.UpdateTeam:output_type -> iam.v1.StructResponse + 30, // 130: iam.v1.IAMService.ListTeamProjects:output_type -> iam.v1.StructResponse + 32, // 131: iam.v1.IAMService.AddTeamMember:output_type -> google.protobuf.Empty + 32, // 132: iam.v1.IAMService.RemoveTeamMember:output_type -> google.protobuf.Empty + 32, // 133: iam.v1.IAMService.UpdateTeamMemberRole:output_type -> google.protobuf.Empty + 30, // 134: iam.v1.IAMService.ListTeamMembers:output_type -> iam.v1.StructResponse + 74, // [74:135] is the sub-list for method output_type + 13, // [13:74] is the sub-list for method input_type 13, // [13:13] is the sub-list for extension type_name 13, // [13:13] is the sub-list for extension extendee 0, // [0:13] is the sub-list for field type_name diff --git a/src/proto/iam/v1/iam.proto b/src/proto/iam/v1/iam.proto index db4053b8..ae5188ad 100644 --- a/src/proto/iam/v1/iam.proto +++ b/src/proto/iam/v1/iam.proto @@ -16,19 +16,20 @@ service IAMService { rpc Logout(LogoutRequest) returns (google.protobuf.Empty); rpc ChangePassword(UserBodyRequest) returns (google.protobuf.Empty); rpc GetProfile(UserIDRequest) returns (StructResponse); - rpc CreateAccessKey(UserBodyRequest) returns (StructResponse); - rpc ListAccessKeys(UserQueryRequest) returns (StructResponse); - rpc GetAccessKey(UserScopedIDRequest) returns (StructResponse); - rpc DeleteAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); - rpc DisableAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); - rpc EnableAccessKey(UserScopedIDRequest) returns (google.protobuf.Empty); - rpc RotateAccessKey(UserScopedIDRequest) returns (StructResponse); + rpc CreateAPIKey(UserBodyRequest) returns (StructResponse); + rpc ListAPIKeys(UserQueryRequest) returns (StructResponse); + rpc GetAPIKey(UserScopedIDRequest) returns (StructResponse); + rpc DeleteAPIKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc DisableAPIKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc EnableAPIKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc RevokeAPIKey(UserScopedIDRequest) returns (google.protobuf.Empty); + rpc RotateAPIKey(UserScopedIDRequest) returns (StructResponse); rpc IsUserTeamAdmin(UserTeamRequest) returns (BoolResponse); rpc IsUserInTeam(UserTeamRequest) returns (BoolResponse); rpc IsTeamPublic(TeamRequest) returns (BoolResponse); rpc IsUserProjectAdmin(UserProjectRequest) returns (BoolResponse); rpc IsUserInProject(UserProjectRequest) returns (BoolResponse); - rpc ExchangeAccessKeyToken(ExchangeAccessKeyTokenRequest) returns (ExchangeAccessKeyTokenResponse); + rpc ExchangeAPIKeyToken(ExchangeAPIKeyTokenRequest) returns (ExchangeAPIKeyTokenResponse); rpc CreateUser(MutationRequest) returns (StructResponse); rpc DeleteUser(IDRequest) returns (google.protobuf.Empty); rpc GetUser(IDRequest) returns (StructResponse); @@ -85,8 +86,9 @@ message VerifyTokenResponse { repeated string roles = 8; int64 expires_at_unix = 9; string auth_type = 10; - int64 access_key_id = 11; + int64 key_id = 11; string task_id = 12; + repeated string api_key_scopes = 13; } message CheckPermissionRequest { @@ -122,8 +124,8 @@ message BoolResponse { bool value = 1; } -message ExchangeAccessKeyTokenRequest { - string access_key = 1; +message ExchangeAPIKeyTokenRequest { + string key_id = 1; string timestamp = 2; string nonce = 3; string signature = 4; @@ -131,12 +133,12 @@ message ExchangeAccessKeyTokenRequest { string path = 6; } -message ExchangeAccessKeyTokenResponse { +message ExchangeAPIKeyTokenResponse { string token = 1; string token_type = 2; int64 expires_at_unix = 3; string auth_type = 4; - string access_key = 5; + string key_id = 5; } message MutationRequest { diff --git a/src/proto/iam/v1/iam_grpc.pb.go b/src/proto/iam/v1/iam_grpc.pb.go index eb326758..23ee8c85 100644 --- a/src/proto/iam/v1/iam_grpc.pb.go +++ b/src/proto/iam/v1/iam_grpc.pb.go @@ -28,19 +28,20 @@ const ( IAMService_Logout_FullMethodName = "/iam.v1.IAMService/Logout" IAMService_ChangePassword_FullMethodName = "/iam.v1.IAMService/ChangePassword" IAMService_GetProfile_FullMethodName = "/iam.v1.IAMService/GetProfile" - IAMService_CreateAccessKey_FullMethodName = "/iam.v1.IAMService/CreateAccessKey" - IAMService_ListAccessKeys_FullMethodName = "/iam.v1.IAMService/ListAccessKeys" - IAMService_GetAccessKey_FullMethodName = "/iam.v1.IAMService/GetAccessKey" - IAMService_DeleteAccessKey_FullMethodName = "/iam.v1.IAMService/DeleteAccessKey" - IAMService_DisableAccessKey_FullMethodName = "/iam.v1.IAMService/DisableAccessKey" - IAMService_EnableAccessKey_FullMethodName = "/iam.v1.IAMService/EnableAccessKey" - IAMService_RotateAccessKey_FullMethodName = "/iam.v1.IAMService/RotateAccessKey" + IAMService_CreateAPIKey_FullMethodName = "/iam.v1.IAMService/CreateAPIKey" + IAMService_ListAPIKeys_FullMethodName = "/iam.v1.IAMService/ListAPIKeys" + IAMService_GetAPIKey_FullMethodName = "/iam.v1.IAMService/GetAPIKey" + IAMService_DeleteAPIKey_FullMethodName = "/iam.v1.IAMService/DeleteAPIKey" + IAMService_DisableAPIKey_FullMethodName = "/iam.v1.IAMService/DisableAPIKey" + IAMService_EnableAPIKey_FullMethodName = "/iam.v1.IAMService/EnableAPIKey" + IAMService_RevokeAPIKey_FullMethodName = "/iam.v1.IAMService/RevokeAPIKey" + IAMService_RotateAPIKey_FullMethodName = "/iam.v1.IAMService/RotateAPIKey" IAMService_IsUserTeamAdmin_FullMethodName = "/iam.v1.IAMService/IsUserTeamAdmin" IAMService_IsUserInTeam_FullMethodName = "/iam.v1.IAMService/IsUserInTeam" IAMService_IsTeamPublic_FullMethodName = "/iam.v1.IAMService/IsTeamPublic" IAMService_IsUserProjectAdmin_FullMethodName = "/iam.v1.IAMService/IsUserProjectAdmin" IAMService_IsUserInProject_FullMethodName = "/iam.v1.IAMService/IsUserInProject" - IAMService_ExchangeAccessKeyToken_FullMethodName = "/iam.v1.IAMService/ExchangeAccessKeyToken" + IAMService_ExchangeAPIKeyToken_FullMethodName = "/iam.v1.IAMService/ExchangeAPIKeyToken" IAMService_CreateUser_FullMethodName = "/iam.v1.IAMService/CreateUser" IAMService_DeleteUser_FullMethodName = "/iam.v1.IAMService/DeleteUser" IAMService_GetUser_FullMethodName = "/iam.v1.IAMService/GetUser" @@ -94,19 +95,20 @@ type IAMServiceClient interface { Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ChangePassword(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetProfile(ctx context.Context, in *UserIDRequest, opts ...grpc.CallOption) (*StructResponse, error) - CreateAccessKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) - ListAccessKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) - GetAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) - DeleteAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - DisableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - EnableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) - RotateAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + CreateAPIKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) + ListAPIKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) + GetAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) + DeleteAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + DisableAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + EnableAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RevokeAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + RotateAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) IsUserTeamAdmin(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) IsUserInTeam(ctx context.Context, in *UserTeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) IsTeamPublic(ctx context.Context, in *TeamRequest, opts ...grpc.CallOption) (*BoolResponse, error) IsUserProjectAdmin(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) IsUserInProject(ctx context.Context, in *UserProjectRequest, opts ...grpc.CallOption) (*BoolResponse, error) - ExchangeAccessKeyToken(ctx context.Context, in *ExchangeAccessKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAccessKeyTokenResponse, error) + ExchangeAPIKeyToken(ctx context.Context, in *ExchangeAPIKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAPIKeyTokenResponse, error) CreateUser(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*StructResponse, error) DeleteUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetUser(ctx context.Context, in *IDRequest, opts ...grpc.CallOption) (*StructResponse, error) @@ -236,70 +238,80 @@ func (c *iAMServiceClient) GetProfile(ctx context.Context, in *UserIDRequest, op return out, nil } -func (c *iAMServiceClient) CreateAccessKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) { +func (c *iAMServiceClient) CreateAPIKey(ctx context.Context, in *UserBodyRequest, opts ...grpc.CallOption) (*StructResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StructResponse) - err := c.cc.Invoke(ctx, IAMService_CreateAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_CreateAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) ListAccessKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { +func (c *iAMServiceClient) ListAPIKeys(ctx context.Context, in *UserQueryRequest, opts ...grpc.CallOption) (*StructResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StructResponse) - err := c.cc.Invoke(ctx, IAMService_ListAccessKeys_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_ListAPIKeys_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) GetAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { +func (c *iAMServiceClient) GetAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StructResponse) - err := c.cc.Invoke(ctx, IAMService_GetAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_GetAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) DeleteAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *iAMServiceClient) DeleteAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, IAMService_DeleteAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_DeleteAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) DisableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *iAMServiceClient) DisableAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, IAMService_DisableAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_DisableAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) EnableAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +func (c *iAMServiceClient) EnableAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(emptypb.Empty) - err := c.cc.Invoke(ctx, IAMService_EnableAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_EnableAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *iAMServiceClient) RotateAccessKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { +func (c *iAMServiceClient) RevokeAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IAMService_RevokeAPIKey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *iAMServiceClient) RotateAPIKey(ctx context.Context, in *UserScopedIDRequest, opts ...grpc.CallOption) (*StructResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(StructResponse) - err := c.cc.Invoke(ctx, IAMService_RotateAccessKey_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, IAMService_RotateAPIKey_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -356,10 +368,10 @@ func (c *iAMServiceClient) IsUserInProject(ctx context.Context, in *UserProjectR return out, nil } -func (c *iAMServiceClient) ExchangeAccessKeyToken(ctx context.Context, in *ExchangeAccessKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAccessKeyTokenResponse, error) { +func (c *iAMServiceClient) ExchangeAPIKeyToken(ctx context.Context, in *ExchangeAPIKeyTokenRequest, opts ...grpc.CallOption) (*ExchangeAPIKeyTokenResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(ExchangeAccessKeyTokenResponse) - err := c.cc.Invoke(ctx, IAMService_ExchangeAccessKeyToken_FullMethodName, in, out, cOpts...) + out := new(ExchangeAPIKeyTokenResponse) + err := c.cc.Invoke(ctx, IAMService_ExchangeAPIKeyToken_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -768,19 +780,20 @@ type IAMServiceServer interface { Logout(context.Context, *LogoutRequest) (*emptypb.Empty, error) ChangePassword(context.Context, *UserBodyRequest) (*emptypb.Empty, error) GetProfile(context.Context, *UserIDRequest) (*StructResponse, error) - CreateAccessKey(context.Context, *UserBodyRequest) (*StructResponse, error) - ListAccessKeys(context.Context, *UserQueryRequest) (*StructResponse, error) - GetAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) - DeleteAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) - DisableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) - EnableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) - RotateAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) + CreateAPIKey(context.Context, *UserBodyRequest) (*StructResponse, error) + ListAPIKeys(context.Context, *UserQueryRequest) (*StructResponse, error) + GetAPIKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) + DeleteAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + DisableAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + EnableAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + RevokeAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) + RotateAPIKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) IsUserTeamAdmin(context.Context, *UserTeamRequest) (*BoolResponse, error) IsUserInTeam(context.Context, *UserTeamRequest) (*BoolResponse, error) IsTeamPublic(context.Context, *TeamRequest) (*BoolResponse, error) IsUserProjectAdmin(context.Context, *UserProjectRequest) (*BoolResponse, error) IsUserInProject(context.Context, *UserProjectRequest) (*BoolResponse, error) - ExchangeAccessKeyToken(context.Context, *ExchangeAccessKeyTokenRequest) (*ExchangeAccessKeyTokenResponse, error) + ExchangeAPIKeyToken(context.Context, *ExchangeAPIKeyTokenRequest) (*ExchangeAPIKeyTokenResponse, error) CreateUser(context.Context, *MutationRequest) (*StructResponse, error) DeleteUser(context.Context, *IDRequest) (*emptypb.Empty, error) GetUser(context.Context, *IDRequest) (*StructResponse, error) @@ -854,26 +867,29 @@ func (UnimplementedIAMServiceServer) ChangePassword(context.Context, *UserBodyRe func (UnimplementedIAMServiceServer) GetProfile(context.Context, *UserIDRequest) (*StructResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetProfile not implemented") } -func (UnimplementedIAMServiceServer) CreateAccessKey(context.Context, *UserBodyRequest) (*StructResponse, error) { - return nil, status.Error(codes.Unimplemented, "method CreateAccessKey not implemented") +func (UnimplementedIAMServiceServer) CreateAPIKey(context.Context, *UserBodyRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateAPIKey not implemented") +} +func (UnimplementedIAMServiceServer) ListAPIKeys(context.Context, *UserQueryRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAPIKeys not implemented") } -func (UnimplementedIAMServiceServer) ListAccessKeys(context.Context, *UserQueryRequest) (*StructResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ListAccessKeys not implemented") +func (UnimplementedIAMServiceServer) GetAPIKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAPIKey not implemented") } -func (UnimplementedIAMServiceServer) GetAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { - return nil, status.Error(codes.Unimplemented, "method GetAccessKey not implemented") +func (UnimplementedIAMServiceServer) DeleteAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteAPIKey not implemented") } -func (UnimplementedIAMServiceServer) DeleteAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "method DeleteAccessKey not implemented") +func (UnimplementedIAMServiceServer) DisableAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method DisableAPIKey not implemented") } -func (UnimplementedIAMServiceServer) DisableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "method DisableAccessKey not implemented") +func (UnimplementedIAMServiceServer) EnableAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method EnableAPIKey not implemented") } -func (UnimplementedIAMServiceServer) EnableAccessKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "method EnableAccessKey not implemented") +func (UnimplementedIAMServiceServer) RevokeAPIKey(context.Context, *UserScopedIDRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RevokeAPIKey not implemented") } -func (UnimplementedIAMServiceServer) RotateAccessKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { - return nil, status.Error(codes.Unimplemented, "method RotateAccessKey not implemented") +func (UnimplementedIAMServiceServer) RotateAPIKey(context.Context, *UserScopedIDRequest) (*StructResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RotateAPIKey not implemented") } func (UnimplementedIAMServiceServer) IsUserTeamAdmin(context.Context, *UserTeamRequest) (*BoolResponse, error) { return nil, status.Error(codes.Unimplemented, "method IsUserTeamAdmin not implemented") @@ -890,8 +906,8 @@ func (UnimplementedIAMServiceServer) IsUserProjectAdmin(context.Context, *UserPr func (UnimplementedIAMServiceServer) IsUserInProject(context.Context, *UserProjectRequest) (*BoolResponse, error) { return nil, status.Error(codes.Unimplemented, "method IsUserInProject not implemented") } -func (UnimplementedIAMServiceServer) ExchangeAccessKeyToken(context.Context, *ExchangeAccessKeyTokenRequest) (*ExchangeAccessKeyTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "method ExchangeAccessKeyToken not implemented") +func (UnimplementedIAMServiceServer) ExchangeAPIKeyToken(context.Context, *ExchangeAPIKeyTokenRequest) (*ExchangeAPIKeyTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExchangeAPIKeyToken not implemented") } func (UnimplementedIAMServiceServer) CreateUser(context.Context, *MutationRequest) (*StructResponse, error) { return nil, status.Error(codes.Unimplemented, "method CreateUser not implemented") @@ -1175,128 +1191,146 @@ func _IAMService_GetProfile_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } -func _IAMService_CreateAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_CreateAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserBodyRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).CreateAccessKey(ctx, in) + return srv.(IAMServiceServer).CreateAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_CreateAccessKey_FullMethodName, + FullMethod: IAMService_CreateAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).CreateAccessKey(ctx, req.(*UserBodyRequest)) + return srv.(IAMServiceServer).CreateAPIKey(ctx, req.(*UserBodyRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_ListAccessKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_ListAPIKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserQueryRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).ListAccessKeys(ctx, in) + return srv.(IAMServiceServer).ListAPIKeys(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_ListAccessKeys_FullMethodName, + FullMethod: IAMService_ListAPIKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).ListAccessKeys(ctx, req.(*UserQueryRequest)) + return srv.(IAMServiceServer).ListAPIKeys(ctx, req.(*UserQueryRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_GetAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_GetAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserScopedIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).GetAccessKey(ctx, in) + return srv.(IAMServiceServer).GetAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_GetAccessKey_FullMethodName, + FullMethod: IAMService_GetAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).GetAccessKey(ctx, req.(*UserScopedIDRequest)) + return srv.(IAMServiceServer).GetAPIKey(ctx, req.(*UserScopedIDRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_DeleteAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_DeleteAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserScopedIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).DeleteAccessKey(ctx, in) + return srv.(IAMServiceServer).DeleteAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_DeleteAccessKey_FullMethodName, + FullMethod: IAMService_DeleteAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).DeleteAccessKey(ctx, req.(*UserScopedIDRequest)) + return srv.(IAMServiceServer).DeleteAPIKey(ctx, req.(*UserScopedIDRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_DisableAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_DisableAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserScopedIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).DisableAccessKey(ctx, in) + return srv.(IAMServiceServer).DisableAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_DisableAccessKey_FullMethodName, + FullMethod: IAMService_DisableAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).DisableAccessKey(ctx, req.(*UserScopedIDRequest)) + return srv.(IAMServiceServer).DisableAPIKey(ctx, req.(*UserScopedIDRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_EnableAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_EnableAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserScopedIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).EnableAccessKey(ctx, in) + return srv.(IAMServiceServer).EnableAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_EnableAccessKey_FullMethodName, + FullMethod: IAMService_EnableAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).EnableAccessKey(ctx, req.(*UserScopedIDRequest)) + return srv.(IAMServiceServer).EnableAPIKey(ctx, req.(*UserScopedIDRequest)) } return interceptor(ctx, in, info, handler) } -func _IAMService_RotateAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { +func _IAMService_RevokeAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(UserScopedIDRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).RotateAccessKey(ctx, in) + return srv.(IAMServiceServer).RevokeAPIKey(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_RotateAccessKey_FullMethodName, + FullMethod: IAMService_RevokeAPIKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).RotateAccessKey(ctx, req.(*UserScopedIDRequest)) + return srv.(IAMServiceServer).RevokeAPIKey(ctx, req.(*UserScopedIDRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IAMService_RotateAPIKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UserScopedIDRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IAMServiceServer).RotateAPIKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IAMService_RotateAPIKey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IAMServiceServer).RotateAPIKey(ctx, req.(*UserScopedIDRequest)) } return interceptor(ctx, in, info, handler) } @@ -1391,20 +1425,20 @@ func _IAMService_IsUserInProject_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } -func _IAMService_ExchangeAccessKeyToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ExchangeAccessKeyTokenRequest) +func _IAMService_ExchangeAPIKeyToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExchangeAPIKeyTokenRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(IAMServiceServer).ExchangeAccessKeyToken(ctx, in) + return srv.(IAMServiceServer).ExchangeAPIKeyToken(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: IAMService_ExchangeAccessKeyToken_FullMethodName, + FullMethod: IAMService_ExchangeAPIKeyToken_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(IAMServiceServer).ExchangeAccessKeyToken(ctx, req.(*ExchangeAccessKeyTokenRequest)) + return srv.(IAMServiceServer).ExchangeAPIKeyToken(ctx, req.(*ExchangeAPIKeyTokenRequest)) } return interceptor(ctx, in, info, handler) } @@ -2151,32 +2185,36 @@ var IAMService_ServiceDesc = grpc.ServiceDesc{ Handler: _IAMService_GetProfile_Handler, }, { - MethodName: "CreateAccessKey", - Handler: _IAMService_CreateAccessKey_Handler, + MethodName: "CreateAPIKey", + Handler: _IAMService_CreateAPIKey_Handler, + }, + { + MethodName: "ListAPIKeys", + Handler: _IAMService_ListAPIKeys_Handler, }, { - MethodName: "ListAccessKeys", - Handler: _IAMService_ListAccessKeys_Handler, + MethodName: "GetAPIKey", + Handler: _IAMService_GetAPIKey_Handler, }, { - MethodName: "GetAccessKey", - Handler: _IAMService_GetAccessKey_Handler, + MethodName: "DeleteAPIKey", + Handler: _IAMService_DeleteAPIKey_Handler, }, { - MethodName: "DeleteAccessKey", - Handler: _IAMService_DeleteAccessKey_Handler, + MethodName: "DisableAPIKey", + Handler: _IAMService_DisableAPIKey_Handler, }, { - MethodName: "DisableAccessKey", - Handler: _IAMService_DisableAccessKey_Handler, + MethodName: "EnableAPIKey", + Handler: _IAMService_EnableAPIKey_Handler, }, { - MethodName: "EnableAccessKey", - Handler: _IAMService_EnableAccessKey_Handler, + MethodName: "RevokeAPIKey", + Handler: _IAMService_RevokeAPIKey_Handler, }, { - MethodName: "RotateAccessKey", - Handler: _IAMService_RotateAccessKey_Handler, + MethodName: "RotateAPIKey", + Handler: _IAMService_RotateAPIKey_Handler, }, { MethodName: "IsUserTeamAdmin", @@ -2199,8 +2237,8 @@ var IAMService_ServiceDesc = grpc.ServiceDesc{ Handler: _IAMService_IsUserInProject_Handler, }, { - MethodName: "ExchangeAccessKeyToken", - Handler: _IAMService_ExchangeAccessKeyToken_Handler, + MethodName: "ExchangeAPIKeyToken", + Handler: _IAMService_ExchangeAPIKeyToken_Handler, }, { MethodName: "CreateUser", diff --git a/src/router/admin.go b/src/router/admin.go index 3b13fccd..28eee692 100644 --- a/src/router/admin.go +++ b/src/router/admin.go @@ -1,6 +1,7 @@ package router import ( + "aegis/consts" "aegis/middleware" "github.com/gin-gonic/gin" @@ -90,23 +91,34 @@ func SetupAdminV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { resources := v2.Group("/resources", middleware.JWTAuth()) { - permissions := resources.Group("/:resource_id/permissions") + resourceRead := resources.Group("", middleware.RequirePermissionRead) { - permissions.GET("", handlers.RBAC.ListResourcePermissions) - } + permissions := resourceRead.Group("/:resource_id/permissions") + { + permissions.GET("", handlers.RBAC.ListResourcePermissions) + } - resources.GET("/:resource_id", handlers.RBAC.GetResource) - resources.GET("", handlers.RBAC.ListResources) + resourceRead.GET("/:resource_id", handlers.RBAC.GetResource) + resourceRead.GET("", handlers.RBAC.ListResources) + } } systems := v2.Group("/systems", middleware.JWTAuth()) { - systems.GET("", handlers.ChaosSystem.ListSystems) - systems.POST("", handlers.ChaosSystem.CreateSystem) - systems.GET("/:id", handlers.ChaosSystem.GetSystem) - systems.PUT("/:id", handlers.ChaosSystem.UpdateSystem) - systems.DELETE("/:id", handlers.ChaosSystem.DeleteSystem) - systems.POST("/:id/metadata", handlers.ChaosSystem.UpsertMetadata) - systems.GET("/:id/metadata", handlers.ChaosSystem.ListMetadata) + systemRead := systems.Group("", middleware.RequireSystemRead) + { + systemRead.GET("", handlers.ChaosSystem.ListSystems) + systemRead.GET("/:id", handlers.ChaosSystem.GetSystem) + systemRead.GET("/:id/metadata", handlers.ChaosSystem.ListMetadata) + } + + systemConfigure := systems.Group("", middleware.RequireSystemConfigure) + { + systemConfigure.POST("", handlers.ChaosSystem.CreateSystem) + systemConfigure.PUT("/:id", handlers.ChaosSystem.UpdateSystem) + systemConfigure.POST("/:id/metadata", handlers.ChaosSystem.UpsertMetadata) + } + + systems.DELETE("/:id", middleware.RequirePermission(consts.PermSystemManage), handlers.ChaosSystem.DeleteSystem) } } diff --git a/src/router/portal.go b/src/router/portal.go index 5e080c80..83df8396 100644 --- a/src/router/portal.go +++ b/src/router/portal.go @@ -57,8 +57,8 @@ func SetupPortalV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { teams := v2.Group("/teams", middleware.JWTAuth()) { - teams.POST("", handlers.Team.CreateTeam) - teams.GET("", handlers.Team.ListTeams) + teams.POST("", middleware.RequireTeamCreate, handlers.Team.CreateTeam) + teams.GET("", middleware.RequireTeamRead, handlers.Team.ListTeams) teamAdmin := teams.Group("/:team_id", middleware.RequireTeamAdminAccess) { @@ -93,14 +93,15 @@ func SetupPortalV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { labels.POST("/batch-delete", middleware.RequireLabelDelete, handlers.Label.BatchDeleteLabels) } - accessKeys := v2.Group("/access-keys", middleware.JWTAuth()) + accessKeys := v2.Group("/api-keys", middleware.JWTAuth(), middleware.RequireHumanUserAuth()) { - accessKeys.GET("", handlers.Auth.ListAccessKeys) - accessKeys.POST("", handlers.Auth.CreateAccessKey) - accessKeys.GET("/:access_key_id", handlers.Auth.GetAccessKey) - accessKeys.DELETE("/:access_key_id", handlers.Auth.DeleteAccessKey) - accessKeys.POST("/:access_key_id/rotate", handlers.Auth.RotateAccessKey) - accessKeys.POST("/:access_key_id/disable", handlers.Auth.DisableAccessKey) - accessKeys.POST("/:access_key_id/enable", handlers.Auth.EnableAccessKey) + accessKeys.GET("", handlers.Auth.ListAPIKeys) + accessKeys.POST("", handlers.Auth.CreateAPIKey) + accessKeys.GET("/:id", handlers.Auth.GetAPIKey) + accessKeys.DELETE("/:id", handlers.Auth.DeleteAPIKey) + accessKeys.POST("/:id/rotate", handlers.Auth.RotateAPIKey) + accessKeys.POST("/:id/disable", handlers.Auth.DisableAPIKey) + accessKeys.POST("/:id/enable", handlers.Auth.EnableAPIKey) + accessKeys.POST("/:id/revoke", handlers.Auth.RevokeAPIKey) } } diff --git a/src/router/public.go b/src/router/public.go index 99fdf165..499e80c4 100644 --- a/src/router/public.go +++ b/src/router/public.go @@ -12,10 +12,10 @@ func SetupPublicV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { auth.POST("/login", handlers.Auth.Login) // User login auth.POST("/register", handlers.Auth.Register) // User registration auth.POST("/refresh", handlers.Auth.RefreshToken) // Token refresh - auth.POST("/access-key/token", handlers.Auth.ExchangeAccessKeyToken) + auth.POST("/api-key/token", handlers.Auth.ExchangeAPIKeyToken) // These require authentication - authProtected := auth.Group("", middleware.JWTAuth()) + authProtected := auth.Group("", middleware.JWTAuth(), middleware.RequireHumanUserAuth()) { authProtected.POST("/logout", handlers.Auth.Logout) // User logout authProtected.POST("/change-password", handlers.Auth.ChangePassword) // Change password diff --git a/src/router/router.go b/src/router/router.go index d7cb5cb9..caf77df4 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -35,12 +35,17 @@ func New(handlers *Handlers, services ...middleware.Service) *gin.Engine { middleware.TracerMiddleware(), ) - // Set up system routes + middleware.StartCleanupRoutine() + + v2 := router.Group("/api/v2") + SetupPublicV2Routes(v2, handlers) + SetupSDKV2Routes(v2, handlers) + SetupRuntimeV2Routes(v2, handlers) + SetupAdminV2Routes(v2, handlers) + SetupPortalV2Routes(v2, handlers) + SetupSystemV2Routes(v2, handlers) SetupSystemRoutes(router, handlers) - // Set up API routes - SetupV2Routes(router, handlers) - // Swagger documentation router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/src/router/router_test.go b/src/router/router_test.go index 7c1270c8..b1470083 100644 --- a/src/router/router_test.go +++ b/src/router/router_test.go @@ -16,6 +16,7 @@ func TestRouterSeparatesRouteGroups(t *testing.T) { requiredPrefixes := []string{ "/api/v2/auth", "/api/v2/projects", + "/api/v2/executions", "/api/v2/users", "/api/v2/sdk", "/system/audit", diff --git a/src/router/runtime.go b/src/router/runtime.go new file mode 100644 index 00000000..c88d7a90 --- /dev/null +++ b/src/router/runtime.go @@ -0,0 +1,15 @@ +package router + +import ( + "aegis/middleware" + + "github.com/gin-gonic/gin" +) + +func SetupRuntimeV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + runtime := v2.Group("/executions", middleware.JWTAuth(), middleware.RequireServiceTokenAuth()) + { + runtime.POST("/:execution_id/detector_results", handlers.Execution.UploadDetectorResults) + runtime.POST("/:execution_id/granularity_results", handlers.Execution.UploadGranularityResults) + } +} diff --git a/src/router/sdk.go b/src/router/sdk.go index 1cf42c00..e8e8a6da 100644 --- a/src/router/sdk.go +++ b/src/router/sdk.go @@ -7,14 +7,14 @@ import ( ) func SetupSDKV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { - sdkEval := v2.Group("/sdk/evaluations", middleware.JWTAuth()) + sdkEval := v2.Group("/sdk/evaluations", middleware.JWTAuth(), middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read")) { sdkEval.GET("", handlers.SDK.ListEvaluations) sdkEval.GET("/experiments", handlers.SDK.ListExperiments) sdkEval.GET("/:id", handlers.SDK.GetEvaluation) } - sdkData := v2.Group("/sdk/datasets", middleware.JWTAuth()) + sdkData := v2.Group("/sdk/datasets", middleware.JWTAuth(), middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:datasets:*", "sdk:datasets:read")) { sdkData.GET("", handlers.SDK.ListDatasetSamples) } diff --git a/src/router/system.go b/src/router/system.go index 1ec88af6..3ebf9124 100644 --- a/src/router/system.go +++ b/src/router/system.go @@ -48,7 +48,7 @@ func SetupSystemRoutes(router *gin.Engine, handlers *Handlers) { } func SetupSystemV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { - system := v2.Group("/system", middleware.JWTAuth()) + system := v2.Group("/system", middleware.JWTAuth(), middleware.RequireSystemRead) { system.GET("/metrics", handlers.SystemMetric.GetSystemMetrics) // Get current system metrics system.GET("/metrics/history", handlers.SystemMetric.GetSystemMetricsHistory) // Get historical system metrics diff --git a/src/router/v2.go b/src/router/v2.go deleted file mode 100644 index 24328b72..00000000 --- a/src/router/v2.go +++ /dev/null @@ -1,364 +0,0 @@ -package router - -import ( - "aegis/middleware" - - "github.com/gin-gonic/gin" -) - -/* -=================================================================================== -API v2 Design Specification - RESTful API Standard -=================================================================================== - -v2 API strictly adheres to RESTful design principles, contrasting with the disorganized design of v1. -v1 API design was rather arbitrary, with non-standard methods and paths. v2 will uniformly follow the standards below. - -📋 HTTP Method Usage Specification: -- GET : Query resources (idempotent, cacheable) -- POST : Create resources / Complex queries (non-idempotent) -- PUT : Full update of resources (idempotent) -- PATCH : Partial update of resources (idempotent) -- DELETE : Delete resources (idempotent) - -🎯 URL Design Specification: -1. Resource names use plural form - ✅ GET /api/v2/users ❌ GET /api/v2/user - ✅ GET /api/v2/projects ❌ GET /api/v2/project - -2. Clear hierarchical relationships - ✅ GET /api/v2/users/{id}/projects - ✅ GET /api/v2/projects/{id}/members - -3. Query parameter specification - ✅ GET /api/v2/users?page=1&size=10&status=active - ✅ GET /api/v2/tasks?project_id=123&type=injection - -📊 Standard CRUD Operation Modes: -- GET /api/v2/{resource} # List query (supports pagination, filtering, sorting) -- POST /api/v2/{resource} # Create resource -- GET /api/v2/{resource}/{id} # Get single resource details -- PUT /api/v2/{resource}/{id} # Full update of resource -- PATCH : Partial update of resource (idempotent) -- DELETE : Delete resource (idempotent) - -🔍 Complex Query Handling: -For complex search conditions, use dedicated search endpoints: -- POST /api/v2/{resource}/search # Complex condition search -- POST /api/v2/{resource}/query # Advanced query -- POST /api/v2/{resource}/batch # Batch operations - -🎨 Business Operation Endpoints: -Semantic business operations use verb forms: -- POST /api/v2/users/{id}/activate # Activate user -- POST /api/v2/tasks/{id}/cancel # Cancel task -- POST /api/v2/injections/{id}/start # Start fault injection -- POST /api/v2/containers/{id}/build # Build container - -📨 Response Format Specification: -1. Successful Response: - { - "code": 200, - "message": "success", - "data": {...}, - "timestamp": "2024-01-01T12:00:00Z" - } - -2. List Response: - { - "code": 200, - "message": "success", - "data": { - "items": [...], - "pagination": { - "page": 1, - "size": 10, - "total": 100, - "pages": 10 - } - } - } - -3. Error Response: - { - "code": 400, - "message": "validation failed", - "errors": ["field xxx is required"], - "timestamp": "2024-01-01T12:00:00Z" - } - -🔐 Authentication and Authorization Specification: -- Use JWT Bearer Token authentication -- Permission checks based on RBAC model -- Sensitive operations require secondary confirmation - -⚡ Performance Optimization: -- GET requests support ETag caching -- List queries default to pagination (page=1, size=20) -- Supports field selection ?fields=id,name,status -- Supports associated queries ?include=project,labels - -🔄 Version Compatibility: -- v2 API ensures backward compatibility -- Deprecated endpoints provide a 6-month transition period -- Major changes handled by new version numbers - -Note: v1 API design is chaotic and does not follow a unified standard. It will gradually migrate to v2 specification later. -=================================================================================== -*/ - -// SetupV2Routes sets up API v2 routes - stable version of the API -func SetupV2Routes(router *gin.Engine, handlers *Handlers) { - middleware.StartCleanupRoutine() - - v2 := router.Group("/api/v2") - SetupPublicV2Routes(v2, handlers) - SetupSDKV2Routes(v2, handlers) - SetupAdminV2Routes(v2, handlers) - SetupPortalV2Routes(v2, handlers) - SetupSystemV2Routes(v2, handlers) - - // ===================================================================== - // Admin Entity API Group - // ===================================================================== - - // Container Management - Container Entity - containers := v2.Group("/containers", middleware.JWTAuth()) - { - // Container Version sub-resource routes - versions := containers.Group("/:container_id/versions") - { - - // Container Version Read operations - versionRead := versions.Group("", middleware.RequireContainerVersionRead) - { - versionRead.GET("/:version_id", handlers.Container.GetContainerVersion) // Get container version by ID - versionRead.GET("", handlers.Container.ListContainerVersions) // List container versions - } - - // Container Version Create operations - versions.POST("", middleware.RequireContainerVersionCreate, handlers.Container.CreateContainerVersion) // Create container version - - // Container Version Upload operations - versions.POST("/:version_id/helm-chart", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmChart) // Upload Helm chart tgz file - versions.POST("/:version_id/helm-values", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmValueFile) // Upload Helm values file - - // Container Version Update operations - versions.PATCH("/:version_id", middleware.RequireContainerVersionUpdate, handlers.Container.UpdateContainerVersion) // Update container version - - // Container Version Delete operations - versions.DELETE("/:version_id", middleware.RequireContainerVersionDelete, handlers.Container.DeleteContainerVersion) - } - - // Container Read operations - containerRead := containers.Group("", middleware.RequireContainerRead) - { - containerRead.GET("/:container_id", handlers.Container.GetContainer) // Get container by ID - containerRead.GET("", handlers.Container.ListContainers) // List containers - } - - // Container Create operations - containers.POST("", middleware.RequireContainerCreate, handlers.Container.CreateContainer) // Create container - - // Container Execute operations (build requires execute permission) - containers.POST("/build", middleware.RequireContainerExecute, handlers.Container.SubmitContainerBuilding) // Build container - - // Container Update operations - containers.PATCH("/:container_id", middleware.RequireContainerUpdate, handlers.Container.UpdateContainer) // Update container - containers.PATCH("/:container_id/labels", middleware.RequireContainerUpdate, handlers.Container.ManageContainerCustomLabels) // Manage container labels - - // Container Delete operations - containers.DELETE("/:container_id", middleware.RequireContainerDelete, handlers.Container.DeleteContainer) // Delete container - } - - // Dataset Management - Dataset Entity - datasets := v2.Group("/datasets", middleware.JWTAuth()) - { - // Dataset Version sub-resource routes - versions := datasets.Group("/:dataset_id/versions") - { - versionRead := versions.Group("", middleware.RequireDatasetVersionRead) - { - versionRead.GET("", handlers.Dataset.ListDatasetVersions) // List dataset versions - versionRead.GET("/:version_id", handlers.Dataset.GetDatasetVersion) // Get dataset version by ID - versionRead.GET("/:version_id/download", handlers.Dataset.DownloadDatasetVersion) // Download dataset version - } - - // Dataset Version Create operations - versions.POST("", middleware.RequireDatasetVersionCreate, handlers.Dataset.CreateDatasetVersion) // Create dataset version - - // Dataset Version Update operations - versions.PATCH("/:version_id", middleware.RequireDatasetVersionUpdate, handlers.Dataset.UpdateDatasetVersion) // Update dataset version - versions.PATCH("/:version_id/injections", middleware.RequireDatasetVersionUpdate, handlers.Dataset.ManageDatasetVersionInjections) // Manage dataset version injections - - versions.DELETE("/:version_id", middleware.RequireDatasetVersionDelete, handlers.Dataset.DeleteDatasetVersion) // Delete dataset version - } - - // Dataset Read operations - datasetRead := datasets.Group("", middleware.RequireDatasetRead) - { - datasetRead.GET("/:dataset_id", handlers.Dataset.GetDataset) // Get dataset by ID - datasetRead.GET("", handlers.Dataset.ListDatasets) // List datasets - } - - // Dataset Create operations - datasets.POST("", middleware.RequireDatasetCreate, handlers.Dataset.CreateDataset) // Create dataset - - // Dataset Update operations - datasets.PATCH("/:dataset_id", middleware.RequireDatasetUpdate, handlers.Dataset.UpdateDataset) // Update dataset - datasets.PATCH("/:dataset_id/labels", middleware.RequireDatasetUpdate, handlers.Dataset.ManageDatasetCustomLabels) // Manage dataset labels - - // Dataset Delete operations - datasets.DELETE("/:dataset_id", middleware.RequireDatasetDelete, handlers.Dataset.DeleteDataset) // Delete dataset - } - - // ===================================================================== - // Core Business Entity API Group - // ===================================================================== - - // Task Management - Task Entity - tasks := v2.Group("/tasks") - { - taskWithAuth := tasks.Group("", middleware.JWTAuth()) - { - - // Task Read operations - taskRead := taskWithAuth.Group("", middleware.RequireTaskRead) - { - taskRead.GET("", handlers.Task.List) // List tasks - taskRead.GET("/:task_id", handlers.Task.Get) // Get task by ID - } - - // Task Delete operations - taskWithAuth.POST("/batch-delete", middleware.RequireTaskDelete, handlers.Task.BatchDelete) // Batch delete tasks - } - - // Task Log streaming (WebSocket) - auth via query param, not middleware - tasks.GET("/:task_id/logs/ws", handlers.Task.LogsWS) // Stream task logs via WebSocket - } - - // Fault Injection Management - FaultInjectionSchedule Entity - // Note: These global routes are for system admins only. Regular users should access injections via /projects/:project_id - injections := v2.Group("/injections", middleware.JWTAuth()) - { - injectionSystemAdmin := injections.Group("", middleware.RequireSystemAdmin()) - { - injectionSystemAdmin.GET("", handlers.Injection.ListInjections) // List injections - injectionSystemAdmin.POST("/search", handlers.Injection.SearchInjections) // Advanced search - } - - // Manual upload (must be before /:id routes) - injections.POST("/upload", handlers.Injection.UploadDatapack) // Upload manual datapack - - // Injection Read operations - injections.GET("/:id", handlers.Injection.GetInjection) // Get injection by ID - injections.GET("/:id/download", handlers.Injection.DownloadDatapack) // Download injection datapack - injections.GET("/:id/logs", handlers.Injection.GetInjectionLogs) // Get injection execution logs - injections.GET("/:id/files", handlers.Injection.ListDatapackFiles) // Get injection file structure - injections.GET("/:id/files/download", handlers.Injection.DownloadDatapackFile) // Download specific injection file - injections.GET("/:id/files/query", handlers.Injection.QueryDatapackFile) // Query parquet file content - injections.GET("/metadata", handlers.Injection.GetInjectionMetadata) // Get injection metadata - - // Injection Clone operations - injections.POST("/:id/clone", handlers.Injection.CloneInjection) // Clone injection - - // Injection Update operations (label management, ground truth) - injections.PUT("/:id/groundtruth", handlers.Injection.UpdateGroundtruth) // Update ground truth - injections.PATCH("/:id/labels", handlers.Injection.ManageInjectionCustomLabels) // Manage injection custom labels - injections.PATCH("/labels/batch", handlers.Injection.BatchManageInjectionLabels) // Batch manage injection labels - - // Injection Delete operations - injections.POST("/batch-delete", handlers.Injection.BatchDeleteInjections) // Batch delete injections - } - - // Execution Result Management - ExecutionResult Entity - // Note: These global routes are for system admins only. Regular users should access executions via /projects/:project_id - executions := v2.Group("/executions", middleware.JWTAuth()) - { - executionSystemAdmin := executions.Group("", middleware.RequireSystemAdmin()) - { - executionSystemAdmin.GET("", handlers.Execution.ListExecutions) // List executions - executionSystemAdmin.GET("/labels", handlers.Execution.ListAvailableExecutionLabels) // List available execution labels - } - - // Execution Read operations - executions.GET("/:execution_id", handlers.Execution.GetExecution) // Get execution by ID - - // Execution Update operations (upload results and manage labels) - executions.POST("/:execution_id/detector_results", handlers.Execution.UploadDetectorResults) // Upload detector results - executions.POST("/:execution_id/granularity_results", handlers.Execution.UploadGranularityResults) // Upload granularity results - executions.PATCH("/:execution_id/labels", handlers.Execution.ManageExecutionCustomLabels) // Manage execution custom labels - - // Execution Delete operations - executions.POST("/batch-delete", handlers.Execution.BatchDeleteExecutions) // Batch delete executions - } - - // Trace Management - Trace Entity - traces := v2.Group("/traces", middleware.JWTAuth()) - { - traces.GET("", handlers.Trace.ListTraces) // List traces - traces.GET("/:trace_id", handlers.Trace.GetTrace) // Get trace by ID - traces.GET("/:trace_id/stream", handlers.Trace.GetTraceStream) // Get trace stream (SSE) - } - - // Group Management - Group stream for real-time batch progress - groups := v2.Group("/groups", middleware.JWTAuth()) - { - groups.GET("/:group_id/stats", handlers.Group.GetGroupStats) // Get group stats (can be used for progress tracking) - groups.GET("/:group_id/stream", handlers.Group.GetGroupStream) // Stream group trace events (SSE) - } - - // ===================================================================== - // Notification API Group - // ===================================================================== - - // Notification Management - Global workflow notifications - notifications := v2.Group("/notifications", middleware.JWTAuth()) - { - notifications.GET("/stream", handlers.Notification.GetStream) // Stream global notifications (SSE) - } - - // ===================================================================== - // Analyzer Service API Group - // ===================================================================== - - // Analyzer related routes (placeholder for future expansion) - analyzer := v2.Group("/analyzer", middleware.JWTAuth()) - _ = analyzer // Temporarily unused to avoid compilation errors - - // ===================================================================== - // Evaluation API Group - // ===================================================================== - - // Evaluation API Group - evaluations := v2.Group("/evaluations", middleware.JWTAuth()) - { - // GET /api/v2/evaluations - List persisted evaluations with pagination - evaluations.GET("", handlers.Evaluation.ListEvaluations) - - // GET /api/v2/evaluations/:id - Get a single evaluation by ID - evaluations.GET("/:id", handlers.Evaluation.GetEvaluation) - - // DELETE /api/v2/evaluations/:id - Delete an evaluation by ID - evaluations.DELETE("/:id", handlers.Evaluation.DeleteEvaluation) - - // POST /api/v2/evaluations/datasets - Get algorithm evaluations on multiple datasets (requires dataset read permission) - evaluations.POST("/datasets", middleware.RequireDatasetRead, handlers.Evaluation.ListDatasetEvaluationResults) - - // POST /api/v2/evaluations/datapacks - Get algorithm evaluations on multiple datapacks (requires dataset read permission) - evaluations.POST("/datapacks", middleware.RequireDatasetRead, handlers.Evaluation.ListDatapackEvaluationResults) - } - - // ===================================================================== - // Metrics API Group - // ===================================================================== - - // Metrics routes - metrics := v2.Group("/metrics", middleware.JWTAuth()) - { - metrics.GET("/injections", handlers.Metric.GetInjectionMetrics) // Get injection metrics - metrics.GET("/executions", handlers.Metric.GetExecutionMetrics) // Get execution metrics - metrics.GET("/algorithms", handlers.Metric.GetAlgorithmMetrics) // Get algorithm comparison metrics - } - -} diff --git a/src/service/initialization/consumer.go b/src/service/initialization/consumer.go index e6c19a6d..26f5667b 100644 --- a/src/service/initialization/consumer.go +++ b/src/service/initialization/consumer.go @@ -49,28 +49,35 @@ func InitializeConsumer( return err } - // Initialize namespaces on startup - critical after restart to re-initialize CRD informers - logrus.Info("Initializing namespaces on startup...") + // Namespace/bootstrap informer initialization can take noticeably longer than + // the Fx startup deadline when the local cluster is cold or slow. Run it in + // the background so consumer/both startup does not fail with + // "context deadline exceeded" during local debugging. if monitor == nil { logrus.Warn("Monitor not initialized, skipping namespace initialization") return nil - } else { - monitor.SetContext(ctx) + } + + monitor.SetContext(ctx) + go func() { + logrus.Info("Initializing namespaces on startup...") + initialized, err := monitor.InitializeNamespaces() if err != nil { - return fmt.Errorf("failed to initialize namespaces: %w", err) + logrus.Errorf("Failed to initialize namespaces: %v", err) + return } if len(initialized) == 0 { logrus.Warn("No namespaces to initialize on startup") - return nil + return } logrus.Infof("Initialized namespaces on startup: %v", initialized) if err := consumer.UpdateK8sController(controller, initialized, []string{}); err != nil { - return fmt.Errorf("failed to update k8s controller: %w", err) + logrus.Errorf("Failed to update k8s controller: %v", err) } - } + }() return nil } diff --git a/src/utils/access_key_crypto.go b/src/utils/access_key_crypto.go index 182b47b1..7f5ec00f 100644 --- a/src/utils/access_key_crypto.go +++ b/src/utils/access_key_crypto.go @@ -11,8 +11,8 @@ import ( "fmt" ) -func EncryptAccessKeySecret(secret string) (string, error) { - block, err := aes.NewCipher(accessKeyCryptoKey()) +func EncryptAPIKeySecret(secret string) (string, error) { + block, err := aes.NewCipher(apiKeyCryptoKey()) if err != nil { return "", fmt.Errorf("failed to initialize cipher: %w", err) } @@ -31,13 +31,13 @@ func EncryptAccessKeySecret(secret string) (string, error) { return base64.StdEncoding.EncodeToString(ciphertext), nil } -func DecryptAccessKeySecret(ciphertext string) (string, error) { +func DecryptAPIKeySecret(ciphertext string) (string, error) { raw, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return "", fmt.Errorf("failed to decode ciphertext: %w", err) } - block, err := aes.NewCipher(accessKeyCryptoKey()) + block, err := aes.NewCipher(apiKeyCryptoKey()) if err != nil { return "", fmt.Errorf("failed to initialize cipher: %w", err) } @@ -61,18 +61,23 @@ func DecryptAccessKeySecret(ciphertext string) (string, error) { return string(plaintext), nil } -func SignAccessKeyRequest(secret, payload string) string { +func SignAPIKeyRequest(secret, payload string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } -func VerifyAccessKeyRequestSignature(secret, payload, signature string) bool { - expected := SignAccessKeyRequest(secret, payload) +func VerifyAPIKeyRequestSignature(secret, payload, signature string) bool { + expected := SignAPIKeyRequest(secret, payload) return hmac.Equal([]byte(expected), []byte(signature)) } -func accessKeyCryptoKey() []byte { +func SHA256Hex(payload []byte) string { + sum := sha256.Sum256(payload) + return hex.EncodeToString(sum[:]) +} + +func apiKeyCryptoKey() []byte { sum := sha256.Sum256([]byte(JWTSecret)) return sum[:] } diff --git a/src/utils/jwt.go b/src/utils/jwt.go index e10f6e6a..0d49d615 100644 --- a/src/utils/jwt.go +++ b/src/utils/jwt.go @@ -21,14 +21,15 @@ const ( // Claims represents JWT claims structure type Claims struct { - UserID int `json:"user_id"` - Username string `json:"username"` - Email string `json:"email"` - IsActive bool `json:"is_active"` - IsAdmin bool `json:"is_admin"` // System admin flag (super_admin or admin) - Roles []string `json:"roles"` // Global role names - AuthType string `json:"auth_type,omitempty"` - AccessKeyID int `json:"access_key_id,omitempty"` + UserID int `json:"user_id"` + Username string `json:"username"` + Email string `json:"email"` + IsActive bool `json:"is_active"` + IsAdmin bool `json:"is_admin"` // System admin flag (super_admin or admin) + Roles []string `json:"roles"` // Global role names + AuthType string `json:"auth_type,omitempty"` + APIKeyID int `json:"api_key_id,omitempty"` + APIKeyScopes []string `json:"api_key_scopes,omitempty"` jwt.RegisteredClaims } @@ -47,25 +48,26 @@ type ServiceClaims struct { // GenerateToken generates a new JWT token for the given user func GenerateToken(userID int, username, email string, isActive, isAdmin bool, roles []string) (string, time.Time, error) { - return generateUserToken(userID, username, email, isActive, isAdmin, roles, "user", 0) + return generateUserToken(userID, username, email, isActive, isAdmin, roles, "user", 0, nil) } -func GenerateAccessKeyToken(userID int, username, email string, isActive, isAdmin bool, roles []string, accessKeyID int) (string, time.Time, error) { - return generateUserToken(userID, username, email, isActive, isAdmin, roles, "access_key", accessKeyID) +func GenerateAPIKeyToken(userID int, username, email string, isActive, isAdmin bool, roles []string, apiKeyID int, apiKeyScopes []string) (string, time.Time, error) { + return generateUserToken(userID, username, email, isActive, isAdmin, roles, "api_key", apiKeyID, apiKeyScopes) } -func generateUserToken(userID int, username, email string, isActive, isAdmin bool, roles []string, authType string, accessKeyID int) (string, time.Time, error) { +func generateUserToken(userID int, username, email string, isActive, isAdmin bool, roles []string, authType string, apiKeyID int, apiKeyScopes []string) (string, time.Time, error) { expirationTime := time.Now().Add(TokenExpiration) claims := &Claims{ - UserID: userID, - Username: username, - Email: email, - IsActive: isActive, - IsAdmin: isAdmin, - Roles: roles, - AuthType: authType, - AccessKeyID: accessKeyID, + UserID: userID, + Username: username, + Email: email, + IsActive: isActive, + IsAdmin: isAdmin, + Roles: roles, + AuthType: authType, + APIKeyID: apiKeyID, + APIKeyScopes: append([]string(nil), apiKeyScopes...), RegisteredClaims: jwt.RegisteredClaims{ ID: fmt.Sprintf("jwt_%s_%d_%d", authType, userID, time.Now().Unix()), ExpiresAt: jwt.NewNumericDate(expirationTime), From 80702b6165bbb1b5cd5a309bd439969034fb0124 Mon Sep 17 00:00:00 2001 From: rainystevn1 Date: Sun, 19 Apr 2026 10:56:16 +0800 Subject: [PATCH 4/4] refactor(architecture): split monolith into six service boundaries with gRPC - flatten nested project routes into resource-oriented REST API structure - rename grpciam -> grpc/iam and align all grpc interface paths - consolidate OpenAPI generator templates under typescript/ directory - add Apifox integration for portal/sdk/admin target uploads - remove ProducerCompatibilityOptions in favor of explicit module composition Co-Authored-By: Claude Opus 4.7 --- .../typescript/client/config.json | 20 - .../typescript/{sdk => }/config.json | 0 .../typescript/sdk/templates/package.mustache | 58 - .../sdk/templates/tsconfig.mustache | 34 - .../{client => }/templates/package.mustache | 0 .../{client => }/templates/tsconfig.mustache | 0 README.md | 72 +- docs/report-index.md | 796 ++---------- docs/todo.md | 860 +++---------- justfile | 63 +- project-index.yaml | 12 +- scripts/command/settings.toml | 31 + scripts/command/src/backup/mysql.py | 2 +- scripts/command/src/cli/main.py | 2 + scripts/command/src/cli/rcabench_.py | 6 +- scripts/command/src/cli/sdk.py | 93 ++ scripts/command/src/cli/swagger.py | 64 +- scripts/command/src/swagger/__init__.py | 12 +- scripts/command/src/swagger/apifox.py | 175 +++ scripts/command/src/swagger/common.py | 56 +- scripts/command/src/swagger/init.py | 49 +- scripts/command/src/swagger/python.py | 17 +- scripts/command/src/swagger/typescript.py | 103 +- scripts/command/src/test.py | 15 +- scripts/generate_swagger_audience_report.py | 281 ----- scripts/migrate_swagger_comments.py | 1104 ----------------- scripts/start.sh | 40 +- scripts/test-push.sh | 2 +- ...{regression-test.sh => test-regression.sh} | 2 +- sdk/python/README.md | 2 +- skaffold.yaml | 4 +- src/app/app.go | 42 +- src/app/both.go | 3 +- src/app/compat_options.go | 49 - src/app/consumer.go | 2 +- src/app/gateway/auth_services.go | 46 +- src/app/gateway/metric_services.go | 40 +- src/app/gateway/metric_services_test.go | 38 +- src/app/gateway/middleware_service.go | 2 +- src/app/gateway/options.go | 84 +- src/app/gateway/orchestrator_services.go | 82 +- src/app/gateway/orchestrator_services_test.go | 40 +- src/app/gateway/rbac_services.go | 50 +- src/app/gateway/remote_required.go | 2 +- src/app/gateway/resource_services.go | 86 +- src/app/gateway/resource_services_test.go | 48 +- src/app/gateway/system_services.go | 32 +- src/app/gateway/team_services.go | 38 +- src/app/gateway/team_services_test.go | 34 +- src/app/gateway/user_services.go | 30 +- src/app/http_modules.go | 80 +- src/app/iam/options.go | 24 +- src/app/orchestrator/options.go | 26 +- src/app/producer.go | 60 +- src/app/producer_init.go | 44 - src/app/resource/options.go | 34 +- src/app/runtime_stack.go | 52 +- src/app/service_entrypoints_test.go | 74 +- src/app/startup_smoke_test.go | 52 +- src/app/system/options.go | 20 +- src/cmd/api-gateway/main.go | 4 +- src/cmd/iam-service/main.go | 4 +- src/cmd/orchestrator-service/main.go | 4 +- src/cmd/resource-service/main.go | 4 +- src/cmd/system-service/main.go | 4 +- src/httpx/common.go | 2 + src/infra/buildkit/gateway.go | 2 +- src/infra/buildkit/module.go | 2 +- src/infra/chaos/module.go | 2 +- src/infra/config/module.go | 2 +- src/infra/db/config.go | 2 +- src/infra/db/migration.go | 2 +- src/infra/db/module.go | 2 +- src/infra/etcd/gateway.go | 2 +- src/infra/etcd/module.go | 2 +- src/infra/harbor/gateway.go | 2 +- src/infra/harbor/module.go | 2 +- src/infra/helm/gateway.go | 2 +- src/infra/helm/module.go | 2 +- src/infra/k8s/controller.go | 2 +- src/infra/k8s/crd.go | 2 +- src/infra/k8s/gateway.go | 2 +- src/infra/k8s/job.go | 2 +- src/infra/k8s/k8s_test.go | 2 +- src/infra/k8s/module.go | 2 +- src/infra/logger/module.go | 2 +- src/infra/loki/client.go | 2 +- src/infra/loki/module.go | 2 +- src/infra/redis/gateway.go | 2 +- src/infra/redis/module.go | 2 +- src/infra/redis/task_queue.go | 2 +- src/infra/tracing/module.go | 2 +- src/infra/tracing/provider.go | 2 +- src/interface/controller/module.go | 12 +- .../{grpciam => grpc/iam}/lifecycle.go | 2 +- src/interface/{grpciam => grpc/iam}/module.go | 2 +- .../{grpciam => grpc/iam}/service.go | 78 +- .../{grpciam => grpc/iam}/service_test.go | 60 +- .../orchestrator}/lifecycle.go | 2 +- .../orchestrator}/module.go | 6 +- .../orchestrator}/project_statistics.go | 8 +- .../orchestrator}/service.go | 108 +- .../orchestrator}/service_test.go | 118 +- .../resource}/lifecycle.go | 2 +- .../{grpcresource => grpc/resource}/module.go | 2 +- .../resource}/service.go | 94 +- .../resource}/service_test.go | 104 +- .../runtime}/lifecycle.go | 2 +- .../{grpcruntime => grpc/runtime}/module.go | 2 +- .../{grpcruntime => grpc/runtime}/service.go | 54 +- .../runtime}/service_test.go | 2 +- .../{grpcsystem => grpc/system}/lifecycle.go | 2 +- .../{grpcsystem => grpc/system}/module.go | 2 +- .../{grpcsystem => grpc/system}/service.go | 34 +- .../system}/service_test.go | 74 +- src/interface/http/module.go | 5 +- src/interface/http/router.go | 12 - src/interface/http/server.go | 2 +- src/interface/receiver/module.go | 6 +- src/interface/worker/module.go | 24 +- src/internalclient/iamclient/client.go | 138 +-- .../orchestratorclient/client.go | 92 +- src/internalclient/resourceclient/client.go | 90 +- src/internalclient/runtimeclient/client.go | 12 +- src/internalclient/systemclient/client.go | 48 +- src/main.go | 20 +- src/module/auth/api_types.go | 10 +- src/module/auth/handler.go | 116 +- src/module/auth/handler_service.go | 2 +- src/module/auth/middleware_adapter.go | 2 +- src/module/auth/module.go | 2 +- src/module/auth/repository.go | 2 +- src/module/auth/service.go | 18 +- src/module/auth/service_test.go | 2 +- src/module/auth/token_store.go | 8 +- src/module/chaossystem/api_types.go | 2 +- src/module/chaossystem/handler.go | 30 +- src/module/chaossystem/handler_service.go | 2 +- src/module/chaossystem/module.go | 2 +- src/module/chaossystem/repository.go | 2 +- src/module/chaossystem/service.go | 2 +- src/module/container/api_types.go | 2 +- src/module/container/build_gateway.go | 2 +- src/module/container/build_gateway_test.go | 2 +- src/module/container/core.go | 2 +- src/module/container/file_store.go | 2 +- src/module/container/file_store_test.go | 2 +- src/module/container/handler.go | 204 +-- src/module/container/handler_service.go | 2 +- src/module/container/module.go | 2 +- src/module/container/repository.go | 2 +- src/module/container/resolve.go | 2 +- src/module/container/service.go | 12 +- src/module/dataset/api_types.go | 2 +- src/module/dataset/core.go | 2 +- src/module/dataset/file_store.go | 2 +- src/module/dataset/file_store_test.go | 2 +- src/module/dataset/handler.go | 184 +-- src/module/dataset/handler_service.go | 2 +- src/module/dataset/module.go | 2 +- src/module/dataset/repository.go | 2 +- src/module/dataset/resolve.go | 2 +- src/module/dataset/service.go | 6 +- src/module/docs/swagger_models.go | 8 +- src/module/evaluation/api_types.go | 12 +- src/module/evaluation/execution_query.go | 16 +- src/module/evaluation/handler.go | 56 +- src/module/evaluation/handler_service.go | 2 +- src/module/evaluation/module.go | 2 +- src/module/evaluation/repository.go | 2 +- src/module/evaluation/service.go | 40 +- src/module/evaluation/service_test.go | 8 +- src/module/execution/api_types.go | 2 +- src/module/execution/handler.go | 143 +-- src/module/execution/handler_service.go | 2 +- src/module/execution/module.go | 2 +- src/module/execution/repository.go | 2 +- src/module/execution/result_types.go | 2 +- src/module/execution/runtime_types.go | 2 +- src/module/execution/service.go | 24 +- src/module/execution/service_test.go | 6 +- src/module/group/api_types.go | 2 +- src/module/group/handler.go | 18 +- src/module/group/handler_service.go | 2 +- src/module/group/module.go | 2 +- src/module/group/repository.go | 2 +- src/module/group/service.go | 8 +- src/module/injection/api_types.go | 2 +- src/module/injection/archive.go | 2 +- src/module/injection/datapack_store.go | 2 +- src/module/injection/datapack_store_test.go | 2 +- src/module/injection/handler.go | 421 ++----- src/module/injection/handler_service.go | 2 +- src/module/injection/module.go | 2 +- src/module/injection/query_datapack_arrow.go | 2 +- .../injection/query_datapack_noarrow.go | 2 +- src/module/injection/repository.go | 2 +- src/module/injection/resolve.go | 8 +- src/module/injection/runtime_types.go | 2 +- src/module/injection/service.go | 44 +- src/module/injection/service_test.go | 6 +- src/module/injection/submit.go | 2 +- src/module/injection/time_range.go | 2 +- src/module/label/api_types.go | 2 +- src/module/label/core.go | 2 +- src/module/label/handler.go | 68 +- src/module/label/handler_service.go | 2 +- src/module/label/module.go | 2 +- src/module/label/repository.go | 2 +- src/module/label/service.go | 2 +- src/module/metric/api_types.go | 2 +- src/module/metric/handler.go | 50 +- src/module/metric/handler_service.go | 2 +- src/module/metric/module.go | 2 +- src/module/metric/repository.go | 2 +- src/module/metric/service.go | 2 +- src/module/notification/api_types.go | 2 +- src/module/notification/handler.go | 4 +- src/module/notification/handler_service.go | 2 +- src/module/notification/module.go | 2 +- src/module/notification/repository.go | 2 +- src/module/notification/service.go | 6 +- src/module/project/api_types.go | 20 +- src/module/project/handler.go | 76 +- src/module/project/handler_service.go | 2 +- src/module/project/module.go | 2 +- src/module/project/project_statistics.go | 2 +- src/module/project/repository.go | 2 +- src/module/project/service.go | 6 +- src/module/project/service_test.go | 2 +- src/module/rbac/api_types.go | 2 +- src/module/rbac/handler.go | 138 +-- src/module/rbac/handler_service.go | 2 +- src/module/rbac/module.go | 2 +- src/module/rbac/repository.go | 2 +- src/module/rbac/service.go | 2 +- src/module/rbac/service_test.go | 2 +- src/module/sdk/api_types.go | 2 +- src/module/sdk/handler.go | 34 +- src/module/sdk/models.go | 2 +- src/module/sdk/module.go | 2 +- src/module/sdk/repository.go | 2 +- src/module/sdk/service.go | 2 +- src/module/sdk/service_test.go | 2 +- src/module/system/api_types.go | 12 +- src/module/system/handler.go | 172 +-- src/module/system/handler_service.go | 2 +- src/module/system/handler_test.go | 2 +- src/module/system/module.go | 2 +- src/module/system/repository.go | 2 +- src/module/system/runtime_query.go | 14 +- src/module/system/service.go | 26 +- src/module/system/service_test.go | 2 +- src/module/systemmetric/api_types.go | 2 +- src/module/systemmetric/collector.go | 2 +- src/module/systemmetric/handler.go | 10 +- src/module/systemmetric/handler_service.go | 2 +- src/module/systemmetric/module.go | 2 +- src/module/systemmetric/repository.go | 2 +- src/module/systemmetric/service.go | 58 +- src/module/task/api_types.go | 2 +- src/module/task/handler.go | 60 +- src/module/task/handler_service.go | 2 +- src/module/task/log_service.go | 2 +- src/module/task/log_types.go | 2 +- src/module/task/loki_gateway.go | 10 +- src/module/task/module.go | 2 +- src/module/task/queue_store.go | 6 +- src/module/task/repository.go | 2 +- src/module/task/service.go | 2 +- src/module/task/service_test.go | 14 +- src/module/team/api_types.go | 8 +- src/module/team/handler.go | 76 +- src/module/team/handler_service.go | 2 +- src/module/team/module.go | 2 +- src/module/team/project_reader.go | 8 +- src/module/team/repository.go | 6 +- src/module/team/service.go | 2 +- src/module/team/service_test.go | 2 +- src/module/trace/api_types.go | 10 +- src/module/trace/handler.go | 42 +- src/module/trace/handler_service.go | 2 +- src/module/trace/module.go | 2 +- src/module/trace/repository.go | 2 +- src/module/trace/service.go | 6 +- src/module/trace/stream.go | 2 +- src/module/user/api_types.go | 14 +- src/module/user/handler.go | 64 +- src/module/user/handler_service.go | 2 +- src/module/user/module.go | 2 +- src/module/user/repository.go | 2 +- src/module/user/service.go | 12 +- src/module/user/service_test.go | 2 +- src/router/admin.go | 34 + src/router/handlers.go | 120 +- src/router/portal.go | 121 +- src/router/public.go | 1 - src/router/router.go | 9 +- src/router/router_test.go | 12 +- src/router/runtime.go | 15 - src/router/sdk.go | 90 ++ src/router/system.go | 56 - src/service/common/config_listener.go | 6 +- src/service/common/injection.go | 4 +- src/service/common/task.go | 4 +- src/service/consumer/algo_execution.go | 16 +- src/service/consumer/build_container.go | 8 +- src/service/consumer/build_datapack.go | 16 +- src/service/consumer/collect_result.go | 10 +- src/service/consumer/common.go | 6 +- src/service/consumer/config_handlers.go | 10 +- src/service/consumer/fault_injection.go | 4 +- src/service/consumer/k8s_handler.go | 18 +- src/service/consumer/monitor.go | 6 +- .../consumer/namespace_catalog_store.go | 6 +- src/service/consumer/namespace_lock_store.go | 20 +- .../consumer/namespace_status_store.go | 4 +- src/service/consumer/owner_adapter.go | 36 +- src/service/consumer/rate_limiter.go | 10 +- src/service/consumer/rate_limiter_store.go | 4 +- src/service/consumer/redis.go | 8 +- src/service/consumer/restart_pedestal.go | 8 +- src/service/consumer/runtime_deps.go | 16 +- src/service/consumer/runtime_snapshot.go | 28 +- src/service/consumer/state_store.go | 10 +- src/service/consumer/task.go | 20 +- src/service/consumer/trace.go | 14 +- src/service/initialization/consumer.go | 8 +- src/service/initialization/producer.go | 32 +- 329 files changed, 3718 insertions(+), 6220 deletions(-) delete mode 100644 .openapi-generator/typescript/client/config.json rename .openapi-generator/typescript/{sdk => }/config.json (100%) delete mode 100644 .openapi-generator/typescript/sdk/templates/package.mustache delete mode 100644 .openapi-generator/typescript/sdk/templates/tsconfig.mustache rename .openapi-generator/typescript/{client => }/templates/package.mustache (100%) rename .openapi-generator/typescript/{client => }/templates/tsconfig.mustache (100%) create mode 100644 scripts/command/src/cli/sdk.py create mode 100644 scripts/command/src/swagger/apifox.py delete mode 100644 scripts/generate_swagger_audience_report.py delete mode 100644 scripts/migrate_swagger_comments.py rename scripts/{regression-test.sh => test-regression.sh} (98%) delete mode 100644 src/app/compat_options.go delete mode 100644 src/app/producer_init.go rename src/interface/{grpciam => grpc/iam}/lifecycle.go (98%) rename src/interface/{grpciam => grpc/iam}/module.go (85%) rename src/interface/{grpciam => grpc/iam}/service.go (93%) rename src/interface/{grpciam => grpc/iam}/service_test.go (82%) rename src/interface/{grpcorchestrator => grpc/orchestrator}/lifecycle.go (98%) rename src/interface/{grpcorchestrator => grpc/orchestrator}/module.go (68%) rename src/interface/{grpcorchestrator => grpc/orchestrator}/project_statistics.go (68%) rename src/interface/{grpcorchestrator => grpc/orchestrator}/service.go (85%) rename src/interface/{grpcorchestrator => grpc/orchestrator}/service_test.go (87%) rename src/interface/{grpcresource => grpc/resource}/lifecycle.go (98%) rename src/interface/{grpcresource => grpc/resource}/module.go (83%) rename src/interface/{grpcresource => grpc/resource}/service.go (80%) rename src/interface/{grpcresource => grpc/resource}/service_test.go (77%) rename src/interface/{grpcruntime => grpc/runtime}/lifecycle.go (98%) rename src/interface/{grpcruntime => grpc/runtime}/module.go (83%) rename src/interface/{grpcruntime => grpc/runtime}/service.go (83%) rename src/interface/{grpcruntime => grpc/runtime}/service_test.go (98%) rename src/interface/{grpcsystem => grpc/system}/lifecycle.go (98%) rename src/interface/{grpcsystem => grpc/system}/module.go (84%) rename src/interface/{grpcsystem => grpc/system}/service.go (82%) rename src/interface/{grpcsystem => grpc/system}/service_test.go (64%) delete mode 100644 src/interface/http/router.go delete mode 100644 src/router/runtime.go delete mode 100644 src/router/system.go diff --git a/.openapi-generator/typescript/client/config.json b/.openapi-generator/typescript/client/config.json deleted file mode 100644 index 88cd130d..00000000 --- a/.openapi-generator/typescript/client/config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "npmName": "@OperationsPAI/client", - "npmVersion": "0.0.0", - "npmDescription": "TypeScript client for RCABench API", - "githost": "github.com", - "gitUserId": "OperationsPAI", - "gitRepoId": "AegisLab", - "licenseName": "MIT", - "supportsES6": true, - "modelPropertyNaming": "original", - "withInterfaces": true, - "useSingleRequestParameter": true, - "typescriptThreePlus": true, - "enumNameSuffix": "", - "enumPropertyNaming": "original", - "hideGenerationTimestamp": true, - "disallowAdditionalPropertiesIfNotPresent": false, - "sortParamsByRequiredFlag": true, - "stringEnums": true -} \ No newline at end of file diff --git a/.openapi-generator/typescript/sdk/config.json b/.openapi-generator/typescript/config.json similarity index 100% rename from .openapi-generator/typescript/sdk/config.json rename to .openapi-generator/typescript/config.json diff --git a/.openapi-generator/typescript/sdk/templates/package.mustache b/.openapi-generator/typescript/sdk/templates/package.mustache deleted file mode 100644 index 136825d6..00000000 --- a/.openapi-generator/typescript/sdk/templates/package.mustache +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "{{npmName}}", - "version": "{{npmVersion}}", - "description": "OpenAPI client for {{npmName}}", - "author": "OpenAPI-Generator Contributors", - "repository": { - "type": "git", - "url": "https://{{gitHost}}/{{gitUserId}}/{{gitRepoId}}.git" - }, - "publishConfig": { - "registry": "https://npm.pkg.github.com" - }, - "keywords": [ - "axios", - "typescript", - "openapi-client", - "openapi-generator", - "{{npmName}}" - ], - "license": "{{licenseName}}", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", -{{#supportsES6}} - "module": "./dist/esm/index.js", - "sideEffects": false, -{{/supportsES6}} - "exports": { - ".": { - "types": "./dist/index.d.ts", - {{#supportsES6}} - "import": "./dist/esm/index.js", - {{/supportsES6}} - "require": "./dist/index.js" - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc{{#supportsES6}} && tsc -p tsconfig.esm.json{{/supportsES6}}", - "prepare": "npm run build" - }, - "dependencies": { - "axios": "{{axiosVersion}}" - {{#withAWSV4Signature}} - "aws4-axios": "^3.3.4" - {{/withAWSV4Signature}} - }, - "devDependencies": { - "@types/node": "12.11.5 - 12.20.42", - "typescript": "^4.0 || ^5.0" - }{{#npmRepository}},{{/npmRepository}} -{{#npmRepository}} - "publishConfig": { - "registry": "{{npmRepository}}" - } -{{/npmRepository}} -} diff --git a/.openapi-generator/typescript/sdk/templates/tsconfig.mustache b/.openapi-generator/typescript/sdk/templates/tsconfig.mustache deleted file mode 100644 index 2a661dbb..00000000 --- a/.openapi-generator/typescript/sdk/templates/tsconfig.mustache +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "target": "ES2020", - "module": "commonjs", - "outDir": "./dist", - "rootDir": "./", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - {{#supportsES6}} - "moduleResolution": "node", - "lib": [ - "ES2020", - "DOM", - "DOM.Iterable" - ], - {{/supportsES6}} - {{^supportsES6}} - "lib": [ - "es6", - "dom" - ], - {{/supportsES6}} - }, -"include": ["*.ts"], - "exclude": [ - "dist", - "node_modules", - "**/*.test.ts" - ] -} diff --git a/.openapi-generator/typescript/client/templates/package.mustache b/.openapi-generator/typescript/templates/package.mustache similarity index 100% rename from .openapi-generator/typescript/client/templates/package.mustache rename to .openapi-generator/typescript/templates/package.mustache diff --git a/.openapi-generator/typescript/client/templates/tsconfig.mustache b/.openapi-generator/typescript/templates/tsconfig.mustache similarity index 100% rename from .openapi-generator/typescript/client/templates/tsconfig.mustache rename to .openapi-generator/typescript/templates/tsconfig.mustache diff --git a/README.md b/README.md index 7d6e6d34..5a6a7980 100644 --- a/README.md +++ b/README.md @@ -160,23 +160,30 @@ cd src && go run ./cmd/api-gateway -conf ./config.dev.toml -port 8082 ```bash # Check prerequisites -make check-prerequisites +just check-prerequisites # Deploy to Kubernetes cluster -make run +just run +``` -# Check deployment status -make status +If you use `scripts/start.sh` directly, the external install URLs can now be overridden with env vars such as: -# View logs -make logs -``` +- `CERT_MANAGER_MANIFEST_URL` +- `CHAOS_MESH_REPO_URL` +- `CLICKSTACK_REPO_URL` +- `OPEN_TELEMETRY_REPO_URL` +- `OTEL_DEMO_REPO_URL` +- `JUICEFS_REPO_URL` +- `TEST_HTTP_PROXY` +- `TEST_HTTPS_PROXY` +- `TEST_NO_PROXY` ## 📖 Documentation - **[Report Index](docs/report-index.md)**: Consolidated backend refactor, runtime, governance, SDK/auth, and validation notes - **[Refactor TODO](docs/todo.md)**: Source-of-truth task list and final acceptance checklist - **[API Key Auth TODO](docs/api-key-auth-execution-todo.md)**: Key ID / Key Secret auth execution checklist and signing contract +- **[Package Rename TODO](docs/package-rename-todo.md)**: Go package naming cleanup record for `interface/module/infra/app` - **[Frontend Redesign](docs/frontend-redesign.md)**: Frontend redesign plan and IA notes - **[Frontend UI Guidelines](docs/frontend-ui-guidelines.md)**: Frontend visual/system guidelines @@ -377,7 +384,7 @@ If the problem only appears in split-service mode, then also check: Start here: - `src/internalclient/*` -- `src/interface/grpc*/*` +- `src/interface/grpc/*` - `src/app/{gateway,iam,resource,orchestrator,runtime,system}/*` #### async runtime issues @@ -407,7 +414,7 @@ Split-service path: - `src/app/gateway/{auth,user,rbac,team}_services.go` - `src/internalclient/iamclient/*` -- `src/interface/grpciam/*` +- `src/interface/grpc/iam/*` #### Project / Label / Container / Dataset @@ -422,7 +429,7 @@ Split-service path: - `src/app/gateway/resource_services.go` - `src/internalclient/resourceclient/*` -- `src/interface/grpcresource/*` +- `src/interface/grpc/resource/*` #### Injection / Execution / Task / Trace / Group / Notification @@ -439,7 +446,7 @@ Split-service path: - `src/app/gateway/orchestrator_services.go` - `src/internalclient/orchestratorclient/*` -- `src/interface/grpcorchestrator/*` +- `src/interface/grpc/orchestrator/*` - `src/service/consumer/*` #### System / Metrics / Monitor / Config / Audit @@ -454,8 +461,8 @@ Split-service path: - `src/app/gateway/system_services.go` - `src/internalclient/systemclient/*` - `src/internalclient/runtimeclient/*` -- `src/interface/grpcsystem/*` -- `src/interface/grpcruntime/*` +- `src/interface/grpc/system/*` +- `src/interface/grpc/runtime/*` #### Runtime / K8s / Build / Helm / Chaos @@ -477,10 +484,17 @@ Check: cd src go build -o rcabench main.go -# Generate API documentation -make swagger +# Regenerate OpenAPI / Swagger artifacts +cd .. +just swagger-init 1.2.3 + +# Generate SDK packages +just generate-portal 1.2.3 +just generate-admin 1.2.3 +just generate-python-sdk 1.2.3 # Run tests +cd src go test ./... ``` @@ -496,17 +510,21 @@ pip install -e . python -m pytest tests/ ``` -## 📦 Available Make Targets +## 📦 Available Just Recipes ```bash -make help # Show all available commands -make run # Build and deploy application -make local-debug # Start local debugging environment -make build # Build application only -make status # Check application status -make logs # View application logs -make clean-all # Clean all resources -make swagger # Generate API documentation +just --list # Show all available commands +just run # Deploy to the configured Kubernetes target +just local-deploy # Boot local infra dependencies with Docker Compose +just local-debug # Start local producer+consumer debug process +just swagger-init 1.2.3 # Regenerate OpenAPI / Swagger artifacts +just generate-portal 1.2.3 # Generate portal TypeScript SDK +just generate-admin 1.2.3 # Generate admin TypeScript SDK +just generate-python-sdk 1.2.3 # Generate Python SDK +just release-portal 1.2.3 # Generate release-ready portal TypeScript SDK +just release-admin 1.2.3 # Generate release-ready admin TypeScript SDK +just release-python-sdk 1.2.3 # Generate release-ready Python SDK +just test-regression # Run the Python SDK regression workflow ``` ## 🐛 Troubleshooting @@ -519,8 +537,8 @@ make swagger # Generate API documentation # Check database status kubectl get pods | grep mysql - # Reset database - make reset-db + # Re-run the local debug stack after fixing config/env + just local-debug ``` 2. **Pod Scheduling Issues** @@ -575,7 +593,7 @@ cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecy ### Getting Help - Review the consolidated notes in `docs/report-index.md` -- Review application logs with `make logs` +- Run `just --list` to inspect the supported local workflows - Verify configuration in `src/config.dev.toml` ## 📊 Performance Considerations diff --git a/docs/report-index.md b/docs/report-index.md index 34662513..a0d8186a 100644 --- a/docs/report-index.md +++ b/docs/report-index.md @@ -1,61 +1,51 @@ # Report Index -> 更新时间:2026-04-18 -> 目的:把本轮后端主线重构、微服务收尾、治理约定、运行口径、SDK/鉴权要点收口到少量总贴文档里。 +> 更新时间:2026-04-19 +> 目的:把当前可运行架构、保留文档、SDK/鉴权口径、调试方式和非阻塞尾项收口到一个总索引里。 -## 1. 最终结论 +## 1. 当前状态 -- Fx + module + infra 主线已完成,`producer / consumer / both` 与六服务入口都已跑通。 -- 微服务主线可视为完成,当前已形成 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` 六个明确边界。 -- 旧运行态兼容面已经完成仓库级清扫:`service/producer`、`handlers/system`、`database.DB`、`GetGateway()`、`redisinfra.GetGateway()` 这批模式已退出主线生产代码。 -- 当前仅剩 1 个未勾项:确认 Fx 启动日志是否可接受;这是人工验收,不阻塞代码主线收口。 +- Fx + module + infra 主线已完成 +- `producer / consumer / both` 三种模式已跑通 +- 六服务入口已落地:`api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` +- 旧兼容层已退出主线运行态 +- SDK 路由已统一收口到 `src/router/sdk.go` +- runtime 上传接口已并入 `src/router/sdk.go`,并只保留 `RequireServiceTokenAuth()` +- `portal / admin / sdk / runtime` 四类 audience 已完成一轮最终对齐补扫 ## 2. 保留文档 - `docs/todo.md` - - 主 TODO 与最终验收清单,仍作为执行源文档。 -- `docs/report-index.md` - - 当前总索引与汇总版说明。 -- `docs/frontend-redesign.md` - - 前端重设计文档,属于独立主题,未纳入本次后端文档合并。 -- `docs/frontend-ui-guidelines.md` - - 前端 UI 规范,属于独立主题,未纳入本次后端文档合并。 - -## 3. 服务边界与 ownership 总结 + - 最终执行状态、SDK 路由核对、验收命令、主线完成/非阻塞剩余项 +- `docs/package-rename-todo.md` + - `src/interface/grpc/*` 与包名统一记录 +- `docs/api-key-auth-execution-todo.md` + - API key / Key ID / Key Secret 执行记录 +- `docs/python-runtime-wrapper-design.md` + - Python runtime wrapper 职责边界设计 +- `docs/python-runtime-wrapper-todo.md` + - runtime wrapper 执行记录 +- `docs/swagger-audience-unmarked-report.md` + - Swagger audience 当前对齐状态、例外项与剩余 1 条空标记路由 + +## 3. 服务边界 ### 3.1 六服务职责 - `api-gateway` - - 对外唯一 HTTP/OpenAPI 入口。 - - 负责 audience、鉴权、参数校验、统一错误壳、聚合响应。 - - 不直接查 DB,不直接做 K8s / Helm / BuildKit 业务判断。 + - 对外唯一 HTTP/OpenAPI 入口;做 audience、鉴权、聚合与边缘协议适配 - `iam-service` - - 承接 `auth / user / rbac / team / access key`。 - - 负责 `AK/SK -> token`、token verify、permission check。 + - `auth / user / rbac / team / api key` - `resource-service` - - 承接 `project / label / container / dataset / evaluation` 元数据与查询视图。 + - `project / label / container / dataset / evaluation / chaos-system` - `orchestrator-service` - - 承接 `execution / injection submit`、`task / trace / retry / dead-letter / cancel` 控制面。 + - `execution / injection / task / trace / notification / group` - `runtime-worker-service` - - 承接 Redis 异步消费、K8s/BuildKit/Helm/Chaos 执行态、limiter、namespace lock、runtime monitor。 - - 异步执行链继续保留 Redis,不改为同步执行 RPC。 + - Redis 异步执行链、K8s / Helm / BuildKit / Chaos 运行态 - `system-service` - - 承接 `config / audit / health / monitor / metrics` 运维控制面。 + - `config / audit / health / monitor / metrics` -### 3.2 owner 约束 - -- `iam-service` - - `users`、`roles`、`permissions`、`resources`、`teams`、`access_keys` 及其授权关系。 -- `resource-service` - - `projects`、`labels`、`containers`、`datasets`、`evaluations` 与资源元数据关系。 -- `orchestrator-service` - - `tasks`、`traces`、`fault_injections`、`executions`、重试/死信/工作流控制面。 -- `system-service` - - `dynamic_configs`、`config_histories`、`audit_logs`、`system_metrics` 等运维数据。 -- `runtime-worker-service` - - Redis runtime state、K8s/build/helm 执行态,不新增跨 owner MySQL 写入。 - -### 3.3 依赖规则 +### 3.2 基本约束 - 允许:`cmd -> app -> interface/module/infra/internalclient` - 允许:`interface -> module/internalclient` @@ -65,666 +55,116 @@ - 禁止:`module A -> module B repository` - 禁止:非 owner 服务新增直接写库逻辑 -## 4. 本地运行与发布口径 - -### 4.1 六服务本地入口 - -| Service | Command | Default Port | -| --- | --- | --- | -| `api-gateway` | `go run ./src/cmd/api-gateway -conf ./src/config.dev.toml -port 8082` | `8082` | -| `iam-service` | `go run ./src/cmd/iam-service -conf ./src/config.dev.toml` | `9091` | -| `orchestrator-service` | `go run ./src/cmd/orchestrator-service -conf ./src/config.dev.toml` | `9092` | -| `resource-service` | `go run ./src/cmd/resource-service -conf ./src/config.dev.toml` | `9093` | -| `runtime-worker-service` | `go run ./src/cmd/runtime-worker-service -conf ./src/config.dev.toml` | `9094` | -| `system-service` | `go run ./src/cmd/system-service -conf ./src/config.dev.toml` | `9095` | - -### 4.2 本地依赖与启动顺序 +## 4. SDK / 鉴权口径 -- 先起基础依赖: - - `docker compose up -d redis mysql etcd jaeger buildkitd loki prometheus grafana` -- 如需本地全量六服务: - - `docker compose -f docker-compose.yaml -f docker-compose.microservices.yaml up --build` -- 手动顺序建议: - - `iam-service` - - `orchestrator-service` - - `resource-service` - - `runtime-worker-service` - - `system-service` - - `api-gateway` +### 4.1 SDK 路由口径 -### 4.3 发布骨架 +- 所有 `@x-api-type {"sdk":"true"}` 的运行态入口统一由 `src/router/sdk.go` 承接 +- `runtime` 视为 SDK 路由中的一个专门子集,但鉴权语义单独保留 +- runtime 上传接口: + - `POST /api/v2/executions/{execution_id}/detector_results` + - `POST /api/v2/executions/{execution_id}/granularity_results` +- 上述 runtime 路由当前仅要求: + - `RequireServiceTokenAuth()` +- 不再叠加 `JWTAuth()` -- 本地 compose 骨架:`docker-compose.microservices.yaml` -- Kubernetes skeleton:`manifests/microservices/aegislab-microservices.yaml` -- Helm 发布主口径:`helm/templates/{configmap,service,deployment}.yaml` -- 当前 Helm 已按六服务拓扑渲染通过:`helm template aegislab ./helm` +### 4.2 API key -## 5. Health / Readiness 约定 - -- `api-gateway` - - 协议:HTTP - - 探针:`GET /system/health` - - 默认端口:`8082` -- 内部 gRPC 服务 - - 服务:`iam-service`、`orchestrator-service`、`resource-service`、`runtime-worker-service`、`system-service` - - 协议:gRPC health checking protocol - - 默认端口:`9091` ~ `9095` -- 启动前 target 校验 - - `api-gateway` 校验 `clients.iam.target`、`clients.orchestrator.target`、`clients.resource.target`、`clients.system.target` - - `runtime-worker-service` 校验 `clients.orchestrator.target` - - `resource-service` 校验 `clients.orchestrator.target` - - `system-service` 校验 `clients.runtime.target` -- 口径 - - 缺 target 时直接启动失败,不把配置缺失留给 readiness 长期兜底 - -## 6. 治理约定 - -### 6.1 Request ID - -- 外部 HTTP: - - 优先读取 `X-Request-Id` - - 缺失时由 gateway 生成并回写响应头 -- 内部 gRPC: - - 统一 metadata key:`x-request-id` -- 当前已落地: - - `src/router/router.go` 挂 request-id middleware - - `src/internalclient/*` 统一透传 - - `src/interface/grpc*` 统一提取/补齐并回写 header - -### 6.2 错误码 - -- HTTP: - - `401`、`403`、`400`、`404`、`409`、`500` -- gRPC: - - `Unauthenticated` - - `PermissionDenied` - - `InvalidArgument` - - `NotFound` - - `AlreadyExists` - - 其他统一 `Internal` - -### 6.3 观测与配置 - -- 基础标签: - - `service.name` - - `service.role` - - `request.id` - - `user.id` - - `project.id` - - `trace.id` - - `task.id` - - `group.id` -- 内部 client target 主键: - - `clients.iam.target` - - `clients.resource.target` - - `clients.orchestrator.target` - - `clients.runtime.target` - - `clients.system.target` -- 服务监听主键: - - `iam.grpc.addr` - - `resource.grpc.addr` - - `orchestrator.grpc.addr` - - `runtime_worker.grpc.addr` - - `system.grpc.addr` - -## 7. SDK / 鉴权 / Swagger 总结 - -### 7.1 Swagger audience 现状 - -- 扫描总操作数:`173` -- 已标记操作:`100` -- 空 `@x-api-type {}`:`73` -- 缺失 `@x-api-type`:`0` -- 已标 audience 统计: - - `sdk=5` - - `portal=43` - - `admin=58` - -### 7.2 AK/SK -> token 规范 - -- 交换接口: - - `POST /api/v2/auth/access-key/token` -- 只有这个接口直接使用 `secret_key` -- 业务 API 继续统一使用: - - `Authorization: Bearer ` - -必需请求头: - -- `X-Access-Key` -- `X-Timestamp` -- `X-Nonce` -- `X-Signature` - -canonical string: +- 入口:`POST /api/v2/auth/api-key/token` +- 请求头: + - `X-Key-Id` + - `X-Timestamp` + - `X-Nonce` + - `X-Signature` +- canonical string: ```text METHOD PATH -ACCESS_KEY TIMESTAMP NONCE +SHA256(BODY) ``` -签名算法: - -```text -signature = hex(hmac_sha256(secret_key, canonical_string)) -``` - -服务端规则: - -- 时间窗:`+- 5 minutes` -- nonce 单次使用 -- disabled / deleted / expired access key 不能换 token -- replay 防护依赖 Redis nonce reservation - -### 7.3 `aegisctl` 鉴权约定 - -- `aegisctl auth login --access-key ... --secret-key ...` - - 走 AK/SK 签名换 token -- `aegisctl auth inspect` - - 查看本地 auth context -- `aegisctl auth sign-debug` - - 输出 canonical string、签名头、curl 示例 -- `aegisctl auth sign-debug --execute` - - 直接发起换 token 请求 -- `aegisctl auth sign-debug --execute --save-context` - - 成功后把 token 落当前 context - -## 8. Model / DTO / repository 收口总结 - -- `src/database` 已整体迁到 `src/model` -- DB 初始化、迁移、生命周期已转到 `src/infra/db` -- 模块专用 API 契约已大量下沉到 `src/module/*/api_types.go` -- 全局 `src/dto/*` 已压缩为极薄共享层,只保留分页/搜索/统一响应/少量跨模块运行时载荷 -- 已删除一批空心化旧仓储文件,模块专用 DB 访问回收到各自 `src/module/*/repository.go` -- 当前 `src/repository/*` 只保留仍有跨模块边界价值的共享查询能力 - -## 9. 当前验收状态 - -- 默认回归: - - `cd src && go test ./...` -- Producer Fx 图校验与 HTTP 主路径: - - `cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke'` -- Consumer / Both 生命周期冒烟: - - `cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions'` -- 路由 / 文档主路径: - - `cd src && go test ./router ./docs ./interface/http` -- 真实 K8s 集群验收: - - `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` - -## 10. 当前唯一未完成人工项 - -- `docs/todo.md` - - `确认 Fx 生成的启动日志是否可接受` - - 性质:人工验收 - - 状态:非阻塞 - - 不影响当前“主线完成”判断 - -## 11. 仓库级补扫结果 - -### 11.1 已确认清空的旧兼容面 - -对 `src` 生产代码补扫后,以下模式当前为 `0` 命中: - -```text -service/producer = 0 -handlers/system = 0 -database.DB = 0 -GetGateway( = 0 -redisinfra.GetGateway = 0 -``` - -说明: - -- 旧 `service/producer` 兼容层已退出运行态代码 -- 旧 `handlers/system` 包级入口已退出运行态代码 -- 全局 `database.DB` 已不再残留在主线包中 -- 旧 infra 全局 gateway fallback 已不再残留在主线包中 - -### 11.2 `context.Background()` 补扫 - -在 `src/app src/interface src/module src/service src/router src/middleware` 范围补扫后: - -- 生产代码未发现新的 `context.Background()` 残留 -- 当前命中均来自测试文件 - -## 12. 微服务主线完成面 - -### 12.1 服务入口 - -当前统一二进制已支持: +- 业务 API 统一使用 `Authorization: Bearer ` +- `aegisctl` 与 Python SDK 已统一到这套签名换 token 流程 + +### 4.3 Python SDK / Runtime Client + +- `RCABenchClient` + - 公共/业务 API client;通过 `Key ID / Key Secret` 从环境变量换 token +- `RCABenchRuntimeClient` + - runtime service-token-only client + - 只保持 thin client,不承载 wrapper 调度语义 + +### 4.4 SDK generation / Apifox + +- `portal` + - 当前只生成 TypeScript SDK +- `admin` + - 当前只生成 TypeScript SDK +- `sdk` + - 当前只生成 Python SDK +- `runtime` + - 当前作为 `sdk` 语义下的运行态子集保留在文档产物里,不单独作为 Apifox 上传目标 +- `swagger init` 现在支持可选上传到 Apifox,但只支持三类目标: + - `sdk` + - `portal` + - `admin` +- SDK 生成主入口改为显式 option 风格: + - `sdk typescript --target portal|admin --env local|release --version ` + - `sdk python --target sdk --env local|release --version ` +- TypeScript OpenAPI Generator 模板目录已压平为: + - `.openapi-generator/typescript/config.json` + - `.openapi-generator/typescript/templates/*` +- `scripts/command` 里的 Apifox / SDK / 测试安装 URL 统一改为优先从 `scripts/command/settings.toml` 读取 +- `scripts/start.sh` 里的外部安装地址与测试代理也已改成顶部 env override 变量 +- 不再需要单独的 `--upload-apifox` +- 只有显式传入 `--apifox-target ...` 时才会上传 +- 若要一次上传全部,使用: + - `--apifox-target all` + +## 5. 启动与调试 + +### 5.1 什么时候用哪种模式 - `producer` + - 调 HTTP、router、handler、Swagger、Portal/Admin API - `consumer` + - 调 worker / controller / receiver / runtime 执行链 - `both` -- `api-gateway` -- `iam-service` -- `orchestrator-service` -- `resource-service` -- `runtime-worker-service` -- `system-service` - -### 12.2 internal client 边界 - -当前已落地: - -- gateway -> IAM -- gateway -> Resource -- gateway -> Orchestrator -- gateway -> System -- system -> Runtime -- resource/evaluation -> Orchestrator -- runtime -> Orchestrator - -关键目录: - -- `src/internalclient/iamclient` -- `src/internalclient/resourceclient` -- `src/internalclient/orchestratorclient` -- `src/internalclient/systemclient` -- `src/internalclient/runtimeclient` - -### 12.3 dedicated service 收口状态 - -- dedicated `api-gateway` 已不再静默回退本地 owner service -- `resource-service` 已通过 remote query/source 收口 project statistics 与 evaluation 查询 -- `system-service` 已通过 runtime RPC 收口 namespace locks / queued tasks -- `runtime-worker-service` 已通过 remote owner option 收口 orchestrator owner 操作 -- `api-gateway` 的 team / auth / user / rbac / label / chaos-system / task / trace / group / notification 主路径已收口到内部服务边界 - -## 13. 当前仍剩余,但不阻塞主线 - -- 兼容入口 `producer / consumer / both` 内部仍可继续压缩本地 owner 组合 -- 少量跨服务 DB 直查/直写仍可继续按 owner 深清 -- 发布层后续仍可继续细化环境参数、镜像策略、HPA、Ingress 与 values 编排 - -## 14. 建议下一阶段顺序 - -1. 继续清兼容入口里的本地 owner 组合面 -2. 继续清跨服务 DB 直查/直写 -3. 做版本级环境参数与发布编排抛光 - -## 15. 开发与调试说明 - -### 15.1 先选调试模式 - -日常开发现在建议按下面三种模式选: - -- `producer` - - 适合只调 HTTP/API、Swagger、handler/service 主链 - - 不需要 runtime worker 异步消费时优先用它 -- `both` - - 适合本地联调 submit -> queue -> worker -> query 的完整闭环 - - 一次起 HTTP + worker,最省事 - - 注意:`both` 不是六服务模式,不会同时起 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` + - 调本地 submit -> queue -> worker -> query 闭环 - 六服务模式 - - 适合调试微服务边界、internal client、remote-first 路径、服务 ownership - - 需要确认 gateway 是否真的走 gRPC、某个 dedicated service 是否不再回退本地实现时,用这一套 - -简单建议: - -- 改接口/页面联调:先用 `producer` -- 改异步执行链:先用 `both` -- 改 internal client / gRPC / 服务边界:直接用六服务模式 - -### 15.2 本地基础依赖 - -先起基础依赖: - -```bash -docker compose up -d redis mysql etcd jaeger buildkitd loki prometheus grafana -``` - -配置主文件: - -- `src/config.dev.toml` - -重点配置: - -- MySQL / Redis / Etcd / Loki / BuildKit 连接 -- `clients.iam.target` -- `clients.resource.target` -- `clients.orchestrator.target` -- `clients.runtime.target` -- `clients.system.target` -- `iam.grpc.addr` -- `resource.grpc.addr` -- `orchestrator.grpc.addr` -- `runtime_worker.grpc.addr` -- `system.grpc.addr` - -### 15.3 最常用启动方式 - -#### A. 只调 HTTP - -```bash -cd src && go run . producer -conf ./config.dev.toml -port 8082 -``` - -适合: - -- router / handler / module service -- Swagger / OpenAPI -- Portal / Admin / SDK HTTP 联调 - -#### B. 调完整单机闭环 - -```bash -cd src && go run . both -conf ./config.dev.toml -port 8082 -``` - -适合: - -- execution / injection submit -- queue 消费 -- task / trace / logs 主链 + - 调 internal gRPC、owner 边界、remote-first 路径 -#### C. 调微服务边界 +说明:`both` 不是“同时启动六服务”,而是单体 HTTP + worker 组合模式。 -建议顺序: +### 5.2 六服务本地入口 -```bash -# terminal 1 -cd src && go run ./cmd/iam-service -conf ./config.dev.toml - -# terminal 2 -cd src && go run ./cmd/orchestrator-service -conf ./config.dev.toml - -# terminal 3 -cd src && go run ./cmd/resource-service -conf ./config.dev.toml - -# terminal 4 -cd src && go run ./cmd/runtime-worker-service -conf ./config.dev.toml - -# terminal 5 -cd src && go run ./cmd/system-service -conf ./config.dev.toml - -# terminal 6 -cd src && go run ./cmd/api-gateway -conf ./config.dev.toml -port 8082 -``` - -如果只想调某一条边界,不需要六个都起: - -- 调 auth/user/rbac/access key:起 `iam-service` + `api-gateway` -- 调 project/container/dataset/evaluation:起 `resource-service` + `api-gateway` -- 调 submit/task/trace:起 `orchestrator-service` + `api-gateway` -- 调 monitor/config/audit:起 `system-service` + `api-gateway` -- 调 worker/runtime:起 `runtime-worker-service`,必要时再带 `orchestrator-service` - -### 15.4 如何判断现在该打在哪一层断点 - -#### HTTP 问题 - -优先看: - -- `src/router/*` -- `src/module/*/handler.go` -- `src/module/*/service.go` - -如果是 dedicated gateway 路径,再看: - -- `src/app/gateway/*` -- `src/internalclient/*` - -判断原则: - -- 请求没进业务:看 router / middleware / handler -- 请求进了业务但结果不对:看 module service / repository -- dedicated gateway 下结果和单体模式不同:看 `app/gateway` remote-aware 装配和 `internalclient/*` - -#### gRPC / 微服务边界问题 - -优先看: - -- `src/internalclient/*` -- `src/interface/grpc*/*` -- 对应 `src/app/{gateway,iam,resource,orchestrator,runtime,system}/*` - -判断原则: - -- 调用没发出去:看 internal client target、dial、interceptor -- 服务收不到:看 grpc service registration / lifecycle -- dedicated service 启动就失败:先查 target 配置是否缺失 - -#### 异步执行链问题 - -优先看: - -- `src/interface/worker/*` -- `src/interface/controller/*` -- `src/service/consumer/*` -- `src/module/task/*` -- `src/module/execution/*` -- `src/module/injection/*` -- `src/infra/k8s/*` - -判断原则: - -- submit 成功但没消费:先看 Redis / consumer -- 消费了但没执行:看 runtime owner、k8s/build/helm gateway -- 执行了但状态没回写:看 orchestrator owner facade / consumer owner adapter - -### 15.5 快速验活命令 - -HTTP: - -```bash -curl -I http://127.0.0.1:8082/docs/doc.json -curl -i http://127.0.0.1:8082/system/health -``` - -gRPC: - -```bash -grpcurl -plaintext 127.0.0.1:9091 list -grpcurl -plaintext 127.0.0.1:9092 list -grpcurl -plaintext 127.0.0.1:9093 list -grpcurl -plaintext 127.0.0.1:9094 list -grpcurl -plaintext 127.0.0.1:9095 list -``` - -### 15.6 推荐调试顺序 - -遇到问题时建议固定按这条顺序排: - -1. 服务有没有起来 -2. 配置 target/addr 对不对 -3. 请求到底走的是本地还是 remote -4. request-id 是否贯通 -5. 业务 service 是否收到正确参数 -6. infra gateway / DB / Redis / K8s 是否返回异常 - -### 15.7 现在最重要的几个判断点 - -- 调 dedicated `api-gateway` 时,不要默认它会静默回退本地 owner service -- 调 `system-service` / `runtime-worker-service` 时,先确认对应 `clients.*.target` 已配 -- 调 submit / task / trace 闭环时,优先用 `both` -- 调 ownership / internal RPC 时,优先用六服务模式 -- 调 repository 逻辑时,优先从各模块 `src/module/*/repository.go` 看,不要再去旧 compat 层找 - -### 15.8 常用回归命令 - -```bash -cd src && go test ./... -cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke' -cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions' -cd src && go test ./router ./docs ./interface/http -``` - -真实 K8s 集群验收: - -```bash -cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration -``` - -### 15.9 一句话建议 - -- 大多数日常功能开发:先 `producer` -- 需要异步闭环:用 `both` -- 需要查微服务边界:直接六服务 -- 查不清时先看 `app/*` 装配,再看 `module/*/service.go`,最后看 `infra/*` - -### 15.10 按模块分类的 debug 路线图 - -#### Auth / User / RBAC / Team - -先看: - -- `src/module/auth/handler.go` -- `src/module/auth/service.go` -- `src/module/auth/repository.go` -- `src/module/user/handler.go` -- `src/module/user/service.go` -- `src/module/user/repository.go` -- `src/module/rbac/handler.go` -- `src/module/rbac/service.go` -- `src/module/rbac/repository.go` -- `src/module/team/handler.go` -- `src/module/team/service.go` -- `src/module/team/repository.go` - -如果是 dedicated gateway 下的认证/权限问题,再看: - -- `src/app/gateway/auth_services.go` -- `src/app/gateway/user_services.go` -- `src/app/gateway/rbac_services.go` -- `src/app/gateway/team_services.go` -- `src/internalclient/iamclient/*` -- `src/interface/grpciam/*` - -常见问题先查: - -- 登录/换 token:`auth/service.go` + `iamclient` -- 权限不对:`middleware/*` + `rbac/service.go` -- team project/member 视图不对:`team/service.go` + remote project reader - -#### Project / Label / Container / Dataset - -先看: - -- `src/module/project/handler.go` -- `src/module/project/service.go` -- `src/module/project/repository.go` -- `src/module/label/handler.go` -- `src/module/label/service.go` -- `src/module/label/repository.go` -- `src/module/container/handler.go` -- `src/module/container/service.go` -- `src/module/container/repository.go` -- `src/module/dataset/handler.go` -- `src/module/dataset/service.go` -- `src/module/dataset/repository.go` - -如果是 dedicated gateway / resource-service 边界问题,再看: - -- `src/app/gateway/resource_services.go` -- `src/internalclient/resourceclient/*` -- `src/interface/grpcresource/*` - -常见问题先查: - -- list/detail 不对:各模块 `repository.go` 查询条件 -- label 关系不对:`project/container/dataset` service 里的 label 管理逻辑 -- 统计字段不对:`project` statistics source 与 orchestrator/resource 边界 - -#### Injection / Execution / Task / Trace / Group / Notification - -先看: - -- `src/module/injection/handler.go` -- `src/module/injection/service.go` -- `src/module/injection/repository.go` -- `src/module/execution/handler.go` -- `src/module/execution/service.go` -- `src/module/execution/repository.go` -- `src/module/task/handler.go` -- `src/module/task/service.go` -- `src/module/task/repository.go` -- `src/module/trace/handler.go` -- `src/module/trace/service.go` -- `src/module/group/handler.go` -- `src/module/group/service.go` -- `src/module/notification/handler.go` -- `src/module/notification/service.go` - -如果是 submit / task / trace / stream 走向问题,再看: - -- `src/app/gateway/orchestrator_services.go` -- `src/internalclient/orchestratorclient/*` -- `src/interface/grpcorchestrator/*` - -如果是异步执行闭环问题,再补看: - -- `src/service/consumer/*` -- `src/interface/worker/*` -- `src/interface/controller/*` - -常见问题先查: - -- submit 成功但 task 不生成:`injection/execution service` -> orchestrator facade -- task 有了但状态不推进:`service/consumer` + owner adapter -- trace/group/notification stream 不对:`orchestrator_services.go` + stream read RPC -- task logs WebSocket 不对:`task/service.go` + orchestrator log poll - -#### System / SystemMetric / Monitor / Config / Audit - -先看: - -- `src/module/system/handler.go` -- `src/module/system/service.go` -- `src/module/system/repository.go` -- `src/module/systemmetric/handler.go` -- `src/module/systemmetric/service.go` - -如果是 dedicated system-service / gateway 边界问题,再看: - -- `src/app/gateway/system_services.go` -- `src/internalclient/systemclient/*` -- `src/internalclient/runtimeclient/*` -- `src/interface/grpcsystem/*` -- `src/interface/grpcruntime/*` - -常见问题先查: - -- config/audit 查询不对:`system/repository.go` -- monitor / queue / lock 不对:`system/service.go` 是否走 runtime RPC -- `/system/health` 异常:`system/handler.go` + service health 依赖 - -#### Runtime / K8s / Build / Helm / Chaos - -先看: - -- `src/service/consumer/*` -- `src/interface/worker/*` -- `src/interface/controller/*` -- `src/infra/k8s/*` -- `src/infra/buildkit/*` -- `src/infra/helm/*` -- `src/infra/chaos/*` -- `src/infra/redis/*` - -如果是 dedicated runtime-worker-service 问题,再看: - -- `src/app/runtime/*` -- `src/internalclient/orchestratorclient/*` -- `src/interface/grpcruntime/*` +| Service | Command | Default Port | +| --- | --- | --- | +| `api-gateway` | `go run ./src/cmd/api-gateway -conf ./src/config.dev.toml -port 8082` | `8082` | +| `iam-service` | `go run ./src/cmd/iam-service -conf ./src/config.dev.toml` | `9091` | +| `orchestrator-service` | `go run ./src/cmd/orchestrator-service -conf ./src/config.dev.toml` | `9092` | +| `resource-service` | `go run ./src/cmd/resource-service -conf ./src/config.dev.toml` | `9093` | +| `runtime-worker-service` | `go run ./src/cmd/runtime-worker-service -conf ./src/config.dev.toml` | `9094` | +| `system-service` | `go run ./src/cmd/system-service -conf ./src/config.dev.toml` | `9095` | -常见问题先查: +## 6. 最终结论 -- queue 不消费:`consumer` + Redis -- k8s job 不创建:`infra/k8s` -- build / helm 失败:对应 `infra/buildkit` / `infra/helm` -- 状态回写不到 orchestrator:`consumer owner` + orchestrator client +### 6.1 主线完成 -#### 看文件顺序的偷懒法 +- 单体 Fx 化完成 +- 六服务边界完成 +- 旧兼容层主线清理完成 +- SDK / audience / API key 主线完成 +- SDK 路由统一收口完成 +- 基础验收与真实 K8s 集群入口完成 -如果你一时不确定从哪进,统一按这个顺序看: +### 6.2 非阻塞剩余项 -1. `src/router/*` 或 `src/interface/grpc*/*` -2. `src/module/*/handler.go` -3. `src/module/*/service.go` -4. `src/module/*/repository.go` -5. `src/app/*` 装配 -6. `src/internalclient/*` -7. `src/infra/*` +- 人工确认 Fx 日志输出是否要再裁剪 +- 继续压 dedicated service 的少量 local fallback +- 继续深清跨 owner DB 直查 +- 补发布参数、values、HPA、Ingress 等环境治理 +- 补更多真实依赖集成回归 diff --git a/docs/todo.md b/docs/todo.md index eb0a2ee2..b41e1595 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -1,710 +1,150 @@ -# Backend Fx Refactor TODO - -> 创建日期:2026-04-15 -> 目标:把后端从全局初始化 + 包级函数,逐步迁移到 Fx app + 明确模块边界 + 生命周期管理。 - -## 使用方式 - -- 先画模块边界,再做 DI。 -- 每次只迁移一个入口或一个模块。 -- 第一阶段不改 URL,不搬大目录,不重写业务逻辑。 -- Fx 先管理启动和生命周期,再逐步替换 handler / service / repository 的包级函数。 -- 当前已有旧 DI 骨架视为临时试验,后续由 Fx 替换。 - -## 0. 准备阶段 - -- [x] 确认项目更适合 Fx,而不是继续扩大旧 DI 方案 -- [x] 确认后端有多入口:producer / consumer / both -- [x] 确认有多基础设施资源:DB / Redis / Etcd / K8s / Loki / tracing / receiver -- [x] 确认第一阶段不改 URL -- [x] 确认第一阶段不大规模搬目录 -- [x] 决定是否立即删除当前旧 DI 骨架 -- [ ] 确认 Fx 生成的启动日志是否可接受 - - 属于人工验收项,不阻塞当前代码主线收口。 - -验证: - -- [x] 阅读汇总文档与主线设计说明 - - 当前总入口已收口到 [report-index.md](./report-index.md) -- [x] `cd src && go test ./app ./router ./handlers/v2` - - 实际执行:`cd src && go test ./app ./interface/http ./router ./handlers/v2` - -## 1. 停止继续旧 DI 扩张 - -- [x] 不再新增旧 DI provider set -- [x] 不再继续按旧 DI TODO 迁移 Project service / repository -- [x] 决定旧 DI 文件处理方式 - - [x] 方案 A:立即删除 app 下旧 DI 生成文件与相关依赖 - - 方案 B 未采用:不再保留旧 DI 骨架等待后删。 -- [x] 文档和 TODO 全部切换到 Fx 方案 - -验证: - -- [x] 检查 app 与依赖中旧 DI 痕迹 - - 代码和依赖已删除;文档中的历史说明也已切成中性表述。 - -## 2. 引入 Fx 基础设施 - -- [x] 在 `src/go.mod` 增加 `go.uber.org/fx` -- [x] 新建或调整 `src/app` 为 Fx app 入口 -- [x] 新建 `src/app/options.go` -- [x] 新建 `src/app/producer.go` -- [x] 新建 `src/app/consumer.go` -- [x] 新建 `src/app/both.go` -- [x] 定义 `CommonOptions()` -- [x] 定义 `ProducerOptions()` -- [x] 定义 `ConsumerOptions()` -- [x] 定义 `BothOptions()` - -目标: - -```go -func ProducerOptions() fx.Option -func ConsumerOptions() fx.Option -func BothOptions() fx.Option -``` - -验证: - -- [x] `cd src && go test ./app` - -## 3. Config / Logger Module - -- [x] 新建 `src/infra/config/module.go` -- [x] 包装现有 `config.Init` -- [x] 让配置路径从 app 参数传入,而不是各处自行读取 -- [x] 新建 `src/infra/logger/module.go` -- [x] 把 logrus 初始化从 `main.go` 收进 logger module -- [x] 确认 logger 初始化只执行一次 - -验收: - -- [x] `main.go` 不再直接配置 logger -- [x] `main.go` 不再直接调用 `config.Init` - - 当前 `config.Init` 仅保留在 `infra/config` module 与少量测试中,producer / consumer / both 主启动链均已通过 Fx 配置模块进入。 -- [x] `cd src && go test ./infra/config ./infra/logger` - -## 4. DB Module - -- [x] 新建 `src/infra/db/module.go` -- [x] 新建 `NewGormDB` -- [x] 将现有 `database.InitDB()` 包装进 Fx provider 或重构为返回 `*gorm.DB` -- [x] DB module 提供 `*gorm.DB` -- [x] 使用 `fx.Lifecycle` 注册 DB close -- [x] 过渡期继续同步 `database.DB = db`,避免一次性修改旧代码 - -目标: - -```go -var Module = fx.Module("db", - fx.Provide(NewGormDB), -) -``` - -验收: - -- [x] producer 可通过 Fx 初始化 DB -- [x] `database.DB` 兼容旧代码 -- [x] DB 关闭逻辑在 `OnStop` - -## 5. Redis / Etcd / Tracing Module - -Redis: - -- [x] 新建 `src/infra/redis/module.go` -- [x] 提供 Redis client -- [x] `OnStop` 关闭 Redis -- [x] Redis 实现已从 `src/client/redis_client.go` 并入 `src/infra/redis/*` -- [x] `src/infra/redis/client.go` 已删除,连接创建已继续并入 `src/infra/redis/gateway.go` 私有方法 -- [x] 过渡期兼容 `client.GetRedisClient()` - - 兼容期已结束;`module/system` / `module/trace` / `module/group` / `module/notification` / `service/common` / `service/consumer` / `service/logreceiver` 等调用点已切到 `redisinfra`。 - -Etcd: - -- [x] 新建 `src/infra/etcd/module.go` -- [x] 提供 Etcd client 或 gateway -- [x] 收口 Etcd watch / get / put 的初始化 -- [x] Etcd 实现已从 `src/client/etcd_client.go` 并入 `src/infra/etcd/*` -- [x] `src/infra/etcd/client.go` 已删除,连接创建已继续并入 `src/infra/etcd/gateway.go` 私有方法 - -Tracing: - -- [x] 新建 `src/infra/tracing/module.go` -- [x] 包装 `client.InitTraceProvider()` -- [x] 如支持 shutdown,则注册 `OnStop` -- [x] tracing provider 实现已从 `src/client/jaeger.go` 并入 `src/infra/tracing/*` - -Loki: - -- [x] 新建 `src/infra/loki/module.go` -- [x] Fx graph 提供 `*client.LokiClient` -- [x] `module/task.LokiGateway` 注入 `*client.LokiClient`,不再自行 `client.NewLokiClient()` -- [x] Loki 实现已从 `src/client/loki.go` 并入 `src/infra/loki/*` -- [x] `module/task` / `module/injection` / `app.CommonResources` 已切到 `lokiinfra.Client` - -验收: - -- [x] 基础设施资源由 Fx module 创建 -- [x] 旧代码仍可运行 -- [x] `cd src && go test ./infra/...` - - 实际执行:`cd src && go test ./app ./infra/config ./infra/logger ./infra/db ./infra/redis ./infra/etcd ./infra/tracing ./interface/http ./router ./handlers/v2` - -## 6. HTTP Interface Module - -- [x] 新建 `src/interface/http/module.go` -- [x] 新建 `src/interface/http/server.go` -- [x] 新建 `src/interface/http/router.go` -- [x] 将现有 `router.New(...)` 包装为 Fx provider -- [x] HTTP server 使用 `http.Server` -- [x] `OnStart` 启动 server goroutine -- [x] `OnStop` graceful shutdown -- [x] producer 模式通过 Fx 启动 HTTP server - -目标: - -```go -var Module = fx.Module("http", - fx.Provide(NewGinEngine, NewHTTPServer), - fx.Invoke(RegisterHTTPServerLifecycle), -) -``` - -验收: - -- [x] `main.go producer` 不再直接 `engine.Run` -- [x] HTTP server 可优雅停止 -- [x] API URL 不变 -- [x] `cd src && go test ./interface/http ./router` - - 实际执行:`cd src && go test ./app ./interface/http ./router ./handlers/v2` - -## 7. main.go 收口 - -- [x] `main.go` 只保留 cobra mode 解析 -- [x] producer mode 调用 `fx.New(app.ProducerOptions(...)).Run()` -- [x] consumer mode 调用 `fx.New(app.ConsumerOptions(...)).Run()` -- [x] both mode 调用 `fx.New(app.BothOptions(...)).Run()` -- [x] 删除 `main.go` 中直接 DB 初始化 -- [x] 删除 `main.go` 中直接 trace 初始化 -- [x] 删除 `main.go` 中直接 HTTP server 启动 - -验收: - -- [x] `main.go` 明显变薄 -- [x] producer 可启动 -- [x] consumer 暂时可保留旧逻辑或已接入 Fx -- [x] both 可启动 - -## 8. Consumer / Scheduler Module - -- [x] 新建 `src/interface/worker/module.go` -- [x] 包装 `consumer.StartScheduler` -- [x] 包装 `consumer.ConsumeTasks` -- [x] 使用 Fx lifecycle 管理 context cancel -- [x] `OnStart` 启动 scheduler goroutine -- [x] `OnStart` 启动 consumer goroutine -- [x] `OnStop` cancel context -- [x] 避免 consumer 阻塞 Fx 启动流程 - -验收: - -- [x] consumer mode 通过 Fx 启动 -- [x] both mode 通过 Fx 同时启动 HTTP 和 consumer -- [x] 停止时能 cancel worker context - -## 9. K8s Controller / Chaos Module - -- [x] 新建 `src/infra/k8s/module.go` -- [x] 包装 `k8s.GetK8sController()` -- [x] 包装 K8s rest config -- [x] 新建 `src/infra/chaos/module.go` -- [x] 包装 `chaosCli.InitWithConfig` -- [x] 新建 `src/interface/controller/module.go` -- [x] 用 lifecycle 启动 K8s controller -- [x] 用 lifecycle 停止 controller context - -验收: - -- [x] consumer / both mode 中 K8s controller 由 Fx 启动 -- [x] 初始化顺序由 Fx 表达 - -## 10. OTLP Receiver Module - -- [x] 新建 `src/interface/receiver/module.go` -- [x] 包装 `logreceiver.NewOTLPLogReceiver` -- [x] receiver port 从 config module 注入 -- [x] `OnStart` 启动 receiver -- [x] `OnStop` shutdown receiver - -验收: - -- [x] consumer / both mode 中 receiver 由 Fx 启动 -- [x] 停止时 receiver 正常关闭 - -## 11. HTTP Routes 按受众拆分 - -先拆注册函数,不改 URL。 - -- [x] 新建或迁移 public routes -- [x] 新建或迁移 sdk routes -- [x] 新建或迁移 portal routes -- [x] 新建或迁移 admin routes -- [x] 整理 system routes -- [x] route 注册依赖 handler 容器 - -建议: - -```go -func RegisterPublicRoutes(...) -func RegisterSDKRoutes(...) -func RegisterPortalRoutes(...) -func RegisterAdminRoutes(...) -func RegisterSystemRoutes(...) -``` - -验收: - -- [x] URL 不变 -- [x] `router/v2.go` 变薄 - - 从 647 行降到 365 行;核心业务路由仍保留在 `v2.go`,后续随业务 module 迁移继续拆。 -- [x] Admin / SDK / Portal 边界在代码上可见 - -## 12. 业务 Module 壳 - -先建立壳,不急着重写内部逻辑。 - -- [x] `module/project` -- [x] `module/auth` -- [x] `module/task` -- [x] `module/injection` -- [x] `module/execution` -- [x] `module/container` -- [x] `module/dataset` -- [x] `module/rbac` -- [x] `module/user` - -每个模块先暴露: - -```go -var Module = fx.Module("project", - fx.Provide(NewHandler), -) -``` - -过渡期 handler 可以 wrapper 旧函数。 - -验收: - -- [x] app 通过业务 module 收集 handler - - 已新增 `app.ProducerHTTPModules()` 与 `router.Module`,Producer/Both 由 app 统一收集业务 module,再向 HTTP interface 提供 `router.Handlers`。 -- [x] router 不直接散装引用所有裸函数 - - 业务路由已统一经 `router.Handlers` 聚合,`interface/http` 不再散装依赖各模块构造;剩余主要是旧兼容层清理与少量 middleware/初始化收尾。 - -## 13. Project 模块正式迁移 - -Project 作为第一个完整业务样板。 - -- [x] 新建 `module/project/repository.go` -- [x] 新建 `module/project/service.go` -- [x] 新建 `module/project/handler.go` -- [x] 新建 `module/project/module.go` -- [x] Repository 注入 `*gorm.DB` -- [x] Service 注入 Repository - - RBAC 独立接口化与 Label 接口进一步抽象保留为后续优化项,不阻塞当前主线。 -- [x] Handler 注入 Service -- [x] Handler method 使用 `c.Request.Context()` -- [x] 移除 Project handler wrapper 对旧包级函数的依赖 - - Project CRUD/labels 已移除旧 wrapper;project 下 injection/execution routes 已切到 `module/injection` 和 `module/execution`。 -- [x] Project routes 使用新 handler - -验收: - -- [x] Project handler 不直接 import `database` -- [x] Project handler 不直接 import repository implementation -- [x] Project service 不直接使用全局 `database.DB` -- [x] Project CRUD 行为不变 - -## 14. Auth / Task 模块迁移 - -Auth: - -- [x] 新建 Auth module -- [x] Token blacklist 从 repository 迁移到 store - - 新路由已走 `module/auth.TokenStore`;旧 `repository/token.go` 与 `service/producer/auth.go` 已删除。 -- [x] Auth service 注入 UserRepository / RoleRepository / TokenStore -- [x] Auth handler method 化 - -Task: - -- [x] 新建 Task module -- [x] Task queue Redis 访问收口到 store -- [x] WebSocket handler 只做认证和连接升级 - - 日志推送已走 `module/task.TaskLogService`。 -- [x] 日志历史查询走 Loki gateway -- [x] 订阅逻辑走 service / store - - Redis Pub/Sub 已收口到 `module/task.TaskQueueStore`;task state polling 已收口到 `module/task.TaskLogService`。 - -验收: - -- [x] `handlers/v2/tasks.go` 不再 direct import `database` -- [x] `handlers/v2/tasks.go` 不再 direct import `repository` - - 旧文件已删除,Task 路由切到 `module/task.Handler`。 -- [x] `repository/token.go` 能力迁移出去 - - Auth 黑名单能力已统一收口到 `module/auth.TokenStore`。 - -## 15. 核心业务模块逐个迁移 - -按顺序推进: - -- [x] Injection - - 已建立 module/handler/service 壳并切 Project 子路由;深层 producer/repository 逻辑后续继续下沉。 -- [x] Execution - - 已建立 module/handler/service 壳并切 Project 子路由;深层 producer/repository 逻辑后续继续下沉。 -- [x] Container -- [x] Dataset -- [x] Evaluation -- [x] Trace -- [x] Metrics -- [x] Group -- [x] Notification -- [x] SDK Evaluation -- [x] Chaos System - -主线完成项: - -- [x] Module / Handler / Service / Repository 壳 -- [x] Fx providers -- [x] route 切换 -- [x] 主路径所需 Store / Gateway 收口 -- [x] 关键模块级测试 - - Container / Dataset / Evaluation / Trace / Group / Metrics / Notification / SDK Evaluation / Chaos System 已完成 module/handler/service/repository 壳、Fx providers 和 route 切换;其中 Metrics / SDK Evaluation / Chaos System 已进一步切离 `service/producer` 包级入口。测试层面已覆盖 auth / project / execution / injection / task / user / sdk / docs / app 等主路径,剩余测试补强属于后续质量项,不阻塞当前主线。 - -## 16. Store / Gateway 拆分 - -- [x] Redis token blacklist -> TokenStore - - 旧 `repository/token.go` 已删除,认证退出逻辑统一走 `module/auth/token_store.go`。 -- [x] Redis task queue -> TaskQueueStore - - 已新增 `src/infra/redis/task_queue.go`,consumer / scheduler / system monitor 队列读写全部从 `repository/task.go` 迁出。 -- [x] Loki -> LokiGateway - - 当前完成 Task 日志查询侧,并将 Loki client 纳入 Fx graph;Injection 日志查询仍待深层 service 迁移。 -- [x] K8s -> K8sGateway - - 已新增 `src/infra/k8s/gateway.go`,统一收口 controller / create job / volume mount / job logs / health check 访问;并已把 `src/client/k8s/*` 的真实实现整体迁入 `src/infra/k8s/*`。其中 `RestConfig / Client / DynamicClient` 这类简单转发已继续收口,改由 `infra/k8s` 内部私有 getter 与 Fx provider 使用。 -- [x] Etcd -> EtcdGateway - - 已新增 `src/infra/etcd/gateway.go`,配置监听与动态配置发布已切到 gateway;`Put/Get/Delete/Watch` 逻辑也已收回 gateway,`client.go` 仅保留底层连接创建与关闭。 -- [x] Harbor -> HarborGateway - - 已新增 `src/infra/harbor/gateway.go`,并把 Harbor client 实现从 `src/client/harbor_client.go` 并入 `src/infra/harbor/*`;对外不再保留空转发 `Client` 抽象,逻辑已直接内聚到 gateway。 -- [x] Helm -> HelmGateway - - 已新增 `src/infra/helm/gateway.go`,consumer pedestal 安装侧已改由 gateway 直接承担 repo/install 逻辑;Helm 实现已从 `src/client/helm.go` 并入 `src/infra/helm/*`,不再额外保留对外 `Client` 层。 -- [x] BuildKit -> BuildKitGateway - - 已新增 `src/infra/buildkit/gateway.go`,BuildKit 健康检查与构建 client 创建已开始从业务层抽离。 - -验收: - -- [x] repository 只负责 DB -- [x] service 依赖接口而不是全局 client - - Redis / Loki / Etcd / Harbor / Helm / K8s 这批调用点已不再依赖 `aegis/client` 包级入口;root `client` 目录现仅剩 debug 与 aegisctl 客户端侧代码。 -- [x] 外部系统资源按需纳入 Fx graph / lifecycle 管理 - - Redis / Etcd / tracing 已在 Fx lifecycle 中管理;Harbor / Helm / Loki 当前以无状态或按需 client 为主,不额外引入 shutdown 生命周期也不阻塞主线。 - -## 17. SDK / Portal / Admin 标记治理 - -当前口径: - -- 不再在代码里 hardcode audience allowlist。 -- audience 归属统一以 `src/docs/openapi3/openapi.json` 里的 `x-api-type` 扩展为准。 -- 只要某个 operation 带有对应 key 且值为 `"true"`,就会被提取进对应产物。 -- 同一个 operation 可以同时落入多个 audience。 -- Python SDK 只消费 `sdk.json`;TypeScript SDK 不再消费共享并集视图,而是分别按 `portal.json` / `admin.json` 生成独立 Portal SDK 与 Admin SDK。 -- SDK audience 改为显式白名单;默认不再把通用登录、Portal/Admin 控制面接口顺手放进 `sdk.json`。 -- SDK / CLI 认证主线改为 `Key ID / Key Secret -> access token`;`username/password login` 仅保留给 Portal / Admin 等人类交互入口。 -- `Key ID / Key Secret -> token` 进一步改为 header 签名模式:`X-Key-Id`、`X-Timestamp`、`X-Nonce`、`X-Signature`,业务接口仍继续走 Bearer token。 - -本轮执行清单: - -- [x] 收缩 `sdk` audience 到最小可维护白名单 - - 已继续把剩余误标的 `sdk` audience 收回,只保留 `POST /api/v2/auth/api-key/token` 与 `src/router/sdk.go` 下 4 个 SDK 样例接口;当前 `sdk.json` 已收缩到 `5 paths / 5 operations`。 -- [x] 从 Swagger audience 中移除 `POST /api/v2/auth/register` 的 `sdk` -- [x] 从 Swagger audience 中移除 `POST /api/v2/auth/login` 的 `sdk` -- [x] 盘点并设计 API key / Key ID / Key Secret 数据模型 - - 已新增 `model.APIKey`,并把物理 schema 一并改到 `api_keys` / `key_id` / `key_secret_hash` / `key_secret_ciphertext` / `active_key_id`,覆盖 `owner`、`enabled/disabled/deleted`、`expires_at`、`last_used_at`、`name/description` 等字段,并纳入 `AutoMigrate`。 -- [x] 增加 API key 管理接口 - - 已补 `portal` 路由:`GET/POST /api/v2/api-keys`、`GET/DELETE /api/v2/api-keys/{id}`、`POST /api/v2/api-keys/{id}/rotate|disable|enable`。 -- [x] 增加 `Key ID / Key Secret -> token` 接口并标为 `sdk` - - 已补 `POST /api/v2/auth/api-key/token`,返回 Bearer token,并在 JWT claims 中标记 `auth_type=api_key` 与 `api_key_id`;当前入口统一使用 `X-Key-Id` / `X-Timestamp` / `X-Nonce` / `X-Signature` 头签名校验,canonical string 已收敛为 `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)`,服务端会校验 5 分钟时间窗并用 Redis 做 nonce 防重放;`iam.proto` / `src/interface/grpciam` / `src/internalclient/iamclient` 这条内部链也已统一到 `key_id` 字段名。 -- [x] 将 Python SDK 的鉴权入口切到 Key ID / Key Secret - - `sdk/python/src/rcabench/client/http_client.py` 已改为优先使用 `token` 或 `key_id + key_secret`;SDK 不再依赖 username/password login,环境变量主入口切到 `RCABENCH_KEY_ID` / `RCABENCH_KEY_SECRET`,并按 `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)` 规范计算 HMAC-SHA256 签名头;重生成后的 `sdk/python/src/rcabench/openapi/*` 也已切到 `X-Key-Id`、`key_id`、`key_secret` schema,不再暴露旧 `X-Access-Key` / `access_key` / `secret_key` 鉴权字段。 -- [x] 将 `aegisctl` 的鉴权入口切到 Key ID / Key Secret - - `src/cmd/aegisctl/cmd/auth.go` / `src/cmd/aegisctl/client/auth.go` 已切到 `--key-id` + `--key-secret` 签名换取 `POST /api/v2/auth/api-key/token`;环境变量主入口改为 `AEGIS_KEY_ID` / `AEGIS_KEY_SECRET`,登录结果继续只落盘 Bearer token 与 `key_id`,不保存 `key_secret`;旧 flag/env 与响应字段兼容读取已删除。 -- [x] 给 `aegisctl` 增加本地签名排障命令 - - 已补 `aegisctl auth inspect` 与 `aegisctl auth sign-debug`;前者可检查当前 context 的 token / auth_type / key_id / expiry,后者可直接打印 canonical string、签名头与 curl 样例,并可通过 `--execute` 直接发起换 token 请求回显响应,或通过 `--save-context` 直接把成功返回的 Bearer token 落盘到当前 CLI context,便于排查 SDK / CLI / 服务端签名不一致问题。 -- [x] 补充 Key ID / Key Secret 头签名规范文档 - - 相关说明现已并入 `docs/report-index.md`:明确 canonical string、Header 约定、HMAC 规则、时间窗与 nonce 防重放语义,并补了 Portal 上 API key 的使用说明与 `aegisctl` 排障命令说明;Swagger 注释与生成文档也已统一到 `X-Key-Id`、`key_id`、`key_secret` 口径,可直接供文档站与 SDK 生成消费。 -- [x] 收口 API key 命名与样例前缀 - - auth handler / service / gRPC / internal client / CLI / Swagger / Python SDK 现已统一使用 `API key`、`key_id`、`key_secret`;公开样例前缀也统一为 `pk_...` / `ks_...`。Go 存储模型与物理 schema 现都已统一到 `APIKey` / `api_keys` / `key_id` 口径,不再保留旧 `user_access_keys` / `access_key` 兼容层。 -- [x] 补 API key 的 `scopes` / `revoked_at` 语义 - - `model.APIKey` 已新增 `scopes` 与 `revoked_at`;创建接口支持提交 `scopes`,默认会归一化为 `["*"]`;列表/详情/创建/轮换响应会返回 scopes 与 revoked_at;同时新增 `POST /api/v2/api-keys/{id}/revoke`,被 revoke 的 API key 会被永久拒绝换 token,且不能再 re-enable / rotate。 -- [x] 把 API key scopes 继续推进到 bearer token / gRPC verify / middleware 上下文 - - `utils.Claims` 已补 `api_key_scopes`,`Key ID / Key Secret -> token` 成功后签发的 JWT 会携带 scopes;`iam.proto` / `src/interface/grpciam` / `src/internalclient/iamclient` 的 verify 响应链也已同步透传,HTTP middleware 现会把 `auth_type` / `api_key_id` / `api_key_scopes` 一并放入请求上下文,后续做 scope enforcement 不用再回查 API key 表。 -- [x] 给 API key scopes 接上首版运行时拦截 - - `src/middleware/permission.go` 现已在 permission middleware 里先按 `api_key_scopes` 做匹配,再落 DB 权限校验;当前支持 `*`、`resource`、`resource:action`、`resource:action:scope` 以及各段 `*` 通配,先覆盖所有基于 `RequirePermission/RequireAnyPermission/RequireAllPermissions` 的路由。 -- [x] 把 team/project 成员关系型中间件也接上 API key scopes 预过滤 - - `RequireTeamMemberAccess` / `RequireTeamAdminAccess` / `RequireProjectAccess(...)` 现在会先按 API key scope 做 read/manage 级别过滤,再执行成员/管理员关系判断,避免 API key bearer token 绕过非 permission 型访问守卫。 -- [x] 再扫一轮 JWTAuth-only 路由,把明显漏掉的敏感守卫补齐 - - 已补上 `team list/create`、`/api/v2/resources*`、`/api/v2/systems*`、`/api/v2/system/metrics*` 这批原先只有 `JWTAuth()` 的敏感入口;当前剩余仅 `auth profile/logout/change-password`、`/api/v2/api-keys/*` 自助凭证管理,以及 `sdk` 样例查询这几类刻意保留的 JWTAuth-only 路由,后两者若要继续收紧可再单独引入更明确的 API key/self-service scope 语义。 -- [x] 给 `sdk/*` 和 `/api/v2/api-keys/*` 落一版明确语义 - - `src/router/sdk.go` 已引入显式 API key scope gate:`/api/v2/sdk/evaluations*` 需要 `sdk:*` / `sdk:evaluations:*` / `sdk:evaluations:read`,`/api/v2/sdk/datasets` 需要 `sdk:*` / `sdk:datasets:*` / `sdk:datasets:read`;同时 `src/router/portal.go` 的 `/api/v2/api-keys/*` 已统一挂 `RequireHumanUserAuth()`,明确只允许人类用户 session 管理 API key,禁止“API key 再管理 API key”。 -- [x] 把 auth 自助接口也限制为 human session - - `src/router/public.go` 的 `/api/v2/auth/profile`、`/logout`、`/change-password` 现已统一挂 `RequireHumanUserAuth()`;当前 API key bearer token 只保留给显式允许的 SDK/业务 API,不再可进入用户账号自助管理接口。 -- [x] 启动 Python SDK / runtime wrapper 分层主线第一批落地 - - 已新增 `docs/python-runtime-wrapper-design.md` 与 `docs/python-runtime-wrapper-todo.md`;Swagger `x-api-type` 现支持 `runtime` audience,并新增 `src/docs/converted/runtime.json`;`module/execution` 的 detector/granularity upload 已标为 `runtime:"true"`,`src/router/runtime.go` 也已把这两条 `/api/v2/executions/{execution_id}/*_results` 路由挂到 `JWTAuth() + RequireServiceTokenAuth()`;同时 `sdk/python/src/rcabench/client/runtime_client.py` 已新增 `RCABenchRuntimeClient`,并从 Python 包根导出,作为后续 `rcabench-platform` wrapper 的 service-token-only 基础客户端。 -- [x] 收紧 hand-written Python client 边界:public client 只保留 API key,runtime client 只保留 service token - - `sdk/python/src/rcabench/client/base.py` 已新增 `BaseRCABenchClient` 抽出共享 session/api-client 生命周期;`RCABenchClient` 已删除直接 bearer token 模式,仅保留 `key_id + key_secret`;`RCABenchRuntimeClient` 现改成与 public client 同结构的 service-token-only connector,不再承载 detector/granularity upload 调度语义,后续 heartbeat / status / artifact/result 的调用时机统一留给外仓 `rcabench-platform` wrapper 控制。 -- [x] 重新生成 `openapi3` / `sdk.json` 并回填最新统计 - - 当前生成结果为:`openapi3/openapi.json` `138 paths / 173 operations`,`sdk.json` `5 / 5`,`portal.json` `31 / 43`,`admin.json` `48 / 58`;Python SDK 已按最新 `sdk.json` 重新生成,TypeScript 侧改为分别消费 `portal.json` 与 `admin.json`。 - -完成项: - -- [x] 统计 OpenAPI3 中的 `x-api-type` audience 标记 - - 当前已按 Go Swagger 注释补齐一批 `portal:"true"` / `admin:"true"` 标记;`converted/portal.json` 与 `converted/admin.json` 会分别作为独立 TypeScript SDK 的输入。 -- [x] Python SDK 只提取 `x-api-type.sdk == "true"` 的接口 -- [x] Portal 产物提取 `x-api-type.portal == "true"` 的接口 -- [x] Admin 产物提取 `x-api-type.admin == "true"` 的接口 -- [x] TypeScript Portal SDK 仅提取 `x-api-type.portal == "true"` 的接口 -- [x] TypeScript Admin SDK 仅提取 `x-api-type.admin == "true"` 的接口 -- [x] 更新 SDK 生成脚本 - - `scripts/command/src/swagger/init.py` 现会先完整重跑 `swag init`,再把 `openapi2/swagger.json` 本地转换成 `openapi3/openapi.json`,并继续产出 `client.json`、`sdk.json`、`portal.json`、`admin.json`;不再依赖 Docker 生成 OpenAPI3,也不再产生 root-owned 文档目录。 -- [x] 修回 `swagger init` 主链可完整再生 - - 通过恢复 `src/handlers/debug.go`、`src/handlers/system/*`、`src/handlers/v2/*` 这批仅用于 Swagger 注释扫描的 build-ignored 文档桩,`swag init` 已重新稳定产出全量接口;当前 `openapi2/swagger.json`、`openapi3/openapi.json`、`converted/client.json` 均为 `132 paths / 165 operations`。 -- [x] 校正 Python SDK 生成链 - - `scripts/command/src/formatter/python.py` 现优先使用本地 `scripts/command/.venv/bin/ruff`,缺失时也不会再因为 formatter 中断;`scripts/command/src/swagger/python.py` 的 Docker 生成步骤继续显式使用当前用户 UID:GID 运行,避免再次产出 root-owned 文件。 -- [x] 重新生成 TypeScript SDK - - 已执行 `cd scripts/command && ./.venv/bin/python main.py swagger generate-sdk -l typescript -v 1.2.1` -- [x] 检查 SDK diff - - TypeScript 不再输出共享 `typescript.json` / `sdk/typescript`;当前口径改为 `sdk/typescript/portal` 与 `sdk/typescript/admin` 两套独立产物,分别只消费 `portal.json` 与 `admin.json`,避免 Portal/Admin 共用同一份 TS SDK。 -- [x] 验证 audience 文档产物 - - 已执行 `cd scripts/command && ./.venv/bin/python main.py swagger init -v 1.2.1` 与 `cd src && go test ./docs` - -## 18. 删除旧 DI 骨架和旧兼容层 - -等 Fx producer / consumer / both 跑通后执行。 - -- [x] 删除 app 下旧 DI 生成文件 -- [x] 删除旧 DI 依赖 -- [x] 删除过渡 handler wrapper - - `src/handlers/v2` 现仅保留空的 `doc.go` 占位包以兼容既有测试命令,已不再承担任何运行态 wrapper 职责;`src/handlers/debug.go`、`src/handlers/system/*` 与 `src/handlers/v2/*` 旧兼容入口均已清空或删除。最近一轮又把 `src/app/producer_init.go`、`src/interface/{worker,controller,receiver}/module.go`、`src/interface/http/server.go` 中仅供 Fx 编排使用的注册 helper 全部缩成包内私有实现,启动链公开暴露面继续收口。 -- [x] 删除旧包级 service 函数 - - `module/user` CRUD / 资源授权、`module/systemmetric` 指标查询、`module/rbac` 已基本切离 `service/producer`;`handlers/system/monitor.go`、`configs.go`、`audit.go` 主路由入口也已并入 `module/system`。此前已删除旧 `service/producer` 中的 system / metrics / sdk / chaos-system / permission / audit / evaluation / notification / team / trace / group 兼容入口;middleware 也不再直接依赖旧 producer。`module/container` 与 `module/dataset` 现已进一步把 CRUD / detail / list / labels / version 元数据、container build / helm upload、dataset filename / download / version injection 路径下沉到模块 service/repository,并把直接碰 `config` / git / 文件系统的部分收成模块内 gateway/store。旧 `service/producer/container.go` / `dataset.go` 已删除;初始化已改走 `module/container` / `module/dataset` 暴露的 core helper。最近几轮里,`module/injection` 已先后接管 datapack download / files / file query / upload / build 提交流程,以及 injection list / project list / detail / labels / logs / submit fault injection / search / no-issues / with-issues / clone / batch delete 主路径;`src/service/producer/injection.go` 已整体删除。随后又继续按“模块语义留在模块 repo、纯转发尽量删除”的口径收缩:`module/injection` 把 search / list / labels / batch label 管理,以及 project injection list 的标签装配收进 `repository.go`,并继续把 `LoadInjection` / `FindInjectionByName` / `CreateInjectionRecord` / `LoadTask` / `LoadPedestalHelmConfig` / label/execution 删除辅助等一批原子转发写实到模块仓储;最近三轮又把 project resolve、detail with labels、existing injection map、label 条件聚合、project injection list、issue/no-issue 视图、label id by key、fault injection 批量 with labels 这批组合查询继续收成模块内实现。`module/user` 这一轮又把 `CreateUser + EnsureUserUnique`、`Get/Update` 这批基础 CRUD 空包装进一步折成 `CreateUserIfUnique`、`GetUserDetailBase`、`UpdateMutableUser`、`ListUserViews`,并把 `DeleteUserCascade`、global/container/dataset/project 的 assign/remove、permission batch create/delete 这批 relation 逻辑也直接写进模块 repo;随后又把 user detail 关系装配,以及 role/container/dataset/project 的加载 helper 继续改为模块内直接查库;最近又把 permission id 批量校验也直接内聚到模块仓储,并把纯存在性校验提升成公开 `EnsureUserExists(...)` 供 service 组合点复用。`module/rbac` 把 role 详情装配、权限批量校验、角色删除级联、resource/permission 关系查询收进模块 repo,并继续把 role / permission / resource 的基础 list/load/create 查询直接内聚到模块仓储;最近又把 role detail、role->user、permission->role、resource->permission 这批组合视图改成模块内直查;上一轮再把 role delete cascade、mutable update、permission id 批量加载也进一步改成模块仓储自管;这一轮继续把“可写 role”校验收口成模块内 `loadWritableRole(...)`,同时把通用 `LoadPermission` / `LoadResource` 改成更贴业务语义的 `GetPermissionDetail(...)` / `GetResourceDetail(...)`。`module/project` 现已把 create-with-owner、delete cascade、detail/list 视图装配、mutable update、label reload 与按 key 移除标签收进自身 repo,这几轮继续把 project owner role 查询、project statistics 聚合、label 批量装配 / project label id 查找 / usage decrease 一并写实;这一轮再把内部 helper 命名继续往语义侧收紧成 `loadProjectRecord(...)` / `listProjectStatistics(...)`。`module/team` 也把 create-with-creator、detail 聚合、visible list、team project list、member add/remove/update role、team visibility 读取等操作收进 repo,并把 team project statistics 聚合也留在模块内;这一轮又把 team 加载进一步收成 `loadTeam(...)`,用于 detail / mutable update / ensure exists / visibility 读取,同时把 project statistics helper 明确成 `listTeamProjectStatistics(...)`。`module/execution` 现已接管 project list / global list / detail / labels / batch delete / detector result / granularity result / submit execution 全链路,新增自身 `repository.go` 并删除旧 `src/service/producer/execution.go`。由于 project 主路径此前早已由 `module/project` 承接,本轮也同步删除了已空心化的 `src/service/producer/project.go`;同时 `service/producer/label.go` 也已删除,初始化阶段改走 `module/label.CreateLabelCore`。`service/producer/relation.go`、`user.go`、`role.go`、`resource.go`、`auth_helpers.go`、`permission_helpers.go`、`datapack_archive.go` 同样已清掉,producer 侧残余重点进一步收敛到更少的共享逻辑;当前 `src/service/producer` 已无 Go 源文件残留。与此同时,旧 `src/client/loki.go` / `jaeger.go` / `redis_client.go` / `etcd_client.go` / `harbor_client.go` / `helm.go` / `client/k8s/*` 及 Helm 对应测试也已从 root `client` 包清走,真实实现统一并入 `src/infra/*`;上一轮已把 `src/infra/k8s/client.go` 删除,rest/client/dynamic/controller 的单例初始化直接吸回 `src/infra/k8s/gateway.go`;这一轮继续把 `service/consumer` / `service/initialization` 中的 `CurrentK8sController()` fallback 干掉,改成由 Fx 注入 `*k8sinfra.Controller`,同时 `service/common` 的 etcd fallback 改为回落到 `infra/etcd.GetGateway()` 单点入口,并进一步删掉 `service/consumer/deps.go` / `service/common/deps.go` 这类旧全局依赖注册文件。`service/consumer` 中剩余的 K8s / BuildKit / Helm 访问也继续改为直接走 `infra/*` 单点入口:新增 `buildkitinfra.GetGateway()`、`helminfra.GetGateway()`,`CurrentK8sGateway()` / `currentBuildkitGateway()` / `currentHelmGateway()` 已全部清掉;这轮又把 `app/startup.go` 删除,并进一步引入 `app.RegisterProducerInitialization`,把 producer 初始化从 `context.Background()` 改成走 Fx `OnStart` 生命周期上下文。随后又继续把 `interface/controller` / `interface/receiver` / `interface/worker` 的生命周期上下文改成从 Fx `OnStart` 派生,不再在模块注册期直接构造 `context.Background()`;再往下一轮又把 `service/consumer/task.go` / `trace.go` / `jvm_runtime_mutator.go` / `k8s_handler.go` 里残余 `context.Background()` 全部清成 consumer 内部 detached context helper。初始化侧原先带 callback 的 `registerHandlers(...)` 旧 helper 也已改成更窄职责的 `activateConfigScope(...)`,consumer / producer 各自显式注册所需 handlers,再统一激活 listener scope;这一轮再把 `GetConfigUpdateListener(...)` 单例 helper 从启动链收掉,改为在 producer / worker Fx `OnStart` 生命周期里显式创建 `ConfigUpdateListener` 后传给 initialization。`service/consumer` 的 Redis 直连也开始往更窄语义收:新增内部 `currentRedisGateway` / `currentRedisClient` / `publishRedisStreamEvent` / `publishTraceStreamEvent` / `loadCachedInjectionAlgorithms` helper,先把 trace/group stream 发布、detector cache 读取,以及 `monitor` / `rate_limiter` 对 Redis gateway 的获取收进更窄入口;随后又把 monitor 的上下文来源收回 worker lifecycle,并把 namespace SMembers/HGet/HSet/Pipeline 这批读取/写入改为统一走 consumer 内部 Redis helper 取 client,同时 `rate_limiter` 也不再自持 Redis client,而是统一经由 consumer Redis helper 获取连接;最近一轮再把 namespace key / exists / field read / seed / lock write 继续折成 `monitor` 内部更窄 helper,减少 monitor 主流程里散落的 Redis 原语;上一轮则继续把 rate limiter Redis 操作下沉成独立 `tokenBucketStore`,把 token acquire/release 的 Redis 细节与 limiter 配置/调度逻辑分开;这一轮再正式把 monitor 按同一路径拆出独立 `namespaceStore`,把 namespace key/list/exists/read/write/watch/status 这批 Redis 操作从 monitor 主流程里抽走;紧接着又继续深拆成 `namespaceCatalogStore` / `namespaceLockStore` / `namespaceStatusStore` 三个更窄 store,把锁读取/抢占/释放、namespace 注册、status 读写彻底从 `monitor.go` 抽开,并删除已空心化的 `src/service/consumer/namespace_store.go`。这一轮再把 startup / interface 链路里对 monitor 的旧包级获取收一批:`consumer.NewMonitor(...)` 作为 Fx provider 现在直接吃 `*redisinfra.Gateway` 并在内部自取 client,monitor 构造期不再向启动链暴露裸 `*redis.Client`,`initialization.InitializeConsumer(...)`、`RegisterConsumerHandlers(...)`、`interface/controller` 的 K8s callback 构造均改为显式注入 monitor,而不再自己碰 `GetMonitor()`;紧接着又继续把运行时执行主流程里的 monitor 单例拿掉,新增 `consumer.RuntimeDeps` 由 worker lifecycle 显式传入,`dispatchTask(...)` / `executeTaskWithRetry(...)` / `executeFaultInjection(...)` / `executeRestartPedestal(...)` 已不再自己碰 `GetMonitor()`。这一轮继续顺着同一主线把 rate limiter 也从进程级单例收成纯 Fx provider:`NewRestartPedestalRateLimiter(...)` / `NewBuildContainerRateLimiter(...)` / `NewAlgoExecutionRateLimiter(...)` 现在直接吃 `*redisinfra.Gateway` 构造 limiter,不再经过 `Get*RateLimiter()` / `sync.Once`;`executeBuildContainer(...)`、`executeAlgorithm(...)`、`executeRestartPedestal(...)` 与 K8s job 回调里的 algorithm token release 也都改为走显式传入 limiter,不再直接碰旧包级 getter。与此同时,`service/common/config_registry.go` / `config_listener.go` 把配置元数据读取继续收成 `service/common/config_store.go` 本地语义 store,不再穿过公共 `repository` 包;随后又把 producer/worker/controller/receiver 的启动执行体再收成显式可替换的 `ProducerInitializer` / `LifecycleRunner` 依赖,避免 lifecycle 本身直接抱一大串底层依赖,主路径更贴近 Fx;在此基础上,`src/app/startup_validate_test.go` 与 `src/app/startup_smoke_test.go` 现在已经补上 producer / consumer / both 三种 app option 的 Fx 图校验与 start/stop smoke(通过替换重型初始化依赖,验证 HTTP/worker/controller/receiver/producer lifecycle 编排本身可启动可停止)。这一轮继续顺着同一条线,把 `service/common/config_registry.go` 里的 `sync.Once` / `globalHandlersOnce` 再压掉,改成常驻 registry + 幂等注册逻辑,并补上 `config_registry_test.go` 锁住“全局 handlers 多次注册不重复”行为,进一步减少 config startup 主路径上的一次性单例状态;紧接着又继续把 listener / publish 周边的剩余全局依赖再收一层:`ConfigUpdateListener` 现在显式携带 `*gorm.DB`,不再在读取配置元数据和处理变更时回落到 `database.DB`;`RegisterGlobalHandlers(...)` / `RegisterConsumerHandlers(...)` 也开始显式接收 `ConfigPublisher`,`PublishWrapper(...)` 改成走传入 publisher,而不再自己碰 `redisinfra.GetGateway()`。对应地 producer / consumer 初始化与 worker lifecycle 现已把 Redis gateway / DB 一路显式传进 config listener 与 handler 注册主链。顺手也暴露并修复了 producer 模式此前缺少 `k8sinfra.Module`、导致 `chaosinfra.Module` 无法解析 `*rest.Config` 的问题。当前 producer / consumer / both 三种 app options 都已能通过 `go test ./app` 的图校验和启动链 smoke。这一轮继续把 `service/common` 热路径往显式 DB 收:`DBMetadataStore` 改成由 initialization 注入 `*gorm.DB` 创建,`container` / `dataset` / `task` 公共能力补上 `WithDB` 变体,`module/execution` / `module/injection` / `module/container` 的提交与 ref 解析主路径已改用模块 repo 自带 DB,不再回落到 `database.DB`。这一轮又继续把 consumer 运行态主链的 DB 依赖显式化:`consumer.RuntimeDeps` 开始携带 DB,worker/controller 生命周期分别把 DB 显式注入 task runtime 与 K8s handler,build/restart/algo reschedule、fault injection 落库、collect result 查询、K8s job/CRD 回调里的 execution/injection 状态推进与后续 task submit 也都改成优先走注入 DB,而不再默认抓全局 `database.DB`。这一轮继续把状态同步链也收进显式 DB:`taskStateUpdate` 新增 DB 上下文,`updateTaskState(...)` / `updateTraceState(...)` / trace optimistic lock 更新现在优先沿调用链携带的 DB 执行;K8s error context 也开始透传 handler 注入 DB,因此 consumer 主链里剩余 `database.DB` 基本只落在少量兼容 fallback 和 `service/common` 默认 wrapper。这一轮顺手再把 `module/evaluation` -> `service/analyzer` 这条链也切到显式 DB:evaluation service 改用 repo 持有 DB 调 analyzer 的 `WithDB` 版本,container/dataset ref 解析与 evaluation 持久化不再依赖 analyzer 内部全局 DB;同时 `module/injection.ExtractDatapacksWithDB(...)` 解析 dataset 时也已改走传入 DB 的 `MapRefsToDatasetVersionsWithDB(...)`。再往下一步,consumer 里 `collect_result` / `fault_injection` / `createExecution` 这类原先“nil 就回落全局 DB”的点也开始直接要求 runtime DB 存在,进一步缩小 fallback 面积。这一轮再继续把兼容层直接砍掉:`service/common` 里默认版 `MapRefsToContainerVersions` / `MapRefsToDatasetVersions` / `ListContainerVersionEnvVars` / `ListHelmConfigValues` / `SubmitTask` / `ProduceFaultInjectionTasks` 已删除,`service/analyzer` 里的默认版 evaluation 入口也删掉,只保留显式 `WithDB` 路径;同时 `consumer/task.go` / `trace.go` / `k8s_handler.go` 里的 DB fallback 也改成显式报错,不再默默回落全局 `database.DB`。紧接着又把 `module/system` 里最后一处直接碰 `database.DB` 的 health check 改成走 `repo.DB()`;目前 `module/*`、`service/common`、`service/consumer`、`service/analyzer` 这批主线包内已无 `database.DB` 残留。顺手又把 repository 层里残留的统计/搜索/资源/注入查询改成统一吃显式 `db` 参数,`repository/task.go` 的 `ListTasksByTimeRange(...)` 也不再偷偷回落全局 DB;现在全仓库只剩 `src/infra/db/module.go` 这一处集中持有 `database.DB`,作为 Fx 提供与关闭数据库连接的基础设施边界。最近两轮又继续把 consumer 外部依赖收窄到 Fx 注入:`interface/worker` 把 `*k8sinfra.Gateway` / `*buildkitinfra.Gateway` / `*helminfra.Gateway` / `*consumer.FaultBatchManager` / `*redisinfra.Gateway` 显式塞进 `consumer.RuntimeDeps`,`build container` / `build datapack` / `algo execution` / `restart pedestal` / `collect result` / task retry / trace state update / K8s callback 已不再直接碰 `GetGateway()` 与 fault batch `sync.Once` 单例;`interface/controller` 同步把 K8s gateway、Redis gateway 和 batch manager 显式交给 `consumer.NewHandler(...)`;`service/logreceiver` 也开始由 `interface/receiver` 注入 Redis publisher,OTLP receiver 不再自己抓 `redisinfra.GetGateway()`。这一轮又继续把 HTTP 链路里的 middleware 全局态收掉:`src/middleware/deps.go` 现在提供 `middleware.Service` 与 `InjectService(...)`,`src/router/router.go` 在根路由中显式注入 middleware service,`src/middleware/permission.go` / `audit.go` 改为按请求从 Gin context 读取 checker/logger,不再持有 `currentPermissionChecker` / `currentAuditLogger` 这类包级默认服务;`src/interface/http/module.go` 也不再用 `fx.Invoke(middleware.RegisterDeps)` 做全局注册。最近这一轮再把 startup 初始化链里的隐藏 fatal 收掉:`newConfigDataWithDB(...)`、`activateConfigScope(...)`、`InitializeProducer(...)`、`InitializeConsumer(...)` 全部改成显式返回 `error`,producer/worker 的 Fx `OnStart` 现在会把初始化失败直接上抛,而不再在 helper 内部 `logrus.Fatalf(...)` 提前退出进程。紧接着这一轮又继续把 consumer startup 链里的 Redis 裸 client 收口到 gateway:worker 初始化改为走 `RedisGateway.InitConcurrencyLock(...)`,`monitor` / `rate limiter` provider 也改成只依赖 `*redisinfra.Gateway`。再下一轮又把模块侧剩余 Redis 全局入口清掉:`module/group` / `module/notification` / `module/trace` / `module/injection` / `module/systemmetric` / `module/system` 现在都改为通过构造注入 `*redisinfra.Gateway`,trace/group/notification stream 读取、injection algorithm cache、system config response subscribe、system metric Redis 查询不再直接碰 `redisinfra.GetGateway()`。这一轮继续把任务队列 helper 也收回 gateway:`infra/redis/task_queue.go` 里的 submit/get/reschedule/dead-letter/queue index/concurrency lock/list/remove 操作全部改成 `Gateway` 方法,`service/common.SubmitTaskWithDB(...)`、`service/consumer` 调度与取消链路、`module/systemmetric` 排队任务查询都已改走显式 Redis gateway。紧接着又把 `infra/redis` / `infra/etcd` / `infra/buildkit` / `infra/helm` / `infra/k8s` 里已经没有调用方的 `GetGateway()` 单例 fallback 全部删除,主线现在只剩少量 lifecycle/startup 组织层 wrapper 需要再压。当前 `src/service/consumer` / `src/middleware` 里残余重点已从“全局 gateway fallback / 全局 default service”收缩到更少的流程组织 helper 与 initialization 邻近收尾。 -- [x] 删除旧包级 repository wrapper - - 已移除 `repository/task.go` 中 Redis 队列职责与 `repository/token.go` 黑名单兼容层。 -- [x] 删除全局 default service -- [x] 清理未使用 imports - -验收: - -- [x] 检查源码与文档中旧 DI 文案残留 -- [x] `cd src && go test ./...` - - 已补齐 `src/cmd/aegisctl/output/output.go`,修复 `cmd/aegisctl` 缺失输出包导致的全量测试阻塞;同时把 `infra/k8s` 的集成 Job 用例改为 `RUN_K8S_INTEGRATION=1` 显式开启,避免默认 `go test ./...` 卡在真实集群状态。 - -## 19. 最终验收 - -功能验收: - -- [x] producer 模式可启动 -- [x] consumer 模式可启动 -- [x] both 模式可启动 -- [x] login / register / refresh 正常 -- [x] Project CRUD 正常 -- [x] Injection 提交流程正常 -- [x] Execution 提交流程正常 -- [x] Task 状态和日志正常 -- [x] Admin 用户管理正常 -- [x] SDK 生成正常 -- [x] Swagger 文档正常 - -架构验收: - -- [x] `main.go` 只负责 mode 和 Fx 启动 -- [x] DB / Redis / HTTP / worker / receiver / controller 都有 lifecycle -- [x] handler 不直接 import `database` -- [x] handler 不直接 import repository implementation -- [x] service 不直接使用全局 `database.DB` -- [x] repository 不直接访问 Redis / K8s / Loki / Etcd -- [x] middleware 不直接依赖具体 producer package -- [x] Public / SDK / Portal / Admin / System 路由分离 -- [x] 业务模块通过 `Module` 暴露 - -说明: - -- `src/app/startup_validate_test.go` 已覆盖 producer / consumer / both 三种 Fx 图校验。 -- `src/app/startup_smoke_test.go` 已覆盖 producer / consumer / both 三种 start/stop smoke,并继续补上 consumer lifecycle 集成冒烟、both 模式的 HTTP + lifecycle 联合冒烟。 -- `src/router/router_test.go` 已锁定 `Public / SDK / Portal / Admin / System` 关键路由前缀分离。 -- `src/app/http_modules.go` 统一通过各业务模块的 `Module` 暴露 HTTP 能力并聚合进 producer app。 -- 启动链补扫后,`src/app` / `src/service/initialization` / `src/interface` / `src/middleware` 生产代码里已无旧 `service/producer` / `handlers/system` / `client/*` 引用,也无残余 `context.Background()` / `GetGateway()` 启动期直拿全局对象。 -- 本轮顺手补齐 `src/cmd/aegisctl/output/output.go`,把 CLI 的 JSON / table / info / error 输出能力收回本地包,`cmd/aegisctl` 不再因缺失输出层而阻塞仓库全量构建。 -- 本轮继续把启动链残余接口壳压掉:`src/app/producer_init.go` 与 `src/interface/{worker,controller,receiver}/module.go` 已从 `ProducerInitializer` / `LifecycleRunner` 接口切到可直接替换的具体 lifecycle struct,smoke test 也同步改成按具体类型替换,启动编排层又薄了一轮。 -- 本轮再顺手清了一批模块仓储纯转发:`src/module/system/repository.go` 的 audit/config/history 查询与写入已直接写实到模块仓储;`src/module/injection/repository.go` 的 groundtruth 更新也不再空转调公共 `repository`;同时 `RegisterProducerInitialization(...)` 已不再额外挂 `CommonResources` 形参。 -- 本轮继续把 `src/module/execution/repository.go` 写实:project resolve / execution list/detail/result / execution labels / result save / batch delete / duration update / labels attach 这一整段已直接落回模块仓储,不再散着空转调 `repository/execution.go`、`repository/detector.go`、`repository/granularity.go`、`repository/label.go`。 -- 本轮再把 `src/module/container/repository.go` 与 `src/module/dataset/repository.go` 两块成片稳定 CRUD 仓储写实:role resolve、container/dataset CRUD、version CRUD、label relation、helm/env/parameter config、dataset version injection 关系等都已回收到模块仓储;目前这两块只保留 dataset search 对共享 query builder 的调用。 -- 本轮继续把剩余一批小模块仓储空转发彻底写回模块:`src/module/{sdk,chaossystem,trace,group,evaluation,task,auth,label}/repository.go` 里的 list/detail/create/update/delete / relation-count / metadata / user-role 查询等都已直接落回模块仓储;`src/module/label/core.go` 也不再直连公共 `repository/label.go`。当前模块侧保留的共享 `repository.ExecuteSearch(...)` 只剩 injection / dataset 两处,作为通用 query builder 基础设施继续复用,不再是无意义兼容层。 -- 本轮继续把 search 这条尾巴也收掉:`src/module/dataset/repository.go` 与 `src/module/injection/repository.go` 已不再调用公共 `repository.ExecuteSearch(...)`,而是直接使用 `repository/query_builder.go` 里的通用 builder 组装查询;公共 `ExecuteSearch` 兼容入口已删除,模块侧只保留对底层 query builder 基础设施的显式使用。 -- 本轮继续顺手压掉一批 raw client / helper 暴露面:`src/module/auth/token_store.go` 与 `src/module/task/queue_store.go` 已改成依赖 `infra/redis.Gateway`,`src/infra/redis/gateway.go` 补齐 `Set/Subscribe` 语义方法,`src/app/common.go` 的 Fx 公共资源探针也改成依赖 `Redis/Etcd Gateway` 而不是裸 client;同时 `src/module/trace/service.go` / `src/module/trace/stream.go` 把 trace stream processor/read 的包级 helper 收回 service,`src/module/group/service.go` 的 group stream processor 初始化也去掉了无意义的 context 包装。 -- 本轮再把剩余 Fx / consumer 暴露面继续压一轮:`src/infra/{redis,etcd}/module.go` 的 `ProvideClient` 已删除,Fx graph 不再向外暴露裸 Redis/Etcd client;`src/service/consumer/{namespace_catalog_store,namespace_lock_store,namespace_status_store,rate_limiter_store}.go` 也都改成直接持有 `infra/redis.Gateway`,`src/service/consumer/{monitor,rate_limiter}.go` 不再在上层显式拿 `gateway.Client()`;另外 `src/app/common.go` 这个仅用于依赖探测的空文件已删除,`src/app/app.go` 不再保留无意义的 `RequireCommonResources` invoke。 -- 本轮继续把模块内部 API 面收紧一层:`src/module/{project,team,dataset,rbac,auth,user,execution,injection,container,system}/repository.go` 的 `WithDB` 已统一缩成包内 `withDB`;`src/module/{execution,injection}/repository.go` 的 `EnsureProjectExists`、`src/module/user/repository.go` 的 `EnsureUserExists` 也已缩成包内 helper;`src/module/{execution,injection,container,system,evaluation}` 里原先为 service 暴露的 `DB()` 访问器已删除,service 直接在包内使用 repository 持有的 db。 -- 本轮顺手再收一批 `context.Background()` 残点:`src/module/task/log_service.go`、`src/module/injection/handler.go`、`src/module/injection/service.go`、`src/module/system/service.go` 已改成沿调用链传递 request/service context;`src/module/systemmetric/collector.go` 也改成 lifecycle 管理的 collector context,在 `OnStop` 时显式 cancel。 -- 本轮继续压掉最后一批显眼的 helper 暴露:`src/module/systemmetric/service.go` 已改成直接使用 `infra/redis.Gateway` 暴露的 `SetMembers / HashGetAll / ZRangeByScore / ZAdd / ZRemRangeByScore` 语义方法,`src/module/system/service.go` 的 Redis 健康检查也切到 `gateway.Ping`;同时 `src/infra/redis/gateway.go` 又补齐这一批语义 API,模块/系统层不再直接拼裸 Redis 命令。当前生产代码里已无 `context.Background()` 残点,剩余 `redisGateway.Client()` 仅收敛在 `service/consumer/*store.go` 这一层 Redis 原语适配代码中。 -- 本轮继续把 consumer 最后一层 Redis 原语适配再往 infra 收:`src/service/consumer/{namespace_catalog_store,namespace_status_store,namespace_lock_store,rate_limiter_store}.go` 里残余的 `gateway.Client()` 已全部清掉,分别改成走 `infra/redis/gateway.go` 新增的 `Exists / HashGet / HashSet / SeedNamespaceState / SetRemove / RunScript / Watch` 等语义方法;当前生产代码中 `service/consumer` / `module` / `app` / `interface` 已无直接 `gateway.Client()` 调用,裸 Redis client 已彻底退回 `infra/redis` 内部实现。 -- 本轮继续把 infra 边界再收紧一层:`src/infra/{redis,etcd}/gateway.go` 的公开 `Client()` 暴露面已删除,连接初始化/关闭统一收进私有 `clientOrInit()/close()`;`src/infra/k8s/{job,controller}.go` 中原先仅供 gateway 转调的 `CreateJob / GetJobPodLogs / GetVolumeMountConfigMap / NewController` 也已缩成包内私有实现,`Gateway` 成为对外唯一主入口。 -- 本轮继续清 initialization 残余全局态:`src/service/initialization/{producer,consumer}.go` 不再持有包级 `producerData / consumerData / resourceIDMap`,初始化配置状态改成局部装配后沿调用链使用;`InitializeSystems(...)` 也已改为显式返回 `error`,producer 启动链不再吞掉系统注册失败。 -- 本轮继续压一轮启动壳与模块内部 helper:`src/app/producer_init.go`、`src/interface/{worker,controller,receiver}/module.go`、`src/interface/http/{module,server}.go` 中的 lifecycle/register helper 已全部收成包内私有;`src/module/project/repository.go` 删掉 `loadProjectLabelView(...)`,`src/module/rbac/repository.go` 删掉 `loadWritableRole(...)` 并把 system-role 校验内聚回具体语义方法,`src/module/user/repository.go` 则把 role/container/dataset/project 四组原子 load helper 收成单个 `ensureActiveRecordExists(...)`,`src/module/team/repository.go` 把 role 校验压成 `ensureRoleExists(...)`,`src/module/injection/repository.go` 的 `ensureProjectExists(...)` 也已删掉并改为 service 包内直接使用 repo DB 校验项目存在性。 -- 本轮最后再把 execution / user 邻近模块尾巴收掉:`src/module/execution/repository.go` 的 `ensureProjectExists(...)` 已删除,project 存在性校验直接回到 `service.go` 包内用 repo DB 执行;`src/module/user/repository.go` 的 `ensureUserExists(...)` 也已删掉,统一并入已有 `ensureActiveRecordExists(...)`,避免“同一语义多套 helper”继续扩散。 -- 最终抛光轮再顺手做了一轮“模块 repo API 面收紧”:`src/module/execution/repository.go` 的 `GetProjectByName / List*View / GetExecution* / ListAvailableExecutionLabels / ListExecutionLabelIDsByKeys`,以及 `src/module/project/repository.go` 的 CRUD/label 管理主方法、`src/module/team/repository.go` 的 team CRUD / list / membership 读取主方法,均已统一缩成包内私有实现,只保留 service 真正需要的模块边界;模块内部语义仍在,但对外可见面进一步变薄。 -- 最终抛光轮又继续做了两件事:一是把 `src/module/user/repository.go` 与 `src/module/rbac/repository.go` 里仅供 service 使用的 repo 主方法再统一缩成包内私有,模块命名/API 面进一步一致;二是在 `src/app/startup_smoke_test.go` 补上 producer HTTP 集成冒烟,真实启动 Fx producer app 后校验 `/docs/doc.json` 可访问、`/system/configs/abc` 会经过真实路由与鉴权链返回 `401`,把“能启动”进一步提升到“能接住实际 HTTP 主路径”。 -- 本轮已补 `src/module/auth/service_test.go`,覆盖 `register / login / refresh` 成功路径,并顺手修正 `module/auth` / `module/user` 创建用户时密码重复 hash 的问题,避免注册后登录链路天然失效。 -- 本轮已扩充 `src/module/project/service_test.go`,覆盖 `create / get detail / list / update / delete` 主路径;`Project CRUD` 现已具备模块级成功路径保护,剩余是更贴近真实依赖的运行态验收。 -- 本轮已扩充 `src/module/execution/service_test.go`,覆盖标签列表、列表过滤、detector / granularity 结果上传成功路径;`execution result` 主路径已有模块级保护。 -- 本轮新增 `src/module/task/service_test.go`,覆盖 task 列表成功路径与 Loki 历史日志读取;`Task 状态 / 日志` 这条线已具备模块级成功路径保护。 -- 本轮继续扩充 `src/module/execution/service_test.go` 与 `src/module/injection/service_test.go`,分别补上 `SubmitAlgorithmExecution` 和 `SubmitDatapackBuilding` 成功路径;`Injection / Execution 提交` 主路径已具备模块级提交保护。 -- 为避免真实 Redis 依赖阻塞主线验收,本轮新增 `src/testutil/redisstub.go` 作为极小测试桩,仅覆盖任务提交用到的 Redis 命令,供模块级 submit 测试使用。 -- 本轮已补 `src/module/user/service_test.go` 的 create / detail / delete 成功路径,`Admin 用户管理` 主路径现已具备模块级成功路径保护。 -- 本轮新增 `src/module/sdk/service_test.go`,覆盖 SDK evaluation / experiment / dataset sample 主路径;`SDK` 主路径已具备模块级成功路径保护。 -- 本轮新增 `src/docs/docs_test.go`,校验 `openapi2` / `openapi3` / `converted/sdk.json` 三类文档产物存在且包含核心接口路径;同时 `src/router/router.go` 已显式注册 `aegis/docs/openapi2`,`src/router/router_test.go` 继续锁定 `/docs/doc.json` 可直接返回 Swagger 文档。 -- 本轮已完成 `cd src && go test ./...`;当前默认全量测试口径已打通,K8s Job 的真实集群冒烟改为按需用 `RUN_K8S_INTEGRATION=1 go test ./infra/k8s` 单独执行。 -- 本轮继续把 `src/infra/k8s/k8s_test.go` 升级成更明确的真实集群验收入口:先做 `Gateway.CheckHealth(...)` 预检,再跑 job create/get/wait/logs/delete 全链路;同时支持 `RUN_K8S_INTEGRATION_NAMESPACE`、`RUN_K8S_INTEGRATION_IMAGE`、`RUN_K8S_INTEGRATION_KEEP_JOB` 三个可选环境变量,便于回填真实环境验收。 -- 本轮继续把 `src/infra/k8s/gateway.go` 收成 K8s job 生命周期主入口,补上 `GetJob / WaitForJobCompletion / DeleteJob` 这组 gateway 语义方法;`WaitForJobCompletion(...)` 也改成尊重 context 取消,并在 Job 失败条件出现时尽早返回。 -- 本轮继续扩充 `src/app/startup_smoke_test.go`:新增 `TestConsumerOptionsLifecycleIntegrationSmoke`,锁定 worker/controller/receiver 的真实 Fx 启停;新增 `TestBothOptionsHTTPAndLifecycleIntegrationSmoke`,在 both 模式下同时校验 producer 初始化、consumer 生命周期和 `/docs/doc.json` / `/system/configs/:id` 这组真实 HTTP 主路径。 - -当前说明: - -- 第 19 节功能验收已按“模块级成功路径 + 路由/文档产物校验 + app 启动 smoke”口径全部补齐。 -- 目前若继续做,已基本进入纯抛光阶段:更激进的命名统一、个别 repo/helper 再折叠、以及更贴近真实外部依赖的集成验收,都不再阻塞 Fx 主线收口。 -- 本轮已再次重跑三组主路径验证命令:`go test ./module/auth ./module/project ./module/execution ./module/injection ./module/task ./module/user ./module/sdk ./router ./docs ./app`、`go test ./app ./service/consumer ./service/logreceiver ./interface/controller ./interface/receiver ./interface/worker`、`go test ./app ./router ./interface/http ./middleware`,当前均通过。 -- 本轮再补跑 `go test ./...`,当前也已通过。 - -## 20. 仓库级收尾检查清单 - -- [x] 默认回归:`cd src && go test ./...` -- [x] Producer Fx 图校验与 HTTP 主路径:`cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke'` -- [x] Consumer / Both 生命周期集成冒烟:`cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions'` -- [x] 路由 / 文档主路径:`cd src && go test ./router ./docs ./interface/http` -- [x] 真实 K8s 集群验收:`cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` -- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_NAMESPACE=` -- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_IMAGE=` -- [x] 可选真实环境参数已提供:`RUN_K8S_INTEGRATION_KEEP_JOB=1` -- [x] producer / consumer / both 主启动链已无旧 `service/producer` / `handlers/system` / `client/*` 运行态依赖 -- [x] K8s / Redis / Etcd / Harbor / Helm / BuildKit 等 infra 主入口已统一收口到 `src/infra/*` -- [x] 仓库级残余兼容面补扫通过 - - 已用 `rg` 对 `service/producer`、`handlers/system`、`GetGateway()`、`CurrentK8s*`、`database.DB`、启动链 `context.Background()` 等模式做补扫;`src/app` / `src/interface` / `src/service` / `src/module` / `src/router` / `src/middleware` 生产代码内未发现这批旧运行态依赖残留。 - -## 当前建议下一步 - -从这里开始: - -1. 如需复验真实集群,执行 `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` -2. 常规回归继续跑 `cd src && go test ./...` -3. 如需继续推进,优先进入第 17 节 SDK 标记治理;其余已基本属于文档/命名/测试抛光 - -## 21. 微服务拆分主线 - -目标:在当前 Fx + module + infra 主线已收口的基础上,把运行时进一步演进为“外部 HTTP、内部 gRPC、执行异步队列”的明确微服务架构,而不是继续在单体模式下扩张。 - -- [x] 微服务设计/治理主文档已收口到 `docs/report-index.md` -- [x] 在 `src/app/` 建立第一批服务边界分组:`gateway / runtime / iam / resource / orchestrator / system` -- [x] 新增第一批可运行服务入口:`src/cmd/api-gateway`、`src/cmd/runtime-worker-service`、`src/cmd/iam-service` -- [x] Runtime Worker Service:补 `runtime.proto` 与 gRPC control-plane(`Ping / GetRuntimeStatus / GetQueueStatus / GetLimiterStatus`) -- [x] IAM Service:补 `iam.proto` 与 token verify / permission check / API key exchange gRPC -- [x] Orchestrator Service:补 `orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service` -- [x] Orchestrator Service:首批 submit / cancel RPC 已收口(`Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask`) -- [x] Gateway -> Orchestrator:execution / injection submit 主路径已支持通过 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 切到内部 gRPC -- [x] Orchestrator Service:workflow state / task query / dead-letter / retry 首批控制面已收口 - - 当前已新增 `GetTask / ListTasks / GetTrace / ListTraces / ListDeadLetterTasks / RetryTask` 六个内部 RPC,并继续保留 Redis 作为执行异步主通道不变。 -- [x] Orchestrator Service:execution owner 的 runtime/evaluation mutation/query facade 已落地 - - 当前已新增 `CreateExecution / CreateInjection / UpdateExecutionState / UpdateInjectionState / UpdateInjectionTimestamps / GetExecution / ListEvaluationExecutionsByDatapack / ListEvaluationExecutionsByDataset` 八个内部 RPC;runtime 状态推进与 evaluation 执行结果查询在配置 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 后已优先走 owner facade。 -- [x] Runtime Worker:已切掉对 orchestrator owner 的共享 repository 直读/直写 - - 当前 `src/service/consumer/*` 已不再直接 import `src/repository/*`;执行创建、fault injection 创建、K8s 回调状态推进、结果收集在未配置 orchestrator gRPC 时也只回退到本地 `executionmodule.Service` / `injectionmodule.Service` owner 实现。 -- [x] `service/common`:首批 container / label 共享 helper 已回收到 owner 模块 - - 当前 `src/service/common/container.go` 与 `src/service/common/label.go` 已删除;container version/parameter 解析与 label upsert 已分别收回 `src/module/container/*`、`src/module/label/*`,`module/{execution,injection,evaluation,project,container,dataset}` 与 consumer K8s 回调已改走 owner 模块实现。 -- [x] `service/common`:dataset version 共享 helper 已回收到 owner 模块 - - 当前 `src/service/common/dataset.go` 已删除;dataset version 解析已收回 `src/module/dataset/resolve.go`,同时 `src/module/dataset/api_types.go` 也已去掉对 `module/injection` 的响应类型依赖,避免再次形成模块循环。 -- [x] Resource / System Service:已补独立启动入口 `src/cmd/resource-service`、`src/cmd/system-service` -- [x] Resource Service:首批资源/评估查询 gRPC 已落地(`Ping / ListProjects / GetProject / ListContainers / GetContainer / ListDatasets / GetDataset / ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation`) -- [x] System Service:首批系统运维 gRPC 已落地(`Ping / GetHealth / GetMetrics / GetSystemInfo / ListConfigs / GetConfig / ListAuditLogs / GetAuditLog / ListNamespaceLocks / ListQueuedTasks / GetSystemMetrics / GetSystemMetricsHistory`) -- [x] Runtime -> System:namespace locks / queued tasks 首批运行态查询已从 Redis 直读收口到 runtime gRPC - - 当前 `src/proto/runtime/v1/runtime.proto` 已新增 `GetNamespaceLocks / GetQueuedTasks`,`src/module/system/service.go` 在配置 `clients.runtime.target` 或 `runtime_worker.grpc.target` 后会优先走 `src/internalclient/runtimeclient/*`,未配置时保留本地回退。 -- [x] Gateway -> Resource:project / container / dataset / evaluation 主路径已支持通过 `clients.resource.target` 或 `resource.grpc.target` 切到内部 gRPC -- [x] Gateway -> System:`system` / `systemmetric` 首批读路径已支持通过 `clients.system.target` 或 `system.grpc.target` 切到内部 gRPC -- [x] `app.CommonOptions()` 已开始按服务边界拆细 - - 当前已落地 `BaseOptions / ObserveOptions / DataOptions / CoordinationOptions / BuildInfraOptions`,`iam/resource/orchestrator/system` 已切到更窄装配口径。 -- [x] 第一轮跨 owner 共享 repository / DB 直查补扫已完成 - - 当前 `src/app` / `src/interface` / `src/internalclient` 侧已无直查 DB;残余主要收敛在 owner 模块内部 repository 和少量本地 fallback 继续压缩。 -- [x] Resource / System Service:资源元数据与运维控制面首轮独立服务边界已落地 - - 当前 `resource-service` 已承接 project / container / dataset / evaluation / label / chaos-system 资源元数据主路径,`system-service` 已承接 health / config / audit / monitor / systemmetric 运维控制面主路径;剩余更细粒度拆分进入后续非阻塞治理阶段,不再阻塞当前主线收口。 - -说明: - -- 当前第一轮“文档 + 骨架 + 可运行入口 + 核心 RPC 主路径”已完成;后续如继续演进,重点转向 ownership 深清与发布治理,而不是主线入口缺失。 -- 当前 `api-gateway` 语义上对应既有 producer HTTP 栈,`runtime-worker-service` 语义上对应既有 consumer 栈;其余服务的核心内部 RPC 与独立启动入口已落地,后续再按边界细化 owner 职责。 -- 本轮已落地 `src/proto/runtime/v1/runtime.proto`、`src/interface/grpcruntime/*` 与 queue/limiter/runtime snapshot 聚合能力,并把 gRPC lifecycle 接入 `ConsumerOptions` / `BothOptions`;默认监听 `:9094`,可通过 `runtime_worker.grpc.addr` 覆盖。 -- 本轮继续扩展 `runtime-worker-service` control-plane:当前额外提供 `GetNamespaceLocks / GetQueuedTasks`,用于承接 runtime Redis 运行态对内查询。 -- 本轮继续落地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/*` 与 `src/cmd/iam-service`,当前 IAM 内部 RPC 已覆盖鉴权、API key、team、user、rbac 五组主路径:除 `VerifyToken / CheckPermission / ExchangeAPIKeyToken` 与 team membership 判定外,也已补齐 `Login / Register / RefreshToken / Logout / ChangePassword / GetProfile / API key CRUD`、`Create/Get/List/Update/Delete user`、user role/permission/resource 绑定、`Create/Get/List/Update/Delete role`、role-permission 绑定以及 permission/resource 查询;默认监听 `:9091`,可通过 `iam.grpc.addr` 覆盖。 -- 本轮继续补上 `src/proto/orchestrator/v1/orchestrator.proto`、`src/interface/grpcorchestrator/*` 与 `src/cmd/orchestrator-service`,当前 Orchestrator 内部 RPC 已提供 `Ping / SubmitExecution / SubmitFaultInjection / SubmitDatapackBuilding / CancelTask` 五个入口;默认监听 `:9092`,可通过 `orchestrator.grpc.addr` 覆盖。 -- 本轮继续扩展 `orchestrator-service` 控制面:当前额外已提供 `GetTask / ListTasks / GetTrace / ListTraces / ListDeadLetterTasks / RetryTask` 六个入口,用于 workflow state 查询、dead-letter 补偿与手动 retry;同时执行/消费异步仍保持 Redis queue/event 主链不变。 -- 本轮继续扩展 `orchestrator-service` owner facade:当前又额外提供 `CreateExecution / CreateInjection / UpdateExecutionState / UpdateInjectionState / UpdateInjectionTimestamps / GetExecution / ListEvaluationExecutionsByDatapack / ListEvaluationExecutionsByDataset` 八个入口,分别承接 runtime 状态回写与 evaluation 执行结果查询。 -- Gateway 侧已补 `src/internalclient/orchestratorclient/*`,并通过 `src/app/gateway/options.go` 把 `execution` / `injection` handler 使用的 submit 服务装饰为 remote-aware;配置 `clients.orchestrator.target` 或 `orchestrator.grpc.target` 后,`SubmitAlgorithmExecution / SubmitFaultInjection / SubmitDatapackBuilding` 会优先走内部 gRPC,未配置时继续回退本地实现。 -- `src/app/consumer.go` 当前也已补 execution/injection owner 模块,使 `runtime-worker-service` 在未配置 orchestrator gRPC 时改为回退到本地 owner service,而不再直接碰共享 repository;`interface/worker` / `interface/controller` 会把这两个 owner service 显式注入 consumer runtime deps 与 K8s handler。 -- 同时已把 `src/cmd/resource-service` 与 `src/cmd/system-service` 补齐,后续可以直接在对应服务边界上继续补 `resource.proto` / `system.proto` 和对内 gRPC。 -- 本轮继续补上 `src/proto/resource/v1/resource.proto`、`src/interface/grpcresource/*` 与 `src/app/resource/options.go` 接线,当前 Resource 内部 RPC 已提供 `Ping / ListProjects / GetProject / ListContainers / GetContainer / ListDatasets / GetDataset / ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation` 十二个入口;默认监听 `:9093`,可通过 `resource.grpc.addr` 覆盖。 -- 本轮继续补上 `src/proto/system/v1/system.proto`、`src/interface/grpcsystem/*` 与 `src/app/system/options.go` 接线,当前 System 内部 RPC 已提供 `Ping / GetHealth / GetMetrics / GetSystemInfo / ListConfigs / GetConfig / ListAuditLogs / GetAuditLog / ListNamespaceLocks / ListQueuedTasks / GetSystemMetrics / GetSystemMetricsHistory` 十二个入口;默认监听 `:9095`,可通过 `system.grpc.addr` 覆盖。 -- `src/app/system/options.go` 现已补齐 `k8sinfra.Module` 与 `runtimeclient.Module`,`module/system.Service` 对 `ListNamespaceLocks / ListQueuedTasks` 已优先走 runtime gRPC,把 system/runtime 的首批运行态交互从直接 Redis 读取改成内部 client 边界。 -- Gateway 侧已继续补 `src/internalclient/resourceclient/*`,并通过 `src/app/gateway/options.go` 把 `project` / `container` / `dataset` handler 使用的稳定 list/detail 读服务装饰为 remote-aware;配置 `clients.resource.target` 或 `resource.grpc.target` 后,`ListProjects / GetProjectDetail / ListContainers / GetContainer / ListDatasets / GetDataset` 会优先走内部 gRPC,未配置时继续回退本地实现。 -- Gateway 侧现已补 `src/internalclient/systemclient/*`,并通过 `src/app/gateway/options.go` 把 `system` / `systemmetric` handler 使用的查询服务装饰为 remote-aware;配置 `clients.system.target` 或 `system.grpc.target` 后,`/system/*` 与 `/api/v2/system/metrics*` 会优先走内部 gRPC,未配置时继续回退本地实现。 -- 这一轮继续把 dedicated service 入口往 remote-first 收紧:`src/app/gateway/options.go` 现在会在启动时显式校验 `iam / orchestrator / resource / system` 四类 internal client target,`src/app/runtime/options.go` 会校验 orchestrator target,`src/app/system/options.go` 会校验 runtime target,避免 `api-gateway` / `runtime-worker-service` / `system-service` 这类独立服务入口继续静默回退本地 owner 实现。 -- 这一轮又继续把 standalone runtime 边界再压一层:`src/app/runtime/options.go` 已不再直接复用整套 `ConsumerOptions()`,而是去掉 `executionmodule.Module` / `injectionmodule.Module`,`src/interface/worker/module.go` 与 `src/interface/controller/module.go` 中对应 owner service 依赖已改成可选,避免 `runtime-worker-service` 独立入口继续显式装配本地 execution/injection owner。 -- 这一轮再把独立服务入口的验收补到位:新增 `src/app/service_entrypoints_test.go`,已覆盖 `api-gateway` 的真实 HTTP 冒烟,以及 `runtime-worker-service / resource-service / system-service` 的真实 gRPC 冒烟;`api-gateway` / `runtime-worker-service` 的独立启动与 runtime control-plane 可用性现在都有自动化保护。 -- 这一轮继续把“启动命令 / 配置 / 本地编排”说明收口到 `docs/report-index.md`,并新增 `docker-compose.microservices.yaml` 作为与现有 `docker-compose.yaml` 叠加的多服务本地 compose 骨架;`src/config.dev.toml` 与 `config.dev.toml` 也已补齐 `clients.*.target` 及 `iam/resource/orchestrator/runtime_worker/system` 的 gRPC 默认端口,方便直接按拆分模式起服务。 -- 这一轮继续把“镜像入口 / probe 规范 / K8s skeleton”说明也并入 `docs/report-index.md`:`src/main.go` 已增加 `api-gateway / iam-service / resource-service / orchestrator-service / runtime-worker-service / system-service` 六个新子命令,现有镜像可直接用同一二进制起拆分服务;同时 `manifests/microservices/aegislab-microservices.yaml` 已统一 gateway 的 HTTP `/system/health` probe 与五个 gRPC 服务的 health probe,并补上第一版多服务 Deployment/Service 骨架。 -- 最终仓库级收尾轮已把补扫与收尾结论并入 `docs/report-index.md`:`src` 生产代码里 `service/producer` / `handlers/system` / `database.DB` / `GetGateway()` / `redisinfra.GetGateway()` 这批旧兼容模式已为零命中,`context.Background()` 残留也只在测试中;当前主线可视为完成,剩余主要转入兼容入口 owner 组合继续压缩与少量跨服务 DB 深清。 -- 同一轮里也已把错误码、request-id、观测标签、internal proto、配置命名和 owner 约束统一写实,并收口到 `docs/report-index.md`;治理项已不再是“规范空缺”,当前更多是发布执行层持续收口。 -- 继续执行层收口后,HTTP/gRPC 的 request-id 主路径也已正式落地:`src/router/router.go` 现已统一挂 `X-Request-Id` middleware,`src/internalclient/*` 已统一透传 `x-request-id` metadata,治理规范不再只停留在文档。 -- 同一批收口里,`src/interface/grpc*` 也已统一在 server 入口提取/补齐 request-id;另外 `module/dataset` / `module/injection` 对旧共享 `repository.NewSearchQueryBuilder` 的依赖已清掉,通用搜索装配收到了独立 `src/searchx`。 -- 这一轮又继续把 dedicated `api-gateway` 入口的语义收紧:`src/app/gateway/*` 中经 `iam/resource/orchestrator/system` 的 remote-aware wrapper 已不再静默回退本地 owner service;同时 `src/repository/scope.go` 也已删除,旧共享排序 helper 不再继续扩大。 -- 同一轮里,`src/interface/worker` / `src/interface/controller` 也不再直接依赖 `executionmodule.Service` / `injectionmodule.Service`;runtime 执行 owner 已统一改由 `consumer.ExecutionOwner` / `consumer.InjectionOwner` 注入,owner fallback 面进一步收到了 `src/service/consumer/owner_adapter.go` 单点。 -- 继续收口后,dedicated `api-gateway` / `runtime-worker-service` / `system-service` 这几条入口已基本形成明确的 remote-required 语义;当前残余 local adapter 主要服务于 `producer` / `consumer` / `both` 兼容入口,而不是新的 dedicated service 主路径。 -- 再往下一轮,`resource-service` / `system-service` / `runtime-worker-service` 也已分别通过 `evaluationmodule.RemoteQueryOption()` / `systemmodule.RemoteRuntimeQueryOption()` / `consumer.RemoteOwnerOptions()` 把 dedicated service 路径上的查询/owner 适配器收成 remote-only,进一步减少“同一服务里同时挂本地和远端两套语义”的过渡态。 -- 这一轮继续沿跨服务 DB/owner 深清推进 `team -> project` 这条线:`src/module/team/project_reader.go` 新增 remote-aware project reader,team detail 的 project count 与 team project list 在配置 `clients.resource.target` 或 `resource.grpc.target` 后会优先经 `resource-service` 获取,`iam-service` 也已显式要求 `resource-service` target;对应地 `src/module/project.ListProjectReq` / `src/module/project/service.go` / `src/module/project/repository.go` 已补 `team_id` 与 `include_statistics`,使 team 侧远程 count 可直接复用 `ListProjects` 且可跳过 project statistics 聚合,先把 IAM/gateway 对 `projects`、`fault_injections`、`executions` 的这条直查面压掉一层。 -- 这一轮继续把 `resource-service` 里的 project statistics 主路径也收进 owner facade:新增 `src/module/project/project_statistics.go`,project service 不再直接在资源侧 repo 中拼 execution/injection 统计,而是统一经 `projectStatisticsSource` 获取;`src/app/resource/options.go` 已用 `projectmodule.RemoteStatisticsOption()` 把 dedicated resource 路径强制到 orchestrator RPC。对应地 `src/proto/orchestrator/v1/orchestrator.proto`、`src/interface/grpcorchestrator/*`、`src/internalclient/orchestratorclient/client.go` 已补 `ListProjectStatistics` 内部 RPC,resource 主路径上的 project detail/list statistics 不再直查 owner 表。 -- 这一轮又继续把兼容入口装配层压实到单点:新增 `src/app/compat_options.go`,把 producer 侧 HTTP/K8s/chaos 与 producer init/http server 装配收成 `ProducerCompatibilityOptions / ProducerHTTPEntryOptions`,把 consumer/both 共享的本地 owner runtime 组合继续收成 `CompatibilityRuntimeOptions()`;`src/app/producer.go`、`consumer.go`、`both.go`、`gateway/options.go` 现在不再各自重复拼 `Base/Observe/Data/Coordination/Build + modules + init + http`,同时已删掉 `NormalizeAddr(...)` 与 gateway 专用 `NewProducerInitializerForGateway / RegisterProducerInitializationForGateway` 这类多余壳函数。 -- 这一轮继续把 dedicated `api-gateway` 的 metrics 边界收紧:`src/module/metric` 已补 `HandlerService`,`src/app/gateway/metric_services.go` 新增 remote-aware metrics wrapper,gateway 上的 `/api/v2/metrics/injections|executions|algorithms` 不再直接落本地 `fault_injections / executions / containers` 表;其中 injection/execution metrics 已走新增的 orchestrator RPC `GetInjectionMetrics / GetExecutionMetrics`,algorithm metrics 则由 gateway 经 `resource-service` 拉 algorithm 列表后再按算法向 orchestrator 聚合执行指标,先把 dedicated gateway 这块跨 owner 直查面收掉。 -- 这一轮继续把 dedicated `api-gateway` 的 team 主路径切到 IAM:`src/module/team` 已补 `HandlerService`,`src/app/gateway/team_services.go` 新增 remote-aware team wrapper,gateway 上的 `/api/v2/teams/*` 现在统一经 `iamclient` 转发 `Create/Get/List/Update/Delete`、member 管理、team project/member 列表,而不再直接吃本地 team owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 已补齐 team RPC 面。同时 `src/module/team/project_reader.go` 又新增 `RemoteProjectReaderOption()`,`src/app/iam/options.go` 已把 dedicated `iam-service` 上的 team->project 视图继续收成 resource RPC-only。 -- 这一轮再把 dedicated `api-gateway` 的 IAM 剩余主路径继续收口:`src/module/{auth,user,rbac}` 已补 `HandlerService`,`src/app/gateway/{auth,user,rbac}_services.go` 新增 remote-aware wrapper,gateway 上的 `/api/v2/auth/*`、`/api/v2/api-keys/*`、`/api/v2/users/*`、`/api/v2/roles|permissions|resources/*` 已统一经 `iamclient` 转发,不再在 dedicated `api-gateway` 入口直接吃本地 IAM owner 实现;对应地 `src/proto/iam/v1/iam.proto`、`src/interface/grpciam/service.go`、`src/internalclient/iamclient/client.go` 也已补齐 auth/user/rbac RPC 面。 -- `src/app/app.go` 已开始按服务边界拆装配层:当前新增 `BaseOptions / ObserveOptions / DataOptions / CoordinationOptions / BuildInfraOptions`,独立服务启动链不再统一吃满所有 infra。 -- `src/app/resource/options.go` 这一轮继续把 standalone 边界推进到 `project / label / container / dataset / evaluation`;`resource-service` 已接入 `orchestratorclient.Module` 承接 evaluation -> orchestrator 的远程查询,gateway 对 `evaluation` handler 也已补上 remote-aware 装饰。 -- 这一轮继续把 dedicated `api-gateway` 的 label 主路径切到 Resource:`src/module/label` 已补 `HandlerService`,`src/proto/resource/v1/resource.proto` / `src/interface/grpcresource/service.go` / `src/internalclient/resourceclient/client.go` 已补齐 `Create/Get/List/Update/Delete/BatchDelete label` 对内 RPC;同时 `src/app/gateway/resource_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/labels/*` 改成统一经 `resource-service` 转发,dedicated gateway 不再直接承载 label owner 读写。 -- 这一轮继续把 dedicated `api-gateway` 的 admin systems 主路径切到 Resource:`src/module/chaossystem` 已补 `HandlerService`,`resource-service` 现已纳入 `chaossystemmodule.Module`,并通过 `src/proto/resource/v1/resource.proto` / `src/interface/grpcresource/service.go` / `src/internalclient/resourceclient/client.go` 承接 `List/Get/Create/Update/Delete chaos system` 与 `metadata upsert/list`;同时 `src/app/gateway/resource_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/systems/*` 改成统一经 `resource-service` 转发,继续缩小 dedicated gateway 上的本地 resource owner 面。 -- 这一轮继续把 dedicated `api-gateway` 的 task / trace 查询主路径切到 Orchestrator:`src/module/{task,trace}` 已补 `HandlerService`,`src/internalclient/orchestratorclient/client.go` 已补 `GetTask / ListTasks / GetTrace / ListTraces`,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/tasks/{id}`、`/api/v2/tasks`、`/api/v2/traces/{id}`、`/api/v2/traces` 改成统一经 `orchestrator-service` 转发;日志 WebSocket 与 trace SSE 仍保留本地实现,留待后续流式通道单独收口。 -- 这一轮继续把 dedicated `api-gateway` 的 group stats 查询主路径切到 Orchestrator:`src/module/group` 已补 `HandlerService`,`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已补 `GetGroupStats` 内部 RPC,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/groups/{group_id}/stats` 改成统一经 `orchestrator-service` 转发;group SSE stream 仍保留本地实现,留待后续流式通道单独收口。 -- 这一轮继续把 dedicated `api-gateway` 的 SSE 主路径切到 Orchestrator:`src/module/notification` 已补 `HandlerService`,`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已新增 `GetTraceStreamState / ReadTraceStreamMessages / GetGroupStreamState / ReadGroupStreamMessages / ReadNotificationStreamMessages` 五个内部 RPC,`src/app/gateway/orchestrator_services.go` 与 `src/app/gateway/options.go` 已把 `/api/v2/traces/{trace_id}/stream`、`/api/v2/groups/{group_id}/stream`、`/api/v2/notifications/stream` 改成统一经 `orchestrator-service` 读取流式批次;当前 dedicated gateway 残余主线只剩 task logs WebSocket 尚未收成内部通道。 -- 这一轮继续把 dedicated `api-gateway` 的 task logs WebSocket 也切到 Orchestrator:`src/proto/orchestrator/v1/orchestrator.proto` / `src/interface/grpcorchestrator/service.go` / `src/internalclient/orchestratorclient/client.go` 已新增 `PollTaskLogs` 内部 RPC,`src/module/task/service.go` 新增基于 Loki 的 owner-side log poll facade,`src/app/gateway/orchestrator_services.go` 则把 `/api/v2/tasks/{task_id}/logs/ws` 改成由 gateway 继续负责边缘 WebSocket、但日志历史/轮询数据统一经 `orchestrator-service` 获取;dedicated gateway 主线上的 task/trace/group/notification 读写 owner 残余面已基本清空。 -- 这一轮再把 `module/evaluation` 的查询源约束收紧一层:`src/module/evaluation/service.go` 里 `Execution` 依赖已改成可选,若既没有 orchestrator client、也没有本地 execution owner,会直接显式报错;同时补了 `src/module/evaluation/service_test.go` 锁住这条行为。 -- 这一轮又继续把 gateway -> system 的旧监控接口 fallback 收紧一层:`src/module/system/handler_service.go` / `src/module/system/handler.go` / `src/app/gateway/system_services.go` / `src/interface/grpcsystem/service.go` 里的 `GetMetrics / GetSystemInfo` 已统一改成返回 `(..., error)`;配置 `systemclient` 后不再在 remote 调用失败时静默吞掉错误并回退本地结果。 -- 这一轮继续把 runtime 的 owner fallback 收口成单点适配器:新增 `src/service/consumer/owner_adapter.go`,`collect_result / fault_injection / algo_execution / state_store / k8s_handler` 不再各自散落判断 `orchestratorclient` 与本地 owner,而是统一经 `ExecutionOwner / InjectionOwner` 做 remote-first 路由;`src/interface/{worker,controller}/module.go` 也改为只在装配层创建这两个 owner 适配器,把 fallback 面进一步压缩到 consumer 单点。 -- 这一轮再把 gateway 请求上下文继续贯穿一层:`src/app/gateway/middleware_service.go` 不再用 `context.Background()` 调 IAM client,`middleware.Service` 的 permission helper 已统一改成显式接收 `context.Context`;同时 `src/module/system/*` / `src/interface/grpcsystem/*` 也把 `GetMetrics / GetSystemInfo / GetAuditLog / ListAuditLogs / GetConfig / ListConfigs` 这批读接口改成透传请求上下文,旧 system remote-aware wrapper 不再自己造背景上下文。 -- 这一轮也顺手把 evaluation -> orchestrator 的本地/远程路由收成单点:新增 `src/module/evaluation/execution_query.go`,`module/evaluation.Service` 不再自己持有 `orchestratorclient + execution service` 两套判断,而是统一走 `executionQuerySource` 适配器。 -- 这一轮继续把 evaluation 主路径真正并进 `resource-service`:`src/interface/grpcresource/service.go`、`src/internalclient/resourceclient/client.go`、`src/app/gateway/resource_services.go` 已补齐 `ListDatapackEvaluationResults / ListDatasetEvaluationResults / ListEvaluations / GetEvaluation / DeleteEvaluation`,gateway 在配置 `clients.resource.target` 或 `resource.grpc.target` 后会优先走 resource gRPC。 -- 这一轮继续把 startup 壳和 system runtime fallback 再压一层:新增 `src/app/runtime_stack.go` 把 runtime worker 的 infra/provider/interface 装配统一抽成共享 stack,`src/app/consumer.go` / `src/app/both.go` / `src/app/runtime/options.go` 不再各自重复拼同一套启动树;同时新增 `src/module/system/runtime_query.go`,`module/system.Service` 对 runtime client / 本地 systemmetric 的切换也已收成单点 `runtimeQuerySource`。 -- 本轮继续把 `service/common` 里的 container/label 共享 helper 回收到 owner 模块:`src/module/container/resolve.go` 与 `src/module/label/core.go` 已承接这批逻辑,`module/{execution,injection,evaluation,project,container,dataset}` 主路径不再经由 `service/common` 读 container 参数/版本或创建 labels。 -- 本轮继续把 `service/common` 里的 dataset version helper 也收回 owner 模块:`src/service/common/dataset.go` 已删除,`src/module/dataset/resolve.go` 负责 dataset version 解析,`src/module/dataset/api_types.go` 同时去掉了对 `module/injection` 的响应耦合。 -- 本轮继续把 `service/common/datapack_resolver.go` 也收回 owner 模块:当前 `src/service/common/datapack_resolver.go` 已删除,datapack 本身与 dataset->datapack 解析已迁入 `src/module/injection/resolve.go`,`module/execution` / `module/injection` 提交主路径不再经由 `service/common`。 -- 这一轮再按“模块 repo 写实、少留裸导出 helper”的口径继续内聚了一批仓储逻辑:`src/module/container/{core,resolve}.go`、`src/module/dataset/{core,resolve}.go`、`src/module/injection/resolve.go`、`src/module/label/core.go` 已改成以 `Repository` 方法为主;`service/initialization`、`module/{execution,evaluation,injection,project,container,dataset}`、`service/consumer/k8s_handler.go` 这批调用点已不再直连 `Create*Core` / `MapRefs*WithDB` / `ExtractDatapacksWithDB` / `CreateOrUpdateLabelsFromItems` 之类裸函数,而是显式走各自模块 repo。 -- 这一轮继续按同一口径压缩 repo/API 面并深清 interface 残余查询:`src/interface/grpcorchestrator/project_statistics.go` 已不再自己持有 Gorm 聚合 SQL,而是改为复用 `src/module/project.Repository.ListProjectStatistics(...)`;`src/module/team/repository.go` 里重复的 `listTeamProjectStatistics(...)` 也已删除,team 本地 project list statistics 改为复用 project 模块仓储。顺手又把 `src/module/{user,team,rbac}/repository.go` 里一批仅供各自 service 使用的 CRUD / assign / remove / batch helper 收成包内私有方法,继续减少模块 repo 对外暴露面。 -- 这一轮再顺着主线补了两处收口:`src/app/compat_options.go` 现已把 consumer 兼容入口里的本地 execution/injection owner 组合显式收成 `CompatibilityOwnerFallbackOptions()` 单点,不再散着写在兼容 runtime 入口里;同时 `src/module/project.Repository.ListProjectStatistics(...)` 已补上对 `fault_injections / executions` 的 `status != deleted` 过滤,和之前 orchestrator interface 的 owner 统计语义重新对齐。`src/module/team` 的 team detail 读取也顺手收成 `loadTeamDetailBase(...)`,避免继续在本地 detail helper 里混入最终由 remote reader 接管的 project count 语义。 -- 这一轮又继续把 repo 暴露面按“没用到就删”收了一层:`src/module/{project,team,user,rbac}/repository.go` 的 `Transaction(...)` 已统一收成包内 `transaction(...)`;顺手补扫了当前 `src/module/*/repository.go`,删除了已无任何调用的 `src/module/execution/repository.go:268` `loadExecutionLabelIDsByItems(...)`。同时 `src/app/http_modules.go` / `src/app/compat_options.go` / `src/app/orchestrator/options.go` 现在通过 `ExecutionInjectionOwnerModules()` 复用 execution/injection owner 组合,compat/orchestrator 邻近不再各自散写相同模块列表。 -- 这一轮再继续按“整个文件没价值就直接删”的口径清理:由于 `src/repository/*.go` 这批旧共享 repository 文件已无任何业务 import 或有效调用(剩余 `repository.DownloadIndexFile()` 仅为 Helm 官方 `repo` 包别名,不是本项目包),当前已整体删除 `src/repository/{container,dataset,detector,execution,granularity,injection,label}.go`。顺手又补扫了 `src/app` / `src/interface` 里的 `Table/Joins/Raw`,目前已无新的“非 owner 层自己拼 DB 查询”残点,残余数据访问基本都收敛在各自 owner 模块 repo 或 runtime owner 内。 -- 这一轮继续顺着你要的两条线往下压:compat 侧新增 `src/app/compat_options.go` 的 `BothCompatibilityOptions(...)`,`src/app/both.go` 不再自己散拼 runtime+HTTP 组合;project/team/user/rbac 这批模块 repo 里又删/折了一批只服务单一路径的内部 helper——例如 `module/project` 把 project label 管理与 label reload、project user count、project list label 装配继续内联回主语义方法,`module/team` 把 team user count、visible team id 查询、team project count / role existence 这批单点 helper 收回主路径或 local reader,`module/user` 把 `ensureUserUnique(...)` 折回 `createUserIfUnique(...)`,`module/rbac` 也继续收掉了旧的 `loadAssignablePermissions(...)` 壳。当前 `project/team/user/rbac` 剩余 repo 方法已基本都对应明确单一 service 语义,不再是“公共但没边界价值”的散 helper。 -- 本轮补扫结果表明,当前最高优先级主线残余已进一步收敛到各服务残余 local fallback 的继续压缩。 -- 同时已继续把 HTTP 鉴权链往 IAM client 收深一轮:`src/middleware/auth.go` 不再直接依赖 `utils.ValidateToken`,而是通过 `middleware.TokenVerifier` 接口走注入实现;当前默认仍由 `authmodule.Service` 提供,本地功能不变。并且 `src/internalclient/iamclient/*` 已继续补齐 team/project 的 member/admin/public 判定 RPC,`src/app/gateway/options.go` 在配置 `clients.iam.target` 或 `iam.grpc.target` 时,已可优先切到 IAM gRPC 做 token verify、`CheckUserPermission(...)` 与 team/project 权限辅助判断。 -- 这一轮继续按“模块内直接写实、删除空包装”的口径再压一层 repo API 面:`src/module/{project,team,user,rbac,execution,injection}/repository.go` 中原先只做 `db.Transaction(...)` / `&Repository{db: tx}` 的 `transaction/Transaction/withDB` 空包装已全部删除,service 组合点统一直接走 `repo.db.Transaction(...)` + `NewRepository(tx)`;同时 `src/module/injection/repository.go` 中只在模块内部使用的一整批方法也已收成包内私有命名,例如 `loadInjection(...)`、`findInjectionByName(...)`、`createInjectionRecord(...)`、`deleteInjectionsCascade(...)`、`listInjectionsView(...)` 等,进一步减少模块仓储对外暴露面并把 compat/local owner 主路径收得更实。 -- 紧接着又把同一口径补到 `src/module/execution/repository.go`:`AddExecutionLabels / ClearExecutionLabels / BatchDecreaseLabelUsages / ListExecutionIDsByLabelItems / BatchDeleteExecutions / UpdateExecutionDuration / LoadExecution / CreateExecutionRecord / UpdateExecutionFields / SaveDetectorResults / SaveGranularityResults` 这批仅供模块内 service/runtime owner 使用的方法已全部私有化,`module/execution/service.go` 也同步改成只走模块内语义方法,进一步减少 execution repo 的公开 API 面。 -- 这一轮继续把同样的收口扩到 `src/module/{container,dataset,system}`:三处 repo 的 `Transaction(...) / withDB(...)` 空包装都已删除,service 组合点统一改成 `repo.db.Transaction(...) + NewRepository(tx)`;同时 `module/container` / `module/dataset` 中大批仅供模块内部使用的 CRUD、label、version、Helm/Datapack 关系方法已收成包内私有实现,`module/system` 中的 `getAuditLogByID(...)`、`getConfigByID(...)`、`getConfigHistory(...)`、`updateConfig(...)`、`createConfigHistory(...)`、`listConfigHistoriesByConfigID(...)` 也已一并私有化,进一步压缩 repo API 面。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/container ./module/dataset ./module/system ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./service/initialization -run '^$'`。 -- 紧接着又继续削了一轮 repo 暴露面和 compat 壳:`src/module/container/repository.go` 的 `ListContainers / ListContainerVersions`、`src/module/dataset/repository.go` 的 `ListDatasets / SearchDatasets / ListDatasetVersions`、`src/module/system/repository.go` 的 `ListAuditLogs / ListConfigs / ListConfigHistories` 已全部收成包内私有实现,当前这三块对外只剩真正有跨模块边界价值的方法(例如 dataset 的 `ListInjectionsByDatasetVersionID(...)`);同时 `src/app/compat_options.go` 里的 `ConsumerRuntimeOptions()` 空转发已删除,`src/app/consumer.go` 直接走 `CompatibilityRuntimeOptions()`,compat 启动壳再薄一层。对应编译检查已再次通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/container ./module/dataset ./module/system ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./service/initialization -run '^$'`。 -- 这一轮继续把 `app/*/options.go` 这批启动壳里的纯组合 helper 删了一层:`src/app/{iam,resource,system,orchestrator}/options.go` 中仅被各自 `Options(...)` 调用一次的 `Modules()` 已全部内联删除,`src/app/gateway/options.go` 里无调用价值的 `Modules()` 也已直接删除;同时 `src/app/compat_options.go` 内部仅被单点使用的 `ProducerInitializationOptions()`、`HTTPServerOptions()`、`CompatibilityOwnerFallbackOptions()` 也已折回主入口。当前启动链保留的 helper 主要只剩确实复用的 `CommonOptions(...)`、`ProducerHTTPModules()`、`RuntimeWorkerStackOptions()`、`ExecutionInjectionOwnerModules()` 这类有明确边界价值的组合。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./app ./app/gateway ./app/runtime ./app/orchestrator ./app/iam ./app/resource ./app/system ./module/container ./module/dataset ./module/system ./service/initialization -run '^$'`。 -- 这一轮继续把“remote + local fallback” 双态适配器再压一层:`src/service/consumer/owner_adapter.go` 已把 execution/injection 的 local fallback 与 remote-only 两套 adapter 合并成统一结构,通过 `requireRemote` 控制 dedicated runtime-worker 是否允许回落本地 owner;`src/module/team/project_reader.go` 同样把 local / remote fallback / remote-only 三套 reader 合并成单一 `projectReaderAdapter`;并顺手把同类模式的 `src/module/project/project_statistics.go`、`src/module/evaluation/execution_query.go`、`src/module/system/runtime_query.go` 也统一成单 adapter + `requireRemote` 形态,减少重复实现与过渡态暴露面。仓库级补扫结果显示,当前生产代码里这类双 adapter 模式已基本清空;残余 `Transaction/withDB` 包装主要集中在 `module/auth` / `module/label` 这类还未进入本轮主线的模块。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./service/consumer ./module/team ./module/project ./module/evaluation ./module/system ./app ./app/runtime ./app/iam ./app/resource ./app/system ./app/gateway -run '^$'`。 -- 这一轮把前面补扫里最后两块明显残余也收掉了:`src/module/auth/repository.go` 的 `UserRepository/RoleRepository withDB(...) + Transaction(...)` 空包装已删除,`src/module/auth/service.go` 改成直接使用 `userRepo.db.Transaction(...)` + `NewUserRepository(tx)` / `NewRoleRepository(tx)`;`src/module/label/repository.go` 的 `Transaction(...)` 包装也已删除,`src/module/label/service.go` 同步切成 `repo.db.Transaction(...)`。复扫结果显示,当前 `src/app` / `src/module` / `src/service/consumer` 主线里已不再存在这类 repo `withDB(...)` / `Transaction(...)` compat 壳;剩余 `withDB(...)` 命中主要只在 consumer task-state builder 这种内部 fluent helper 上,不再是 repository 兼容层。对应编译检查已通过:`cd src && GOCACHE=/tmp/aegis-go-cache go test ./module/auth ./module/label ./app ./app/gateway ./app/iam ./service/consumer ./module/team ./module/project ./module/evaluation ./module/system -run '^$'`。 +# Backend Refactor TODO + +> 更新时间:2026-04-19 +> 口径:只保留当前主线状态、验收命令和非阻塞尾项,不再保留逐轮施工日志。 + +## 1. 当前判断 + +- [x] Fx + module + infra 主线完成 +- [x] `producer / consumer / both` 三种模式可启动 +- [x] 六服务入口已落地并可运行 +- [x] 旧兼容层已退出主线运行态 +- [x] SDK audience / API key 鉴权主线完成 +- [x] SDK 路由已统一收口到 `src/router/sdk.go` +- [x] runtime 上传接口并入 `src/router/sdk.go`,并只保留 `RequireServiceTokenAuth()` + +结论:当前可按“主线完成”判断,剩余工作主要是非阻塞治理和真实环境补强。 + +## 2. 主线完成清单 + +### 2.1 启动与基础设施 + +- [x] `main.go` 只负责 mode 选择与 Fx 启动 +- [x] DB / Redis / Etcd / Tracing / Loki / K8s / Harbor / Helm / BuildKit 已收口到 `src/infra/*` +- [x] HTTP server / worker / controller / receiver 均已纳入 lifecycle +- [x] `src/app` 已按边界拆成基础 options 与服务 options +- [x] `src/interface/grpc/*` 已迁到 `src/interface/grpc/{iam,resource,orchestrator,runtime,system}` + +### 2.2 模块边界 + +- [x] 业务主模块已完成 `module -> service -> repository` 收口 +- [x] handler 不再直接依赖全局 DB / 旧 producer / 旧 repository wrapper +- [x] repository 主体回收到各模块 `repository.go` +- [x] 外部系统访问已通过 gateway/store 收口 +- [x] middleware 已从旧 producer 依赖切到模块服务 / 独立接口 + +### 2.3 旧兼容层清理 + +- [x] `src/service/producer` 已退出生产代码 +- [x] `src/handlers/system` 已退出运行态主线 +- [x] `database.DB` 已退回 `src/infra/db` 集中管理 +- [x] `GetGateway()` / `redisinfra.GetGateway()` / `CurrentK8s*` 这类全局 fallback 已退出主线 +- [x] `src/interface/http/router.go`、`src/app/compat_options.go`、`src/router/runtime.go` 这类单层组织文件已继续压缩/删除 + +### 2.4 路由 / 文档 / SDK / 鉴权 + +- [x] Public / SDK / Portal / Admin 路由已拆分 +- [x] 所有 `@x-api-type {"sdk":"true"}` 运行态入口已统一收口到 `src/router/sdk.go` +- [x] runtime 结果上传接口已作为 `sdk + runtime` 路由并入 `src/router/sdk.go` +- [x] runtime 结果上传接口鉴权已改为仅 `RequireServiceTokenAuth()` +- [x] `portal / admin / sdk / runtime` 四类 audience 路由与 Swagger 标记已完成对齐补扫 +- [x] Swagger audience 以 `x-api-type` 为准 +- [x] Python SDK 只消费 `sdk.json` +- [x] TypeScript SDK 分别消费 `portal.json` / `admin.json` +- [x] TypeScript OpenAPI Generator 模板目录已压平成 `.openapi-generator/typescript/*` +- [x] `scripts/command` 中 Apifox / SDK / 测试安装 URL 已优先从 `scripts/command/settings.toml` 读取 +- [x] API key 主线已统一到 `Key ID / Key Secret` + 签名换 token +- [x] `aegisctl` 与 Python SDK 已切到同一套签名口径 + +### 2.5 微服务主线 + +- [x] `api-gateway` 对外 HTTP 入口已形成 +- [x] `iam-service` 承接 auth / user / rbac / team / api key +- [x] `resource-service` 承接 project / label / container / dataset / evaluation +- [x] `orchestrator-service` 承接 execution / injection / task / trace / notification / group 控制面 +- [x] `runtime-worker-service` 保留 Redis 异步执行链,承接运行态消费、K8s/Helm/BuildKit/Chaos +- [x] `system-service` 承接 config / audit / health / monitor / metrics + +## 3. SDK 路由核对结论 + +已核对 `src/module/*/handler.go` 中所有 `@x-api-type {"sdk":"true"}` 注释,当前运行态路由均由 `src/router/sdk.go` 承接: + +- [x] `POST /api/v2/auth/api-key/token` +- [x] `GET /api/v2/sdk/evaluations` +- [x] `GET /api/v2/sdk/evaluations/experiments` +- [x] `GET /api/v2/sdk/evaluations/{id}` +- [x] `GET /api/v2/sdk/datasets` +- [x] `GET /api/v2/datasets/{dataset_id}/versions/{version_id}/download` +- [x] `PATCH /api/v2/datasets/{dataset_id}/version/{version_id}/injections` +- [x] `GET /api/v2/projects/{project_id}/injections` +- [x] `GET /api/v2/projects/{project_id}/injections/analysis/no-issues` +- [x] `GET /api/v2/projects/{project_id}/injections/analysis/with-issues` +- [x] `POST /api/v2/projects/{project_id}/injections/inject` +- [x] `POST /api/v2/projects/{project_id}/injections/build` +- [x] `GET /api/v2/projects/{project_id}/executions` +- [x] `POST /api/v2/projects/{project_id}/executions/execute` +- [x] `POST /api/v2/evaluations/datapacks` +- [x] `POST /api/v2/evaluations/datasets` +- [x] `GET /api/v2/evaluations` +- [x] `GET /api/v2/evaluations/{id}` +- [x] `GET /api/v2/executions/{id}` +- [x] `PATCH /api/v2/executions/{id}/labels` +- [x] `GET /api/v2/injections/metadata` +- [x] `GET /api/v2/injections/{id}` +- [x] `POST /api/v2/injections/{id}/clone` +- [x] `GET /api/v2/injections/{id}/download` +- [x] `GET /api/v2/injections/{id}/files` +- [x] `GET /api/v2/injections/{id}/files/download` +- [x] `GET /api/v2/injections/{id}/files/query` +- [x] `PATCH /api/v2/injections/{id}/labels` +- [x] `GET /api/v2/metrics/algorithms` +- [x] `GET /api/v2/metrics/executions` +- [x] `GET /api/v2/metrics/injections` +- [x] `POST /api/v2/executions/{execution_id}/detector_results` +- [x] `POST /api/v2/executions/{execution_id}/granularity_results` + +补充说明: + +- `src/module/docs/swagger_models.go` 里的 `sdk` 标记只用于 Swagger model 聚合,不对应独立运行态路由。 +- `GET /api/v2/executions/labels` 当前不是 `sdk:true`,所以仍保留在 Portal 侧,不在本次 SDK 收口范围内。 + +## 4. 验收命令 + +- [x] 默认回归 + - `cd src && go test ./...` +- [x] Producer Fx 图与 HTTP 冒烟 + - `cd src && go test ./app -run 'TestProducerOptionsValidate|TestProducerOptionsStartStopSmoke|TestProducerOptionsHTTPIntegrationSmoke'` +- [x] Consumer / Both 生命周期冒烟 + - `cd src && go test ./app -run 'TestConsumerOptions|TestBothOptions'` +- [x] 路由 / 文档主路径 + - `cd src && go test ./router ./docs ./interface/http` +- [x] 真实 K8s 集群验收 + - `cd src && RUN_K8S_INTEGRATION=1 go test ./infra/k8s -run TestK8sGatewayJobLifecycleIntegration` + +## 5. 主线完成 / 非阻塞剩余项 + +### 5.1 主线完成 + +- [x] 单体 Fx 启动主线完成 +- [x] 六服务边界主线完成 +- [x] 旧兼容层主线完成清扫 +- [x] SDK / audience / API key 主线完成 +- [x] SDK 路由统一收口完成 +- [x] 真实 K8s 集群验收入口完成 + +### 5.2 非阻塞剩余项 + +- [ ] 人工确认 Fx 启动日志是否需要进一步裁剪 +- [ ] 少量 dedicated service 的 local fallback 还可以继续压窄 +- [ ] 少量跨 owner 直查仍可继续按 owner 深清 +- [ ] 发布层可继续补 values/HPA/Ingress/镜像策略等环境治理 +- [ ] 更贴近真实外部依赖的集成回归仍可继续补强 + +## 6. 参考文档 + +- `docs/report-index.md` +- `docs/package-rename-todo.md` +- `docs/api-key-auth-execution-todo.md` +- `docs/python-runtime-wrapper-design.md` +- `docs/python-runtime-wrapper-todo.md` +- `docs/swagger-audience-unmarked-report.md` diff --git a/justfile b/justfile index 48e4252e..d6b661b4 100644 --- a/justfile +++ b/justfile @@ -61,7 +61,7 @@ check-prerequisites: printf "{{green}}✅ All dependency checks passed{{reset}}\n\n" # 🔗 Start port forwarding to access application -forward-ports env="prod": +port-forward env="prod": just run-command port start -e {{env}} -n {{ns}} # 🛠️ Setup development environment @@ -99,8 +99,8 @@ setup-test-env: check-prerequisites # Pedestal Function # ============================================================================= -# 🔍 Install pedestals in namespaces (usage: just install-pedestals ) -install-pedestals pedestal_name pedestal_count: +# 🔍 Install pedestals in namespaces (usage: just pedestal-install ) +pedestal-install pedestal_name pedestal_count: just run-command pedestal install -e {{env_mode}} -n {{pedestal_name}} -c {{pedestal_count}} -f # ============================================================================= @@ -108,7 +108,7 @@ install-pedestals pedestal_name pedestal_count: # ============================================================================= # Deploy OpenEBS -install-openebs: +openebs-install: #!/usr/bin/env bash set -euo pipefail printf "{{blue}}Deploying OpenEBS...{{reset}}\n" @@ -119,7 +119,7 @@ install-openebs: printf "{{green}}✅ OpenEBS installed successfully{{reset}}\n\n" # 🔧 Deploy RCABench application in prod environment -install-rcabench: +rcabench-install: #!/usr/bin/env bash set -euo pipefail printf "{{blue}}🔧 Deploying RCABench application...{{reset}}\n" @@ -132,23 +132,23 @@ install-rcabench: --atomic --timeout 10m printf "{{green}}✅ RCABench installed successfully{{reset}}\n\n" printf "{{blue}}🔗 Starting automatic port forwarding...{{reset}}\n" - just forward-ports + just port-forward # 🛠️ Setup local development environment with basic services local-deploy: just run-command rcabench local-deploy -f - just init-etcd + just etcd-init # 🚀 Build and deploy application (using skaffold) run: check-prerequisites ENV_MODE=staging devbox run skaffold run # Initialize etcd -init-etcd: +etcd-init: just run-command etcd init -e {{env_mode}} -f -update-version version: - just run-command rcabench update-version -v {{version}} +version-update version: + just run-command rcabench version-update -v {{version}} # ============================================================================= # Backup @@ -167,8 +167,8 @@ test version: SDK_VERSION={{version}} ENV_MODE=test devbox run skaffold run # Run regression tests -regression-test: - chmod +x ./scripts/regression-test.sh && ./scripts/regression-test.sh +test-regression: + chmod +x ./scripts/test-regression.sh && ./scripts/test-regression.sh # ============================================================================= # Development Tools @@ -205,7 +205,7 @@ delete-chaos ns_prefix ns_count: # ============================================================================= # 🔄 Sync Docker images from DockerHub to prod repository -sync-images bv="latest" fv="latest": +images-sync bv="latest" fv="latest": #!/usr/bin/env bash set -euo pipefail source {{root}}/.secret @@ -242,23 +242,32 @@ sync-images bv="latest" fv="latest": # ============================================================================= # 📝 Initialize Swagger documentation -swag-init version: - just run-command swagger init -v {{version}} +swagger-init v: + just run-command swagger init -v {{v}} --apifox-target all -# ⚙️ Generate TypeScript Client from Swagger documentation -generate-typescript-client version: - just swag-init {{version}} - just run-command swagger generate-client -l typescript -v {{version}} +# ⚙️ Generate Portal TypeScript SDK from Swagger documentation +generate-portal v: + just run-command sdk typescript --target portal --env local --version {{v}} + +# ⚙️ Generate Admin TypeScript SDK from Swagger documentation +generate-admin v: + just run-command sdk typescript --target admin --env local --version {{v}} # ⚙️ Generate Python SDK from Swagger documentation -generate-python-sdk version: - just swag-init {{version}} - just run-command swagger generate-sdk -l python -v {{version}} +generate-python-sdk v: + just run-command sdk python --target sdk --env local --version {{v}} + +# 🚀 Generate release-ready Portal TypeScript SDK +release-portal v: + just run-command sdk typescript --target portal --env release --version {{v}} + +# 🚀 Generate release-ready Admin TypeScript SDK +release-admin v: + just run-command sdk typescript --target admin --env release --version {{v}} -# ⚙️ Generate TypeScript SDK from Swagger documentation -generate-typescript-sdk version: - just swag-init {{version}} - just run-command swagger generate-sdk -l typescript -v {{version}} +# 🚀 Generate release-ready Python SDK +release-python-sdk v: + just run-command sdk python --target sdk --env release --version {{v}} # ============================================================================= # Utilities @@ -286,7 +295,7 @@ release version: #!/usr/bin/env bash set -euo pipefail printf "{{blue}}🚀 Releasing version {{version}}...{{reset}}\n" - just update-version {{version}} + just version-update {{version}} just changelog git add {{root}}/CHANGELOG.md {{root}}/helm/Chart.yaml {{root}}/helm/values.yaml \ {{root}}/src/config.dev.toml {{root}}/src/main.go diff --git a/project-index.yaml b/project-index.yaml index fd372dda..136e29c7 100644 --- a/project-index.yaml +++ b/project-index.yaml @@ -1614,7 +1614,7 @@ requirements: priority: P1 status: implemented confidence: inferred - source: "sdk/python/, scripts/command/src/swagger.py" + source: "sdk/python/, scripts/command/src/swagger/python.py, scripts/command/src/cli/sdk.py" code: - path: sdk/python/src/rcabench/client @@ -1635,7 +1635,7 @@ requirements: depends_on: [REQ-602] conflicts: [] - notes: "Generated via 'make generate-python-sdk'" + notes: "Generated via 'just generate-python-sdk '; consumes src/docs/converted/sdk.json" - id: REQ-601 title: TypeScript SDK (Auto-generated) @@ -1646,7 +1646,7 @@ requirements: priority: P1 status: implemented confidence: inferred - source: "CLAUDE.md" + source: "scripts/command/src/swagger/typescript.py, scripts/command/src/cli/sdk.py" code: [] @@ -1667,7 +1667,7 @@ requirements: depends_on: [REQ-602] conflicts: [] - notes: "Generated via 'make generate-typescript-sdk SDK_VERSION=x.x.x'; sdk/typescript/ directory" + notes: "Generated via 'just generate-portal ' / 'just generate-admin '; output in sdk/typescript/{portal,admin}; generator config lives in .openapi-generator/typescript/*" - id: REQ-602 title: Swagger/OpenAPI Documentation @@ -1680,7 +1680,7 @@ requirements: priority: P1 status: implemented confidence: confirmed - source: "src/docs/openapi3/openapi.json" + source: "src/docs/openapi3/openapi.json, scripts/command/src/swagger/init.py" code: - path: src/handlers/docs.go @@ -1701,7 +1701,7 @@ requirements: depends_on: [] conflicts: [] - notes: "scripts/command/src/swagger/init.py extracts sdk / portal / admin audience specs from OpenAPI3 x-api-type metadata" + notes: "scripts/command/src/swagger/init.py extracts sdk / portal / admin audience specs from OpenAPI3 x-api-type metadata and writes converted artifacts under src/docs/converted/" # =========================================================================== # REQ-7xx: Deployment & Infrastructure (Backend) diff --git a/scripts/command/settings.toml b/scripts/command/settings.toml index 3c065b90..7b085ff2 100644 --- a/scripts/command/settings.toml +++ b/scripts/command/settings.toml @@ -8,6 +8,37 @@ release_name = "rcabench" python_sdk_dir = "sdk/python" time_format = "%Y%m%d_%H%M%S" +[default.openapi] +generator_volume_root = "/local" + +[default.apifox] +api_base_url = "https://api.apifox.com/v1" +api_version = "2024-03-28" +locale = "zh-CN" + +[default.sdk.python] +git_host = "github.com" +git_user_id = "OperationsPAI" +git_repo_id = "AegisLab" + +[default.sdk.typescript.portal] +npm_name = "@OperationsPAI/portal" +npm_description = "TypeScript Portal SDK for RCABench API" + +[default.sdk.typescript.admin] +npm_name = "@OperationsPAI/admin" +npm_description = "TypeScript Admin SDK for RCABench API" + +[default.command_urls] +cert_manager_manifest_url = "https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml" +mysql_apt_config_deb_url = "https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb" + +[default.command_urls.helm_repo_urls] +chaos_mesh = "https://charts.chaos-mesh.org" +cilium = "https://helm.cilium.io/" +open_telemetry = "https://open-telemetry.github.io/opentelemetry-helm-charts" +clickstack = "https://hyperdxio.github.io/helm-charts" + [default.database.mysql] host = "localhost" port = "3306" diff --git a/scripts/command/src/backup/mysql.py b/scripts/command/src/backup/mysql.py index 24788544..f2d0ebf1 100644 --- a/scripts/command/src/backup/mysql.py +++ b/scripts/command/src/backup/mysql.py @@ -448,7 +448,7 @@ def install_tools() -> None: run_command( [ "wget", - "https://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb", + settings.command_urls.mysql_apt_config_deb_url, "-O", "/tmp/mysql-apt-config.deb", ], diff --git a/scripts/command/src/cli/main.py b/scripts/command/src/cli/main.py index 68584444..055118f4 100644 --- a/scripts/command/src/cli/main.py +++ b/scripts/command/src/cli/main.py @@ -17,6 +17,7 @@ def main(): pedestal, port_manager, rcabench_, + sdk, swagger, test, ) @@ -32,6 +33,7 @@ def main(): port_manager.app, name="port", help="Kubernetes port forwarding manager." ) app.add_typer(rcabench_.app, name="rcabench", help="RCABench utilities.") + app.add_typer(sdk.app, name="sdk", help="SDK generation utilities.") app.add_typer(swagger.app, name="swagger", help="Swagger/OpenAPI utilities.") app.add_typer(test.app, name="test", help="Test environment utilities.") diff --git a/scripts/command/src/cli/rcabench_.py b/scripts/command/src/cli/rcabench_.py index 63aadb31..dc41f293 100644 --- a/scripts/command/src/cli/rcabench_.py +++ b/scripts/command/src/cli/rcabench_.py @@ -51,11 +51,11 @@ def rcabench_local_deploy( "\n[bold yellow]You can start the application manually later: [/bold yellow]" ) console.print( - f"[gray]cd {PROJECT_ROOT / 'src'} && go run main.go both --port 8082 [/gray]" + f"[gray]cd {PROJECT_ROOT / 'src'} && go run . both --port 8082 [/gray]" ) -@app.command(name="update-version") +@app.command(name="version-update") def rcabench_update_version( version: str = typer.Option( ..., @@ -64,5 +64,5 @@ def rcabench_update_version( help="The new version to set in project files (e.g., 1.2.3).", ), ): - """Updates the version information in project files.""" + """Update project version markers in source and Helm files.""" update_version(version) diff --git a/scripts/command/src/cli/sdk.py b/scripts/command/src/cli/sdk.py new file mode 100644 index 00000000..44d7777f --- /dev/null +++ b/scripts/command/src/cli/sdk.py @@ -0,0 +1,93 @@ +from enum import Enum + +import typer + +from src.common.common import console, settings +from src.swagger import init +from src.swagger.common import RunMode +from src.swagger.python import PythonSDK +from src.swagger.typescript import TypeScriptSDK + +app = typer.Typer(help="Target-specific SDK generation utilities.") + + +class GenerationEnv(str, Enum): + LOCAL = "local" + RELEASE = "release" + + +class TypeScriptTarget(str, Enum): + PORTAL = "portal" + ADMIN = "admin" + + +class PythonTarget(str, Enum): + SDK = "sdk" + + +@app.command(name="typescript") +def generate_typescript_sdk( + target: TypeScriptTarget = typer.Option( + ..., + "--target", + "-t", + help="SDK target: portal or admin.", + ), + env: GenerationEnv = typer.Option( + GenerationEnv.LOCAL, + "--env", + "-e", + help="Generation environment: local or release.", + ), + version: str = typer.Option( + "0.0.0", + "--version", + "-v", + help="SDK package version.", + ), +): + """Generate one TypeScript SDK package.""" + + settings.reload() + init(version) + TypeScriptSDK(version, target=RunMode(target.value)).generate() + + if env == GenerationEnv.RELEASE: + console.print( + "[dim]Release-ready TypeScript package generated. Publish with your registry step when needed.[/dim]" + ) + + +@app.command(name="python") +def generate_python_sdk( + target: PythonTarget = typer.Option( + ..., + "--target", + "-t", + help="SDK target: sdk.", + ), + env: GenerationEnv = typer.Option( + GenerationEnv.LOCAL, + "--env", + "-e", + help="Generation environment: local or release.", + ), + version: str = typer.Option( + "0.0.0", + "--version", + "-v", + help="SDK package version.", + ), +): + """Generate the Python SDK package.""" + + del target + + settings.reload() + init(version) + PythonSDK(version).generate() + + if env == GenerationEnv.RELEASE: + console.print( + "[dim]Release-ready Python package generated. Publish with your registry step when needed.[/dim]" + ) diff --git a/scripts/command/src/cli/swagger.py b/scripts/command/src/cli/swagger.py index d9bfd392..be459b19 100644 --- a/scripts/command/src/cli/swagger.py +++ b/scripts/command/src/cli/swagger.py @@ -1,64 +1,22 @@ import typer -from src.common.common import LanguageType, console, settings -from src.swagger import Generator, init +from src.common.common import settings +from src.swagger import init +from src.swagger.apifox import ApifoxTarget -app = typer.Typer() +app = typer.Typer(help="Swagger/OpenAPI generation utilities.") @app.command(name="init") def swagger_init( version: str = typer.Option(..., "--version", "-v", help="API version."), -): - """Initializes Swagger documentation setup.""" - init(version) - - -@app.command() -def generate_client( - language: LanguageType = typer.Option( - LanguageType.TYPESCRIPT, - "--language", - "-l", - help="SDK language.", - ), - version: str = typer.Option( - "1.0.0", - "--version", - "-v", - help="API version.", + apifox_targets: list[ApifoxTarget] | None = typer.Option( + None, + "--apifox-target", + "-t", + help="Optional Apifox upload targets: sdk, portal, admin, or all. Omit to skip upload.", ), ): - """Generates Swagger client documentation.""" - + """Generate normalized OpenAPI artifacts from Go Swagger annotations.""" settings.reload() - - if language != LanguageType.TYPESCRIPT: - console.print( - f"[bold red]❌ Client generation for {language} is not supported yet.[/bold red]" - ) - raise typer.Exit(code=1) - - Generator.get_client_generator(language, version).generate() - - -@app.command() -def generate_sdk( - language: LanguageType = typer.Option( - LanguageType.PYTHON, - "--language", - "-l", - help="SDK language.", - ), - version: str = typer.Option( - "1.0.0", - "--version", - "-v", - help="API version.", - ), -): - """Generates SDK Swagger documentation.""" - - settings.reload() - - Generator.get_sdk_generator(language, version).generate() + init(version, apifox_targets=apifox_targets) diff --git a/scripts/command/src/swagger/__init__.py b/scripts/command/src/swagger/__init__.py index 07af1b19..b9a20d27 100644 --- a/scripts/command/src/swagger/__init__.py +++ b/scripts/command/src/swagger/__init__.py @@ -1,12 +1,4 @@ -from src.common.common import LanguageType -from src.swagger.common import Generator +from src.swagger.apifox import ApifoxTarget from src.swagger.init import init -from src.swagger.python import PythonSDK -from src.swagger.typescript import TypeScriptClient, TypeScriptSDK -__all__ = ["init", "Generator"] - -Generator.register_client(LanguageType.TYPESCRIPT, TypeScriptClient) - -Generator.register_sdk(LanguageType.PYTHON, PythonSDK) -Generator.register_sdk(LanguageType.TYPESCRIPT, TypeScriptSDK) +__all__ = ["ApifoxTarget", "init"] diff --git a/scripts/command/src/swagger/apifox.py b/scripts/command/src/swagger/apifox.py new file mode 100644 index 00000000..3a8d3694 --- /dev/null +++ b/scripts/command/src/swagger/apifox.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +import json +import os +import urllib.error +import urllib.request +from enum import Enum +from pathlib import Path + +import typer +from rich.panel import Panel + +from src.common.common import console, settings + +__all__ = [ + "ApifoxTarget", + "upload_targets_to_apifox", +] + + +class ApifoxTarget(str, Enum): + SDK = "sdk" + PORTAL = "portal" + ADMIN = "admin" + ALL = "all" + + +TARGET_OPENAPI_FILES: dict[ApifoxTarget, str] = { + ApifoxTarget.SDK: "sdk.json", + ApifoxTarget.PORTAL: "portal.json", + ApifoxTarget.ADMIN: "admin.json", +} + + +def upload_targets_to_apifox( + converted_dir: Path, + targets: list[ApifoxTarget] | None = None, +) -> None: + """Upload one or more generated OpenAPI documents to Apifox.""" + normalized_targets = _normalize_targets(targets or [ApifoxTarget.ALL]) + _ensure_common_config() + + for target in normalized_targets: + openapi_path = converted_dir / TARGET_OPENAPI_FILES[target] + endpoint_folder_id = _required_env( + f"APIFOX_{target.upper()}_ENDPOINT_FOLDER_ID" + ) + schema_folder_id = _required_env(f"APIFOX_{target.upper()}_SCHEMA_FOLDER_ID") + _upload_openapi( + openapi_path=openapi_path, + label=target.value, + endpoint_folder_id=int(endpoint_folder_id), + schema_folder_id=int(schema_folder_id), + ) + + +def _normalize_targets(targets: list[ApifoxTarget]) -> list[ApifoxTarget]: + """Expand and de-duplicate Apifox upload targets.""" + normalized: list[ApifoxTarget] = [] + for target in targets: + if target == ApifoxTarget.ALL: + normalized.extend( + [ + ApifoxTarget.SDK, + ApifoxTarget.PORTAL, + ApifoxTarget.ADMIN, + ] + ) + continue + normalized.append(target) + + deduped: list[ApifoxTarget] = [] + seen: set[ApifoxTarget] = set() + for target in normalized: + if target in seen: + continue + seen.add(target) + deduped.append(target) + return deduped + + +def _ensure_common_config() -> None: + """Ensure project-level Apifox credentials exist before uploading.""" + missing = [ + name + for name in ("APIFOX_PROJECT_ID", "APIFOX_ACCESS_TOKEN") + if not os.getenv(name) + ] + if missing: + console.print( + "[bold red]Missing required Apifox config:[/bold red] " + ", ".join(missing) + ) + raise typer.Exit(2) + + +def _required_env(name: str) -> str: + """Return a required env var or exit with a clear message.""" + value = os.getenv(name) + if value: + return value + console.print(f"[bold red]Missing required Apifox config:[/bold red] {name}") + raise typer.Exit(2) + + +def _upload_openapi( + *, + openapi_path: Path, + label: str, + endpoint_folder_id: int, + schema_folder_id: int, +) -> None: + """Upload one OpenAPI document to Apifox.""" + if not openapi_path.exists(): + console.print(f"[bold red]OpenAPI file not found:[/bold red] {openapi_path}") + raise typer.Exit(2) + + apifox_settings = settings.apifox + project_id = _required_env("APIFOX_PROJECT_ID") + access_token = _required_env("APIFOX_ACCESS_TOKEN") + payload = { + "input": openapi_path.read_text(encoding="utf-8"), + "options": { + "targetEndpointFolderId": endpoint_folder_id, + "targetSchemaFolderId": schema_folder_id, + "endpointOverwriteBehavior": "OVERWRITE_EXISTING", + "schemaOverwriteBehavior": "OVERWRITE_EXISTING", + "updateFolderOfChangedEndpoint": True, + "prependBasePath": True, + }, + } + + request = urllib.request.Request( + ( + f"{str(apifox_settings.api_base_url).rstrip('/')}/projects/{project_id}/import-openapi" + f"?locale={apifox_settings.locale}" + ), + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + method="POST", + headers={ + "X-Apifox-Api-Version": apifox_settings.api_version, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + }, + ) + + console.print( + Panel( + f"file: {openapi_path}\n" + f"endpoint folder: {endpoint_folder_id}\n" + f"schema folder: {schema_folder_id}", + title=f"Uploading {label} OpenAPI to Apifox", + ) + ) + try: + with urllib.request.urlopen(request) as response: + body = response.read().decode("utf-8") + console.print( + f"[green]Apifox upload succeeded for {label} (HTTP {response.status}).[/green]" + ) + _print_response(body) + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8", errors="replace") + console.print( + f"[bold red]Apifox upload failed for {label} (HTTP {exc.code}).[/bold red]" + ) + _print_response(body) + raise typer.Exit(1) from exc + + +def _print_response(response_body: str) -> None: + """Pretty-print Apifox responses when possible.""" + try: + console.print_json(response_body) + except Exception: + console.print(response_body) diff --git a/scripts/command/src/swagger/common.py b/scripts/command/src/swagger/common.py index d6b2a46e..b9010ad7 100644 --- a/scripts/command/src/swagger/common.py +++ b/scripts/command/src/swagger/common.py @@ -1,7 +1,6 @@ -from abc import ABC from enum import Enum -from src.common.common import PROJECT_ROOT, LanguageType +from src.common.common import PROJECT_ROOT SWAGGER_ROOT = PROJECT_ROOT / "src" / "docs" OPENAPI2_DIR = SWAGGER_ROOT / "openapi2" @@ -10,60 +9,7 @@ class RunMode(str, Enum): - CLIENT = "client" SDK = "sdk" RUNTIME = "runtime" PORTAL = "portal" ADMIN = "admin" - - -class Generator(ABC): - """Base generator class with factory pattern.""" - - _client_registry: dict[LanguageType, type["Generator"]] = {} - _sdk_registry: dict[LanguageType, type["Generator"]] = {} - - @classmethod - def register_client( - cls, name: LanguageType, generator_class: type["Generator"] - ) -> None: - """Register a client generator class with a name.""" - cls._client_registry[name] = generator_class - - @classmethod - def register_sdk( - cls, name: LanguageType, generator_class: type["Generator"] - ) -> None: - """Register a sdk generator class with a name.""" - cls._sdk_registry[name] = generator_class - - @staticmethod - def get_client_generator(generator_type: LanguageType, version: str) -> "Generator": - """Factory method to get a client generator instance based on type.""" - generator_class = Generator._client_registry.get(generator_type) - if not generator_class: - available = ", ".join(Generator._client_registry.keys()) - raise ValueError( - f"Unknown client generator type: {generator_type}. Available: {available}" - ) - - return generator_class(version) - - @staticmethod - def get_sdk_generator(generator_type: LanguageType, version: str) -> "Generator": - """Factory method to get a sdk generator instance based on type.""" - generator_class = Generator._sdk_registry.get(generator_type) - if not generator_class: - available = ", ".join(Generator._sdk_registry.keys()) - raise ValueError( - f"Unknown sdk generator type: {generator_type}. Available: {available}" - ) - - return generator_class(version) - - def __init__(self, version: str) -> None: - self.version = version - - def generate(self) -> None: - """Generate the client or SDK.""" - raise NotImplementedError diff --git a/scripts/command/src/swagger/init.py b/scripts/command/src/swagger/init.py index 5efac184..258ae01b 100644 --- a/scripts/command/src/swagger/init.py +++ b/scripts/command/src/swagger/init.py @@ -7,6 +7,7 @@ from src.common.command import run_command from src.common.common import console +from src.swagger.apifox import ApifoxTarget, upload_targets_to_apifox from src.swagger.common import SWAGGER_ROOT, RunMode from src.util import get_longest_common_substring @@ -638,13 +639,15 @@ def convert_inline_enums_to_refs(self) -> None: return schema_path = "#/components/schemas/" + available_schemas = set(self.data.get("components", {}).get("schemas", {})) converted_count = 0 + skipped_count = 0 def process_parameters( params: list[dict[str, Any]], path: str, method: str ) -> None: """Process parameters and convert inline enums to refs.""" - nonlocal converted_count + nonlocal converted_count, skipped_count for param in params: if not isinstance(param, dict): @@ -680,13 +683,18 @@ def process_parameters( wildcard_key = f"*|{param_name}" target_schema = self.PARAMETER_SCHEMA_MAPPING.get(wildcard_key) - if target_schema: + if target_schema and target_schema in available_schemas: # Replace inline enum with $ref param["schema"] = {"$ref": f"{schema_path}{target_schema}"} converted_count += 1 console.print( f"[gray] -> Converted {method.upper()} {path} parameter '{param_name}' to use schema '{target_schema}'[/gray]" ) + elif target_schema: + skipped_count += 1 + console.print( + f"[gray] -> Kept inline enum for {method.upper()} {path} parameter '{param_name}' because schema '{target_schema}' is not present in components[/gray]" + ) # Process all paths and their operations for path, operations in self.data["paths"].items(): @@ -705,14 +713,16 @@ def process_parameters( console.print( f"[bold green]✅ Converted {converted_count} inline enum parameters to schema references[/bold green]" ) + if skipped_count > 0: + console.print( + f"[bold yellow]⚠ Skipped {skipped_count} inline enum parameter ref conversions because the target schema was not present[/bold yellow]" + ) def output(self, output_file: Path, category: RunMode) -> None: - output_data = self.data - if category != RunMode.CLIENT: - output_data = self._filter_apis_by_audience(category) - if output_data is None: - console.print("[bold red]Processing function returned None[/bold red]") - sys.exit(1) + output_data = self._filter_apis_by_audience(category) + if output_data is None: + console.print("[bold red]Processing function returned None[/bold red]") + sys.exit(1) with open(output_file, "w", encoding="utf-8") as f: json.dump(output_data, f, indent=2) @@ -823,7 +833,11 @@ def collect_refs(obj: dict[str, Any] | list[dict[str, Any]]) -> None: return new_data -def init(version: str) -> None: +def init( + version: str, + *, + apifox_targets: list[ApifoxTarget] | None = None, +) -> None: """ Initialize Swagger documentation by generating OpenAPI 2.0 and converting to OpenAPI 3.0. """ @@ -863,22 +877,22 @@ def init(version: str) -> None: json.dump(openapi3_data, f, indent=2) # 3. Post-process Swagger JSON - console.print("[bold blue]📦 Post-processing swagger initiaization...[/bold blue]") + console.print( + "[bold blue]📦 Post-processing generated OpenAPI artifacts...[/bold blue]" + ) if not CONVERTED_DIR.exists(): CONVERTED_DIR.mkdir(parents=True) else: - legacy_typescript_file = CONVERTED_DIR / "typescript.json" - legacy_typescript_file.unlink(missing_ok=True) + stale_typescript_file = CONVERTED_DIR / "typescript.json" + stale_typescript_file.unlink(missing_ok=True) post_input_file = OPENAPI3_DIR / "openapi.json" - client_file = CONVERTED_DIR / "client.json" sdk_file = CONVERTED_DIR / "sdk.json" runtime_file = CONVERTED_DIR / "runtime.json" portal_file = CONVERTED_DIR / "portal.json" admin_file = CONVERTED_DIR / "admin.json" - shutil.copyfile(post_input_file, dst=client_file) shutil.copyfile(post_input_file, dst=sdk_file) shutil.copyfile(post_input_file, dst=runtime_file) shutil.copyfile(post_input_file, dst=portal_file) @@ -891,12 +905,17 @@ def init(version: str) -> None: processor.deduplicate_enum_values() # Remove duplicate enum values processor.convert_inline_enums_to_refs() - processor.output(client_file, RunMode.CLIENT) processor.output(sdk_file, RunMode.SDK) processor.output(runtime_file, RunMode.RUNTIME) processor.output(portal_file, RunMode.PORTAL) processor.output(admin_file, RunMode.ADMIN) + if apifox_targets: + console.print( + "[bold blue]☁ Uploading generated OpenAPI documents to Apifox...[/bold blue]" + ) + upload_targets_to_apifox(CONVERTED_DIR, apifox_targets) + console.print( "[bold green]✅ Swagger documentation generation completed successfully![/bold green]" ) diff --git a/scripts/command/src/swagger/python.py b/scripts/command/src/swagger/python.py index eb69807a..bfa3e529 100644 --- a/scripts/command/src/swagger/python.py +++ b/scripts/command/src/swagger/python.py @@ -8,10 +8,10 @@ from src.common.common import PROJECT_ROOT, ScopeType, console, settings from src.formatter import PythonFormatter -from src.swagger.common import SWAGGER_ROOT, Generator +from src.swagger.common import SWAGGER_ROOT -class PythonSDK(Generator): +class PythonSDK: """Class to generate Python SDK from Swagger JSON using OpenAPI Generator.""" PYTHON_SDK_DIR = PROJECT_ROOT / "sdk" / "python" @@ -21,6 +21,10 @@ class PythonSDK(Generator): def __init__(self, version: str) -> None: self.version = version + @property + def package_settings(self): + return settings.sdk.python + def _update_version(self) -> None: """ Update version information in various project files. @@ -69,7 +73,7 @@ def generate(self) -> None: self.PYTHON_SDK_GEN_DIR.mkdir(parents=True) - volume_path = Path("/local") + volume_path = Path(settings.openapi.generator_volume_root) relative_swagger = SWAGGER_ROOT.relative_to(PROJECT_ROOT) relative_sdk_gen = self.PYTHON_SDK_GEN_DIR.relative_to(PROJECT_ROOT) relative_generator_config = self.PYTHON_GENERATOR_CONFIG_DIR.relative_to( @@ -89,6 +93,7 @@ def generate(self) -> None: current_user = os.getuid() current_group = os.getgid() + package_settings = self.package_settings try: docker.run( settings.generator_image, @@ -105,11 +110,11 @@ def generate(self) -> None: "-t", container_templates_path.as_posix(), "--git-host", - "github.com", + package_settings.git_host, "--git-repo-id", - "AegisLab", + package_settings.git_repo_id, "--git-user-id", - "OperationsPAI", + package_settings.git_user_id, ], volumes=[(PROJECT_ROOT, volume_path)], user=f"{current_user}:{current_group}", diff --git a/scripts/command/src/swagger/typescript.py b/scripts/command/src/swagger/typescript.py index 24e9f874..20396f22 100644 --- a/scripts/command/src/swagger/typescript.py +++ b/scripts/command/src/swagger/typescript.py @@ -8,65 +8,51 @@ from src.common.command import run_command from src.common.common import PROJECT_ROOT, console, settings -from src.swagger.common import SWAGGER_ROOT, Generator, RunMode +from src.swagger.common import SWAGGER_ROOT, RunMode -class TypeScriptClient(Generator): - """TypeScript client generator using OpenAPI Generator.""" - - MODE = RunMode.CLIENT - CLIENT_DIR = PROJECT_ROOT / "client" / "typescript" - CLIENT_GEN_DIR = PROJECT_ROOT / "client" / "typescript-gen" - GENERATOR_CONFIG_DIR = PROJECT_ROOT / ".openapi-generator" / "typescript" / "client" - - def __init__(self, version: str) -> None: - self.version = version - - def generate(self) -> None: - _generate_typescript_helper( - self.MODE, - self.version, - self.CLIENT_DIR, - self.CLIENT_GEN_DIR, - self.GENERATOR_CONFIG_DIR, - ) - - -class TypeScriptSDK(Generator): +class TypeScriptSDK: """TypeScript generator for separate portal/admin audience specs.""" SDK_ROOT_DIR = PROJECT_ROOT / "sdk" / "typescript" SDK_GEN_ROOT_DIR = PROJECT_ROOT / "sdk" / "typescript-gen" - GENERATOR_CONFIG_DIR = PROJECT_ROOT / ".openapi-generator" / "typescript" / "sdk" + GENERATOR_CONFIG_DIR = PROJECT_ROOT / ".openapi-generator" / "typescript" - def __init__(self, version: str) -> None: + def __init__(self, version: str, target: RunMode | None = None) -> None: self.version = version + self.target = target def generate(self) -> None: - legacy_shared_sdk = self.SDK_ROOT_DIR - if legacy_shared_sdk.exists() and legacy_shared_sdk.is_dir(): - shutil.rmtree(legacy_shared_sdk) + _cleanup_stale_sdk_root_files(self.SDK_ROOT_DIR) + typescript_settings = settings.sdk.typescript audience_packages = { RunMode.PORTAL: { "dst_dir": self.SDK_ROOT_DIR / "portal", "gen_dir": self.SDK_GEN_ROOT_DIR / "portal", "config_overrides": { - "npmName": "@OperationsPAI/portal", - "npmDescription": "TypeScript Portal SDK for RCABench API", + "npmName": typescript_settings.portal.npm_name, + "npmDescription": typescript_settings.portal.npm_description, }, }, RunMode.ADMIN: { "dst_dir": self.SDK_ROOT_DIR / "admin", "gen_dir": self.SDK_GEN_ROOT_DIR / "admin", "config_overrides": { - "npmName": "@OperationsPAI/admin", - "npmDescription": "TypeScript Admin SDK for RCABench API", + "npmName": typescript_settings.admin.npm_name, + "npmDescription": typescript_settings.admin.npm_description, }, }, } - for mode, spec in audience_packages.items(): + target_modes = ( + [self.target] + if self.target is not None + else [RunMode.PORTAL, RunMode.ADMIN] + ) + + for mode in target_modes: + spec = audience_packages[mode] _generate_typescript_helper( mode, self.version, @@ -77,6 +63,24 @@ def generate(self) -> None: ) +def _cleanup_stale_sdk_root_files(root_dir: Path) -> None: + """Remove stale flat sdk/typescript files while keeping portal/admin packages.""" + if not root_dir.exists() or not root_dir.is_dir(): + return + + # Skip when the root only acts as a parent directory for portal/admin packages. + if not (root_dir / "package.json").exists(): + return + + for child in root_dir.iterdir(): + if child.name in {"portal", "admin"}: + continue + if child.is_dir(): + shutil.rmtree(child) + continue + child.unlink(missing_ok=True) + + def _generate_typescript_helper( mode: RunMode, version: str, @@ -86,30 +90,19 @@ def _generate_typescript_helper( config_overrides: dict[str, str] | None = None, ) -> None: """ - Helper function to generate TypeScript client or SDK. + Helper function to generate one TypeScript SDK package. 1. Updates the generator config with the specified version. - 2. Generates the client/SDK using OpenAPI Generator in a Docker container. - 3. Post-processes the generated client/SDK. + 2. Generates the SDK using OpenAPI Generator in a Docker container. + 3. Post-processes the generated SDK. 4. Cleans up temporary directories. """ - if mode not in { - RunMode.CLIENT, - RunMode.SDK, - RunMode.PORTAL, - RunMode.ADMIN, - }: - raise ValueError( - f"Invalid mode: {mode}. Must be 'client', 'sdk', 'portal', or 'admin'." - ) + if mode not in {RunMode.PORTAL, RunMode.ADMIN}: + raise ValueError(f"Invalid mode: {mode}. Must be 'portal' or 'admin'.") - if mode == RunMode.CLIENT: - msg = "Client" - elif mode == RunMode.PORTAL: + if mode == RunMode.PORTAL: msg = "Portal SDK" - elif mode == RunMode.ADMIN: - msg = "Admin SDK" else: - msg = "SDK" + msg = "Admin SDK" # 1. Update generator config with the specified version generator_config = generator_config_dir / "config.json" @@ -134,7 +127,7 @@ def _generate_typescript_helper( gen_dir.mkdir(parents=True) - volume_path = Path("/local") + volume_path = Path(settings.openapi.generator_volume_root) relative_swagger = SWAGGER_ROOT.relative_to(PROJECT_ROOT) relative_gen = gen_dir.relative_to(PROJECT_ROOT) relative_generator_config = generator_config_dir.relative_to(PROJECT_ROOT) @@ -180,11 +173,11 @@ def _generate_typescript_helper( tmp_generator_config.unlink(missing_ok=True) console.print( - f"[bold green]✅ Original TypeScript {msg} generated successfully![/bold green]" + f"[bold green]✅ Generated TypeScript {msg} successfully![/bold green]" ) console.print() - # 3. Post-process generated client/SDK + # 3. Post-process generated SDK console.print(f"[bold blue]Step 2: Post-processing generated {msg}...[/bold blue]") # Clean up existing @@ -203,7 +196,7 @@ def _generate_typescript_helper( if gen_dir.exists(): shutil.rmtree(gen_dir) - # 5. Build the TypeScript client/SDK + # 5. Build the TypeScript SDK console.print(f"[bold blue]Step 3: Building TypeScript {msg}...[/bold blue]") # Check if pnpm is available, fallback to npm diff --git a/scripts/command/src/test.py b/scripts/command/src/test.py index fa3803fa..d7e3738f 100644 --- a/scripts/command/src/test.py +++ b/scripts/command/src/test.py @@ -1,6 +1,6 @@ from concurrent.futures import ThreadPoolExecutor, as_completed -from src.common.common import ENV, PROJECT_ROOT, console +from src.common.common import ENV, PROJECT_ROOT, console, settings from src.common.helm_cli import HelmCLI, HelmRelease from src.common.kubernetes_manager import ( KubernetesManager, @@ -77,9 +77,7 @@ def _install_helm_releases(env: ENV, k8s_manager: KubernetesManager, is_ci: bool # Install cert-manager (prerequisite for otel-kube-stack) console.print("[bold blue]📦 Installing cert-manager...[/bold blue]") - kubectl_apply( - "https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml" - ) + kubectl_apply(settings.command_urls.cert_manager_manifest_url) if not k8s_manager.watch_deployments_ready( ["cert-manager"], namespace="cert-manager", timeout_seconds=300 @@ -203,6 +201,7 @@ def teardown_env( def _get_helm_releases() -> list[HelmRelease]: """Define all Helm releases for test development.""" + helm_repo_urls = settings.command_urls.helm_repo_urls return [ # Chaos Mesh HelmRelease( @@ -210,7 +209,7 @@ def _get_helm_releases() -> list[HelmRelease]: chart="chaos-mesh/chaos-mesh", namespace="chaos-mesh", repo_name="chaos-mesh", - repo_url="https://charts.chaos-mesh.org", + repo_url=helm_repo_urls.chaos_mesh, version="2.8.0", create_namespace=True, ), @@ -220,7 +219,7 @@ def _get_helm_releases() -> list[HelmRelease]: chart="cilium/cilium", namespace="kube-system", repo_name="cilium", - repo_url="https://helm.cilium.io/", + repo_url=helm_repo_urls.cilium, version="1.18.4", ), # OpenTelemetry Kube Stack @@ -229,7 +228,7 @@ def _get_helm_releases() -> list[HelmRelease]: chart="open-telemetry/opentelemetry-kube-stack", namespace="monitoring", repo_name="open-telemetry", - repo_url="https://open-telemetry.github.io/opentelemetry-helm-charts", + repo_url=helm_repo_urls.open_telemetry, values_file=LOCAL_DEV_DIR / "otel-kube-stack.yaml", create_namespace=True, ), @@ -239,7 +238,7 @@ def _get_helm_releases() -> list[HelmRelease]: chart="clickstack/clickstack", namespace="monitoring", repo_name="clickstack", - repo_url="https://hyperdxio.github.io/helm-charts", + repo_url=helm_repo_urls.clickstack, values_file=LOCAL_DEV_DIR / "click-stack.yaml", ), # OpenTelemetry Demo diff --git a/scripts/generate_swagger_audience_report.py b/scripts/generate_swagger_audience_report.py deleted file mode 100644 index 5816bee9..00000000 --- a/scripts/generate_swagger_audience_report.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import json -import re -from collections import Counter -from dataclasses import dataclass -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -SOURCE_ROOT = REPO_ROOT / "src" / "module" -OUTPUT_FILE = REPO_ROOT / "docs" / "swagger-audience-marking-report.md" - -ROUTER_RE = re.compile(r"@Router\s+(\S+)\s+\[(\w+)\]") -SUMMARY_RE = re.compile(r"@Summary\s+(.+)") -X_API_TYPE_RE = re.compile(r"@x-api-type\s+(.+)") -FUNC_RE = re.compile(r"^func\s+([A-Za-z0-9_]+)\s*\(") - - -@dataclass -class Operation: - method: str - path: str - summary: str - file_path: str - router_line: int - x_api_type_line: int | None - function_name: str | None - function_line: int | None - audiences: list[str] - raw_x_api_type: str | None - status: str - - @property - def source(self) -> str: - parts = [f"`{self.file_path}:{self.router_line}`"] - if self.x_api_type_line is not None: - parts.append(f"`{self.file_path}:{self.x_api_type_line}`") - if self.function_line is not None: - parts.append(f"`{self.file_path}:{self.function_line}`") - return " / ".join(parts) - - @property - def audience_text(self) -> str: - return ", ".join(self.audiences) if self.audiences else "-" - - -def parse_x_api_type(raw_value: str | None) -> list[str]: - if raw_value is None: - return [] - - raw_value = raw_value.strip() - try: - parsed = json.loads(raw_value) - except json.JSONDecodeError: - return [] - - if not isinstance(parsed, dict): - return [] - - audiences: list[str] = [] - for key in ("sdk", "portal", "admin"): - value = parsed.get(key) - if isinstance(value, bool) and value: - audiences.append(key) - elif isinstance(value, str) and value.strip().lower() == "true": - audiences.append(key) - return audiences - - -def iter_handler_files() -> list[Path]: - files: list[Path] = [] - files.extend( - path - for path in SOURCE_ROOT.rglob("*.go") - if path.is_file() and not path.name.endswith("_test.go") - ) - return sorted(files) - - -def collect_operations() -> list[Operation]: - operations: list[Operation] = [] - - for file_path in iter_handler_files(): - rel_path = file_path.relative_to(REPO_ROOT).as_posix() - lines = file_path.read_text(encoding="utf-8").splitlines() - - for index, line in enumerate(lines): - router_match = ROUTER_RE.search(line) - if not router_match: - continue - - path = router_match.group(1) - method = router_match.group(2).upper() - - summary = "" - raw_x_api_type: str | None = None - x_api_type_line: int | None = None - - # Search around the router annotation inside the current comment block. - start = max(0, index - 40) - end = min(len(lines), index + 12) - for scan_index in range(index, end): - x_match = X_API_TYPE_RE.search(lines[scan_index]) - if x_match: - raw_x_api_type = x_match.group(1).strip() - x_api_type_line = scan_index + 1 - break - - for scan_index in range(index, start - 1, -1): - summary_match = SUMMARY_RE.search(lines[scan_index]) - if summary_match: - summary = summary_match.group(1).strip() - break - if ( - scan_index != index - and lines[scan_index].strip() - and not lines[scan_index].lstrip().startswith("//") - ): - break - - function_name: str | None = None - function_line: int | None = None - for scan_index in range(index + 1, min(len(lines), index + 20)): - func_match = FUNC_RE.match(lines[scan_index].strip()) - if func_match: - function_name = func_match.group(1) - function_line = scan_index + 1 - break - - audiences = parse_x_api_type(raw_x_api_type) - if audiences: - status = "marked" - elif raw_x_api_type is None: - status = "missing" - else: - status = "empty" - - operations.append( - Operation( - method=method, - path=path, - summary=summary, - file_path=rel_path, - router_line=index + 1, - x_api_type_line=x_api_type_line, - function_name=function_name, - function_line=function_line, - audiences=audiences, - raw_x_api_type=raw_x_api_type, - status=status, - ) - ) - - operations.sort( - key=lambda item: (item.path, item.method, item.file_path, item.router_line) - ) - return operations - - -def markdown_table(rows: list[list[str]]) -> list[str]: - if not rows: - return ["_None_"] - - header = rows[0] - lines = [ - "| " + " | ".join(header) + " |", - "| " + " | ".join(["---"] * len(header)) + " |", - ] - for row in rows[1:]: - lines.append("| " + " | ".join(row) + " |") - return lines - - -def build_report(operations: list[Operation]) -> str: - marked = [item for item in operations if item.status == "marked"] - empty = [item for item in operations if item.status == "empty"] - missing = [item for item in operations if item.status == "missing"] - - audience_counter: Counter[str] = Counter() - for item in marked: - audience_counter.update(item.audiences) - - lines: list[str] = [] - lines.append("# Swagger Audience Marking Report") - lines.append("") - lines.append("> Source of truth: Swagger annotations in `src/module/**/*.go`.") - lines.append( - "> Route position column uses the `@Router` line, then `@x-api-type`, then function line when available." - ) - lines.append("") - lines.append("## Summary") - lines.append("") - lines.append(f"- Total operations scanned: **{len(operations)}**") - lines.append(f"- Marked operations: **{len(marked)}**") - lines.append(f"- Empty `@x-api-type {{}}` operations: **{len(empty)}**") - lines.append(f"- Missing `@x-api-type` operations: **{len(missing)}**") - lines.append( - "- Audience counts among marked operations: " - f"`sdk={audience_counter.get('sdk', 0)}` " - f"`portal={audience_counter.get('portal', 0)}` " - f"`admin={audience_counter.get('admin', 0)}`" - ) - lines.append("") - - lines.append("## Marked Operations") - lines.append("") - lines.extend( - markdown_table( - [ - ["Method", "Path", "Audience", "Summary", "Location"], - *[ - [ - item.method, - f"`{item.path}`", - f"`{item.audience_text}`", - item.summary or "-", - item.source, - ] - for item in marked - ], - ] - ) - ) - lines.append("") - - lines.append("## Empty `@x-api-type {}` Operations") - lines.append("") - lines.extend( - markdown_table( - [ - ["Method", "Path", "Summary", "Raw", "Location"], - *[ - [ - item.method, - f"`{item.path}`", - item.summary or "-", - f"`{item.raw_x_api_type or ''}`", - item.source, - ] - for item in empty - ], - ] - ) - ) - lines.append("") - - lines.append("## Missing `@x-api-type` Operations") - lines.append("") - lines.extend( - markdown_table( - [ - ["Method", "Path", "Summary", "Location"], - *[ - [ - item.method, - f"`{item.path}`", - item.summary or "-", - item.source, - ] - for item in missing - ], - ] - ) - ) - lines.append("") - - return "\n".join(lines) + "\n" - - -def main() -> None: - operations = collect_operations() - OUTPUT_FILE.write_text(build_report(operations), encoding="utf-8") - print( - f"generated {OUTPUT_FILE.relative_to(REPO_ROOT)} with {len(operations)} operations" - ) - - -if __name__ == "__main__": - main() diff --git a/scripts/migrate_swagger_comments.py b/scripts/migrate_swagger_comments.py deleted file mode 100644 index 0f18a9a4..00000000 --- a/scripts/migrate_swagger_comments.py +++ /dev/null @@ -1,1104 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import re -from dataclasses import dataclass -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] - -OLD_FUNC_RE = re.compile(r"^func\s+([A-Za-z0-9_]+)\s*\(") -NEW_METHOD_RE = re.compile(r"^func\s+\([^)]*\)\s+([A-Za-z0-9_]+)\s*\(") - - -@dataclass(frozen=True) -class Mapping: - source: str - source_func: str - target: str - target_func: str - - -MAPPINGS = [ - Mapping( - "src/handlers/v2/access_keys.go", - "CreateAccessKey", - "src/module/auth/handler.go", - "CreateAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "ListAccessKeys", - "src/module/auth/handler.go", - "ListAccessKeys", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "GetAccessKey", - "src/module/auth/handler.go", - "GetAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "DeleteAccessKey", - "src/module/auth/handler.go", - "DeleteAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "RotateAccessKey", - "src/module/auth/handler.go", - "RotateAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "DisableAccessKey", - "src/module/auth/handler.go", - "DisableAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "EnableAccessKey", - "src/module/auth/handler.go", - "EnableAccessKey", - ), - Mapping( - "src/handlers/v2/access_keys.go", - "ExchangeAccessKeyToken", - "src/module/auth/handler.go", - "ExchangeAccessKeyToken", - ), - Mapping( - "src/handlers/v2/auth.go", "Register", "src/module/auth/handler.go", "Register" - ), - Mapping("src/handlers/v2/auth.go", "Login", "src/module/auth/handler.go", "Login"), - Mapping( - "src/handlers/v2/auth.go", - "RefreshToken", - "src/module/auth/handler.go", - "RefreshToken", - ), - Mapping( - "src/handlers/v2/auth.go", "Logout", "src/module/auth/handler.go", "Logout" - ), - Mapping( - "src/handlers/v2/auth.go", - "ChangePassword", - "src/module/auth/handler.go", - "ChangePassword", - ), - Mapping( - "src/handlers/v2/auth.go", - "GetProfile", - "src/module/auth/handler.go", - "GetProfile", - ), - Mapping( - "src/handlers/v2/containers.go", - "CreateContainer", - "src/module/container/handler.go", - "CreateContainer", - ), - Mapping( - "src/handlers/v2/containers.go", - "DeleteContainer", - "src/module/container/handler.go", - "DeleteContainer", - ), - Mapping( - "src/handlers/v2/containers.go", - "GetContainer", - "src/module/container/handler.go", - "GetContainer", - ), - Mapping( - "src/handlers/v2/containers.go", - "ListContainers", - "src/module/container/handler.go", - "ListContainers", - ), - Mapping( - "src/handlers/v2/containers.go", - "UpdateContainer", - "src/module/container/handler.go", - "UpdateContainer", - ), - Mapping( - "src/handlers/v2/containers.go", - "CreateContainerVersion", - "src/module/container/handler.go", - "CreateContainerVersion", - ), - Mapping( - "src/handlers/v2/containers.go", - "DeleteContainerVersion", - "src/module/container/handler.go", - "DeleteContainerVersion", - ), - Mapping( - "src/handlers/v2/containers.go", - "GetContainerVersion", - "src/module/container/handler.go", - "GetContainerVersion", - ), - Mapping( - "src/handlers/v2/containers.go", - "ListContainerVersions", - "src/module/container/handler.go", - "ListContainerVersions", - ), - Mapping( - "src/handlers/v2/containers.go", - "UpdateContainerVersion", - "src/module/container/handler.go", - "UpdateContainerVersion", - ), - Mapping( - "src/handlers/v2/containers.go", - "ManageContainerCustomLabels", - "src/module/container/handler.go", - "ManageContainerCustomLabels", - ), - Mapping( - "src/handlers/v2/containers.go", - "SubmitContainerBuilding", - "src/module/container/handler.go", - "SubmitContainerBuilding", - ), - Mapping( - "src/handlers/v2/containers.go", - "UploadHelmChart", - "src/module/container/handler.go", - "UploadHelmChart", - ), - Mapping( - "src/handlers/v2/containers.go", - "UploadHelmValueFile", - "src/module/container/handler.go", - "UploadHelmValueFile", - ), - Mapping( - "src/handlers/v2/datasets.go", - "CreateDataset", - "src/module/dataset/handler.go", - "CreateDataset", - ), - Mapping( - "src/handlers/v2/datasets.go", - "DeleteDataset", - "src/module/dataset/handler.go", - "DeleteDataset", - ), - Mapping( - "src/handlers/v2/datasets.go", - "GetDataset", - "src/module/dataset/handler.go", - "GetDataset", - ), - Mapping( - "src/handlers/v2/datasets.go", - "ListDatasets", - "src/module/dataset/handler.go", - "ListDatasets", - ), - Mapping( - "src/handlers/v2/datasets.go", - "SearchDataset", - "src/module/dataset/handler.go", - "SearchDataset", - ), - Mapping( - "src/handlers/v2/datasets.go", - "UpdateDataset", - "src/module/dataset/handler.go", - "UpdateDataset", - ), - Mapping( - "src/handlers/v2/datasets.go", - "ManageDatasetCustomLabels", - "src/module/dataset/handler.go", - "ManageDatasetCustomLabels", - ), - Mapping( - "src/handlers/v2/datasets.go", - "CreateDatasetVersion", - "src/module/dataset/handler.go", - "CreateDatasetVersion", - ), - Mapping( - "src/handlers/v2/datasets.go", - "DeleteDatasetVersion", - "src/module/dataset/handler.go", - "DeleteDatasetVersion", - ), - Mapping( - "src/handlers/v2/datasets.go", - "GetDatasetVersion", - "src/module/dataset/handler.go", - "GetDatasetVersion", - ), - Mapping( - "src/handlers/v2/datasets.go", - "ListDatasetVersions", - "src/module/dataset/handler.go", - "ListDatasetVersions", - ), - Mapping( - "src/handlers/v2/datasets.go", - "UpdateDatasetVersion", - "src/module/dataset/handler.go", - "UpdateDatasetVersion", - ), - Mapping( - "src/handlers/v2/datasets.go", - "DownloadDatasetVersion", - "src/module/dataset/handler.go", - "DownloadDatasetVersion", - ), - Mapping( - "src/handlers/v2/datasets.go", - "ManageDatasetVersionInjections", - "src/module/dataset/handler.go", - "ManageDatasetVersionInjections", - ), - Mapping( - "src/handlers/v2/evaluations.go", - "ListDatapackEvaluationResults", - "src/module/evaluation/handler.go", - "ListDatapackEvaluationResults", - ), - Mapping( - "src/handlers/v2/evaluations.go", - "ListDatasetEvaluationResults", - "src/module/evaluation/handler.go", - "ListDatasetEvaluationResults", - ), - Mapping( - "src/handlers/v2/evaluations.go", - "ListEvaluations", - "src/module/evaluation/handler.go", - "ListEvaluations", - ), - Mapping( - "src/handlers/v2/evaluations.go", - "GetEvaluation", - "src/module/evaluation/handler.go", - "GetEvaluation", - ), - Mapping( - "src/handlers/v2/evaluations.go", - "DeleteEvaluation", - "src/module/evaluation/handler.go", - "DeleteEvaluation", - ), - Mapping( - "src/handlers/v2/executions.go", - "BatchDeleteExecutions", - "src/module/execution/handler.go", - "BatchDeleteExecutions", - ), - Mapping( - "src/handlers/v2/executions.go", - "GetExecution", - "src/module/execution/handler.go", - "GetExecution", - ), - Mapping( - "src/handlers/v2/executions.go", - "ListExecutions", - "src/module/execution/handler.go", - "ListExecutions", - ), - Mapping( - "src/handlers/v2/executions.go", - "ListAvaliableExecutionLabels", - "src/module/execution/handler.go", - "ListAvailableExecutionLabels", - ), - Mapping( - "src/handlers/v2/executions.go", - "ManageExecutionCustomLabels", - "src/module/execution/handler.go", - "ManageExecutionCustomLabels", - ), - Mapping( - "src/handlers/v2/executions.go", - "SubmitAlgorithmExecution", - "src/module/execution/handler.go", - "SubmitAlgorithmExecution", - ), - Mapping( - "src/handlers/v2/executions.go", - "UploadDetectorResults", - "src/module/execution/handler.go", - "UploadDetectorResults", - ), - Mapping( - "src/handlers/v2/executions.go", - "UploadGranularityResults", - "src/module/execution/handler.go", - "UploadGranularityResults", - ), - Mapping( - "src/handlers/v2/groups.go", - "GetGroupStats", - "src/module/group/handler.go", - "GetGroupStats", - ), - Mapping( - "src/handlers/v2/groups.go", - "GetGroupStream", - "src/module/group/handler.go", - "GetGroupStream", - ), - Mapping( - "src/handlers/v2/injections.go", - "BatchDeleteInjections", - "src/module/injection/handler.go", - "BatchDeleteInjections", - ), - Mapping( - "src/handlers/v2/injections.go", - "GetInjection", - "src/module/injection/handler.go", - "GetInjection", - ), - Mapping( - "src/handlers/v2/injections.go", - "GetInjectionMetadata", - "src/module/injection/handler.go", - "GetInjectionMetadata", - ), - Mapping( - "src/handlers/v2/injections.go", - "ListInjections", - "src/module/injection/handler.go", - "ListInjections", - ), - Mapping( - "src/handlers/v2/injections.go", - "SearchInjections", - "src/module/injection/handler.go", - "SearchInjections", - ), - Mapping( - "src/handlers/v2/injections.go", - "ListFaultInjectionNoIssues", - "src/module/injection/handler.go", - "ListFaultInjectionNoIssues", - ), - Mapping( - "src/handlers/v2/injections.go", - "ListFaultInjectionWithIssues", - "src/module/injection/handler.go", - "ListFaultInjectionWithIssues", - ), - Mapping( - "src/handlers/v2/injections.go", - "ManageInjectionCustomLabels", - "src/module/injection/handler.go", - "ManageInjectionCustomLabels", - ), - Mapping( - "src/handlers/v2/injections.go", - "BatchManageInjectionLabels", - "src/module/injection/handler.go", - "BatchManageInjectionLabels", - ), - Mapping( - "src/handlers/v2/injections.go", - "SubmitFaultInjection", - "src/module/injection/handler.go", - "SubmitFaultInjection", - ), - Mapping( - "src/handlers/v2/injections.go", - "SubmitDatapackBuilding", - "src/module/injection/handler.go", - "SubmitDatapackBuilding", - ), - Mapping( - "src/handlers/v2/injections.go", - "CloneInjection", - "src/module/injection/handler.go", - "CloneInjection", - ), - Mapping( - "src/handlers/v2/injections.go", - "GetInjectionLogs", - "src/module/injection/handler.go", - "GetInjectionLogs", - ), - Mapping( - "src/handlers/v2/injections.go", - "DownloadDatapack", - "src/module/injection/handler.go", - "DownloadDatapack", - ), - Mapping( - "src/handlers/v2/injections.go", - "ListDatapackFiles", - "src/module/injection/handler.go", - "ListDatapackFiles", - ), - Mapping( - "src/handlers/v2/injections.go", - "DownloadDatapackFile", - "src/module/injection/handler.go", - "DownloadDatapackFile", - ), - Mapping( - "src/handlers/v2/injections.go", - "QueryDatapackFile", - "src/module/injection/handler.go", - "QueryDatapackFile", - ), - Mapping( - "src/handlers/v2/injections.go", - "UploadDatapack", - "src/module/injection/handler.go", - "UploadDatapack", - ), - Mapping( - "src/handlers/v2/injections.go", - "UpdateGroundtruth", - "src/module/injection/handler.go", - "UpdateGroundtruth", - ), - Mapping( - "src/handlers/v2/labels.go", - "BatchDeleteLabels", - "src/module/label/handler.go", - "BatchDeleteLabels", - ), - Mapping( - "src/handlers/v2/labels.go", - "CreateLabel", - "src/module/label/handler.go", - "CreateLabel", - ), - Mapping( - "src/handlers/v2/labels.go", - "DeleteLabel", - "src/module/label/handler.go", - "DeleteLabel", - ), - Mapping( - "src/handlers/v2/labels.go", - "GetLabelDetail", - "src/module/label/handler.go", - "GetLabelDetail", - ), - Mapping( - "src/handlers/v2/labels.go", - "ListLabels", - "src/module/label/handler.go", - "ListLabels", - ), - Mapping( - "src/handlers/v2/labels.go", - "UpdateLabel", - "src/module/label/handler.go", - "UpdateLabel", - ), - Mapping( - "src/handlers/v2/metrics.go", - "GetInjectionMetrics", - "src/module/metric/handler.go", - "GetInjectionMetrics", - ), - Mapping( - "src/handlers/v2/metrics.go", - "GetExecutionMetrics", - "src/module/metric/handler.go", - "GetExecutionMetrics", - ), - Mapping( - "src/handlers/v2/metrics.go", - "GetAlgorithmMetrics", - "src/module/metric/handler.go", - "GetAlgorithmMetrics", - ), - Mapping( - "src/handlers/v2/notifications.go", - "GetNotificationStream", - "src/module/notification/handler.go", - "GetStream", - ), - Mapping( - "src/handlers/v2/permissions.go", - "GetPermission", - "src/module/rbac/handler.go", - "GetPermission", - ), - Mapping( - "src/handlers/v2/permissions.go", - "ListPermissions", - "src/module/rbac/handler.go", - "ListPermissions", - ), - Mapping( - "src/handlers/v2/permissions.go", - "ListRolesFromPermission", - "src/module/rbac/handler.go", - "ListRolesFromPermission", - ), - Mapping( - "src/handlers/v2/projects.go", - "CreateProject", - "src/module/project/handler.go", - "CreateProject", - ), - Mapping( - "src/handlers/v2/projects.go", - "DeleteProject", - "src/module/project/handler.go", - "DeleteProject", - ), - Mapping( - "src/handlers/v2/projects.go", - "GetProjectDetail", - "src/module/project/handler.go", - "GetProjectDetail", - ), - Mapping( - "src/handlers/v2/projects.go", - "ListProjects", - "src/module/project/handler.go", - "ListProjects", - ), - Mapping( - "src/handlers/v2/projects.go", - "UpdateProject", - "src/module/project/handler.go", - "UpdateProject", - ), - Mapping( - "src/handlers/v2/projects.go", - "ManageProjectCustomLabels", - "src/module/project/handler.go", - "ManageProjectCustomLabels", - ), - Mapping( - "src/handlers/v2/projects.go", - "ListProjectInjections", - "src/module/injection/handler.go", - "ListProjectInjections", - ), - Mapping( - "src/handlers/v2/projects.go", - "SearchProjectInjections", - "src/module/injection/handler.go", - "SearchProjectInjections", - ), - Mapping( - "src/handlers/v2/projects.go", - "ListProjectFaultInjectionNoIssues", - "src/module/injection/handler.go", - "ListProjectFaultInjectionNoIssues", - ), - Mapping( - "src/handlers/v2/projects.go", - "ListProjectFaultInjectionWithIssues", - "src/module/injection/handler.go", - "ListProjectFaultInjectionWithIssues", - ), - Mapping( - "src/handlers/v2/projects.go", - "SubmitProjectFaultInjection", - "src/module/injection/handler.go", - "SubmitProjectFaultInjection", - ), - Mapping( - "src/handlers/v2/projects.go", - "SubmitProjectDatapackBuilding", - "src/module/injection/handler.go", - "SubmitProjectDatapackBuilding", - ), - Mapping( - "src/handlers/v2/projects.go", - "ListProjectExecutions", - "src/module/execution/handler.go", - "ListProjectExecutions", - ), - Mapping( - "src/handlers/v2/resources.go", - "GetResourceDetail", - "src/module/rbac/handler.go", - "GetResource", - ), - Mapping( - "src/handlers/v2/resources.go", - "ListResources", - "src/module/rbac/handler.go", - "ListResources", - ), - Mapping( - "src/handlers/v2/resources.go", - "ListResourcePermissions", - "src/module/rbac/handler.go", - "ListResourcePermissions", - ), - Mapping( - "src/handlers/v2/roles.go", - "CreateRole", - "src/module/rbac/handler.go", - "CreateRole", - ), - Mapping( - "src/handlers/v2/roles.go", - "DeleteRole", - "src/module/rbac/handler.go", - "DeleteRole", - ), - Mapping( - "src/handlers/v2/roles.go", "GetRole", "src/module/rbac/handler.go", "GetRole" - ), - Mapping( - "src/handlers/v2/roles.go", - "ListRoles", - "src/module/rbac/handler.go", - "ListRoles", - ), - Mapping( - "src/handlers/v2/roles.go", - "UpdateRole", - "src/module/rbac/handler.go", - "UpdateRole", - ), - Mapping( - "src/handlers/v2/roles.go", - "AssignRolePermission", - "src/module/rbac/handler.go", - "AssignRolePermissions", - ), - Mapping( - "src/handlers/v2/roles.go", - "RemovePermissionsFromRole", - "src/module/rbac/handler.go", - "RemoveRolePermissions", - ), - Mapping( - "src/handlers/v2/sdk_evaluations.go", - "ListSDKEvaluations", - "src/module/sdk/handler.go", - "ListEvaluations", - ), - Mapping( - "src/handlers/v2/sdk_evaluations.go", - "GetSDKEvaluation", - "src/module/sdk/handler.go", - "GetEvaluation", - ), - Mapping( - "src/handlers/v2/sdk_evaluations.go", - "ListSDKExperiments", - "src/module/sdk/handler.go", - "ListExperiments", - ), - Mapping( - "src/handlers/v2/sdk_evaluations.go", - "ListSDKDatasetSamples", - "src/module/sdk/handler.go", - "ListDatasetSamples", - ), - Mapping( - "src/handlers/v2/system.go", - "GetSystemMetrics", - "src/module/systemmetric/handler.go", - "GetSystemMetrics", - ), - Mapping( - "src/handlers/v2/system.go", - "GetSystemMetricsHistory", - "src/module/systemmetric/handler.go", - "GetSystemMetricsHistory", - ), - Mapping( - "src/handlers/v2/systems.go", - "ListChaosSystemsHandler", - "src/module/chaossystem/handler.go", - "ListSystems", - ), - Mapping( - "src/handlers/v2/systems.go", - "GetChaosSystemHandler", - "src/module/chaossystem/handler.go", - "GetSystem", - ), - Mapping( - "src/handlers/v2/systems.go", - "CreateChaosSystemHandler", - "src/module/chaossystem/handler.go", - "CreateSystem", - ), - Mapping( - "src/handlers/v2/systems.go", - "UpdateChaosSystemHandler", - "src/module/chaossystem/handler.go", - "UpdateSystem", - ), - Mapping( - "src/handlers/v2/systems.go", - "DeleteChaosSystemHandler", - "src/module/chaossystem/handler.go", - "DeleteSystem", - ), - Mapping( - "src/handlers/v2/systems.go", - "UpsertChaosSystemMetadataHandler", - "src/module/chaossystem/handler.go", - "UpsertMetadata", - ), - Mapping( - "src/handlers/v2/systems.go", - "ListChaosSystemMetadataHandler", - "src/module/chaossystem/handler.go", - "ListMetadata", - ), - Mapping( - "src/handlers/v2/tasks.go", - "BatchDeleteTasks", - "src/module/task/handler.go", - "BatchDelete", - ), - Mapping("src/handlers/v2/tasks.go", "GetTask", "src/module/task/handler.go", "Get"), - Mapping( - "src/handlers/v2/tasks.go", "ListTasks", "src/module/task/handler.go", "List" - ), - Mapping( - "src/handlers/v2/tasks.go", - "GetTaskLogsWS", - "src/module/task/handler.go", - "LogsWS", - ), - Mapping( - "src/handlers/v2/teams.go", - "CreateTeam", - "src/module/team/handler.go", - "CreateTeam", - ), - Mapping( - "src/handlers/v2/teams.go", - "DeleteTeam", - "src/module/team/handler.go", - "DeleteTeam", - ), - Mapping( - "src/handlers/v2/teams.go", - "GetTeamDetail", - "src/module/team/handler.go", - "GetTeamDetail", - ), - Mapping( - "src/handlers/v2/teams.go", - "ListTeams", - "src/module/team/handler.go", - "ListTeams", - ), - Mapping( - "src/handlers/v2/teams.go", - "UpdateTeam", - "src/module/team/handler.go", - "UpdateTeam", - ), - Mapping( - "src/handlers/v2/teams.go", - "ListTeamProjects", - "src/module/team/handler.go", - "ListTeamProjects", - ), - Mapping( - "src/handlers/v2/teams.go", - "AddTeamMember", - "src/module/team/handler.go", - "AddTeamMember", - ), - Mapping( - "src/handlers/v2/teams.go", - "RemoveTeamMember", - "src/module/team/handler.go", - "RemoveTeamMember", - ), - Mapping( - "src/handlers/v2/teams.go", - "UpdateTeamMemberRole", - "src/module/team/handler.go", - "UpdateTeamMemberRole", - ), - Mapping( - "src/handlers/v2/teams.go", - "ListTeamMembers", - "src/module/team/handler.go", - "ListTeamMembers", - ), - Mapping( - "src/handlers/v2/traces.go", - "GetTrace", - "src/module/trace/handler.go", - "GetTrace", - ), - Mapping( - "src/handlers/v2/traces.go", - "ListTraces", - "src/module/trace/handler.go", - "ListTraces", - ), - Mapping( - "src/handlers/v2/traces.go", - "GetTraceStream", - "src/module/trace/handler.go", - "GetTraceStream", - ), - Mapping( - "src/handlers/v2/users.go", - "CreateUser", - "src/module/user/handler.go", - "CreateUser", - ), - Mapping( - "src/handlers/v2/users.go", - "DeleteUser", - "src/module/user/handler.go", - "DeleteUser", - ), - Mapping( - "src/handlers/v2/users.go", - "GetUserDetailV2", - "src/module/user/handler.go", - "GetUserDetail", - ), - Mapping( - "src/handlers/v2/users.go", - "ListUsersV2", - "src/module/user/handler.go", - "ListUsers", - ), - Mapping( - "src/handlers/v2/users.go", - "UpdateUser", - "src/module/user/handler.go", - "UpdateUser", - ), - Mapping( - "src/handlers/v2/users.go", - "AssignUserRole", - "src/module/user/handler.go", - "AssignRole", - ), - Mapping( - "src/handlers/v2/users.go", - "RemoveGlobalRole", - "src/module/user/handler.go", - "RemoveRole", - ), - Mapping( - "src/handlers/v2/users.go", - "AssignUserPermission", - "src/module/user/handler.go", - "AssignPermissions", - ), - Mapping( - "src/handlers/v2/users.go", - "RemoveUserPermission", - "src/module/user/handler.go", - "RemovePermissions", - ), - Mapping( - "src/handlers/v2/users.go", - "AssignUserContainer", - "src/module/user/handler.go", - "AssignContainer", - ), - Mapping( - "src/handlers/v2/users.go", - "RemoveUserContainer", - "src/module/user/handler.go", - "RemoveContainer", - ), - Mapping( - "src/handlers/v2/users.go", - "AssignUserDataset", - "src/module/user/handler.go", - "AssignDataset", - ), - Mapping( - "src/handlers/v2/users.go", - "RemoveUserDataset", - "src/module/user/handler.go", - "RemoveDataset", - ), - Mapping( - "src/handlers/v2/users.go", - "AssignUserProject", - "src/module/user/handler.go", - "AssignProject", - ), - Mapping( - "src/handlers/v2/users.go", - "RemoveUserProject", - "src/module/user/handler.go", - "RemoveProject", - ), - Mapping( - "src/handlers/v2/users.go", - "ListUsersFromRole", - "src/module/rbac/handler.go", - "ListUsersFromRole", - ), - Mapping( - "src/handlers/system/audit.go", - "GetAuditLog", - "src/module/system/handler.go", - "GetAuditLog", - ), - Mapping( - "src/handlers/system/audit.go", - "ListAuditLogs", - "src/module/system/handler.go", - "ListAuditLogs", - ), - Mapping( - "src/handlers/system/configs.go", - "GetConfig", - "src/module/system/handler.go", - "GetConfig", - ), - Mapping( - "src/handlers/system/configs.go", - "ListConfigs", - "src/module/system/handler.go", - "ListConfigs", - ), - Mapping( - "src/handlers/system/configs.go", - "RollbackConfigValue", - "src/module/system/handler.go", - "RollbackConfigValue", - ), - Mapping( - "src/handlers/system/configs.go", - "RollbackConfigMetadata", - "src/module/system/handler.go", - "RollbackConfigMetadata", - ), - Mapping( - "src/handlers/system/configs.go", - "UpdateConfigValue", - "src/module/system/handler.go", - "UpdateConfigValue", - ), - Mapping( - "src/handlers/system/configs.go", - "UpdateConfigMetadata", - "src/module/system/handler.go", - "UpdateConfigMetadata", - ), - Mapping( - "src/handlers/system/configs.go", - "ListConfigHistories", - "src/module/system/handler.go", - "ListConfigHistories", - ), - Mapping( - "src/handlers/system/health.go", - "GetHealth", - "src/module/system/handler.go", - "GetHealth", - ), - Mapping( - "src/handlers/system/monitor.go", - "GetMetrics", - "src/module/system/handler.go", - "GetMetrics", - ), - Mapping( - "src/handlers/system/monitor.go", - "GetSystemInfo", - "src/module/system/handler.go", - "GetSystemInfo", - ), - Mapping( - "src/handlers/system/monitor.go", - "ListNamespaceLocks", - "src/module/system/handler.go", - "ListNamespaceLocks", - ), - Mapping( - "src/handlers/system/monitor.go", - "ListQueuedTasks", - "src/module/system/handler.go", - "ListQueuedTasks", - ), -] - - -def read_lines(path: str) -> list[str]: - return (REPO_ROOT / path).read_text(encoding="utf-8").splitlines() - - -def extract_comments(path: str) -> dict[str, list[str]]: - lines = read_lines(path) - comments: dict[str, list[str]] = {} - for idx, line in enumerate(lines): - match = OLD_FUNC_RE.match(line.strip()) - if not match: - continue - func_name = match.group(1) - start = idx - 1 - while start >= 0 and lines[start].startswith("//"): - start -= 1 - block = lines[start + 1 : idx] - if block and any("@Router" in item for item in block): - comments[func_name] = block - return comments - - -def inject_comments(path: str, blocks: dict[str, list[str]]) -> None: - lines = read_lines(path) - output: list[str] = [] - idx = 0 - while idx < len(lines): - stripped = lines[idx].strip() - match = NEW_METHOD_RE.match(stripped) - if match and match.group(1) in blocks: - start = len(output) - while start > 0 and output[start - 1].startswith("//"): - start -= 1 - if start > 0 and output[start - 1] == "": - # Preserve a single separator before the comment block. - pass - if start < len(output): - output = output[:start] - if output and output[-1] != "": - output.append("") - elif output and output[-1] != "": - output.append("") - output.extend(blocks[match.group(1)]) - output.append(lines[idx]) - idx += 1 - (REPO_ROOT / path).write_text("\n".join(output) + "\n", encoding="utf-8") - - -def main() -> None: - source_cache: dict[str, dict[str, list[str]]] = {} - target_blocks: dict[str, dict[str, list[str]]] = {} - - for mapping in MAPPINGS: - if mapping.source not in source_cache: - source_cache[mapping.source] = extract_comments(mapping.source) - block = source_cache[mapping.source].get(mapping.source_func) - if not block: - raise RuntimeError( - f"missing comment block for {mapping.source}:{mapping.source_func}" - ) - target_blocks.setdefault(mapping.target, {})[mapping.target_func] = block - - for target, blocks in target_blocks.items(): - inject_comments(target, blocks) - print(f"updated {target} ({len(blocks)} methods)") - - -if __name__ == "__main__": - main() diff --git a/scripts/start.sh b/scripts/start.sh index 4ae79fe7..de193a69 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -4,6 +4,16 @@ set -e # Exit on error # Get ENV_MODE parameter (default: test) ENV_MODE=${1:-test} +CERT_MANAGER_MANIFEST_URL=${CERT_MANAGER_MANIFEST_URL:-"https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml"} +CHAOS_MESH_REPO_URL=${CHAOS_MESH_REPO_URL:-"https://charts.chaos-mesh.org"} +CLICKSTACK_REPO_URL=${CLICKSTACK_REPO_URL:-"https://hyperdxio.github.io/helm-charts"} +OPEN_TELEMETRY_REPO_URL=${OPEN_TELEMETRY_REPO_URL:-"https://open-telemetry.github.io/opentelemetry-helm-charts"} +OTEL_DEMO_REPO_URL=${OTEL_DEMO_REPO_URL:-"https://operationspai.github.io/opentelemetry-demo"} +JUICEFS_REPO_URL=${JUICEFS_REPO_URL:-"https://juicedata.github.io/charts"} +TEST_HTTP_PROXY=${TEST_HTTP_PROXY:-"http://crash:crash@172.18.0.1:7890"} +TEST_HTTPS_PROXY=${TEST_HTTPS_PROXY:-"http://crash:crash@172.18.0.1:7890"} +TEST_NO_PROXY=${TEST_NO_PROXY:-"localhost,127.0.0.1,10.96.0.0/12,172.18.0.0/16,cluster.local,svc"} + echo "Running in $ENV_MODE mode" echo "" @@ -52,7 +62,7 @@ if [ "$ENV_MODE" = "prod" ]; then # Install chaos-mesh echo "Installing Chaos Mesh..." - helm repo add chaos-mesh https://charts.chaos-mesh.org --force-update + helm repo add chaos-mesh "$CHAOS_MESH_REPO_URL" --force-update retry_helm_install 3 helm install chaos-mesh chaos-mesh/chaos-mesh \ --namespace chaos-mesh \ --create-namespace \ @@ -70,7 +80,7 @@ if [ "$ENV_MODE" = "prod" ]; then # Install cert-manager echo "Installing cert-manager..." - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml + kubectl apply -f "$CERT_MANAGER_MANIFEST_URL" echo "Waiting for cert-manager to be ready..." kubectl wait --for=condition=available --timeout=5m deployment/cert-manager -n cert-manager kubectl wait --for=condition=available --timeout=5m deployment/cert-manager-webhook -n cert-manager @@ -79,7 +89,7 @@ if [ "$ENV_MODE" = "prod" ]; then # Install ClickHouse only (no JuiceFS in prod) echo "Installing ClickHouse stack..." - helm repo add clickstack https://hyperdxio.github.io/helm-charts --force-update + helm repo add clickstack "$CLICKSTACK_REPO_URL" --force-update retry_helm_install 3 helm install clickstack clickstack/clickstack \ --namespace monitoring \ --create-namespace \ @@ -92,7 +102,7 @@ if [ "$ENV_MODE" = "prod" ]; then # Install otel-kube-stack echo "Installing OpenTelemetry Kube Stack..." - helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts --force-update + helm repo add open-telemetry "$OPEN_TELEMETRY_REPO_URL" --force-update retry_helm_install 3 helm install opentelemetry-kube-stack open-telemetry/opentelemetry-kube-stack \ --namespace monitoring \ --create-namespace \ @@ -104,7 +114,7 @@ if [ "$ENV_MODE" = "prod" ]; then # Install otel-demo echo "Installing OpenTelemetry Demo application..." - helm repo add opentelemetry-demo https://operationspai.github.io/opentelemetry-demo --force-update + helm repo add opentelemetry-demo "$OTEL_DEMO_REPO_URL" --force-update retry_helm_install 3 helm install otel-demo0 opentelemetry-demo/opentelemetry-demo \ --namespace otel-demo0 \ --create-namespace \ @@ -123,9 +133,9 @@ else # Create Kind cluster echo "Creating Kind cluster..." - HTTP_PROXY=http://crash:crash@172.18.0.1:7890 \ - HTTPS_PROXY=http://crash:crash@172.18.0.1:7890 \ - NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,172.18.0.0/16,cluster.local,svc \ + HTTP_PROXY="$TEST_HTTP_PROXY" \ + HTTPS_PROXY="$TEST_HTTPS_PROXY" \ + NO_PROXY="$TEST_NO_PROXY" \ kind create cluster --config=manifests/test/kind-config.yaml --name test kubectx kind-test echo "✅ Kind cluster created successfully" @@ -133,7 +143,7 @@ else # Install chaos-mesh echo "Installing Chaos Mesh..." - helm repo add chaos-mesh https://charts.chaos-mesh.org --force-update + helm repo add chaos-mesh "$CHAOS_MESH_REPO_URL" --force-update retry_helm_install 3 helm install chaos-mesh chaos-mesh/chaos-mesh \ --namespace chaos-mesh \ --create-namespace \ @@ -151,7 +161,7 @@ else # Install cert-manager echo "Installing cert-manager..." - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml + kubectl apply -f "$CERT_MANAGER_MANIFEST_URL" echo "Waiting for cert-manager to be ready..." kubectl wait --for=condition=available --timeout=5m deployment/cert-manager -n cert-manager kubectl wait --for=condition=available --timeout=5m deployment/cert-manager-webhook -n cert-manager @@ -162,7 +172,7 @@ else echo "Installing ClickHouse and JuiceFS CSI Driver in parallel..." ( echo " Installing ClickHouse stack..." - helm repo add clickstack https://hyperdxio.github.io/helm-charts --force-update + helm repo add clickstack "$CLICKSTACK_REPO_URL" --force-update retry_helm_install 3 helm install clickstack clickstack/clickstack \ --namespace monitoring \ --create-namespace \ @@ -175,7 +185,7 @@ else ( echo " Installing JuiceFS CSI Driver..." - helm repo add juicefs https://juicedata.github.io/charts --force-update + helm repo add juicefs "$JUICEFS_REPO_URL" --force-update retry_helm_install 3 helm install juicefs-csi-driver juicefs/juicefs-csi-driver \ --namespace kube-system \ -f manifests/cn_mirror/juicefs-csi-driver.yaml \ @@ -193,7 +203,7 @@ else # Install otel-kube-stack echo "Installing OpenTelemetry Kube Stack..." - helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts --force-update + helm repo add open-telemetry "$OPEN_TELEMETRY_REPO_URL" --force-update retry_helm_install 3 helm install opentelemetry-kube-stack open-telemetry/opentelemetry-kube-stack \ --namespace monitoring \ --create-namespace \ @@ -205,7 +215,7 @@ else # Install otel-demo echo "Installing OpenTelemetry Demo application..." - helm repo add opentelemetry-demo https://operationspai.github.io/opentelemetry-demo --force-update + helm repo add opentelemetry-demo "$OTEL_DEMO_REPO_URL" --force-update retry_helm_install 3 helm install otel-demo0 opentelemetry-demo/opentelemetry-demo \ --namespace otel-demo0 \ --create-namespace \ @@ -218,4 +228,4 @@ fi echo "=============================================" echo "✅ Cluster setup completed successfully!" -echo "=============================================" \ No newline at end of file +echo "=============================================" diff --git a/scripts/test-push.sh b/scripts/test-push.sh index 00dfb99f..29884050 100644 --- a/scripts/test-push.sh +++ b/scripts/test-push.sh @@ -41,4 +41,4 @@ cd "$PROJECT_ROOT" rm -rf sdk/python echo "✅ Cleaned up test server environment" -exit $TEST_RESULT \ No newline at end of file +exit $TEST_RESULT diff --git a/scripts/regression-test.sh b/scripts/test-regression.sh similarity index 98% rename from scripts/regression-test.sh rename to scripts/test-regression.sh index cff5e985..3048842b 100644 --- a/scripts/regression-test.sh +++ b/scripts/test-regression.sh @@ -38,4 +38,4 @@ cd "$PROJECT_ROOT" rm -rf sdk/python/src/rcabench/openapi echo "✅ Cleaned up test server environment" -exit $TEST_RESULT \ No newline at end of file +exit $TEST_RESULT diff --git a/sdk/python/README.md b/sdk/python/README.md index a5303420..b859f5c4 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -79,7 +79,7 @@ api_client = runtime_client.get_client() print(api_client.configuration.host) ``` -Runtime-tagged API classes are generated from the current OpenAPI audience split. Use `runtime_client.get_client()` with the generated API module that corresponds to those runtime-only routes. +`RCABenchRuntimeClient` stays as a thin authenticated connector only. Runtime upload/report timing and orchestration semantics belong in the external managed wrapper layer, not in this SDK client. ## Development diff --git a/skaffold.yaml b/skaffold.yaml index 2b96c96c..1beada63 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -78,5 +78,5 @@ profiles: hooks: after: - host: - command: ["just", "regression-test"] - os: [darwin, linux] \ No newline at end of file + command: ["just", "test-regression"] + os: [darwin, linux] diff --git a/src/app/app.go b/src/app/app.go index 5a51e094..216a12f4 100644 --- a/src/app/app.go +++ b/src/app/app.go @@ -1,53 +1,53 @@ package app import ( - buildkitinfra "aegis/infra/buildkit" - configinfra "aegis/infra/config" - dbinfra "aegis/infra/db" - etcdinfra "aegis/infra/etcd" - harborinfra "aegis/infra/harbor" - helminfra "aegis/infra/helm" - loggerinfra "aegis/infra/logger" - lokiinfra "aegis/infra/loki" - redisinfra "aegis/infra/redis" - tracinginfra "aegis/infra/tracing" + buildkit "aegis/infra/buildkit" + config "aegis/infra/config" + db "aegis/infra/db" + etcd "aegis/infra/etcd" + harbor "aegis/infra/harbor" + helm "aegis/infra/helm" + logger "aegis/infra/logger" + loki "aegis/infra/loki" + redis "aegis/infra/redis" + tracing "aegis/infra/tracing" "go.uber.org/fx" ) func BaseOptions(confPath string) fx.Option { return fx.Options( - fx.Supply(configinfra.Params{Path: confPath}), - configinfra.Module, - loggerinfra.Module, + fx.Supply(config.Params{Path: confPath}), + config.Module, + logger.Module, ) } func ObserveOptions() fx.Option { return fx.Options( - lokiinfra.Module, - tracinginfra.Module, + loki.Module, + tracing.Module, ) } func DataOptions() fx.Option { return fx.Options( - dbinfra.Module, - redisinfra.Module, + db.Module, + redis.Module, ) } func CoordinationOptions() fx.Option { return fx.Options( - etcdinfra.Module, + etcd.Module, ) } func BuildInfraOptions() fx.Option { return fx.Options( - harborinfra.Module, - helminfra.Module, - buildkitinfra.Module, + harbor.Module, + helm.Module, + buildkit.Module, ) } diff --git a/src/app/both.go b/src/app/both.go index 5ac8b907..7b04cde0 100644 --- a/src/app/both.go +++ b/src/app/both.go @@ -5,6 +5,7 @@ import "go.uber.org/fx" func BothOptions(confPath string, port string) fx.Option { return fx.Options( CommonOptions(confPath), - BothCompatibilityOptions(port), + RuntimeWorkerStackOptions(), + ProducerHTTPOptions(port), ) } diff --git a/src/app/compat_options.go b/src/app/compat_options.go deleted file mode 100644 index e54d25d7..00000000 --- a/src/app/compat_options.go +++ /dev/null @@ -1,49 +0,0 @@ -package app - -import ( - chaosinfra "aegis/infra/chaos" - k8sinfra "aegis/infra/k8s" - httpinterface "aegis/interface/http" - - "go.uber.org/fx" -) - -// ProducerCompatibilityOptions captures the standalone producer/api-gateway -// HTTP stack, including the HTTP-side K8s/chaos infra. -func ProducerCompatibilityOptions(port string) fx.Option { - return fx.Options( - chaosinfra.Module, - k8sinfra.Module, - ProducerHTTPEntryOptions(port), - ) -} - -// ProducerHTTPEntryOptions captures the compatibility producer HTTP surface -// shared by producer, both, and api-gateway entrypoints. -func ProducerHTTPEntryOptions(port string) fx.Option { - return fx.Options( - fx.Provide(newProducerInitializer), - fx.Invoke(registerProducerInitialization), - ProducerHTTPModules(), - fx.Supply(httpinterface.ServerConfig{Addr: normalizeAddr(port)}), - httpinterface.Module, - ) -} - -// CompatibilityRuntimeOptions centralizes the legacy runtime stack shared by -// consumer/both entrypoints. Local execution/injection owner modules are added -// explicitly by the caller when that entrypoint needs local owner fallback. -func CompatibilityRuntimeOptions() fx.Option { - return fx.Options( - RuntimeWorkerStackOptions(), - ) -} - -// BothCompatibilityOptions captures the legacy combined producer+consumer -// runtime surface in one place. -func BothCompatibilityOptions(port string) fx.Option { - return fx.Options( - CompatibilityRuntimeOptions(), - ProducerHTTPEntryOptions(port), - ) -} diff --git a/src/app/consumer.go b/src/app/consumer.go index ad057a63..d848eb3d 100644 --- a/src/app/consumer.go +++ b/src/app/consumer.go @@ -5,7 +5,7 @@ import "go.uber.org/fx" func ConsumerOptions(confPath string) fx.Option { return fx.Options( CommonOptions(confPath), - CompatibilityRuntimeOptions(), + RuntimeWorkerStackOptions(), ExecutionInjectionOwnerModules(), ) } diff --git a/src/app/gateway/auth_services.go b/src/app/gateway/auth_services.go index fd29720f..f19332f8 100644 --- a/src/app/gateway/auth_services.go +++ b/src/app/gateway/auth_services.go @@ -1,51 +1,51 @@ -package gatewayapp +package gateway import ( "context" - authmodule "aegis/module/auth" + auth "aegis/module/auth" "aegis/utils" ) type authIAMClient interface { Enabled() bool - Login(context.Context, *authmodule.LoginReq) (*authmodule.LoginResp, error) - Register(context.Context, *authmodule.RegisterReq) (*authmodule.UserInfo, error) - RefreshToken(context.Context, *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) + Login(context.Context, *auth.LoginReq) (*auth.LoginResp, error) + Register(context.Context, *auth.RegisterReq) (*auth.UserInfo, error) + RefreshToken(context.Context, *auth.TokenRefreshReq) (*auth.TokenRefreshResp, error) Logout(context.Context, *utils.Claims) error - ChangePassword(context.Context, *authmodule.ChangePasswordReq, int) error - GetProfile(context.Context, int) (*authmodule.UserProfileResp, error) - CreateAPIKey(context.Context, int, *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) - ListAPIKeys(context.Context, int, *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) - GetAPIKey(context.Context, int, int) (*authmodule.APIKeyInfo, error) + ChangePassword(context.Context, *auth.ChangePasswordReq, int) error + GetProfile(context.Context, int) (*auth.UserProfileResp, error) + CreateAPIKey(context.Context, int, *auth.CreateAPIKeyReq) (*auth.APIKeyWithSecretResp, error) + ListAPIKeys(context.Context, int, *auth.ListAPIKeyReq) (*auth.ListAPIKeyResp, error) + GetAPIKey(context.Context, int, int) (*auth.APIKeyInfo, error) DeleteAPIKey(context.Context, int, int) error DisableAPIKey(context.Context, int, int) error EnableAPIKey(context.Context, int, int) error RevokeAPIKey(context.Context, int, int) error - RotateAPIKey(context.Context, int, int) (*authmodule.APIKeyWithSecretResp, error) - ExchangeAPIKeyToken(context.Context, *authmodule.APIKeyTokenReq, string, string) (*authmodule.APIKeyTokenResp, error) + RotateAPIKey(context.Context, int, int) (*auth.APIKeyWithSecretResp, error) + ExchangeAPIKeyToken(context.Context, *auth.APIKeyTokenReq, string, string) (*auth.APIKeyTokenResp, error) } type remoteAwareAuthService struct { - authmodule.HandlerService + auth.HandlerService iam authIAMClient } -func (s remoteAwareAuthService) Login(ctx context.Context, req *authmodule.LoginReq) (*authmodule.LoginResp, error) { +func (s remoteAwareAuthService) Login(ctx context.Context, req *auth.LoginReq) (*auth.LoginResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.Login(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) Register(ctx context.Context, req *authmodule.RegisterReq) (*authmodule.UserInfo, error) { +func (s remoteAwareAuthService) Register(ctx context.Context, req *auth.RegisterReq) (*auth.UserInfo, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.Register(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) RefreshToken(ctx context.Context, req *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) { +func (s remoteAwareAuthService) RefreshToken(ctx context.Context, req *auth.TokenRefreshReq) (*auth.TokenRefreshResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.RefreshToken(ctx, req) } @@ -59,35 +59,35 @@ func (s remoteAwareAuthService) Logout(ctx context.Context, claims *utils.Claims return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) ChangePassword(ctx context.Context, req *authmodule.ChangePasswordReq, userID int) error { +func (s remoteAwareAuthService) ChangePassword(ctx context.Context, req *auth.ChangePasswordReq, userID int) error { if s.iam != nil && s.iam.Enabled() { return s.iam.ChangePassword(ctx, req, userID) } return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) GetProfile(ctx context.Context, userID int) (*authmodule.UserProfileResp, error) { +func (s remoteAwareAuthService) GetProfile(ctx context.Context, userID int) (*auth.UserProfileResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetProfile(ctx, userID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) CreateAPIKey(ctx context.Context, userID int, req *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) { +func (s remoteAwareAuthService) CreateAPIKey(ctx context.Context, userID int, req *auth.CreateAPIKeyReq) (*auth.APIKeyWithSecretResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.CreateAPIKey(ctx, userID, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) ListAPIKeys(ctx context.Context, userID int, req *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) { +func (s remoteAwareAuthService) ListAPIKeys(ctx context.Context, userID int, req *auth.ListAPIKeyReq) (*auth.ListAPIKeyResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListAPIKeys(ctx, userID, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyInfo, error) { +func (s remoteAwareAuthService) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*auth.APIKeyInfo, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetAPIKey(ctx, userID, accessKeyID) } @@ -122,14 +122,14 @@ func (s remoteAwareAuthService) RevokeAPIKey(ctx context.Context, userID, access return missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyWithSecretResp, error) { +func (s remoteAwareAuthService) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*auth.APIKeyWithSecretResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.RotateAPIKey(ctx, userID, accessKeyID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareAuthService) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKeyTokenReq, method, path string) (*authmodule.APIKeyTokenResp, error) { +func (s remoteAwareAuthService) ExchangeAPIKeyToken(ctx context.Context, req *auth.APIKeyTokenReq, method, path string) (*auth.APIKeyTokenResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ExchangeAPIKeyToken(ctx, req, method, path) } diff --git a/src/app/gateway/metric_services.go b/src/app/gateway/metric_services.go index 83df9d7b..caf6d5f1 100644 --- a/src/app/gateway/metric_services.go +++ b/src/app/gateway/metric_services.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import ( "context" @@ -6,42 +6,42 @@ import ( "aegis/consts" "aegis/dto" - containermodule "aegis/module/container" - metricmodule "aegis/module/metric" + container "aegis/module/container" + metric "aegis/module/metric" ) type metricOrchestratorClient interface { Enabled() bool - GetInjectionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) - GetExecutionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) + GetInjectionMetrics(context.Context, *metric.GetMetricsReq) (*metric.InjectionMetrics, error) + GetExecutionMetrics(context.Context, *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) } type metricResourceClient interface { Enabled() bool - ListContainers(context.Context, *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) + ListContainers(context.Context, *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) } type remoteAwareMetricService struct { - metricmodule.HandlerService + metric.HandlerService orchestrator metricOrchestratorClient resource metricResourceClient } -func (s remoteAwareMetricService) GetInjectionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { +func (s remoteAwareMetricService) GetInjectionMetrics(ctx context.Context, req *metric.GetMetricsReq) (*metric.InjectionMetrics, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.GetInjectionMetrics(ctx, req) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareMetricService) GetExecutionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { +func (s remoteAwareMetricService) GetExecutionMetrics(ctx context.Context, req *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.GetExecutionMetrics(ctx, req) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.AlgorithmMetrics, error) { +func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req *metric.GetMetricsReq) (*metric.AlgorithmMetrics, error) { if s.orchestrator == nil || !s.orchestrator.Enabled() { return nil, missingRemoteDependency("orchestrator-service") } @@ -54,12 +54,12 @@ func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req * return nil, err } - metrics := &metricmodule.AlgorithmMetrics{ - Algorithms: make([]metricmodule.AlgorithmMetricItem, 0, len(algorithms)), + metrics := &metric.AlgorithmMetrics{ + Algorithms: make([]metric.AlgorithmMetricItem, 0, len(algorithms)), } for _, algorithm := range algorithms { algorithmID := algorithm.ID - executionMetrics, err := s.orchestrator.GetExecutionMetrics(ctx, &metricmodule.GetMetricsReq{ + executionMetrics, err := s.orchestrator.GetExecutionMetrics(ctx, &metric.GetMetricsReq{ StartTime: req.StartTime, EndTime: req.EndTime, AlgorithmID: &algorithmID, @@ -67,7 +67,7 @@ func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req * if err != nil || executionMetrics == nil || executionMetrics.TotalCount == 0 { continue } - metrics.Algorithms = append(metrics.Algorithms, metricmodule.AlgorithmMetricItem{ + metrics.Algorithms = append(metrics.Algorithms, metric.AlgorithmMetricItem{ AlgorithmID: algorithm.ID, AlgorithmName: algorithm.Name, ExecutionCount: executionMetrics.TotalCount, @@ -80,14 +80,14 @@ func (s remoteAwareMetricService) GetAlgorithmMetrics(ctx context.Context, req * return metrics, nil } -func (s remoteAwareMetricService) listAlgorithmContainers(ctx context.Context, req *metricmodule.GetMetricsReq) ([]containermodule.ContainerResp, error) { +func (s remoteAwareMetricService) listAlgorithmContainers(ctx context.Context, req *metric.GetMetricsReq) ([]container.ContainerResp, error) { containerType := consts.ContainerTypeAlgorithm status := consts.CommonEnabled page := 1 - items := make([]containermodule.ContainerResp, 0) + items := make([]container.ContainerResp, 0) for { - resp, err := s.resource.ListContainers(ctx, &containermodule.ListContainerReq{ + resp, err := s.resource.ListContainers(ctx, &container.ListContainerReq{ PaginationReq: dto.PaginationReq{ Page: page, Size: consts.PageSizeXLarge, @@ -108,11 +108,11 @@ func (s remoteAwareMetricService) listAlgorithmContainers(ctx context.Context, r if req.AlgorithmID == nil { return items, nil } - index := slices.IndexFunc(items, func(item containermodule.ContainerResp) bool { + index := slices.IndexFunc(items, func(item container.ContainerResp) bool { return item.ID == *req.AlgorithmID }) if index < 0 { - return []containermodule.ContainerResp{}, nil + return []container.ContainerResp{}, nil } - return []containermodule.ContainerResp{items[index]}, nil + return []container.ContainerResp{items[index]}, nil } diff --git a/src/app/gateway/metric_services_test.go b/src/app/gateway/metric_services_test.go index db1ea854..61417468 100644 --- a/src/app/gateway/metric_services_test.go +++ b/src/app/gateway/metric_services_test.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import ( "context" @@ -7,15 +7,15 @@ import ( "aegis/consts" "aegis/dto" - containermodule "aegis/module/container" - metricmodule "aegis/module/metric" + container "aegis/module/container" + metric "aegis/module/metric" ) type orchestratorMetricClientStub struct { - injectionReqs []*metricmodule.GetMetricsReq - executionReqs []*metricmodule.GetMetricsReq - injection *metricmodule.InjectionMetrics - execution map[int]metricmodule.ExecutionMetrics + injectionReqs []*metric.GetMetricsReq + executionReqs []*metric.GetMetricsReq + injection *metric.InjectionMetrics + execution map[int]metric.ExecutionMetrics enabled bool } @@ -23,12 +23,12 @@ func (s *orchestratorMetricClientStub) Enabled() bool { return s.enabled } -func (s *orchestratorMetricClientStub) GetInjectionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { +func (s *orchestratorMetricClientStub) GetInjectionMetrics(_ context.Context, req *metric.GetMetricsReq) (*metric.InjectionMetrics, error) { s.injectionReqs = append(s.injectionReqs, req) return s.injection, nil } -func (s *orchestratorMetricClientStub) GetExecutionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { +func (s *orchestratorMetricClientStub) GetExecutionMetrics(_ context.Context, req *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) { s.executionReqs = append(s.executionReqs, req) if req != nil && req.AlgorithmID != nil { if metric, ok := s.execution[*req.AlgorithmID]; ok { @@ -36,11 +36,11 @@ func (s *orchestratorMetricClientStub) GetExecutionMetrics(_ context.Context, re return &result, nil } } - return &metricmodule.ExecutionMetrics{}, nil + return &metric.ExecutionMetrics{}, nil } type resourceMetricClientStub struct { - responses []*dto.ListResp[containermodule.ContainerResp] + responses []*dto.ListResp[container.ContainerResp] enabled bool calls int } @@ -49,18 +49,18 @@ func (s *resourceMetricClientStub) Enabled() bool { return s.enabled } -func (s *resourceMetricClientStub) ListContainers(_ context.Context, _ *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { +func (s *resourceMetricClientStub) ListContainers(_ context.Context, _ *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) { idx := s.calls s.calls++ if idx >= len(s.responses) { - return &dto.ListResp[containermodule.ContainerResp]{}, nil + return &dto.ListResp[container.ContainerResp]{}, nil } return s.responses[idx], nil } func TestRemoteAwareMetricServiceGetInjectionMetricsRemoteOnly(t *testing.T) { service := remoteAwareMetricService{} - _, err := service.GetInjectionMetrics(context.Background(), &metricmodule.GetMetricsReq{}) + _, err := service.GetInjectionMetrics(context.Background(), &metric.GetMetricsReq{}) if err == nil { t.Fatal("GetInjectionMetrics() error = nil, want missing dependency") } @@ -71,7 +71,7 @@ func TestRemoteAwareMetricServiceGetAlgorithmMetricsBuildsFromRemoteSources(t *t end := time.Now() orchestrator := &orchestratorMetricClientStub{ enabled: true, - execution: map[int]metricmodule.ExecutionMetrics{ + execution: map[int]metric.ExecutionMetrics{ 1: {TotalCount: 3, SuccessCount: 2, FailedCount: 1, SuccessRate: 66.7, AvgDuration: 12.5}, 2: {TotalCount: 0}, 3: {TotalCount: 5, SuccessCount: 5, FailedCount: 0, SuccessRate: 100, AvgDuration: 8}, @@ -79,16 +79,16 @@ func TestRemoteAwareMetricServiceGetAlgorithmMetricsBuildsFromRemoteSources(t *t } resource := &resourceMetricClientStub{ enabled: true, - responses: []*dto.ListResp[containermodule.ContainerResp]{ + responses: []*dto.ListResp[container.ContainerResp]{ { - Items: []containermodule.ContainerResp{ + Items: []container.ContainerResp{ {ID: 1, Name: "algo-a", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, {ID: 2, Name: "algo-b", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, }, Pagination: &dto.PaginationInfo{Page: 1, Size: 100, Total: 3, TotalPages: 2}, }, { - Items: []containermodule.ContainerResp{ + Items: []container.ContainerResp{ {ID: 3, Name: "algo-c", Type: consts.GetContainerTypeName(consts.ContainerTypeAlgorithm)}, }, Pagination: &dto.PaginationInfo{Page: 2, Size: 100, Total: 3, TotalPages: 2}, @@ -101,7 +101,7 @@ func TestRemoteAwareMetricServiceGetAlgorithmMetricsBuildsFromRemoteSources(t *t resource: resource, } - resp, err := service.GetAlgorithmMetrics(context.Background(), &metricmodule.GetMetricsReq{ + resp, err := service.GetAlgorithmMetrics(context.Background(), &metric.GetMetricsReq{ StartTime: &start, EndTime: &end, }) diff --git a/src/app/gateway/middleware_service.go b/src/app/gateway/middleware_service.go index ddef6d26..b8632cff 100644 --- a/src/app/gateway/middleware_service.go +++ b/src/app/gateway/middleware_service.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import ( "context" diff --git a/src/app/gateway/options.go b/src/app/gateway/options.go index 717db2e3..3c2d3940 100644 --- a/src/app/gateway/options.go +++ b/src/app/gateway/options.go @@ -1,31 +1,33 @@ -package gatewayapp +package gateway import ( "aegis/app" + chaos "aegis/infra/chaos" + k8s "aegis/infra/k8s" "aegis/internalclient/iamclient" "aegis/internalclient/orchestratorclient" "aegis/internalclient/resourceclient" "aegis/internalclient/systemclient" "aegis/middleware" - authmodule "aegis/module/auth" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - labelmodule "aegis/module/label" - metricmodule "aegis/module/metric" - notificationmodule "aegis/module/notification" - projectmodule "aegis/module/project" - rbacmodule "aegis/module/rbac" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" - teammodule "aegis/module/team" - tracemodule "aegis/module/trace" - usermodule "aegis/module/user" + auth "aegis/module/auth" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + label "aegis/module/label" + metric "aegis/module/metric" + notification "aegis/module/notification" + project "aegis/module/project" + rbac "aegis/module/rbac" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" + team "aegis/module/team" + trace "aegis/module/trace" + user "aegis/module/user" "go.uber.org/fx" ) @@ -38,7 +40,9 @@ func Options(confPath, port string) fx.Option { app.DataOptions(), app.CoordinationOptions(), app.BuildInfraOptions(), - app.ProducerCompatibilityOptions(port), + chaos.Module, + k8s.Module, + app.ProducerHTTPOptions(port), app.RequireConfiguredTargets( "api-gateway", app.RequiredConfigTarget{Name: "iam-service", PrimaryKey: "clients.iam.target", LegacyKey: "iam.grpc.target"}, @@ -50,7 +54,7 @@ func Options(confPath, port string) fx.Option { orchestratorclient.Module, resourceclient.Module, systemclient.Module, - fx.Decorate(func(local authmodule.HandlerService, remote *iamclient.Client) authmodule.HandlerService { + fx.Decorate(func(local auth.HandlerService, remote *iamclient.Client) auth.HandlerService { return remoteAwareAuthService{ HandlerService: local, iam: remote, @@ -62,110 +66,110 @@ func Options(confPath, port string) fx.Option { iam: remote, } }), - fx.Decorate(func(local usermodule.HandlerService, remote *iamclient.Client) usermodule.HandlerService { + fx.Decorate(func(local user.HandlerService, remote *iamclient.Client) user.HandlerService { return remoteAwareUserService{ HandlerService: local, iam: remote, } }), - fx.Decorate(func(local rbacmodule.HandlerService, remote *iamclient.Client) rbacmodule.HandlerService { + fx.Decorate(func(local rbac.HandlerService, remote *iamclient.Client) rbac.HandlerService { return remoteAwareRBACService{ HandlerService: local, iam: remote, } }), - fx.Decorate(func(local teammodule.HandlerService, remote *iamclient.Client) teammodule.HandlerService { + fx.Decorate(func(local team.HandlerService, remote *iamclient.Client) team.HandlerService { return remoteAwareTeamService{ HandlerService: local, iam: remote, } }), - fx.Decorate(func(local executionmodule.HandlerService, remote *orchestratorclient.Client) executionmodule.HandlerService { + fx.Decorate(func(local execution.HandlerService, remote *orchestratorclient.Client) execution.HandlerService { return remoteAwareExecutionService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local injectionmodule.HandlerService, remote *orchestratorclient.Client) injectionmodule.HandlerService { + fx.Decorate(func(local injection.HandlerService, remote *orchestratorclient.Client) injection.HandlerService { return remoteAwareInjectionService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local taskmodule.HandlerService, remote *orchestratorclient.Client) taskmodule.HandlerService { + fx.Decorate(func(local task.HandlerService, remote *orchestratorclient.Client) task.HandlerService { return remoteAwareTaskService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local tracemodule.HandlerService, remote *orchestratorclient.Client) tracemodule.HandlerService { + fx.Decorate(func(local trace.HandlerService, remote *orchestratorclient.Client) trace.HandlerService { return remoteAwareTraceService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local groupmodule.HandlerService, remote *orchestratorclient.Client) groupmodule.HandlerService { + fx.Decorate(func(local group.HandlerService, remote *orchestratorclient.Client) group.HandlerService { return remoteAwareGroupService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local notificationmodule.HandlerService, remote *orchestratorclient.Client) notificationmodule.HandlerService { + fx.Decorate(func(local notification.HandlerService, remote *orchestratorclient.Client) notification.HandlerService { return remoteAwareNotificationService{ HandlerService: local, orchestrator: remote, } }), - fx.Decorate(func(local projectmodule.HandlerService, remote *resourceclient.Client) projectmodule.HandlerService { + fx.Decorate(func(local project.HandlerService, remote *resourceclient.Client) project.HandlerService { return remoteAwareProjectService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local containermodule.HandlerService, remote *resourceclient.Client) containermodule.HandlerService { + fx.Decorate(func(local container.HandlerService, remote *resourceclient.Client) container.HandlerService { return remoteAwareContainerService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local datasetmodule.HandlerService, remote *resourceclient.Client) datasetmodule.HandlerService { + fx.Decorate(func(local dataset.HandlerService, remote *resourceclient.Client) dataset.HandlerService { return remoteAwareDatasetService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local evaluationmodule.HandlerService, remote *resourceclient.Client) evaluationmodule.HandlerService { + fx.Decorate(func(local evaluation.HandlerService, remote *resourceclient.Client) evaluation.HandlerService { return remoteAwareEvaluationService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local labelmodule.HandlerService, remote *resourceclient.Client) labelmodule.HandlerService { + fx.Decorate(func(local label.HandlerService, remote *resourceclient.Client) label.HandlerService { return remoteAwareLabelService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local chaossystemmodule.HandlerService, remote *resourceclient.Client) chaossystemmodule.HandlerService { + fx.Decorate(func(local chaossystem.HandlerService, remote *resourceclient.Client) chaossystem.HandlerService { return remoteAwareChaosSystemService{ HandlerService: local, resource: remote, } }), - fx.Decorate(func(local metricmodule.HandlerService, orchestrator *orchestratorclient.Client, resource *resourceclient.Client) metricmodule.HandlerService { + fx.Decorate(func(local metric.HandlerService, orchestrator *orchestratorclient.Client, resource *resourceclient.Client) metric.HandlerService { return remoteAwareMetricService{ HandlerService: local, orchestrator: orchestrator, resource: resource, } }), - fx.Decorate(func(local systemmodule.HandlerService, remote *systemclient.Client) systemmodule.HandlerService { + fx.Decorate(func(local system.HandlerService, remote *systemclient.Client) system.HandlerService { return remoteAwareSystemService{ HandlerService: local, system: remote, } }), - fx.Decorate(func(local systemmetricmodule.HandlerService, remote *systemclient.Client) systemmetricmodule.HandlerService { + fx.Decorate(func(local systemmetric.HandlerService, remote *systemclient.Client) systemmetric.HandlerService { return remoteAwareSystemMetricService{ HandlerService: local, system: remote, diff --git a/src/app/gateway/orchestrator_services.go b/src/app/gateway/orchestrator_services.go index 91890341..ec2ac977 100644 --- a/src/app/gateway/orchestrator_services.go +++ b/src/app/gateway/orchestrator_services.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import ( "context" @@ -8,23 +8,23 @@ import ( "aegis/dto" "aegis/internalclient/orchestratorclient" "aegis/model" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - notificationmodule "aegis/module/notification" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + notification "aegis/module/notification" + task "aegis/module/task" + trace "aegis/module/trace" "github.com/gorilla/websocket" "github.com/redis/go-redis/v9" ) type remoteAwareExecutionService struct { - executionmodule.HandlerService + execution.HandlerService orchestrator *orchestratorclient.Client } -func (s remoteAwareExecutionService) SubmitAlgorithmExecution(ctx context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { +func (s remoteAwareExecutionService) SubmitAlgorithmExecution(ctx context.Context, req *execution.SubmitExecutionReq, groupID string, userID int) (*execution.SubmitExecutionResp, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.SubmitExecution(ctx, req, groupID, userID) } @@ -32,18 +32,18 @@ func (s remoteAwareExecutionService) SubmitAlgorithmExecution(ctx context.Contex } type remoteAwareInjectionService struct { - injectionmodule.HandlerService + injection.HandlerService orchestrator *orchestratorclient.Client } -func (s remoteAwareInjectionService) SubmitFaultInjection(ctx context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { +func (s remoteAwareInjectionService) SubmitFaultInjection(ctx context.Context, req *injection.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injection.SubmitInjectionResp, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.SubmitFaultInjection(ctx, req, groupID, userID, projectID) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareInjectionService) SubmitDatapackBuilding(ctx context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { +func (s remoteAwareInjectionService) SubmitDatapackBuilding(ctx context.Context, req *injection.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injection.SubmitDatapackBuildingResp, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.SubmitDatapackBuilding(ctx, req, groupID, userID, projectID) } @@ -52,32 +52,32 @@ func (s remoteAwareInjectionService) SubmitDatapackBuilding(ctx context.Context, type taskOrchestratorClient interface { Enabled() bool - GetTask(context.Context, string) (*taskmodule.TaskDetailResp, error) - PollTaskLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) - ListTasks(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) + GetTask(context.Context, string) (*task.TaskDetailResp, error) + PollTaskLogs(context.Context, string, time.Time) (*task.TaskLogPollResp, error) + ListTasks(context.Context, *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) } type traceOrchestratorClient interface { Enabled() bool - GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) - ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) + GetTrace(context.Context, string) (*trace.TraceDetailResp, error) + ListTraces(context.Context, *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) } type remoteAwareTaskService struct { - taskmodule.HandlerService + task.HandlerService orchestrator taskOrchestratorClient } -func (s remoteAwareTaskService) GetDetail(ctx context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { +func (s remoteAwareTaskService) GetDetail(ctx context.Context, taskID string) (*task.TaskDetailResp, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.GetTask(ctx, taskID) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareTaskService) List(ctx context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { +func (s remoteAwareTaskService) List(ctx context.Context, req *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.ListTasks(ctx, req) } @@ -94,9 +94,9 @@ func (s remoteAwareTaskService) GetForLogStream(ctx context.Context, taskID stri return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareTaskService) StreamLogs(ctx context.Context, conn *websocket.Conn, task *model.Task) { +func (s remoteAwareTaskService) StreamLogs(ctx context.Context, conn *websocket.Conn, taskModel *model.Task) { if s.orchestrator == nil || !s.orchestrator.Enabled() { - writeTaskWSMessage(conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(conn, task.WSLogMessage{ Type: consts.WSLogTypeError, Message: missingRemoteDependency("orchestrator-service").Error(), }) @@ -107,7 +107,7 @@ func (s remoteAwareTaskService) StreamLogs(ctx context.Context, conn *websocket. streamer := remoteTaskLogStreamer{ conn: conn, orchestrator: s.orchestrator, - taskID: task.ID, + taskID: taskModel.ID, } streamer.stream(ctx) } @@ -143,7 +143,7 @@ func (s remoteTaskLogStreamer) stream(ctx context.Context) { initial, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, time.Time{}) if err != nil { - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeError, Message: err.Error(), }) @@ -152,7 +152,7 @@ func (s remoteTaskLogStreamer) stream(ctx context.Context) { } lastTimestamp := initial.CreatedAt if len(initial.Logs) > 0 { - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeHistory, Logs: initial.Logs, Total: len(initial.Logs), @@ -174,14 +174,14 @@ func (s remoteTaskLogStreamer) stream(ctx context.Context) { case <-ticker.C: resp, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, lastTimestamp) if err != nil { - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeError, Message: err.Error(), }) return } if len(resp.Logs) > 0 { - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeRealtime, Logs: resp.Logs, }) @@ -200,7 +200,7 @@ func (s remoteTaskLogStreamer) flushTerminalLogs(ctx context.Context, lastTimest for time.Now().Before(deadline) { resp, err := s.orchestrator.PollTaskLogs(ctx, s.taskID, lastTimestamp) if err == nil && len(resp.Logs) > 0 { - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeRealtime, Logs: resp.Logs, }) @@ -208,7 +208,7 @@ func (s remoteTaskLogStreamer) flushTerminalLogs(ctx context.Context, lastTimest } time.Sleep(remoteTaskPollInterval) } - writeTaskWSMessage(s.conn, taskmodule.WSLogMessage{ + writeTaskWSMessage(s.conn, task.WSLogMessage{ Type: consts.WSLogTypeEnd, Message: "task completed", }) @@ -241,37 +241,37 @@ func (s remoteTaskLogStreamer) pingLoop(ctx context.Context, cancel context.Canc } } -func writeTaskWSMessage(conn *websocket.Conn, msg taskmodule.WSLogMessage) { +func writeTaskWSMessage(conn *websocket.Conn, msg task.WSLogMessage) { _ = conn.SetWriteDeadline(time.Now().Add(remoteTaskLogWriteWait)) _ = conn.WriteJSON(msg) } type remoteAwareTraceService struct { - tracemodule.HandlerService + trace.HandlerService orchestrator traceOrchestratorClient } -func (s remoteAwareTraceService) GetTrace(ctx context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { +func (s remoteAwareTraceService) GetTrace(ctx context.Context, traceID string) (*trace.TraceDetailResp, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.GetTrace(ctx, traceID) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareTraceService) ListTraces(ctx context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { +func (s remoteAwareTraceService) ListTraces(ctx context.Context, req *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.ListTraces(ctx, req) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareTraceService) GetTraceStreamProcessor(ctx context.Context, traceID string) (*tracemodule.StreamProcessor, error) { +func (s remoteAwareTraceService) GetTraceStreamProcessor(ctx context.Context, traceID string) (*trace.StreamProcessor, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { algorithms, err := s.orchestrator.GetTraceStreamAlgorithms(ctx, traceID) if err != nil { return nil, err } - return tracemodule.NewStreamProcessor(algorithms), nil + return trace.NewStreamProcessor(algorithms), nil } return nil, missingRemoteDependency("orchestrator-service") } @@ -285,30 +285,30 @@ func (s remoteAwareTraceService) ReadTraceStreamMessages(ctx context.Context, st type groupOrchestratorClient interface { Enabled() bool - GetGroupStats(context.Context, string) (*groupmodule.GroupStats, error) + GetGroupStats(context.Context, string) (*group.GroupStats, error) GetGroupTraceCount(context.Context, string) (int, error) ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) } type remoteAwareGroupService struct { - groupmodule.HandlerService + group.HandlerService orchestrator groupOrchestratorClient } -func (s remoteAwareGroupService) GetGroupStats(ctx context.Context, req *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) { +func (s remoteAwareGroupService) GetGroupStats(ctx context.Context, req *group.GetGroupStatsReq) (*group.GroupStats, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { return s.orchestrator.GetGroupStats(ctx, req.GroupID) } return nil, missingRemoteDependency("orchestrator-service") } -func (s remoteAwareGroupService) NewGroupStreamProcessor(ctx context.Context, groupID string) (*groupmodule.GroupStreamProcessor, error) { +func (s remoteAwareGroupService) NewGroupStreamProcessor(ctx context.Context, groupID string) (*group.GroupStreamProcessor, error) { if s.orchestrator != nil && s.orchestrator.Enabled() { totalTraces, err := s.orchestrator.GetGroupTraceCount(ctx, groupID) if err != nil { return nil, err } - return groupmodule.NewGroupStreamProcessor(totalTraces), nil + return group.NewGroupStreamProcessor(totalTraces), nil } return nil, missingRemoteDependency("orchestrator-service") } @@ -326,7 +326,7 @@ type notificationOrchestratorClient interface { } type remoteAwareNotificationService struct { - notificationmodule.HandlerService + notification.HandlerService orchestrator notificationOrchestratorClient } diff --git a/src/app/gateway/orchestrator_services_test.go b/src/app/gateway/orchestrator_services_test.go index 6bb55448..3e2b332d 100644 --- a/src/app/gateway/orchestrator_services_test.go +++ b/src/app/gateway/orchestrator_services_test.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import ( "context" @@ -6,9 +6,9 @@ import ( "time" "aegis/dto" - groupmodule "aegis/module/group" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + group "aegis/module/group" + task "aegis/module/task" + trace "aegis/module/trace" "github.com/redis/go-redis/v9" ) @@ -19,12 +19,12 @@ type orchestratorTaskClientStub struct { func (s *orchestratorTaskClientStub) Enabled() bool { return s.enabled } -func (s *orchestratorTaskClientStub) GetTask(context.Context, string) (*taskmodule.TaskDetailResp, error) { - return &taskmodule.TaskDetailResp{TaskResp: taskmodule.TaskResp{ID: "task-1"}}, nil +func (s *orchestratorTaskClientStub) GetTask(context.Context, string) (*task.TaskDetailResp, error) { + return &task.TaskDetailResp{TaskResp: task.TaskResp{ID: "task-1"}}, nil } -func (s *orchestratorTaskClientStub) PollTaskLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) { - return &taskmodule.TaskLogPollResp{ +func (s *orchestratorTaskClientStub) PollTaskLogs(context.Context, string, time.Time) (*task.TaskLogPollResp, error) { + return &task.TaskLogPollResp{ Logs: []dto.LogEntry{{TaskID: "task-1", Line: "hello"}}, Terminal: true, State: "completed", @@ -32,8 +32,8 @@ func (s *orchestratorTaskClientStub) PollTaskLogs(context.Context, string, time. }, nil } -func (s *orchestratorTaskClientStub) ListTasks(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { - return &dto.ListResp[taskmodule.TaskResp]{Items: []taskmodule.TaskResp{{ID: "task-1"}}}, nil +func (s *orchestratorTaskClientStub) ListTasks(context.Context, *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) { + return &dto.ListResp[task.TaskResp]{Items: []task.TaskResp{{ID: "task-1"}}}, nil } type orchestratorTraceClientStub struct { @@ -42,12 +42,12 @@ type orchestratorTraceClientStub struct { func (s *orchestratorTraceClientStub) Enabled() bool { return s.enabled } -func (s *orchestratorTraceClientStub) GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) { - return &tracemodule.TraceDetailResp{TraceResp: tracemodule.TraceResp{ID: "trace-1"}}, nil +func (s *orchestratorTraceClientStub) GetTrace(context.Context, string) (*trace.TraceDetailResp, error) { + return &trace.TraceDetailResp{TraceResp: trace.TraceResp{ID: "trace-1"}}, nil } -func (s *orchestratorTraceClientStub) ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { - return &dto.ListResp[tracemodule.TraceResp]{Items: []tracemodule.TraceResp{{ID: "trace-1"}}}, nil +func (s *orchestratorTraceClientStub) ListTraces(context.Context, *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) { + return &dto.ListResp[trace.TraceResp]{Items: []trace.TraceResp{{ID: "trace-1"}}}, nil } func (s *orchestratorTraceClientStub) GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) { @@ -64,8 +64,8 @@ type orchestratorGroupClientStub struct { func (s *orchestratorGroupClientStub) Enabled() bool { return s.enabled } -func (s *orchestratorGroupClientStub) GetGroupStats(context.Context, string) (*groupmodule.GroupStats, error) { - return &groupmodule.GroupStats{TotalTraces: 2}, nil +func (s *orchestratorGroupClientStub) GetGroupStats(context.Context, string) (*group.GroupStats, error) { + return &group.GroupStats{TotalTraces: 2}, nil } func (s *orchestratorGroupClientStub) GetGroupTraceCount(context.Context, string) (int, error) { @@ -88,7 +88,7 @@ func (s *orchestratorNotificationClientStub) ReadNotificationStreamMessages(cont func TestRemoteAwareTaskServiceRequiresOrchestrator(t *testing.T) { service := remoteAwareTaskService{} - if _, err := service.List(context.Background(), &taskmodule.ListTaskReq{}); err == nil { + if _, err := service.List(context.Background(), &task.ListTaskReq{}); err == nil { t.Fatal("List() error = nil, want missing dependency") } } @@ -114,7 +114,7 @@ func TestRemoteAwareTaskServiceUsesOrchestratorClient(t *testing.T) { func TestRemoteAwareTraceServiceRequiresOrchestrator(t *testing.T) { service := remoteAwareTraceService{} - if _, err := service.ListTraces(context.Background(), &tracemodule.ListTraceReq{}); err == nil { + if _, err := service.ListTraces(context.Background(), &trace.ListTraceReq{}); err == nil { t.Fatal("ListTraces() error = nil, want missing dependency") } } @@ -140,7 +140,7 @@ func TestRemoteAwareTraceServiceUsesOrchestratorClient(t *testing.T) { func TestRemoteAwareGroupServiceRequiresOrchestrator(t *testing.T) { service := remoteAwareGroupService{} - if _, err := service.GetGroupStats(context.Background(), &groupmodule.GetGroupStatsReq{ + if _, err := service.GetGroupStats(context.Background(), &group.GetGroupStatsReq{ GroupID: "d7a4ed4b-1c91-4cdb-8af8-5520fa8d0ce0", }); err == nil { t.Fatal("GetGroupStats() error = nil, want missing dependency") @@ -149,7 +149,7 @@ func TestRemoteAwareGroupServiceRequiresOrchestrator(t *testing.T) { func TestRemoteAwareGroupServiceUsesOrchestratorClient(t *testing.T) { service := remoteAwareGroupService{orchestrator: &orchestratorGroupClientStub{enabled: true}} - resp, err := service.GetGroupStats(context.Background(), &groupmodule.GetGroupStatsReq{ + resp, err := service.GetGroupStats(context.Background(), &group.GetGroupStatsReq{ GroupID: "d7a4ed4b-1c91-4cdb-8af8-5520fa8d0ce0", }) if err != nil { diff --git a/src/app/gateway/rbac_services.go b/src/app/gateway/rbac_services.go index 765603e5..158a6600 100644 --- a/src/app/gateway/rbac_services.go +++ b/src/app/gateway/rbac_services.go @@ -1,36 +1,36 @@ -package gatewayapp +package gateway import ( "context" "aegis/dto" - rbacmodule "aegis/module/rbac" + rbac "aegis/module/rbac" ) type rbacIAMClient interface { Enabled() bool - CreateRole(context.Context, *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) + CreateRole(context.Context, *rbac.CreateRoleReq) (*rbac.RoleResp, error) DeleteRole(context.Context, int) error - GetRole(context.Context, int) (*rbacmodule.RoleDetailResp, error) - ListRoles(context.Context, *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) - UpdateRole(context.Context, *rbacmodule.UpdateRoleReq, int) (*rbacmodule.RoleResp, error) + GetRole(context.Context, int) (*rbac.RoleDetailResp, error) + ListRoles(context.Context, *rbac.ListRoleReq) (*dto.ListResp[rbac.RoleResp], error) + UpdateRole(context.Context, *rbac.UpdateRoleReq, int) (*rbac.RoleResp, error) AssignRolePermissions(context.Context, int, []int) error RemoveRolePermissions(context.Context, int, []int) error - ListUsersFromRole(context.Context, int) ([]rbacmodule.UserListItem, error) - GetPermission(context.Context, int) (*rbacmodule.PermissionDetailResp, error) - ListPermissions(context.Context, *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) - ListRolesFromPermission(context.Context, int) ([]rbacmodule.RoleResp, error) - GetResource(context.Context, int) (*rbacmodule.ResourceResp, error) - ListResources(context.Context, *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) - ListResourcePermissions(context.Context, int) ([]rbacmodule.PermissionResp, error) + ListUsersFromRole(context.Context, int) ([]rbac.UserListItem, error) + GetPermission(context.Context, int) (*rbac.PermissionDetailResp, error) + ListPermissions(context.Context, *rbac.ListPermissionReq) (*dto.ListResp[rbac.PermissionResp], error) + ListRolesFromPermission(context.Context, int) ([]rbac.RoleResp, error) + GetResource(context.Context, int) (*rbac.ResourceResp, error) + ListResources(context.Context, *rbac.ListResourceReq) (*dto.ListResp[rbac.ResourceResp], error) + ListResourcePermissions(context.Context, int) ([]rbac.PermissionResp, error) } type remoteAwareRBACService struct { - rbacmodule.HandlerService + rbac.HandlerService iam rbacIAMClient } -func (s remoteAwareRBACService) CreateRole(ctx context.Context, req *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) { +func (s remoteAwareRBACService) CreateRole(ctx context.Context, req *rbac.CreateRoleReq) (*rbac.RoleResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.CreateRole(ctx, req) } @@ -44,21 +44,21 @@ func (s remoteAwareRBACService) DeleteRole(ctx context.Context, roleID int) erro return missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) GetRole(ctx context.Context, roleID int) (*rbacmodule.RoleDetailResp, error) { +func (s remoteAwareRBACService) GetRole(ctx context.Context, roleID int) (*rbac.RoleDetailResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetRole(ctx, roleID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListRoles(ctx context.Context, req *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) { +func (s remoteAwareRBACService) ListRoles(ctx context.Context, req *rbac.ListRoleReq) (*dto.ListResp[rbac.RoleResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListRoles(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) UpdateRole(ctx context.Context, req *rbacmodule.UpdateRoleReq, roleID int) (*rbacmodule.RoleResp, error) { +func (s remoteAwareRBACService) UpdateRole(ctx context.Context, req *rbac.UpdateRoleReq, roleID int) (*rbac.RoleResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.UpdateRole(ctx, req, roleID) } @@ -79,49 +79,49 @@ func (s remoteAwareRBACService) RemoveRolePermissions(ctx context.Context, permi return missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListUsersFromRole(ctx context.Context, roleID int) ([]rbacmodule.UserListItem, error) { +func (s remoteAwareRBACService) ListUsersFromRole(ctx context.Context, roleID int) ([]rbac.UserListItem, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListUsersFromRole(ctx, roleID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) GetPermission(ctx context.Context, permissionID int) (*rbacmodule.PermissionDetailResp, error) { +func (s remoteAwareRBACService) GetPermission(ctx context.Context, permissionID int) (*rbac.PermissionDetailResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetPermission(ctx, permissionID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListPermissions(ctx context.Context, req *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) { +func (s remoteAwareRBACService) ListPermissions(ctx context.Context, req *rbac.ListPermissionReq) (*dto.ListResp[rbac.PermissionResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListPermissions(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbacmodule.RoleResp, error) { +func (s remoteAwareRBACService) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbac.RoleResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListRolesFromPermission(ctx, permissionID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) GetResource(ctx context.Context, resourceID int) (*rbacmodule.ResourceResp, error) { +func (s remoteAwareRBACService) GetResource(ctx context.Context, resourceID int) (*rbac.ResourceResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetResource(ctx, resourceID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListResources(ctx context.Context, req *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) { +func (s remoteAwareRBACService) ListResources(ctx context.Context, req *rbac.ListResourceReq) (*dto.ListResp[rbac.ResourceResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListResources(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareRBACService) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbacmodule.PermissionResp, error) { +func (s remoteAwareRBACService) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbac.PermissionResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListResourcePermissions(ctx, resourceID) } diff --git a/src/app/gateway/remote_required.go b/src/app/gateway/remote_required.go index 5d3c8fc4..cf4ca55a 100644 --- a/src/app/gateway/remote_required.go +++ b/src/app/gateway/remote_required.go @@ -1,4 +1,4 @@ -package gatewayapp +package gateway import "fmt" diff --git a/src/app/gateway/resource_services.go b/src/app/gateway/resource_services.go index 7d6d13e8..344f4503 100644 --- a/src/app/gateway/resource_services.go +++ b/src/app/gateway/resource_services.go @@ -1,31 +1,31 @@ -package gatewayapp +package gateway import ( "context" "aegis/dto" "aegis/internalclient/resourceclient" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - labelmodule "aegis/module/label" - projectmodule "aegis/module/project" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + label "aegis/module/label" + project "aegis/module/project" ) type remoteAwareProjectService struct { - projectmodule.HandlerService + project.HandlerService resource *resourceclient.Client } -func (s remoteAwareProjectService) GetProjectDetail(ctx context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { +func (s remoteAwareProjectService) GetProjectDetail(ctx context.Context, projectID int) (*project.ProjectDetailResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetProject(ctx, projectID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareProjectService) ListProjects(ctx context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { +func (s remoteAwareProjectService) ListProjects(ctx context.Context, req *project.ListProjectReq) (*dto.ListResp[project.ProjectResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListProjects(ctx, req) } @@ -33,18 +33,18 @@ func (s remoteAwareProjectService) ListProjects(ctx context.Context, req *projec } type remoteAwareContainerService struct { - containermodule.HandlerService + container.HandlerService resource *resourceclient.Client } -func (s remoteAwareContainerService) GetContainer(ctx context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { +func (s remoteAwareContainerService) GetContainer(ctx context.Context, containerID int) (*container.ContainerDetailResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetContainer(ctx, containerID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareContainerService) ListContainers(ctx context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { +func (s remoteAwareContainerService) ListContainers(ctx context.Context, req *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListContainers(ctx, req) } @@ -52,18 +52,18 @@ func (s remoteAwareContainerService) ListContainers(ctx context.Context, req *co } type remoteAwareDatasetService struct { - datasetmodule.HandlerService + dataset.HandlerService resource *resourceclient.Client } -func (s remoteAwareDatasetService) GetDataset(ctx context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { +func (s remoteAwareDatasetService) GetDataset(ctx context.Context, datasetID int) (*dataset.DatasetDetailResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetDataset(ctx, datasetID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareDatasetService) ListDatasets(ctx context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { +func (s remoteAwareDatasetService) ListDatasets(ctx context.Context, req *dataset.ListDatasetReq) (*dto.ListResp[dataset.DatasetResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListDatasets(ctx, req) } @@ -71,32 +71,32 @@ func (s remoteAwareDatasetService) ListDatasets(ctx context.Context, req *datase } type remoteAwareEvaluationService struct { - evaluationmodule.HandlerService + evaluation.HandlerService resource *resourceclient.Client } -func (s remoteAwareEvaluationService) ListDatapackEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { +func (s remoteAwareEvaluationService) ListDatapackEvaluationResults(ctx context.Context, req *evaluation.BatchEvaluateDatapackReq, userID int) (*evaluation.BatchEvaluateDatapackResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListDatapackEvaluationResults(ctx, req, userID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareEvaluationService) ListDatasetEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { +func (s remoteAwareEvaluationService) ListDatasetEvaluationResults(ctx context.Context, req *evaluation.BatchEvaluateDatasetReq, userID int) (*evaluation.BatchEvaluateDatasetResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListDatasetEvaluationResults(ctx, req, userID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareEvaluationService) ListEvaluations(ctx context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { +func (s remoteAwareEvaluationService) ListEvaluations(ctx context.Context, req *evaluation.ListEvaluationReq) (*dto.ListResp[evaluation.EvaluationResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListEvaluations(ctx, req) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareEvaluationService) GetEvaluation(ctx context.Context, evaluationID int) (*evaluationmodule.EvaluationResp, error) { +func (s remoteAwareEvaluationService) GetEvaluation(ctx context.Context, evaluationID int) (*evaluation.EvaluationResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetEvaluation(ctx, evaluationID) } @@ -111,18 +111,18 @@ func (s remoteAwareEvaluationService) DeleteEvaluation(ctx context.Context, eval } type remoteAwareLabelService struct { - labelmodule.HandlerService + label.HandlerService resource labelResourceClient } type labelResourceClient interface { Enabled() bool BatchDeleteLabels(context.Context, []int) error - CreateLabel(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) + CreateLabel(context.Context, *label.CreateLabelReq) (*label.LabelResp, error) DeleteLabel(context.Context, int) error - GetLabel(context.Context, int) (*labelmodule.LabelDetailResp, error) - ListLabels(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) - UpdateLabel(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) + GetLabel(context.Context, int) (*label.LabelDetailResp, error) + ListLabels(context.Context, *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) + UpdateLabel(context.Context, *label.UpdateLabelReq, int) (*label.LabelResp, error) } func (s remoteAwareLabelService) BatchDelete(ctx context.Context, ids []int) error { @@ -132,7 +132,7 @@ func (s remoteAwareLabelService) BatchDelete(ctx context.Context, ids []int) err return missingRemoteDependency("resource-service") } -func (s remoteAwareLabelService) Create(ctx context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { +func (s remoteAwareLabelService) Create(ctx context.Context, req *label.CreateLabelReq) (*label.LabelResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.CreateLabel(ctx, req) } @@ -146,21 +146,21 @@ func (s remoteAwareLabelService) Delete(ctx context.Context, labelID int) error return missingRemoteDependency("resource-service") } -func (s remoteAwareLabelService) GetDetail(ctx context.Context, labelID int) (*labelmodule.LabelDetailResp, error) { +func (s remoteAwareLabelService) GetDetail(ctx context.Context, labelID int) (*label.LabelDetailResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetLabel(ctx, labelID) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareLabelService) List(ctx context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { +func (s remoteAwareLabelService) List(ctx context.Context, req *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListLabels(ctx, req) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareLabelService) Update(ctx context.Context, req *labelmodule.UpdateLabelReq, labelID int) (*labelmodule.LabelResp, error) { +func (s remoteAwareLabelService) Update(ctx context.Context, req *label.UpdateLabelReq, labelID int) (*label.LabelResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.UpdateLabel(ctx, req, labelID) } @@ -168,43 +168,43 @@ func (s remoteAwareLabelService) Update(ctx context.Context, req *labelmodule.Up } type remoteAwareChaosSystemService struct { - chaossystemmodule.HandlerService + chaossystem.HandlerService resource chaosSystemResourceClient } type chaosSystemResourceClient interface { Enabled() bool - ListChaosSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) - GetChaosSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) - CreateChaosSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) - UpdateChaosSystem(context.Context, *chaossystemmodule.UpdateChaosSystemReq, int) (*chaossystemmodule.ChaosSystemResp, error) + ListChaosSystems(context.Context, *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) + GetChaosSystem(context.Context, int) (*chaossystem.ChaosSystemResp, error) + CreateChaosSystem(context.Context, *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) + UpdateChaosSystem(context.Context, *chaossystem.UpdateChaosSystemReq, int) (*chaossystem.ChaosSystemResp, error) DeleteChaosSystem(context.Context, int) error - UpsertChaosSystemMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error - ListChaosSystemMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) + UpsertChaosSystemMetadata(context.Context, int, *chaossystem.BulkUpsertSystemMetadataReq) error + ListChaosSystemMetadata(context.Context, int, string) ([]chaossystem.SystemMetadataResp, error) } -func (s remoteAwareChaosSystemService) ListSystems(ctx context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { +func (s remoteAwareChaosSystemService) ListSystems(ctx context.Context, req *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListChaosSystems(ctx, req) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareChaosSystemService) GetSystem(ctx context.Context, id int) (*chaossystemmodule.ChaosSystemResp, error) { +func (s remoteAwareChaosSystemService) GetSystem(ctx context.Context, id int) (*chaossystem.ChaosSystemResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.GetChaosSystem(ctx, id) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareChaosSystemService) CreateSystem(ctx context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { +func (s remoteAwareChaosSystemService) CreateSystem(ctx context.Context, req *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.CreateChaosSystem(ctx, req) } return nil, missingRemoteDependency("resource-service") } -func (s remoteAwareChaosSystemService) UpdateSystem(ctx context.Context, id int, req *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { +func (s remoteAwareChaosSystemService) UpdateSystem(ctx context.Context, id int, req *chaossystem.UpdateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.UpdateChaosSystem(ctx, req, id) } @@ -218,14 +218,14 @@ func (s remoteAwareChaosSystemService) DeleteSystem(ctx context.Context, id int) return missingRemoteDependency("resource-service") } -func (s remoteAwareChaosSystemService) UpsertMetadata(ctx context.Context, id int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { +func (s remoteAwareChaosSystemService) UpsertMetadata(ctx context.Context, id int, req *chaossystem.BulkUpsertSystemMetadataReq) error { if s.resource != nil && s.resource.Enabled() { return s.resource.UpsertChaosSystemMetadata(ctx, id, req) } return missingRemoteDependency("resource-service") } -func (s remoteAwareChaosSystemService) ListMetadata(ctx context.Context, id int, metadataType string) ([]chaossystemmodule.SystemMetadataResp, error) { +func (s remoteAwareChaosSystemService) ListMetadata(ctx context.Context, id int, metadataType string) ([]chaossystem.SystemMetadataResp, error) { if s.resource != nil && s.resource.Enabled() { return s.resource.ListChaosSystemMetadata(ctx, id, metadataType) } diff --git a/src/app/gateway/resource_services_test.go b/src/app/gateway/resource_services_test.go index b0c49fa8..2c4947e7 100644 --- a/src/app/gateway/resource_services_test.go +++ b/src/app/gateway/resource_services_test.go @@ -1,12 +1,12 @@ -package gatewayapp +package gateway import ( "context" "testing" "aegis/dto" - chaossystemmodule "aegis/module/chaossystem" - labelmodule "aegis/module/label" + chaossystem "aegis/module/chaossystem" + label "aegis/module/label" ) type resourceLabelClientStub struct { @@ -15,20 +15,20 @@ type resourceLabelClientStub struct { func (s *resourceLabelClientStub) Enabled() bool { return s.enabled } -func (s *resourceLabelClientStub) CreateLabel(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { - return &labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil +func (s *resourceLabelClientStub) CreateLabel(context.Context, *label.CreateLabelReq) (*label.LabelResp, error) { + return &label.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil } -func (s *resourceLabelClientStub) GetLabel(context.Context, int) (*labelmodule.LabelDetailResp, error) { - return &labelmodule.LabelDetailResp{LabelResp: labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}}, nil +func (s *resourceLabelClientStub) GetLabel(context.Context, int) (*label.LabelDetailResp, error) { + return &label.LabelDetailResp{LabelResp: label.LabelResp{ID: 3, Key: "env", Value: "prod"}}, nil } -func (s *resourceLabelClientStub) ListLabels(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { - return &dto.ListResp[labelmodule.LabelResp]{Items: []labelmodule.LabelResp{{ID: 3, Key: "env", Value: "prod"}}}, nil +func (s *resourceLabelClientStub) ListLabels(context.Context, *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) { + return &dto.ListResp[label.LabelResp]{Items: []label.LabelResp{{ID: 3, Key: "env", Value: "prod"}}}, nil } -func (s *resourceLabelClientStub) UpdateLabel(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) { - return &labelmodule.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil +func (s *resourceLabelClientStub) UpdateLabel(context.Context, *label.UpdateLabelReq, int) (*label.LabelResp, error) { + return &label.LabelResp{ID: 3, Key: "env", Value: "prod"}, nil } func (s *resourceLabelClientStub) DeleteLabel(context.Context, int) error { return nil } @@ -37,7 +37,7 @@ func (s *resourceLabelClientStub) BatchDeleteLabels(context.Context, []int) erro func TestRemoteAwareLabelServiceRequiresResource(t *testing.T) { service := remoteAwareLabelService{} - if _, err := service.List(context.Background(), &labelmodule.ListLabelReq{}); err == nil { + if _, err := service.List(context.Background(), &label.ListLabelReq{}); err == nil { t.Fatal("List() error = nil, want missing dependency") } } @@ -59,35 +59,35 @@ type resourceChaosSystemClientStub struct { func (s *resourceChaosSystemClientStub) Enabled() bool { return s.enabled } -func (s *resourceChaosSystemClientStub) ListChaosSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { - return &dto.ListResp[chaossystemmodule.ChaosSystemResp]{Items: []chaossystemmodule.ChaosSystemResp{{ID: 8, Name: "k8s"}}}, nil +func (s *resourceChaosSystemClientStub) ListChaosSystems(context.Context, *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) { + return &dto.ListResp[chaossystem.ChaosSystemResp]{Items: []chaossystem.ChaosSystemResp{{ID: 8, Name: "k8s"}}}, nil } -func (s *resourceChaosSystemClientStub) GetChaosSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) { - return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +func (s *resourceChaosSystemClientStub) GetChaosSystem(context.Context, int) (*chaossystem.ChaosSystemResp, error) { + return &chaossystem.ChaosSystemResp{ID: 8, Name: "k8s"}, nil } -func (s *resourceChaosSystemClientStub) CreateChaosSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { - return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +func (s *resourceChaosSystemClientStub) CreateChaosSystem(context.Context, *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { + return &chaossystem.ChaosSystemResp{ID: 8, Name: "k8s"}, nil } -func (s *resourceChaosSystemClientStub) UpdateChaosSystem(context.Context, *chaossystemmodule.UpdateChaosSystemReq, int) (*chaossystemmodule.ChaosSystemResp, error) { - return &chaossystemmodule.ChaosSystemResp{ID: 8, Name: "k8s"}, nil +func (s *resourceChaosSystemClientStub) UpdateChaosSystem(context.Context, *chaossystem.UpdateChaosSystemReq, int) (*chaossystem.ChaosSystemResp, error) { + return &chaossystem.ChaosSystemResp{ID: 8, Name: "k8s"}, nil } func (s *resourceChaosSystemClientStub) DeleteChaosSystem(context.Context, int) error { return nil } -func (s *resourceChaosSystemClientStub) UpsertChaosSystemMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error { +func (s *resourceChaosSystemClientStub) UpsertChaosSystemMetadata(context.Context, int, *chaossystem.BulkUpsertSystemMetadataReq) error { return nil } -func (s *resourceChaosSystemClientStub) ListChaosSystemMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) { - return []chaossystemmodule.SystemMetadataResp{{ID: 1, SystemName: "k8s"}}, nil +func (s *resourceChaosSystemClientStub) ListChaosSystemMetadata(context.Context, int, string) ([]chaossystem.SystemMetadataResp, error) { + return []chaossystem.SystemMetadataResp{{ID: 1, SystemName: "k8s"}}, nil } func TestRemoteAwareChaosSystemServiceRequiresResource(t *testing.T) { service := remoteAwareChaosSystemService{} - if _, err := service.ListSystems(context.Background(), &chaossystemmodule.ListChaosSystemReq{}); err == nil { + if _, err := service.ListSystems(context.Background(), &chaossystem.ListChaosSystemReq{}); err == nil { t.Fatal("ListSystems() error = nil, want missing dependency") } } diff --git a/src/app/gateway/system_services.go b/src/app/gateway/system_services.go index 0d5ebc45..b6d31a65 100644 --- a/src/app/gateway/system_services.go +++ b/src/app/gateway/system_services.go @@ -1,76 +1,76 @@ -package gatewayapp +package gateway import ( "context" "aegis/dto" "aegis/internalclient/systemclient" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" ) type remoteAwareSystemService struct { - systemmodule.HandlerService + system.HandlerService system *systemclient.Client } -func (s remoteAwareSystemService) GetHealth(ctx context.Context) (*systemmodule.HealthCheckResp, error) { +func (s remoteAwareSystemService) GetHealth(ctx context.Context) (*system.HealthCheckResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetHealth(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) GetMetrics(ctx context.Context) (*systemmodule.MonitoringMetricsResp, error) { +func (s remoteAwareSystemService) GetMetrics(ctx context.Context) (*system.MonitoringMetricsResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetMetrics(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) GetSystemInfo(ctx context.Context) (*systemmodule.SystemInfo, error) { +func (s remoteAwareSystemService) GetSystemInfo(ctx context.Context) (*system.SystemInfo, error) { if s.system != nil && s.system.Enabled() { return s.system.GetSystemInfo(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) ListNamespaceLocks(ctx context.Context) (*systemmodule.ListNamespaceLockResp, error) { +func (s remoteAwareSystemService) ListNamespaceLocks(ctx context.Context) (*system.ListNamespaceLockResp, error) { if s.system != nil && s.system.Enabled() { return s.system.ListNamespaceLocks(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) ListQueuedTasks(ctx context.Context) (*systemmodule.QueuedTasksResp, error) { +func (s remoteAwareSystemService) ListQueuedTasks(ctx context.Context) (*system.QueuedTasksResp, error) { if s.system != nil && s.system.Enabled() { return s.system.ListQueuedTasks(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) GetAuditLog(ctx context.Context, id int) (*systemmodule.AuditLogDetailResp, error) { +func (s remoteAwareSystemService) GetAuditLog(ctx context.Context, id int) (*system.AuditLogDetailResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetAuditLog(ctx, id) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) ListAuditLogs(ctx context.Context, req *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { +func (s remoteAwareSystemService) ListAuditLogs(ctx context.Context, req *system.ListAuditLogReq) (*dto.ListResp[system.AuditLogResp], error) { if s.system != nil && s.system.Enabled() { return s.system.ListAuditLogs(ctx, req) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) GetConfig(ctx context.Context, configID int) (*systemmodule.ConfigDetailResp, error) { +func (s remoteAwareSystemService) GetConfig(ctx context.Context, configID int) (*system.ConfigDetailResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetConfig(ctx, configID) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemService) ListConfigs(ctx context.Context, req *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { +func (s remoteAwareSystemService) ListConfigs(ctx context.Context, req *system.ListConfigReq) (*dto.ListResp[system.ConfigResp], error) { if s.system != nil && s.system.Enabled() { return s.system.ListConfigs(ctx, req) } @@ -78,18 +78,18 @@ func (s remoteAwareSystemService) ListConfigs(ctx context.Context, req *systemmo } type remoteAwareSystemMetricService struct { - systemmetricmodule.HandlerService + systemmetric.HandlerService system *systemclient.Client } -func (s remoteAwareSystemMetricService) GetSystemMetrics(ctx context.Context) (*systemmetricmodule.SystemMetricsResp, error) { +func (s remoteAwareSystemMetricService) GetSystemMetrics(ctx context.Context) (*systemmetric.SystemMetricsResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetSystemMetrics(ctx) } return nil, missingRemoteDependency("system-service") } -func (s remoteAwareSystemMetricService) GetSystemMetricsHistory(ctx context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { +func (s remoteAwareSystemMetricService) GetSystemMetricsHistory(ctx context.Context) (*systemmetric.SystemMetricsHistoryResp, error) { if s.system != nil && s.system.Enabled() { return s.system.GetSystemMetricsHistory(ctx) } diff --git a/src/app/gateway/team_services.go b/src/app/gateway/team_services.go index e586df54..1174e371 100644 --- a/src/app/gateway/team_services.go +++ b/src/app/gateway/team_services.go @@ -1,32 +1,32 @@ -package gatewayapp +package gateway import ( "context" "aegis/dto" - teammodule "aegis/module/team" + team "aegis/module/team" ) type teamIAMClient interface { Enabled() bool - CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) + CreateTeam(context.Context, *team.CreateTeamReq, int) (*team.TeamResp, error) DeleteTeam(context.Context, int) error - GetTeam(context.Context, int) (*teammodule.TeamDetailResp, error) - ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) - UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) - ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) - AddTeamMember(context.Context, *teammodule.AddTeamMemberReq, int) error + GetTeam(context.Context, int) (*team.TeamDetailResp, error) + ListTeams(context.Context, *team.ListTeamReq, int, bool) (*dto.ListResp[team.TeamResp], error) + UpdateTeam(context.Context, *team.UpdateTeamReq, int) (*team.TeamResp, error) + ListTeamProjects(context.Context, *team.TeamProjectListReq, int) (*dto.ListResp[team.TeamProjectItem], error) + AddTeamMember(context.Context, *team.AddTeamMemberReq, int) error RemoveTeamMember(context.Context, int, int, int) error - UpdateTeamMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error - ListTeamMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) + UpdateTeamMemberRole(context.Context, *team.UpdateTeamMemberRoleReq, int, int, int) error + ListTeamMembers(context.Context, *team.ListTeamMemberReq, int) (*dto.ListResp[team.TeamMemberResp], error) } type remoteAwareTeamService struct { - teammodule.HandlerService + team.HandlerService iam teamIAMClient } -func (s remoteAwareTeamService) CreateTeam(ctx context.Context, req *teammodule.CreateTeamReq, userID int) (*teammodule.TeamResp, error) { +func (s remoteAwareTeamService) CreateTeam(ctx context.Context, req *team.CreateTeamReq, userID int) (*team.TeamResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.CreateTeam(ctx, req, userID) } @@ -40,35 +40,35 @@ func (s remoteAwareTeamService) DeleteTeam(ctx context.Context, teamID int) erro return missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) GetTeamDetail(ctx context.Context, teamID int) (*teammodule.TeamDetailResp, error) { +func (s remoteAwareTeamService) GetTeamDetail(ctx context.Context, teamID int) (*team.TeamDetailResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetTeam(ctx, teamID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) ListTeams(ctx context.Context, req *teammodule.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[teammodule.TeamResp], error) { +func (s remoteAwareTeamService) ListTeams(ctx context.Context, req *team.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[team.TeamResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListTeams(ctx, req, userID, isAdmin) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) UpdateTeam(ctx context.Context, req *teammodule.UpdateTeamReq, teamID int) (*teammodule.TeamResp, error) { +func (s remoteAwareTeamService) UpdateTeam(ctx context.Context, req *team.UpdateTeamReq, teamID int) (*team.TeamResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.UpdateTeam(ctx, req, teamID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) ListTeamProjects(ctx context.Context, req *teammodule.TeamProjectListReq, teamID int) (*dto.ListResp[teammodule.TeamProjectItem], error) { +func (s remoteAwareTeamService) ListTeamProjects(ctx context.Context, req *team.TeamProjectListReq, teamID int) (*dto.ListResp[team.TeamProjectItem], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListTeamProjects(ctx, req, teamID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) AddMember(ctx context.Context, req *teammodule.AddTeamMemberReq, teamID int) error { +func (s remoteAwareTeamService) AddMember(ctx context.Context, req *team.AddTeamMemberReq, teamID int) error { if s.iam != nil && s.iam.Enabled() { return s.iam.AddTeamMember(ctx, req, teamID) } @@ -82,14 +82,14 @@ func (s remoteAwareTeamService) RemoveMember(ctx context.Context, teamID, curren return missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) UpdateMemberRole(ctx context.Context, req *teammodule.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { +func (s remoteAwareTeamService) UpdateMemberRole(ctx context.Context, req *team.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { if s.iam != nil && s.iam.Enabled() { return s.iam.UpdateTeamMemberRole(ctx, req, teamID, targetUserID, currentUserID) } return missingRemoteDependency("iam-service") } -func (s remoteAwareTeamService) ListMembers(ctx context.Context, req *teammodule.ListTeamMemberReq, teamID int) (*dto.ListResp[teammodule.TeamMemberResp], error) { +func (s remoteAwareTeamService) ListMembers(ctx context.Context, req *team.ListTeamMemberReq, teamID int) (*dto.ListResp[team.TeamMemberResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListTeamMembers(ctx, req, teamID) } diff --git a/src/app/gateway/team_services_test.go b/src/app/gateway/team_services_test.go index 2a09a1c6..07201210 100644 --- a/src/app/gateway/team_services_test.go +++ b/src/app/gateway/team_services_test.go @@ -1,11 +1,11 @@ -package gatewayapp +package gateway import ( "context" "testing" "aegis/dto" - teammodule "aegis/module/team" + team "aegis/module/team" ) type iamTeamClientStub struct { @@ -14,36 +14,36 @@ type iamTeamClientStub struct { func (s *iamTeamClientStub) Enabled() bool { return s.enabled } -func (s *iamTeamClientStub) CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) { - return &teammodule.TeamResp{ID: 1, Name: "core"}, nil +func (s *iamTeamClientStub) CreateTeam(context.Context, *team.CreateTeamReq, int) (*team.TeamResp, error) { + return &team.TeamResp{ID: 1, Name: "core"}, nil } func (s *iamTeamClientStub) DeleteTeam(context.Context, int) error { return nil } -func (s *iamTeamClientStub) GetTeam(context.Context, int) (*teammodule.TeamDetailResp, error) { - return &teammodule.TeamDetailResp{TeamResp: teammodule.TeamResp{ID: 1, Name: "core"}}, nil +func (s *iamTeamClientStub) GetTeam(context.Context, int) (*team.TeamDetailResp, error) { + return &team.TeamDetailResp{TeamResp: team.TeamResp{ID: 1, Name: "core"}}, nil } -func (s *iamTeamClientStub) ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) { - return &dto.ListResp[teammodule.TeamResp]{Items: []teammodule.TeamResp{{ID: 1, Name: "core"}}}, nil +func (s *iamTeamClientStub) ListTeams(context.Context, *team.ListTeamReq, int, bool) (*dto.ListResp[team.TeamResp], error) { + return &dto.ListResp[team.TeamResp]{Items: []team.TeamResp{{ID: 1, Name: "core"}}}, nil } -func (s *iamTeamClientStub) UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) { - return &teammodule.TeamResp{ID: 1, Name: "core"}, nil +func (s *iamTeamClientStub) UpdateTeam(context.Context, *team.UpdateTeamReq, int) (*team.TeamResp, error) { + return &team.TeamResp{ID: 1, Name: "core"}, nil } -func (s *iamTeamClientStub) ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) { - return &dto.ListResp[teammodule.TeamProjectItem]{}, nil +func (s *iamTeamClientStub) ListTeamProjects(context.Context, *team.TeamProjectListReq, int) (*dto.ListResp[team.TeamProjectItem], error) { + return &dto.ListResp[team.TeamProjectItem]{}, nil } -func (s *iamTeamClientStub) AddTeamMember(context.Context, *teammodule.AddTeamMemberReq, int) error { +func (s *iamTeamClientStub) AddTeamMember(context.Context, *team.AddTeamMemberReq, int) error { return nil } func (s *iamTeamClientStub) RemoveTeamMember(context.Context, int, int, int) error { return nil } -func (s *iamTeamClientStub) UpdateTeamMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error { +func (s *iamTeamClientStub) UpdateTeamMemberRole(context.Context, *team.UpdateTeamMemberRoleReq, int, int, int) error { return nil } -func (s *iamTeamClientStub) ListTeamMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) { - return &dto.ListResp[teammodule.TeamMemberResp]{}, nil +func (s *iamTeamClientStub) ListTeamMembers(context.Context, *team.ListTeamMemberReq, int) (*dto.ListResp[team.TeamMemberResp], error) { + return &dto.ListResp[team.TeamMemberResp]{}, nil } func TestRemoteAwareTeamServiceRequiresIAM(t *testing.T) { service := remoteAwareTeamService{} - if _, err := service.ListTeams(context.Background(), &teammodule.ListTeamReq{}, 7, true); err == nil { + if _, err := service.ListTeams(context.Background(), &team.ListTeamReq{}, 7, true); err == nil { t.Fatal("ListTeams() error = nil, want missing dependency") } } diff --git a/src/app/gateway/user_services.go b/src/app/gateway/user_services.go index 3610f4c4..10712452 100644 --- a/src/app/gateway/user_services.go +++ b/src/app/gateway/user_services.go @@ -1,23 +1,23 @@ -package gatewayapp +package gateway import ( "context" "aegis/dto" - usermodule "aegis/module/user" + user "aegis/module/user" ) type userIAMClient interface { Enabled() bool - CreateUser(context.Context, *usermodule.CreateUserReq) (*usermodule.UserResp, error) + CreateUser(context.Context, *user.CreateUserReq) (*user.UserResp, error) DeleteUser(context.Context, int) error - GetUser(context.Context, int) (*usermodule.UserDetailResp, error) - ListUsers(context.Context, *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) - UpdateUser(context.Context, *usermodule.UpdateUserReq, int) (*usermodule.UserResp, error) + GetUser(context.Context, int) (*user.UserDetailResp, error) + ListUsers(context.Context, *user.ListUserReq) (*dto.ListResp[user.UserResp], error) + UpdateUser(context.Context, *user.UpdateUserReq, int) (*user.UserResp, error) AssignUserRole(context.Context, int, int) error RemoveUserRole(context.Context, int, int) error - AssignUserPermissions(context.Context, int, *usermodule.AssignUserPermissionReq) error - RemoveUserPermissions(context.Context, int, *usermodule.RemoveUserPermissionReq) error + AssignUserPermissions(context.Context, int, *user.AssignUserPermissionReq) error + RemoveUserPermissions(context.Context, int, *user.RemoveUserPermissionReq) error AssignUserContainer(context.Context, int, int, int) error RemoveUserContainer(context.Context, int, int) error AssignUserDataset(context.Context, int, int, int) error @@ -27,11 +27,11 @@ type userIAMClient interface { } type remoteAwareUserService struct { - usermodule.HandlerService + user.HandlerService iam userIAMClient } -func (s remoteAwareUserService) CreateUser(ctx context.Context, req *usermodule.CreateUserReq) (*usermodule.UserResp, error) { +func (s remoteAwareUserService) CreateUser(ctx context.Context, req *user.CreateUserReq) (*user.UserResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.CreateUser(ctx, req) } @@ -45,21 +45,21 @@ func (s remoteAwareUserService) DeleteUser(ctx context.Context, userID int) erro return missingRemoteDependency("iam-service") } -func (s remoteAwareUserService) GetUserDetail(ctx context.Context, userID int) (*usermodule.UserDetailResp, error) { +func (s remoteAwareUserService) GetUserDetail(ctx context.Context, userID int) (*user.UserDetailResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.GetUser(ctx, userID) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareUserService) ListUsers(ctx context.Context, req *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) { +func (s remoteAwareUserService) ListUsers(ctx context.Context, req *user.ListUserReq) (*dto.ListResp[user.UserResp], error) { if s.iam != nil && s.iam.Enabled() { return s.iam.ListUsers(ctx, req) } return nil, missingRemoteDependency("iam-service") } -func (s remoteAwareUserService) UpdateUser(ctx context.Context, req *usermodule.UpdateUserReq, userID int) (*usermodule.UserResp, error) { +func (s remoteAwareUserService) UpdateUser(ctx context.Context, req *user.UpdateUserReq, userID int) (*user.UserResp, error) { if s.iam != nil && s.iam.Enabled() { return s.iam.UpdateUser(ctx, req, userID) } @@ -80,14 +80,14 @@ func (s remoteAwareUserService) RemoveRole(ctx context.Context, userID, roleID i return missingRemoteDependency("iam-service") } -func (s remoteAwareUserService) AssignPermissions(ctx context.Context, req *usermodule.AssignUserPermissionReq, userID int) error { +func (s remoteAwareUserService) AssignPermissions(ctx context.Context, req *user.AssignUserPermissionReq, userID int) error { if s.iam != nil && s.iam.Enabled() { return s.iam.AssignUserPermissions(ctx, userID, req) } return missingRemoteDependency("iam-service") } -func (s remoteAwareUserService) RemovePermissions(ctx context.Context, req *usermodule.RemoveUserPermissionReq, userID int) error { +func (s remoteAwareUserService) RemovePermissions(ctx context.Context, req *user.RemoveUserPermissionReq, userID int) error { if s.iam != nil && s.iam.Enabled() { return s.iam.RemoveUserPermissions(ctx, userID, req) } diff --git a/src/app/http_modules.go b/src/app/http_modules.go index 91984304..718b0b6c 100644 --- a/src/app/http_modules.go +++ b/src/app/http_modules.go @@ -1,26 +1,26 @@ package app import ( - authmodule "aegis/module/auth" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - labelmodule "aegis/module/label" - metricmodule "aegis/module/metric" - notificationmodule "aegis/module/notification" - projectmodule "aegis/module/project" - rbacmodule "aegis/module/rbac" - sdkmodule "aegis/module/sdk" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" - teammodule "aegis/module/team" - tracemodule "aegis/module/trace" - usermodule "aegis/module/user" + auth "aegis/module/auth" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + label "aegis/module/label" + metric "aegis/module/metric" + notification "aegis/module/notification" + project "aegis/module/project" + rbac "aegis/module/rbac" + sdk "aegis/module/sdk" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" + team "aegis/module/team" + trace "aegis/module/trace" + user "aegis/module/user" "aegis/router" "go.uber.org/fx" @@ -28,32 +28,32 @@ import ( func ExecutionInjectionOwnerModules() fx.Option { return fx.Options( - executionmodule.Module, - injectionmodule.Module, + execution.Module, + injection.Module, ) } func ProducerHTTPModules() fx.Option { return fx.Options( - authmodule.Module, - chaossystemmodule.Module, - containermodule.Module, - datasetmodule.Module, - evaluationmodule.Module, + auth.Module, + chaossystem.Module, + container.Module, + dataset.Module, + evaluation.Module, ExecutionInjectionOwnerModules(), - groupmodule.Module, - labelmodule.Module, - metricmodule.Module, - notificationmodule.Module, - projectmodule.Module, - rbacmodule.Module, - sdkmodule.Module, - systemmodule.Module, - systemmetricmodule.Module, - taskmodule.Module, - teammodule.Module, - tracemodule.Module, - usermodule.Module, + group.Module, + label.Module, + metric.Module, + notification.Module, + project.Module, + rbac.Module, + sdk.Module, + system.Module, + systemmetric.Module, + task.Module, + team.Module, + trace.Module, + user.Module, router.Module, ) } diff --git a/src/app/iam/options.go b/src/app/iam/options.go index 48919e64..d3f27d9f 100644 --- a/src/app/iam/options.go +++ b/src/app/iam/options.go @@ -1,14 +1,14 @@ -package iamapp +package iam import ( "aegis/app" - grpciaminterface "aegis/interface/grpciam" + grpciam "aegis/interface/grpc/iam" "aegis/internalclient/resourceclient" "aegis/middleware" - authmodule "aegis/module/auth" - rbacmodule "aegis/module/rbac" - teammodule "aegis/module/team" - usermodule "aegis/module/user" + auth "aegis/module/auth" + rbac "aegis/module/rbac" + team "aegis/module/team" + user "aegis/module/user" "go.uber.org/fx" ) @@ -24,12 +24,12 @@ func Options(confPath string) fx.Option { app.RequiredConfigTarget{Name: "resource-service", PrimaryKey: "clients.resource.target", LegacyKey: "resource.grpc.target"}, ), resourceclient.Module, - teammodule.RemoteProjectReaderOption(), - authmodule.Module, - rbacmodule.Module, - teammodule.Module, - usermodule.Module, + team.RemoteProjectReaderOption(), + auth.Module, + rbac.Module, + team.Module, + user.Module, fx.Provide(middleware.NewService), - grpciaminterface.Module, + grpciam.Module, ) } diff --git a/src/app/orchestrator/options.go b/src/app/orchestrator/options.go index 54a0487e..102fdfd6 100644 --- a/src/app/orchestrator/options.go +++ b/src/app/orchestrator/options.go @@ -1,13 +1,13 @@ -package orchestratorapp +package orchestrator import ( "aegis/app" - grpcorchestratorinterface "aegis/interface/grpcorchestrator" - groupmodule "aegis/module/group" - metricmodule "aegis/module/metric" - notificationmodule "aegis/module/notification" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + grpcorchestrator "aegis/interface/grpc/orchestrator" + group "aegis/module/group" + metric "aegis/module/metric" + notification "aegis/module/notification" + task "aegis/module/task" + trace "aegis/module/trace" "go.uber.org/fx" ) @@ -19,11 +19,11 @@ func Options(confPath string) fx.Option { app.ObserveOptions(), app.DataOptions(), app.ExecutionInjectionOwnerModules(), - groupmodule.Module, - metricmodule.Module, - notificationmodule.Module, - taskmodule.Module, - tracemodule.Module, - grpcorchestratorinterface.Module, + group.Module, + metric.Module, + notification.Module, + task.Module, + trace.Module, + grpcorchestrator.Module, ) } diff --git a/src/app/producer.go b/src/app/producer.go index 40c17939..f0390511 100644 --- a/src/app/producer.go +++ b/src/app/producer.go @@ -1,10 +1,66 @@ package app -import "go.uber.org/fx" +import ( + "context" + + chaos "aegis/infra/chaos" + etcd "aegis/infra/etcd" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" + httpapi "aegis/interface/http" + commonservice "aegis/service/common" + "aegis/service/initialization" + "aegis/utils" + + "go.uber.org/fx" + "gorm.io/gorm" +) func ProducerOptions(confPath string, port string) fx.Option { return fx.Options( CommonOptions(confPath), - ProducerCompatibilityOptions(port), + chaos.Module, + k8s.Module, + ProducerHTTPOptions(port), ) } + +func ProducerHTTPOptions(port string) fx.Option { + return fx.Options( + fx.Provide(newProducerInitializer), + fx.Invoke(registerProducerInitialization), + ProducerHTTPModules(), + fx.Supply(httpapi.ServerConfig{Addr: normalizeAddr(port)}), + httpapi.Module, + ) +} + +type ProducerInitializer struct { + etcd *etcd.Gateway + redis *redis.Gateway + db *gorm.DB + StartFunc func(context.Context) error +} + +func newProducerInitializer(etcd *etcd.Gateway, redis *redis.Gateway, db *gorm.DB) *ProducerInitializer { + return &ProducerInitializer{etcd: etcd, redis: redis, db: db} +} + +func (i *ProducerInitializer) start(ctx context.Context) error { + if i.StartFunc != nil { + return i.StartFunc(ctx) + } + if err := initialization.InitializeProducer(i.db, i.redis, commonservice.NewConfigUpdateListener(ctx, i.db, i.etcd)); err != nil { + return err + } + utils.InitValidator() + return nil +} + +func registerProducerInitialization(lc fx.Lifecycle, initializer *ProducerInitializer) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return initializer.start(ctx) + }, + }) +} diff --git a/src/app/producer_init.go b/src/app/producer_init.go deleted file mode 100644 index fb17006a..00000000 --- a/src/app/producer_init.go +++ /dev/null @@ -1,44 +0,0 @@ -package app - -import ( - "context" - - etcdinfra "aegis/infra/etcd" - redisinfra "aegis/infra/redis" - commonservice "aegis/service/common" - "aegis/service/initialization" - "aegis/utils" - - "go.uber.org/fx" - "gorm.io/gorm" -) - -type ProducerInitializer struct { - etcd *etcdinfra.Gateway - redis *redisinfra.Gateway - db *gorm.DB - StartFunc func(context.Context) error -} - -func newProducerInitializer(etcd *etcdinfra.Gateway, redis *redisinfra.Gateway, db *gorm.DB) *ProducerInitializer { - return &ProducerInitializer{etcd: etcd, redis: redis, db: db} -} - -func (i *ProducerInitializer) start(ctx context.Context) error { - if i.StartFunc != nil { - return i.StartFunc(ctx) - } - if err := initialization.InitializeProducer(i.db, i.redis, commonservice.NewConfigUpdateListener(ctx, i.db, i.etcd)); err != nil { - return err - } - utils.InitValidator() - return nil -} - -func registerProducerInitialization(lc fx.Lifecycle, initializer *ProducerInitializer) { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - return initializer.start(ctx) - }, - }) -} diff --git a/src/app/resource/options.go b/src/app/resource/options.go index e58bfbbd..91e87547 100644 --- a/src/app/resource/options.go +++ b/src/app/resource/options.go @@ -1,15 +1,15 @@ -package resourceapp +package resource import ( "aegis/app" - grpcresourceinterface "aegis/interface/grpcresource" + grpcresource "aegis/interface/grpc/resource" "aegis/internalclient/orchestratorclient" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - labelmodule "aegis/module/label" - projectmodule "aegis/module/project" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + label "aegis/module/label" + project "aegis/module/project" "go.uber.org/fx" ) @@ -25,14 +25,14 @@ func Options(confPath string) fx.Option { app.RequiredConfigTarget{Name: "orchestrator-service", PrimaryKey: "clients.orchestrator.target", LegacyKey: "orchestrator.grpc.target"}, ), orchestratorclient.Module, - evaluationmodule.RemoteQueryOption(), - projectmodule.RemoteStatisticsOption(), - chaossystemmodule.Module, - containermodule.Module, - datasetmodule.Module, - evaluationmodule.Module, - labelmodule.Module, - projectmodule.Module, - grpcresourceinterface.Module, + evaluation.RemoteQueryOption(), + project.RemoteStatisticsOption(), + chaossystem.Module, + container.Module, + dataset.Module, + evaluation.Module, + label.Module, + project.Module, + grpcresource.Module, ) } diff --git a/src/app/runtime_stack.go b/src/app/runtime_stack.go index 2b0c85c2..8920c2be 100644 --- a/src/app/runtime_stack.go +++ b/src/app/runtime_stack.go @@ -1,13 +1,13 @@ package app import ( - chaosinfra "aegis/infra/chaos" - k8sinfra "aegis/infra/k8s" + chaos "aegis/infra/chaos" + k8s "aegis/infra/k8s" runtimeinfra "aegis/infra/runtime" - controllerinterface "aegis/interface/controller" - grpcruntimeinterface "aegis/interface/grpcruntime" - receiverinterface "aegis/interface/receiver" - workerinterface "aegis/interface/worker" + controller "aegis/interface/controller" + grpcruntime "aegis/interface/grpc/runtime" + receiver "aegis/interface/receiver" + worker "aegis/interface/worker" "aegis/internalclient/orchestratorclient" "aegis/service/consumer" @@ -17,31 +17,21 @@ import ( func RuntimeWorkerStackOptions() fx.Option { return fx.Options( runtimeinfra.Module, - chaosinfra.Module, - k8sinfra.Module, + chaos.Module, + k8s.Module, orchestratorclient.Module, - RuntimeWorkerProviderOptions(), - RuntimeWorkerInterfaceOptions(), - ) -} - -func RuntimeWorkerProviderOptions() fx.Option { - return fx.Provide( - consumer.NewMonitor, - fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), - fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), - fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), - consumer.NewFaultBatchManager, - consumer.NewExecutionOwner, - consumer.NewInjectionOwner, - ) -} - -func RuntimeWorkerInterfaceOptions() fx.Option { - return fx.Options( - workerinterface.Module, - controllerinterface.Module, - grpcruntimeinterface.Module, - receiverinterface.Module, + fx.Provide( + consumer.NewMonitor, + fx.Annotate(consumer.NewRestartPedestalRateLimiter, fx.ResultTags(`name:"restart_limiter"`)), + fx.Annotate(consumer.NewBuildContainerRateLimiter, fx.ResultTags(`name:"build_limiter"`)), + fx.Annotate(consumer.NewAlgoExecutionRateLimiter, fx.ResultTags(`name:"algo_limiter"`)), + consumer.NewFaultBatchManager, + consumer.NewExecutionOwner, + consumer.NewInjectionOwner, + ), + worker.Module, + controller.Module, + grpcruntime.Module, + receiver.Module, ) } diff --git a/src/app/service_entrypoints_test.go b/src/app/service_entrypoints_test.go index 1ea4e38f..e983dbdb 100644 --- a/src/app/service_entrypoints_test.go +++ b/src/app/service_entrypoints_test.go @@ -9,29 +9,29 @@ import ( "time" "aegis/app" - gatewayapp "aegis/app/gateway" - iamapp "aegis/app/iam" - orchestratorapp "aegis/app/orchestrator" - resourceapp "aegis/app/resource" + gateway "aegis/app/gateway" + iam "aegis/app/iam" + orchestrator "aegis/app/orchestrator" + resource "aegis/app/resource" runtimeapp "aegis/app/runtime" - systemapp "aegis/app/system" - buildkitinfra "aegis/infra/buildkit" - etcdinfra "aegis/infra/etcd" - harborinfra "aegis/infra/harbor" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - lokiinfra "aegis/infra/loki" + system "aegis/app/system" + buildkit "aegis/infra/buildkit" + etcd "aegis/infra/etcd" + harbor "aegis/infra/harbor" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + loki "aegis/infra/loki" redisinfra "aegis/infra/redis" - controllerinterface "aegis/interface/controller" - httpinterface "aegis/interface/http" - receiverinterface "aegis/interface/receiver" - workerinterface "aegis/interface/worker" + controllerapi "aegis/interface/controller" + httpapi "aegis/interface/http" + receiverapi "aegis/interface/receiver" + workerapi "aegis/interface/worker" resourcev1 "aegis/proto/resource/v1" runtimev1 "aegis/proto/runtime/v1" systemv1 "aegis/proto/system/v1" "github.com/DATA-DOG/go-sqlmock" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" "github.com/spf13/viper" clientv3 "go.etcd.io/etcd/client/v3" "go.opentelemetry.io/otel/sdk/trace" @@ -69,13 +69,13 @@ func newDedicatedServiceReplacements(t *testing.T) (fx.Option, func()) { t.Helper() db, cleanupDB := newSmokeDB(t) - redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}) + redisClient := goredis.NewClient(&goredis.Options{Addr: "127.0.0.1:0"}) redisGateway := redisinfra.NewGateway(redisClient) etcdClient := &clientv3.Client{} - etcdGateway := etcdinfra.NewGateway(etcdClient) + etcdGateway := etcd.NewGateway(etcdClient) traceProvider := trace.NewTracerProvider() - controller := &k8sinfra.Controller{} - k8sGateway := k8sinfra.NewGateway(controller) + controller := &k8s.Controller{} + k8sGateway := k8s.NewGateway(controller) return fx.Replace( db, @@ -83,18 +83,18 @@ func newDedicatedServiceReplacements(t *testing.T) (fx.Option, func()) { redisClient, etcdGateway, etcdClient, - &lokiinfra.Client{}, + &loki.Client{}, traceProvider, &rest.Config{}, controller, k8sGateway, - harborinfra.NewGateway(), - helminfra.NewGateway(), - buildkitinfra.NewGateway(), + harbor.NewGateway(), + helm.NewGateway(), + buildkit.NewGateway(), &app.ProducerInitializer{StartFunc: func(context.Context) error { return nil }}, - &workerinterface.Lifecycle{StartFunc: func(context.Context) error { return nil }}, - &controllerinterface.Lifecycle{RunFunc: func(context.Context, context.CancelFunc) error { return nil }}, - &receiverinterface.Lifecycle{StartFunc: func(context.Context) error { return nil }}, + &workerapi.Lifecycle{StartFunc: func(context.Context) error { return nil }}, + &controllerapi.Lifecycle{RunFunc: func(context.Context, context.CancelFunc) error { return nil }}, + &receiverapi.Lifecycle{StartFunc: func(context.Context) error { return nil }}, ), func() { _ = redisClient.Close() _ = traceProvider.Shutdown(context.Background()) @@ -252,12 +252,12 @@ func TestDedicatedServiceOptionsValidate(t *testing.T) { name string option fx.Option }{ - {name: "gateway", option: gatewayapp.Options("..", "0")}, + {name: "gateway", option: gateway.Options("..", "0")}, {name: "runtime", option: runtimeapp.Options("..")}, - {name: "resource", option: resourceapp.Options("..")}, - {name: "system", option: systemapp.Options("..")}, - {name: "iam", option: iamapp.Options("..")}, - {name: "orchestrator", option: orchestratorapp.Options("..")}, + {name: "resource", option: resource.Options("..")}, + {name: "system", option: system.Options("..")}, + {name: "iam", option: iam.Options("..")}, + {name: "orchestrator", option: orchestrator.Options("..")}, } { t.Run(tc.name, func(t *testing.T) { if err := fx.ValidateApp(tc.option); err != nil { @@ -278,9 +278,9 @@ func TestAPIGatewayStandaloneHTTPIntegrationSmoke(t *testing.T) { addr := reserveLoopbackAddr(t) appInstance := fx.New( - gatewayapp.Options("..", "0"), + gateway.Options("..", "0"), replacements, - fx.Replace(httpinterface.ServerConfig{Addr: addr}), + fx.Replace(httpapi.ServerConfig{Addr: addr}), ) startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -299,7 +299,7 @@ func TestAPIGatewayStandaloneHTTPIntegrationSmoke(t *testing.T) { client := &http.Client{Timeout: time.Second} baseURL := fmt.Sprintf("http://%s", addr) waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) - waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/api/v2/system/configs/abc", http.StatusUnauthorized) } func TestRuntimeWorkerStandaloneGRPCIntegrationSmoke(t *testing.T) { @@ -340,7 +340,7 @@ func TestResourceServiceStandaloneGRPCIntegrationSmoke(t *testing.T) { setConfigValue(t, "resource.grpc.addr", addr) appInstance := fx.New( - resourceapp.Options(".."), + resource.Options(".."), replacements, ) @@ -369,7 +369,7 @@ func TestSystemServiceStandaloneGRPCIntegrationSmoke(t *testing.T) { setConfigValue(t, "system.grpc.addr", addr) appInstance := fx.New( - systemapp.Options(".."), + system.Options(".."), replacements, ) diff --git a/src/app/startup_smoke_test.go b/src/app/startup_smoke_test.go index 997ccc7c..6690e9f0 100644 --- a/src/app/startup_smoke_test.go +++ b/src/app/startup_smoke_test.go @@ -9,20 +9,20 @@ import ( "testing" "time" - buildkitinfra "aegis/infra/buildkit" - etcdinfra "aegis/infra/etcd" - harborinfra "aegis/infra/harbor" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - lokiinfra "aegis/infra/loki" + buildkit "aegis/infra/buildkit" + etcd "aegis/infra/etcd" + harbor "aegis/infra/harbor" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + loki "aegis/infra/loki" redisinfra "aegis/infra/redis" - controllerinterface "aegis/interface/controller" - httpinterface "aegis/interface/http" - receiverinterface "aegis/interface/receiver" - workerinterface "aegis/interface/worker" + controllerapi "aegis/interface/controller" + httpapi "aegis/interface/http" + receiverapi "aegis/interface/receiver" + workerapi "aegis/interface/worker" "github.com/DATA-DOG/go-sqlmock" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" clientv3 "go.etcd.io/etcd/client/v3" "go.opentelemetry.io/otel/sdk/trace" "go.uber.org/fx" @@ -67,13 +67,13 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, t.Helper() db, cleanupDB := newSmokeDB(t) - redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:0"}) + redisClient := goredis.NewClient(&goredis.Options{Addr: "127.0.0.1:0"}) redisGateway := redisinfra.NewGateway(redisClient) etcdClient := &clientv3.Client{} - etcdGateway := etcdinfra.NewGateway(etcdClient) + etcdGateway := etcd.NewGateway(etcdClient) traceProvider := trace.NewTracerProvider() - controller := &k8sinfra.Controller{} - k8sGateway := k8sinfra.NewGateway(controller) + controller := &k8s.Controller{} + k8sGateway := k8s.NewGateway(controller) producerInitializer := &ProducerInitializer{StartFunc: func(context.Context) error { if spies != nil { @@ -81,7 +81,7 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, } return nil }} - workerLifecycle := &workerinterface.Lifecycle{ + workerLifecycle := &workerapi.Lifecycle{ StartFunc: func(context.Context) error { if spies != nil { atomic.AddInt32(&spies.workerStarts, 1) @@ -94,7 +94,7 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, } }, } - controllerLifecycle := &controllerinterface.Lifecycle{ + controllerLifecycle := &controllerapi.Lifecycle{ RunFunc: func(context.Context, context.CancelFunc) error { if spies != nil { atomic.AddInt32(&spies.controllerStarts, 1) @@ -107,7 +107,7 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, } }, } - receiverLifecycle := &receiverinterface.Lifecycle{ + receiverLifecycle := &receiverapi.Lifecycle{ StartFunc: func(context.Context) error { if spies != nil { atomic.AddInt32(&spies.receiverStarts, 1) @@ -127,14 +127,14 @@ func newSmokeReplacements(t *testing.T, spies *smokeLifecycleSpies) (fx.Option, redisClient, etcdGateway, etcdClient, - &lokiinfra.Client{}, + &loki.Client{}, traceProvider, &rest.Config{}, controller, k8sGateway, - harborinfra.NewGateway(), - helminfra.NewGateway(), - buildkitinfra.NewGateway(), + harbor.NewGateway(), + helm.NewGateway(), + buildkit.NewGateway(), producerInitializer, workerLifecycle, controllerLifecycle, @@ -254,7 +254,7 @@ func TestProducerOptionsHTTPIntegrationSmoke(t *testing.T) { app := fx.New( ProducerOptions("..", "0"), replacements, - fx.Replace(httpinterface.ServerConfig{Addr: addr}), + fx.Replace(httpapi.ServerConfig{Addr: addr}), ) startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -273,7 +273,7 @@ func TestProducerOptionsHTTPIntegrationSmoke(t *testing.T) { client := &http.Client{Timeout: time.Second} baseURL := fmt.Sprintf("http://%s", addr) waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) - waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/api/v2/system/configs/abc", http.StatusUnauthorized) } func TestConsumerOptionsLifecycleIntegrationSmoke(t *testing.T) { @@ -303,7 +303,7 @@ func TestBothOptionsHTTPAndLifecycleIntegrationSmoke(t *testing.T) { app := fx.New( BothOptions("..", "0"), replacements, - fx.Replace(httpinterface.ServerConfig{Addr: addr}), + fx.Replace(httpapi.ServerConfig{Addr: addr}), ) startCtx, startCancel := context.WithTimeout(context.Background(), 3*time.Second) @@ -315,7 +315,7 @@ func TestBothOptionsHTTPAndLifecycleIntegrationSmoke(t *testing.T) { client := &http.Client{Timeout: time.Second} baseURL := fmt.Sprintf("http://%s", addr) waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/docs/doc.json", http.StatusOK) - waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/system/configs/abc", http.StatusUnauthorized) + waitForHTTPStatus(t, client, http.MethodGet, baseURL+"/api/v2/system/configs/abc", http.StatusUnauthorized) requireLifecycleCallCount(t, "producer start", &spies.producerStarts, 1) requireLifecycleCallCount(t, "worker start", &spies.workerStarts, 1) requireLifecycleCallCount(t, "controller start", &spies.controllerStarts, 1) diff --git a/src/app/system/options.go b/src/app/system/options.go index da9ccf20..81842430 100644 --- a/src/app/system/options.go +++ b/src/app/system/options.go @@ -1,12 +1,12 @@ -package systemapp +package system import ( "aegis/app" - k8sinfra "aegis/infra/k8s" - grpcsysteminterface "aegis/interface/grpcsystem" + k8s "aegis/infra/k8s" + grpcsystem "aegis/interface/grpc/system" "aegis/internalclient/runtimeclient" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" "go.uber.org/fx" ) @@ -23,11 +23,11 @@ func Options(confPath string) fx.Option { "system-service", app.RequiredConfigTarget{Name: "runtime-worker-service", PrimaryKey: "clients.runtime.target", LegacyKey: "runtime_worker.grpc.target"}, ), - systemmodule.RemoteRuntimeQueryOption(), - k8sinfra.Module, + system.RemoteRuntimeQueryOption(), + k8s.Module, runtimeclient.Module, - systemmodule.Module, - systemmetricmodule.Module, - grpcsysteminterface.Module, + system.Module, + systemmetric.Module, + grpcsystem.Module, ) } diff --git a/src/cmd/api-gateway/main.go b/src/cmd/api-gateway/main.go index 2575528b..00f7df11 100644 --- a/src/cmd/api-gateway/main.go +++ b/src/cmd/api-gateway/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - gatewayapp "aegis/app/gateway" + gateway "aegis/app/gateway" "go.uber.org/fx" ) @@ -13,5 +13,5 @@ func main() { port := flag.String("port", "8080", "port to run the API gateway on") flag.Parse() - fx.New(gatewayapp.Options(*conf, *port)).Run() + fx.New(gateway.Options(*conf, *port)).Run() } diff --git a/src/cmd/iam-service/main.go b/src/cmd/iam-service/main.go index fa6713bd..dbe1b5de 100644 --- a/src/cmd/iam-service/main.go +++ b/src/cmd/iam-service/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - iamapp "aegis/app/iam" + iam "aegis/app/iam" "go.uber.org/fx" ) @@ -12,5 +12,5 @@ func main() { conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") flag.Parse() - fx.New(iamapp.Options(*conf)).Run() + fx.New(iam.Options(*conf)).Run() } diff --git a/src/cmd/orchestrator-service/main.go b/src/cmd/orchestrator-service/main.go index c7345957..123255fe 100644 --- a/src/cmd/orchestrator-service/main.go +++ b/src/cmd/orchestrator-service/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - orchestratorapp "aegis/app/orchestrator" + orchestrator "aegis/app/orchestrator" "go.uber.org/fx" ) @@ -12,5 +12,5 @@ func main() { conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") flag.Parse() - fx.New(orchestratorapp.Options(*conf)).Run() + fx.New(orchestrator.Options(*conf)).Run() } diff --git a/src/cmd/resource-service/main.go b/src/cmd/resource-service/main.go index bdab26c7..06ffd811 100644 --- a/src/cmd/resource-service/main.go +++ b/src/cmd/resource-service/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - resourceapp "aegis/app/resource" + resource "aegis/app/resource" "go.uber.org/fx" ) @@ -12,5 +12,5 @@ func main() { conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") flag.Parse() - fx.New(resourceapp.Options(*conf)).Run() + fx.New(resource.Options(*conf)).Run() } diff --git a/src/cmd/system-service/main.go b/src/cmd/system-service/main.go index b23eeabe..fa5cf539 100644 --- a/src/cmd/system-service/main.go +++ b/src/cmd/system-service/main.go @@ -3,7 +3,7 @@ package main import ( "flag" - systemapp "aegis/app/system" + system "aegis/app/system" "go.uber.org/fx" ) @@ -12,5 +12,5 @@ func main() { conf := flag.String("conf", "/etc/rcabench/config.prod.toml", "path to configuration file") flag.Parse() - fx.New(systemapp.Options(*conf)).Run() + fx.New(system.Options(*conf)).Run() } diff --git a/src/httpx/common.go b/src/httpx/common.go index 9ddcf210..31674464 100644 --- a/src/httpx/common.go +++ b/src/httpx/common.go @@ -66,6 +66,8 @@ func HandleServiceError(c *gin.Context, err error) bool { switch innermostErr { case consts.ErrAuthenticationFailed: dto.ErrorResponse(c, http.StatusUnauthorized, msg) + case consts.ErrPermissionDenied: + dto.ErrorResponse(c, http.StatusForbidden, msg) case consts.ErrBadRequest: dto.ErrorResponse(c, http.StatusBadRequest, msg) case consts.ErrNotFound: diff --git a/src/infra/buildkit/gateway.go b/src/infra/buildkit/gateway.go index 389bfb02..5914bf04 100644 --- a/src/infra/buildkit/gateway.go +++ b/src/infra/buildkit/gateway.go @@ -1,4 +1,4 @@ -package buildkitinfra +package buildkit import ( "context" diff --git a/src/infra/buildkit/module.go b/src/infra/buildkit/module.go index 1fc9a957..28c3f26f 100644 --- a/src/infra/buildkit/module.go +++ b/src/infra/buildkit/module.go @@ -1,4 +1,4 @@ -package buildkitinfra +package buildkit import "go.uber.org/fx" diff --git a/src/infra/chaos/module.go b/src/infra/chaos/module.go index 0fb6fd8d..d93d4cfc 100644 --- a/src/infra/chaos/module.go +++ b/src/infra/chaos/module.go @@ -1,4 +1,4 @@ -package chaosinfra +package chaos import ( chaosCli "github.com/OperationsPAI/chaos-experiment/client" diff --git a/src/infra/config/module.go b/src/infra/config/module.go index 666b8bdb..15b28985 100644 --- a/src/infra/config/module.go +++ b/src/infra/config/module.go @@ -1,4 +1,4 @@ -package configinfra +package config import ( "aegis/config" diff --git a/src/infra/db/config.go b/src/infra/db/config.go index 05db6ccb..96b46665 100644 --- a/src/infra/db/config.go +++ b/src/infra/db/config.go @@ -1,4 +1,4 @@ -package dbinfra +package db import ( "fmt" diff --git a/src/infra/db/migration.go b/src/infra/db/migration.go index b0ee43e4..ff3c3669 100644 --- a/src/infra/db/migration.go +++ b/src/infra/db/migration.go @@ -1,4 +1,4 @@ -package dbinfra +package db import ( "aegis/model" diff --git a/src/infra/db/module.go b/src/infra/db/module.go index 5a0eaf89..80922dd2 100644 --- a/src/infra/db/module.go +++ b/src/infra/db/module.go @@ -1,4 +1,4 @@ -package dbinfra +package db import ( "context" diff --git a/src/infra/etcd/gateway.go b/src/infra/etcd/gateway.go index 3cf4c3f8..79156a43 100644 --- a/src/infra/etcd/gateway.go +++ b/src/infra/etcd/gateway.go @@ -1,4 +1,4 @@ -package etcdinfra +package etcd import ( "context" diff --git a/src/infra/etcd/module.go b/src/infra/etcd/module.go index 4cf0887f..228c97ee 100644 --- a/src/infra/etcd/module.go +++ b/src/infra/etcd/module.go @@ -1,4 +1,4 @@ -package etcdinfra +package etcd import "go.uber.org/fx" diff --git a/src/infra/harbor/gateway.go b/src/infra/harbor/gateway.go index e9585373..f690f914 100644 --- a/src/infra/harbor/gateway.go +++ b/src/infra/harbor/gateway.go @@ -1,4 +1,4 @@ -package harborinfra +package harbor import ( "context" diff --git a/src/infra/harbor/module.go b/src/infra/harbor/module.go index febae93c..b8ecfa6c 100644 --- a/src/infra/harbor/module.go +++ b/src/infra/harbor/module.go @@ -1,4 +1,4 @@ -package harborinfra +package harbor import "go.uber.org/fx" diff --git a/src/infra/helm/gateway.go b/src/infra/helm/gateway.go index 126450ca..89731264 100644 --- a/src/infra/helm/gateway.go +++ b/src/infra/helm/gateway.go @@ -1,4 +1,4 @@ -package helminfra +package helm import ( "context" diff --git a/src/infra/helm/module.go b/src/infra/helm/module.go index 82b71e17..3b6607a7 100644 --- a/src/infra/helm/module.go +++ b/src/infra/helm/module.go @@ -1,4 +1,4 @@ -package helminfra +package helm import "go.uber.org/fx" diff --git a/src/infra/k8s/controller.go b/src/infra/k8s/controller.go index c0ea0f90..c7c9f163 100644 --- a/src/infra/k8s/controller.go +++ b/src/infra/k8s/controller.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "context" diff --git a/src/infra/k8s/crd.go b/src/infra/k8s/crd.go index 22a88bea..25585a90 100644 --- a/src/infra/k8s/crd.go +++ b/src/infra/k8s/crd.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "context" diff --git a/src/infra/k8s/gateway.go b/src/infra/k8s/gateway.go index ec0b3fd4..0b866aba 100644 --- a/src/infra/k8s/gateway.go +++ b/src/infra/k8s/gateway.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "context" diff --git a/src/infra/k8s/job.go b/src/infra/k8s/job.go index 772c4796..0ce2e7a7 100644 --- a/src/infra/k8s/job.go +++ b/src/infra/k8s/job.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "bufio" diff --git a/src/infra/k8s/k8s_test.go b/src/infra/k8s/k8s_test.go index afb1f097..78e80496 100644 --- a/src/infra/k8s/k8s_test.go +++ b/src/infra/k8s/k8s_test.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "aegis/config" diff --git a/src/infra/k8s/module.go b/src/infra/k8s/module.go index 661ee6ab..34673da1 100644 --- a/src/infra/k8s/module.go +++ b/src/infra/k8s/module.go @@ -1,4 +1,4 @@ -package k8sinfra +package k8s import ( "k8s.io/client-go/rest" diff --git a/src/infra/logger/module.go b/src/infra/logger/module.go index 673a01d1..b4033dee 100644 --- a/src/infra/logger/module.go +++ b/src/infra/logger/module.go @@ -1,4 +1,4 @@ -package loggerinfra +package logger import ( "fmt" diff --git a/src/infra/loki/client.go b/src/infra/loki/client.go index 27799ba9..9314b933 100644 --- a/src/infra/loki/client.go +++ b/src/infra/loki/client.go @@ -1,4 +1,4 @@ -package lokiinfra +package loki import ( "context" diff --git a/src/infra/loki/module.go b/src/infra/loki/module.go index 9ea0a235..a4760d62 100644 --- a/src/infra/loki/module.go +++ b/src/infra/loki/module.go @@ -1,4 +1,4 @@ -package lokiinfra +package loki import "go.uber.org/fx" diff --git a/src/infra/redis/gateway.go b/src/infra/redis/gateway.go index 9d5846c1..46752155 100644 --- a/src/infra/redis/gateway.go +++ b/src/infra/redis/gateway.go @@ -1,4 +1,4 @@ -package redisinfra +package redis import ( "context" diff --git a/src/infra/redis/module.go b/src/infra/redis/module.go index b45ca0c6..690ae323 100644 --- a/src/infra/redis/module.go +++ b/src/infra/redis/module.go @@ -1,4 +1,4 @@ -package redisinfra +package redis import "go.uber.org/fx" diff --git a/src/infra/redis/task_queue.go b/src/infra/redis/task_queue.go index 2fc4d08a..6decfd2c 100644 --- a/src/infra/redis/task_queue.go +++ b/src/infra/redis/task_queue.go @@ -1,4 +1,4 @@ -package redisinfra +package redis import ( "context" diff --git a/src/infra/tracing/module.go b/src/infra/tracing/module.go index 492d2e87..6eef7107 100644 --- a/src/infra/tracing/module.go +++ b/src/infra/tracing/module.go @@ -1,4 +1,4 @@ -package tracinginfra +package tracing import ( "context" diff --git a/src/infra/tracing/provider.go b/src/infra/tracing/provider.go index b376178b..9f286312 100644 --- a/src/infra/tracing/provider.go +++ b/src/infra/tracing/provider.go @@ -1,4 +1,4 @@ -package tracinginfra +package tracing import ( "context" diff --git a/src/interface/controller/module.go b/src/interface/controller/module.go index c27751a3..2966f8c3 100644 --- a/src/interface/controller/module.go +++ b/src/interface/controller/module.go @@ -1,12 +1,12 @@ -package controllerinterface +package controller import ( "context" "log" "os" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" "aegis/service/consumer" "github.com/go-logr/stdr" @@ -23,9 +23,9 @@ var Module = fx.Module("controller", type Params struct { fx.In - Controller *k8sinfra.Controller - K8sGateway *k8sinfra.Gateway - RedisGateway *redisinfra.Gateway + Controller *k8s.Controller + K8sGateway *k8s.Gateway + RedisGateway *redis.Gateway DB *gorm.DB Monitor consumer.NamespaceMonitor AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` diff --git a/src/interface/grpciam/lifecycle.go b/src/interface/grpc/iam/lifecycle.go similarity index 98% rename from src/interface/grpciam/lifecycle.go rename to src/interface/grpc/iam/lifecycle.go index da8ff852..75a7e6b2 100644 --- a/src/interface/grpciam/lifecycle.go +++ b/src/interface/grpc/iam/lifecycle.go @@ -1,4 +1,4 @@ -package grpciaminterface +package grpciam import ( "context" diff --git a/src/interface/grpciam/module.go b/src/interface/grpc/iam/module.go similarity index 85% rename from src/interface/grpciam/module.go rename to src/interface/grpc/iam/module.go index b52e02a1..f88bd64c 100644 --- a/src/interface/grpciam/module.go +++ b/src/interface/grpc/iam/module.go @@ -1,4 +1,4 @@ -package grpciaminterface +package grpciam import "go.uber.org/fx" diff --git a/src/interface/grpciam/service.go b/src/interface/grpc/iam/service.go similarity index 93% rename from src/interface/grpciam/service.go rename to src/interface/grpc/iam/service.go index c84c3124..fdba7be9 100644 --- a/src/interface/grpciam/service.go +++ b/src/interface/grpc/iam/service.go @@ -1,4 +1,4 @@ -package grpciaminterface +package grpciam import ( "context" @@ -9,10 +9,10 @@ import ( "aegis/consts" "aegis/dto" "aegis/middleware" - authmodule "aegis/module/auth" - rbacmodule "aegis/module/rbac" - teammodule "aegis/module/team" - usermodule "aegis/module/user" + auth "aegis/module/auth" + rbac "aegis/module/rbac" + team "aegis/module/team" + user "aegis/module/user" iamv1 "aegis/proto/iam/v1" "aegis/utils" @@ -25,20 +25,20 @@ import ( type iamServer struct { iamv1.UnimplementedIAMServiceServer - auth *authmodule.Service - authAPI authmodule.HandlerService - team teammodule.HandlerService - user usermodule.HandlerService - rbac rbacmodule.HandlerService + auth *auth.Service + authAPI auth.HandlerService + team team.HandlerService + user user.HandlerService + rbac rbac.HandlerService middleware middleware.Service } func newIAMServer( - auth *authmodule.Service, - authAPI authmodule.HandlerService, - team teammodule.HandlerService, - user usermodule.HandlerService, - rbac rbacmodule.HandlerService, + auth *auth.Service, + authAPI auth.HandlerService, + team team.HandlerService, + user user.HandlerService, + rbac rbac.HandlerService, middlewareService middleware.Service, ) *iamServer { return &iamServer{ @@ -114,7 +114,7 @@ func (s *iamServer) CheckPermission(ctx context.Context, req *iamv1.CheckPermiss } func (s *iamServer) Login(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { - body, err := decodeBody[authmodule.LoginReq](req.GetBody()) + body, err := decodeBody[auth.LoginReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -129,7 +129,7 @@ func (s *iamServer) Login(ctx context.Context, req *iamv1.MutationRequest) (*iam } func (s *iamServer) Register(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { - body, err := decodeBody[authmodule.RegisterReq](req.GetBody()) + body, err := decodeBody[auth.RegisterReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -144,7 +144,7 @@ func (s *iamServer) Register(ctx context.Context, req *iamv1.MutationRequest) (* } func (s *iamServer) RefreshToken(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { - body, err := decodeBody[authmodule.TokenRefreshReq](req.GetBody()) + body, err := decodeBody[auth.TokenRefreshReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -179,7 +179,7 @@ func (s *iamServer) ChangePassword(ctx context.Context, req *iamv1.UserBodyReque if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[authmodule.ChangePasswordReq](req.GetBody()) + body, err := decodeBody[auth.ChangePasswordReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -207,7 +207,7 @@ func (s *iamServer) CreateAPIKey(ctx context.Context, req *iamv1.UserBodyRequest if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[authmodule.CreateAPIKeyReq](req.GetBody()) + body, err := decodeBody[auth.CreateAPIKeyReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -225,7 +225,7 @@ func (s *iamServer) ListAPIKeys(ctx context.Context, req *iamv1.UserQueryRequest if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - query, err := decodeQuery[authmodule.ListAPIKeyReq](req.GetQuery()) + query, err := decodeQuery[auth.ListAPIKeyReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -302,7 +302,7 @@ func (s *iamServer) RotateAPIKey(ctx context.Context, req *iamv1.UserScopedIDReq } func (s *iamServer) CreateUser(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { - body, err := decodeBody[usermodule.CreateUserReq](req.GetBody()) + body, err := decodeBody[user.CreateUserReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -338,7 +338,7 @@ func (s *iamServer) GetUser(ctx context.Context, req *iamv1.IDRequest) (*iamv1.S } func (s *iamServer) ListUsers(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { - query, err := decodeQuery[usermodule.ListUserReq](req.GetQuery()) + query, err := decodeQuery[user.ListUserReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -356,7 +356,7 @@ func (s *iamServer) UpdateUser(ctx context.Context, req *iamv1.UpdateByIDRequest if req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "id is required") } - body, err := decodeBody[usermodule.UpdateUserReq](req.GetBody()) + body, err := decodeBody[user.UpdateUserReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -394,7 +394,7 @@ func (s *iamServer) AssignUserPermissions(ctx context.Context, req *iamv1.UserBo if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[usermodule.AssignUserPermissionReq](req.GetBody()) + body, err := decodeBody[user.AssignUserPermissionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -411,7 +411,7 @@ func (s *iamServer) RemoveUserPermissions(ctx context.Context, req *iamv1.UserBo if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[usermodule.RemoveUserPermissionReq](req.GetBody()) + body, err := decodeBody[user.RemoveUserPermissionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -485,7 +485,7 @@ func (s *iamServer) RemoveUserProject(ctx context.Context, req *iamv1.UserScoped } func (s *iamServer) CreateRole(ctx context.Context, req *iamv1.MutationRequest) (*iamv1.StructResponse, error) { - body, err := decodeBody[rbacmodule.CreateRoleReq](req.GetBody()) + body, err := decodeBody[rbac.CreateRoleReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -518,7 +518,7 @@ func (s *iamServer) GetRole(ctx context.Context, req *iamv1.IDRequest) (*iamv1.S } func (s *iamServer) ListRoles(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { - query, err := decodeQuery[rbacmodule.ListRoleReq](req.GetQuery()) + query, err := decodeQuery[rbac.ListRoleReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -536,7 +536,7 @@ func (s *iamServer) UpdateRole(ctx context.Context, req *iamv1.UpdateByIDRequest if req.GetId() <= 0 { return nil, status.Error(codes.InvalidArgument, "id is required") } - body, err := decodeBody[rbacmodule.UpdateRoleReq](req.GetBody()) + body, err := decodeBody[rbac.UpdateRoleReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -599,7 +599,7 @@ func (s *iamServer) GetPermission(ctx context.Context, req *iamv1.IDRequest) (*i } func (s *iamServer) ListPermissions(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { - query, err := decodeQuery[rbacmodule.ListPermissionReq](req.GetQuery()) + query, err := decodeQuery[rbac.ListPermissionReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -636,7 +636,7 @@ func (s *iamServer) GetResource(ctx context.Context, req *iamv1.IDRequest) (*iam } func (s *iamServer) ListResources(ctx context.Context, req *iamv1.QueryRequest) (*iamv1.StructResponse, error) { - query, err := decodeQuery[rbacmodule.ListResourceReq](req.GetQuery()) + query, err := decodeQuery[rbac.ListResourceReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -722,7 +722,7 @@ func (s *iamServer) IsUserInProject(ctx context.Context, req *iamv1.UserProjectR } func (s *iamServer) ExchangeAPIKeyToken(ctx context.Context, req *iamv1.ExchangeAPIKeyTokenRequest) (*iamv1.ExchangeAPIKeyTokenResponse, error) { - authReq := &authmodule.APIKeyTokenReq{ + authReq := &auth.APIKeyTokenReq{ KeyID: req.GetKeyId(), Timestamp: req.GetTimestamp(), Nonce: req.GetNonce(), @@ -752,7 +752,7 @@ func (s *iamServer) CreateTeam(ctx context.Context, req *iamv1.CreateTeamRequest if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[teammodule.CreateTeamReq](req.GetBody()) + body, err := decodeBody[team.CreateTeamReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -791,7 +791,7 @@ func (s *iamServer) ListTeams(ctx context.Context, req *iamv1.ListTeamsRequest) if req.GetUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "user_id is required") } - query, err := decodeQuery[teammodule.ListTeamReq](req.GetQuery()) + query, err := decodeQuery[team.ListTeamReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -809,7 +809,7 @@ func (s *iamServer) UpdateTeam(ctx context.Context, req *iamv1.UpdateTeamRequest if req.GetTeamId() <= 0 { return nil, status.Error(codes.InvalidArgument, "team_id is required") } - body, err := decodeBody[teammodule.UpdateTeamReq](req.GetBody()) + body, err := decodeBody[team.UpdateTeamReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -827,7 +827,7 @@ func (s *iamServer) ListTeamProjects(ctx context.Context, req *iamv1.ListTeamPro if req.GetTeamId() <= 0 { return nil, status.Error(codes.InvalidArgument, "team_id is required") } - query, err := decodeQuery[teammodule.TeamProjectListReq](req.GetQuery()) + query, err := decodeQuery[team.TeamProjectListReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -845,7 +845,7 @@ func (s *iamServer) AddTeamMember(ctx context.Context, req *iamv1.AddTeamMemberR if req.GetTeamId() <= 0 { return nil, status.Error(codes.InvalidArgument, "team_id is required") } - body, err := decodeBody[teammodule.AddTeamMemberReq](req.GetBody()) + body, err := decodeBody[team.AddTeamMemberReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -872,7 +872,7 @@ func (s *iamServer) UpdateTeamMemberRole(ctx context.Context, req *iamv1.UpdateT if req.GetTeamId() <= 0 || req.GetTargetUserId() <= 0 || req.GetCurrentUserId() <= 0 { return nil, status.Error(codes.InvalidArgument, "team_id, target_user_id, and current_user_id are required") } - body, err := decodeBody[teammodule.UpdateTeamMemberRoleReq](req.GetBody()) + body, err := decodeBody[team.UpdateTeamMemberRoleReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -889,7 +889,7 @@ func (s *iamServer) ListTeamMembers(ctx context.Context, req *iamv1.ListTeamMemb if req.GetTeamId() <= 0 { return nil, status.Error(codes.InvalidArgument, "team_id is required") } - query, err := decodeQuery[teammodule.ListTeamMemberReq](req.GetQuery()) + query, err := decodeQuery[team.ListTeamMemberReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } diff --git a/src/interface/grpciam/service_test.go b/src/interface/grpc/iam/service_test.go similarity index 82% rename from src/interface/grpciam/service_test.go rename to src/interface/grpc/iam/service_test.go index 0f6acaaa..04b423f9 100644 --- a/src/interface/grpciam/service_test.go +++ b/src/interface/grpc/iam/service_test.go @@ -1,4 +1,4 @@ -package grpciaminterface +package grpciam import ( "context" @@ -9,8 +9,8 @@ import ( "aegis/consts" "aegis/dto" "aegis/middleware" - authmodule "aegis/module/auth" - teammodule "aegis/module/team" + auth "aegis/module/auth" + team "aegis/module/team" iamv1 "aegis/proto/iam/v1" "aegis/utils" @@ -61,44 +61,44 @@ func (middlewareStub) LogUserAction(string, string, string, string, int, int, co var _ middleware.Service = middlewareStub{} type teamHandlerStub struct { - createResp *teammodule.TeamResp - detailResp *teammodule.TeamDetailResp - listResp *dto.ListResp[teammodule.TeamResp] - projectsResp *dto.ListResp[teammodule.TeamProjectItem] - membersResp *dto.ListResp[teammodule.TeamMemberResp] - updateResp *teammodule.TeamResp + createResp *team.TeamResp + detailResp *team.TeamDetailResp + listResp *dto.ListResp[team.TeamResp] + projectsResp *dto.ListResp[team.TeamProjectItem] + membersResp *dto.ListResp[team.TeamMemberResp] + updateResp *team.TeamResp createCalled bool listCalled bool listProjectsCalled bool } -func (s *teamHandlerStub) CreateTeam(context.Context, *teammodule.CreateTeamReq, int) (*teammodule.TeamResp, error) { +func (s *teamHandlerStub) CreateTeam(context.Context, *team.CreateTeamReq, int) (*team.TeamResp, error) { s.createCalled = true return s.createResp, nil } func (*teamHandlerStub) DeleteTeam(context.Context, int) error { return nil } -func (s *teamHandlerStub) GetTeamDetail(context.Context, int) (*teammodule.TeamDetailResp, error) { +func (s *teamHandlerStub) GetTeamDetail(context.Context, int) (*team.TeamDetailResp, error) { return s.detailResp, nil } -func (s *teamHandlerStub) ListTeams(context.Context, *teammodule.ListTeamReq, int, bool) (*dto.ListResp[teammodule.TeamResp], error) { +func (s *teamHandlerStub) ListTeams(context.Context, *team.ListTeamReq, int, bool) (*dto.ListResp[team.TeamResp], error) { s.listCalled = true return s.listResp, nil } -func (s *teamHandlerStub) UpdateTeam(context.Context, *teammodule.UpdateTeamReq, int) (*teammodule.TeamResp, error) { +func (s *teamHandlerStub) UpdateTeam(context.Context, *team.UpdateTeamReq, int) (*team.TeamResp, error) { return s.updateResp, nil } -func (s *teamHandlerStub) ListTeamProjects(context.Context, *teammodule.TeamProjectListReq, int) (*dto.ListResp[teammodule.TeamProjectItem], error) { +func (s *teamHandlerStub) ListTeamProjects(context.Context, *team.TeamProjectListReq, int) (*dto.ListResp[team.TeamProjectItem], error) { s.listProjectsCalled = true return s.projectsResp, nil } -func (*teamHandlerStub) AddMember(context.Context, *teammodule.AddTeamMemberReq, int) error { +func (*teamHandlerStub) AddMember(context.Context, *team.AddTeamMemberReq, int) error { return nil } func (*teamHandlerStub) RemoveMember(context.Context, int, int, int) error { return nil } -func (*teamHandlerStub) UpdateMemberRole(context.Context, *teammodule.UpdateTeamMemberRoleReq, int, int, int) error { +func (*teamHandlerStub) UpdateMemberRole(context.Context, *team.UpdateTeamMemberRoleReq, int, int, int) error { return nil } -func (s *teamHandlerStub) ListMembers(context.Context, *teammodule.ListTeamMemberReq, int) (*dto.ListResp[teammodule.TeamMemberResp], error) { +func (s *teamHandlerStub) ListMembers(context.Context, *team.ListTeamMemberReq, int) (*dto.ListResp[team.TeamMemberResp], error) { return s.membersResp, nil } @@ -108,7 +108,7 @@ func TestIAMServerVerifyTokenUser(t *testing.T) { t.Fatalf("GenerateToken() error = %v", err) } - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) if err != nil { @@ -129,7 +129,7 @@ func TestIAMServerVerifyTokenAPIKeyScopes(t *testing.T) { t.Fatalf("GenerateAPIKeyToken() error = %v", err) } - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) if err != nil { @@ -145,7 +145,7 @@ func TestIAMServerVerifyTokenAPIKeyScopes(t *testing.T) { } func TestIAMServerCheckPermission(t *testing.T) { - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) resp, err := server.CheckPermission(context.Background(), &iamv1.CheckPermissionRequest{ UserId: 7, @@ -167,7 +167,7 @@ func TestIAMServerVerifyTokenService(t *testing.T) { t.Fatalf("GenerateServiceToken() error = %v", err) } - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{allowed: true}) resp, err := server.VerifyToken(context.Background(), &iamv1.VerifyTokenRequest{Token: token}) if err != nil { @@ -182,7 +182,7 @@ func TestIAMServerVerifyTokenService(t *testing.T) { } func TestIAMServerMembershipChecks(t *testing.T) { - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, &teamHandlerStub{}, nil, nil, middlewareStub{ teamAdmin: true, teamMember: true, @@ -244,22 +244,22 @@ func TestIAMServerMembershipChecks(t *testing.T) { func TestIAMServerTeamRPCs(t *testing.T) { teamStub := &teamHandlerStub{ - createResp: &teammodule.TeamResp{ID: 9, Name: "core"}, - detailResp: &teammodule.TeamDetailResp{ - TeamResp: teammodule.TeamResp{ID: 9, Name: "core"}, + createResp: &team.TeamResp{ID: 9, Name: "core"}, + detailResp: &team.TeamDetailResp{ + TeamResp: team.TeamResp{ID: 9, Name: "core"}, UserCount: 2, ProjectCount: 3, }, - listResp: &dto.ListResp[teammodule.TeamResp]{ - Items: []teammodule.TeamResp{{ID: 9, Name: "core"}}, + listResp: &dto.ListResp[team.TeamResp]{ + Items: []team.TeamResp{{ID: 9, Name: "core"}}, Pagination: &dto.PaginationInfo{Page: 1, Size: 20, Total: 1, TotalPages: 1}, }, - projectsResp: &dto.ListResp[teammodule.TeamProjectItem]{ - Items: []teammodule.TeamProjectItem{{ID: 11, Name: "proj-a"}}, + projectsResp: &dto.ListResp[team.TeamProjectItem]{ + Items: []team.TeamProjectItem{{ID: 11, Name: "proj-a"}}, Pagination: &dto.PaginationInfo{Page: 1, Size: 20, Total: 1, TotalPages: 1}, }, } - authSvc := authmodule.NewService(nil, nil, nil, nil) + authSvc := auth.NewService(nil, nil, nil, nil) server := newIAMServer(authSvc, authSvc, teamStub, nil, nil, middlewareStub{}) createBody, _ := structpb.NewStruct(map[string]any{"name": "core"}) diff --git a/src/interface/grpcorchestrator/lifecycle.go b/src/interface/grpc/orchestrator/lifecycle.go similarity index 98% rename from src/interface/grpcorchestrator/lifecycle.go rename to src/interface/grpc/orchestrator/lifecycle.go index 977f2bd2..dbbec843 100644 --- a/src/interface/grpcorchestrator/lifecycle.go +++ b/src/interface/grpc/orchestrator/lifecycle.go @@ -1,4 +1,4 @@ -package grpcorchestratorinterface +package grpcorchestrator import ( "context" diff --git a/src/interface/grpcorchestrator/module.go b/src/interface/grpc/orchestrator/module.go similarity index 68% rename from src/interface/grpcorchestrator/module.go rename to src/interface/grpc/orchestrator/module.go index d5dca709..ae7555a4 100644 --- a/src/interface/grpcorchestrator/module.go +++ b/src/interface/grpc/orchestrator/module.go @@ -1,14 +1,14 @@ -package grpcorchestratorinterface +package grpcorchestrator import ( - projectmodule "aegis/module/project" + project "aegis/module/project" "go.uber.org/fx" ) var Module = fx.Module("grpc_orchestrator", fx.Provide( - projectmodule.NewRepository, + project.NewRepository, newProjectStatisticsReader, newTaskQueueController, newOrchestratorServer, diff --git a/src/interface/grpcorchestrator/project_statistics.go b/src/interface/grpc/orchestrator/project_statistics.go similarity index 68% rename from src/interface/grpcorchestrator/project_statistics.go rename to src/interface/grpc/orchestrator/project_statistics.go index fb4beeb3..963de0c6 100644 --- a/src/interface/grpcorchestrator/project_statistics.go +++ b/src/interface/grpc/orchestrator/project_statistics.go @@ -1,8 +1,8 @@ -package grpcorchestratorinterface +package grpcorchestrator import ( "aegis/dto" - projectmodule "aegis/module/project" + project "aegis/module/project" ) type projectStatisticsReader interface { @@ -10,10 +10,10 @@ type projectStatisticsReader interface { } type projectRepositoryStatisticsReader struct { - repo *projectmodule.Repository + repo *project.Repository } -func newProjectStatisticsReader(repo *projectmodule.Repository) projectStatisticsReader { +func newProjectStatisticsReader(repo *project.Repository) projectStatisticsReader { return &projectRepositoryStatisticsReader{repo: repo} } diff --git a/src/interface/grpcorchestrator/service.go b/src/interface/grpc/orchestrator/service.go similarity index 85% rename from src/interface/grpcorchestrator/service.go rename to src/interface/grpc/orchestrator/service.go index 20aa2662..518c623d 100644 --- a/src/interface/grpcorchestrator/service.go +++ b/src/interface/grpc/orchestrator/service.go @@ -1,4 +1,4 @@ -package grpcorchestratorinterface +package grpcorchestrator import ( "context" @@ -10,18 +10,18 @@ import ( "aegis/consts" "aegis/dto" redisinfra "aegis/infra/redis" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - metricmodule "aegis/module/metric" - notificationmodule "aegis/module/notification" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + metric "aegis/module/metric" + notification "aegis/module/notification" + task "aegis/module/task" + trace "aegis/module/trace" orchestratorv1 "aegis/proto/orchestrator/v1" "aegis/service/consumer" "github.com/google/uuid" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" @@ -30,48 +30,48 @@ import ( const orchestratorServiceName = "orchestrator-service" type executionSubmitter interface { - SubmitAlgorithmExecution(context.Context, *executionmodule.SubmitExecutionReq, string, int) (*executionmodule.SubmitExecutionResp, error) - CreateExecutionRecord(context.Context, *executionmodule.RuntimeCreateExecutionReq) (int, error) - UpdateExecutionState(context.Context, *executionmodule.RuntimeUpdateExecutionStateReq) error - GetExecution(context.Context, int) (*executionmodule.ExecutionDetailResp, error) - ListEvaluationExecutionsByDatapack(context.Context, *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) - ListEvaluationExecutionsByDataset(context.Context, *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) + SubmitAlgorithmExecution(context.Context, *execution.SubmitExecutionReq, string, int) (*execution.SubmitExecutionResp, error) + CreateExecutionRecord(context.Context, *execution.RuntimeCreateExecutionReq) (int, error) + UpdateExecutionState(context.Context, *execution.RuntimeUpdateExecutionStateReq) error + GetExecution(context.Context, int) (*execution.ExecutionDetailResp, error) + ListEvaluationExecutionsByDatapack(context.Context, *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) + ListEvaluationExecutionsByDataset(context.Context, *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) } type injectionSubmitter interface { - SubmitFaultInjection(context.Context, *injectionmodule.SubmitInjectionReq, string, int, *int) (*injectionmodule.SubmitInjectionResp, error) - SubmitDatapackBuilding(context.Context, *injectionmodule.SubmitDatapackBuildingReq, string, int, *int) (*injectionmodule.SubmitDatapackBuildingResp, error) - CreateInjectionRecord(context.Context, *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) - UpdateInjectionState(context.Context, *injectionmodule.RuntimeUpdateInjectionStateReq) error - UpdateInjectionTimestamps(context.Context, *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) + SubmitFaultInjection(context.Context, *injection.SubmitInjectionReq, string, int, *int) (*injection.SubmitInjectionResp, error) + SubmitDatapackBuilding(context.Context, *injection.SubmitDatapackBuildingReq, string, int, *int) (*injection.SubmitDatapackBuildingResp, error) + CreateInjectionRecord(context.Context, *injection.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) + UpdateInjectionState(context.Context, *injection.RuntimeUpdateInjectionStateReq) error + UpdateInjectionTimestamps(context.Context, *injection.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) } type metricsReader interface { - GetInjectionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) - GetExecutionMetrics(context.Context, *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) + GetInjectionMetrics(context.Context, *metric.GetMetricsReq) (*metric.InjectionMetrics, error) + GetExecutionMetrics(context.Context, *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) } type taskReader interface { - GetDetail(context.Context, string) (*taskmodule.TaskDetailResp, error) - PollLogs(context.Context, string, time.Time) (*taskmodule.TaskLogPollResp, error) - List(context.Context, *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) + GetDetail(context.Context, string) (*task.TaskDetailResp, error) + PollLogs(context.Context, string, time.Time) (*task.TaskLogPollResp, error) + List(context.Context, *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) } type traceReader interface { - GetTrace(context.Context, string) (*tracemodule.TraceDetailResp, error) - ListTraces(context.Context, *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) + GetTrace(context.Context, string) (*trace.TraceDetailResp, error) + ListTraces(context.Context, *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) GetTraceStreamAlgorithms(context.Context, string) ([]dto.ContainerVersionItem, error) - ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) + ReadTraceStreamMessages(context.Context, string, string, int64, time.Duration) ([]goredis.XStream, error) } type groupReader interface { - GetGroupStats(context.Context, *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) + GetGroupStats(context.Context, *group.GetGroupStatsReq) (*group.GroupStats, error) GetGroupTraceCount(string) (int64, error) - ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) + ReadGroupStreamMessages(context.Context, string, string, int64, time.Duration) ([]goredis.XStream, error) } type notificationReader interface { - ReadStreamMessages(context.Context, string, string, int64, time.Duration) ([]redis.XStream, error) + ReadStreamMessages(context.Context, string, string, int64, time.Duration) ([]goredis.XStream, error) } type taskController interface { @@ -247,15 +247,15 @@ type orchestratorServer struct { } func newOrchestratorServer( - execution *executionmodule.Service, - injection *injectionmodule.Service, - metrics *metricmodule.Service, + execution *execution.Service, + injection *injection.Service, + metrics *metric.Service, projects projectStatisticsReader, tasks taskController, - taskRead *taskmodule.Service, - traceRead *tracemodule.Service, - groupRead *groupmodule.Service, - notify *notificationmodule.Service, + taskRead *task.Service, + traceRead *trace.Service, + groupRead *group.Service, + notify *notification.Service, ) *orchestratorServer { return &orchestratorServer{ execution: execution, @@ -284,7 +284,7 @@ func (s *orchestratorServer) SubmitExecution(ctx context.Context, req *orchestra return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[executionmodule.SubmitExecutionReq](req.GetBody()) + body, err := decodeBody[execution.SubmitExecutionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -328,7 +328,7 @@ func (s *orchestratorServer) SubmitFaultInjection(ctx context.Context, req *orch return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[injectionmodule.SubmitInjectionReq](req.GetBody()) + body, err := decodeBody[injection.SubmitInjectionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -372,7 +372,7 @@ func (s *orchestratorServer) SubmitDatapackBuilding(ctx context.Context, req *or return nil, status.Error(codes.InvalidArgument, "user_id is required") } - body, err := decodeBody[injectionmodule.SubmitDatapackBuildingReq](req.GetBody()) + body, err := decodeBody[injection.SubmitDatapackBuildingReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -401,7 +401,7 @@ func (s *orchestratorServer) SubmitDatapackBuilding(ctx context.Context, req *or } func (s *orchestratorServer) CreateExecution(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[executionmodule.RuntimeCreateExecutionReq](req.GetBody()) + body, err := decodeBody[execution.RuntimeCreateExecutionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -414,7 +414,7 @@ func (s *orchestratorServer) CreateExecution(ctx context.Context, req *orchestra } func (s *orchestratorServer) CreateInjection(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[injectionmodule.RuntimeCreateInjectionReq](req.GetBody()) + body, err := decodeBody[injection.RuntimeCreateInjectionReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -427,7 +427,7 @@ func (s *orchestratorServer) CreateInjection(ctx context.Context, req *orchestra } func (s *orchestratorServer) UpdateExecutionState(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[executionmodule.RuntimeUpdateExecutionStateReq](req.GetBody()) + body, err := decodeBody[execution.RuntimeUpdateExecutionStateReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -438,7 +438,7 @@ func (s *orchestratorServer) UpdateExecutionState(ctx context.Context, req *orch } func (s *orchestratorServer) UpdateInjectionState(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[injectionmodule.RuntimeUpdateInjectionStateReq](req.GetBody()) + body, err := decodeBody[injection.RuntimeUpdateInjectionStateReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -449,7 +449,7 @@ func (s *orchestratorServer) UpdateInjectionState(ctx context.Context, req *orch } func (s *orchestratorServer) UpdateInjectionTimestamps(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[injectionmodule.RuntimeUpdateInjectionTimestampReq](req.GetBody()) + body, err := decodeBody[injection.RuntimeUpdateInjectionTimestampReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -483,7 +483,7 @@ func (s *orchestratorServer) GetExecution(ctx context.Context, req *orchestrator } func (s *orchestratorServer) GetInjectionMetrics(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[metricmodule.GetMetricsReq](req.GetBody()) + body, err := decodeBody[metric.GetMetricsReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -498,7 +498,7 @@ func (s *orchestratorServer) GetInjectionMetrics(ctx context.Context, req *orche } func (s *orchestratorServer) GetExecutionMetrics(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[metricmodule.GetMetricsReq](req.GetBody()) + body, err := decodeBody[metric.GetMetricsReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -527,7 +527,7 @@ func (s *orchestratorServer) ListProjectStatistics(ctx context.Context, req *orc } func (s *orchestratorServer) ListEvaluationExecutionsByDatapack(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[executionmodule.EvaluationExecutionsByDatapackReq](req.GetBody()) + body, err := decodeBody[execution.EvaluationExecutionsByDatapackReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -539,7 +539,7 @@ func (s *orchestratorServer) ListEvaluationExecutionsByDatapack(ctx context.Cont } func (s *orchestratorServer) ListEvaluationExecutionsByDataset(ctx context.Context, req *orchestratorv1.MutationRequest) (*orchestratorv1.StructResponse, error) { - body, err := decodeBody[executionmodule.EvaluationExecutionsByDatasetReq](req.GetBody()) + body, err := decodeBody[execution.EvaluationExecutionsByDatasetReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -577,7 +577,7 @@ func (s *orchestratorServer) PollTaskLogs(ctx context.Context, req *orchestrator } func (s *orchestratorServer) ListTasks(ctx context.Context, req *orchestratorv1.ListTasksRequest) (*orchestratorv1.StructResponse, error) { - query, err := decodeQuery[taskmodule.ListTaskReq](req.GetQuery()) + query, err := decodeQuery[task.ListTaskReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -603,7 +603,7 @@ func (s *orchestratorServer) GetTrace(ctx context.Context, req *orchestratorv1.G } func (s *orchestratorServer) ListTraces(ctx context.Context, req *orchestratorv1.ListTracesRequest) (*orchestratorv1.StructResponse, error) { - query, err := decodeQuery[tracemodule.ListTraceReq](req.GetQuery()) + query, err := decodeQuery[trace.ListTraceReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -621,7 +621,7 @@ func (s *orchestratorServer) GetGroupStats(ctx context.Context, req *orchestrato if req.GetGroupId() == "" { return nil, status.Error(codes.InvalidArgument, "group_id is required") } - resp, err := s.groupRead.GetGroupStats(ctx, &groupmodule.GetGroupStatsReq{GroupID: req.GetGroupId()}) + resp, err := s.groupRead.GetGroupStats(ctx, &group.GetGroupStatsReq{GroupID: req.GetGroupId()}) if err != nil { return nil, mapOrchestratorError(err) } @@ -809,7 +809,7 @@ type streamMessageResp struct { Values map[string]any `json:"values"` } -func encodeStreamMessages(streams []redis.XStream) (*orchestratorv1.StructResponse, error) { +func encodeStreamMessages(streams []goredis.XStream) (*orchestratorv1.StructResponse, error) { messages := []streamMessageResp{} if len(streams) > 0 { messages = make([]streamMessageResp, 0, len(streams[0].Messages)) diff --git a/src/interface/grpcorchestrator/service_test.go b/src/interface/grpc/orchestrator/service_test.go similarity index 87% rename from src/interface/grpcorchestrator/service_test.go rename to src/interface/grpc/orchestrator/service_test.go index 507cbb8c..d60739c8 100644 --- a/src/interface/grpcorchestrator/service_test.go +++ b/src/interface/grpc/orchestrator/service_test.go @@ -1,4 +1,4 @@ -package grpcorchestratorinterface +package grpcorchestrator import ( "context" @@ -8,12 +8,12 @@ import ( "aegis/consts" "aegis/dto" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - metricmodule "aegis/module/metric" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + metric "aegis/module/metric" + task "aegis/module/task" + trace "aegis/module/trace" orchestratorv1 "aegis/proto/orchestrator/v1" "github.com/redis/go-redis/v9" @@ -23,49 +23,49 @@ import ( ) type executionSubmitterStub struct { - resp *executionmodule.SubmitExecutionResp + resp *execution.SubmitExecutionResp id int - item *executionmodule.ExecutionDetailResp - evaluationItems []executionmodule.EvaluationExecutionItem + item *execution.ExecutionDetailResp + evaluationItems []execution.EvaluationExecutionItem err error } -func (s executionSubmitterStub) SubmitAlgorithmExecution(_ context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { +func (s executionSubmitterStub) SubmitAlgorithmExecution(_ context.Context, req *execution.SubmitExecutionReq, groupID string, userID int) (*execution.SubmitExecutionResp, error) { if req.ProjectName == "" || groupID == "" || userID <= 0 { return nil, errors.New("unexpected request") } return s.resp, s.err } -func (s executionSubmitterStub) CreateExecutionRecord(_ context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { +func (s executionSubmitterStub) CreateExecutionRecord(_ context.Context, req *execution.RuntimeCreateExecutionReq) (int, error) { if req.TaskID == "" || req.AlgorithmVersionID <= 0 || req.DatapackID <= 0 { return 0, errors.New("unexpected runtime execution request") } return s.id, s.err } -func (s executionSubmitterStub) UpdateExecutionState(_ context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { +func (s executionSubmitterStub) UpdateExecutionState(_ context.Context, req *execution.RuntimeUpdateExecutionStateReq) error { if req.ExecutionID <= 0 { return errors.New("unexpected execution state request") } return s.err } -func (s executionSubmitterStub) GetExecution(_ context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { +func (s executionSubmitterStub) GetExecution(_ context.Context, executionID int) (*execution.ExecutionDetailResp, error) { if executionID <= 0 { return nil, errors.New("missing execution id") } return s.item, s.err } -func (s executionSubmitterStub) ListEvaluationExecutionsByDatapack(_ context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (s executionSubmitterStub) ListEvaluationExecutionsByDatapack(_ context.Context, req *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) { if req.AlgorithmVersionID <= 0 || req.DatapackName == "" { return nil, errors.New("unexpected datapack evaluation query") } return s.evaluationItems, s.err } -func (s executionSubmitterStub) ListEvaluationExecutionsByDataset(_ context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (s executionSubmitterStub) ListEvaluationExecutionsByDataset(_ context.Context, req *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) { if req.AlgorithmVersionID <= 0 || req.DatasetVersionID <= 0 { return nil, errors.New("unexpected dataset evaluation query") } @@ -73,13 +73,13 @@ func (s executionSubmitterStub) ListEvaluationExecutionsByDataset(_ context.Cont } type injectionSubmitterStub struct { - injectionResp *injectionmodule.SubmitInjectionResp - buildResp *injectionmodule.SubmitDatapackBuildingResp + injectionResp *injection.SubmitInjectionResp + buildResp *injection.SubmitDatapackBuildingResp item *dto.InjectionItem err error } -func (s injectionSubmitterStub) SubmitFaultInjection(_ context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { +func (s injectionSubmitterStub) SubmitFaultInjection(_ context.Context, req *injection.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injection.SubmitInjectionResp, error) { if req.Pedestal == nil || req.Benchmark == nil || groupID == "" || userID <= 0 { return nil, errors.New("unexpected injection request") } @@ -89,7 +89,7 @@ func (s injectionSubmitterStub) SubmitFaultInjection(_ context.Context, req *inj return s.injectionResp, s.err } -func (s injectionSubmitterStub) SubmitDatapackBuilding(_ context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { +func (s injectionSubmitterStub) SubmitDatapackBuilding(_ context.Context, req *injection.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injection.SubmitDatapackBuildingResp, error) { if len(req.Specs) == 0 || groupID == "" || userID <= 0 { return nil, errors.New("unexpected datapack request") } @@ -99,7 +99,7 @@ func (s injectionSubmitterStub) SubmitDatapackBuilding(_ context.Context, req *i return s.buildResp, s.err } -func (s injectionSubmitterStub) CreateInjectionRecord(_ context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { +func (s injectionSubmitterStub) CreateInjectionRecord(_ context.Context, req *injection.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { if req.Name == "" || req.TaskID == "" { return nil, errors.New("unexpected runtime injection request") } @@ -107,33 +107,33 @@ func (s injectionSubmitterStub) CreateInjectionRecord(_ context.Context, req *in } type metricsReaderStub struct { - injection *metricmodule.InjectionMetrics - execution *metricmodule.ExecutionMetrics + injection *metric.InjectionMetrics + execution *metric.ExecutionMetrics err error } -func (s metricsReaderStub) GetInjectionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { +func (s metricsReaderStub) GetInjectionMetrics(_ context.Context, req *metric.GetMetricsReq) (*metric.InjectionMetrics, error) { if req == nil { return nil, errors.New("nil request") } return s.injection, s.err } -func (s metricsReaderStub) GetExecutionMetrics(_ context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { +func (s metricsReaderStub) GetExecutionMetrics(_ context.Context, req *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) { if req == nil { return nil, errors.New("nil request") } return s.execution, s.err } -func (s injectionSubmitterStub) UpdateInjectionState(_ context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { +func (s injectionSubmitterStub) UpdateInjectionState(_ context.Context, req *injection.RuntimeUpdateInjectionStateReq) error { if req.Name == "" { return errors.New("unexpected injection state request") } return s.err } -func (s injectionSubmitterStub) UpdateInjectionTimestamps(_ context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { +func (s injectionSubmitterStub) UpdateInjectionTimestamps(_ context.Context, req *injection.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { if req.Name == "" { return nil, errors.New("unexpected injection timestamp request") } @@ -175,23 +175,23 @@ func (s taskControllerStub) ListDeadLetterTasks(_ context.Context, limit int64) } type taskReaderStub struct { - detail *taskmodule.TaskDetailResp - list *dto.ListResp[taskmodule.TaskResp] + detail *task.TaskDetailResp + list *dto.ListResp[task.TaskResp] err error } -func (s taskReaderStub) GetDetail(_ context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { +func (s taskReaderStub) GetDetail(_ context.Context, taskID string) (*task.TaskDetailResp, error) { if taskID == "" { return nil, errors.New("missing task id") } return s.detail, s.err } -func (s taskReaderStub) PollLogs(_ context.Context, taskID string, _ time.Time) (*taskmodule.TaskLogPollResp, error) { +func (s taskReaderStub) PollLogs(_ context.Context, taskID string, _ time.Time) (*task.TaskLogPollResp, error) { if taskID == "" { return nil, errors.New("missing task id") } - return &taskmodule.TaskLogPollResp{ + return &task.TaskLogPollResp{ Logs: []dto.LogEntry{{TaskID: taskID, Line: "hello"}}, Terminal: false, State: consts.GetTaskStateName(consts.TaskPending), @@ -199,7 +199,7 @@ func (s taskReaderStub) PollLogs(_ context.Context, taskID string, _ time.Time) }, s.err } -func (s taskReaderStub) List(_ context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { +func (s taskReaderStub) List(_ context.Context, req *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) { if req == nil { return nil, errors.New("nil request") } @@ -207,21 +207,21 @@ func (s taskReaderStub) List(_ context.Context, req *taskmodule.ListTaskReq) (*d } type traceReaderStub struct { - detail *tracemodule.TraceDetailResp - list *dto.ListResp[tracemodule.TraceResp] + detail *trace.TraceDetailResp + list *dto.ListResp[trace.TraceResp] algorithms []dto.ContainerVersionItem messages []redis.XStream err error } -func (s traceReaderStub) GetTrace(_ context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { +func (s traceReaderStub) GetTrace(_ context.Context, traceID string) (*trace.TraceDetailResp, error) { if traceID == "" { return nil, errors.New("missing trace id") } return s.detail, s.err } -func (s traceReaderStub) ListTraces(_ context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { +func (s traceReaderStub) ListTraces(_ context.Context, req *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) { if req == nil { return nil, errors.New("nil request") } @@ -243,13 +243,13 @@ func (s traceReaderStub) ReadTraceStreamMessages(_ context.Context, streamKey, _ } type groupReaderStub struct { - stats *groupmodule.GroupStats + stats *group.GroupStats count int64 messages []redis.XStream err error } -func (s groupReaderStub) GetGroupStats(_ context.Context, req *groupmodule.GetGroupStatsReq) (*groupmodule.GroupStats, error) { +func (s groupReaderStub) GetGroupStats(_ context.Context, req *group.GetGroupStatsReq) (*group.GroupStats, error) { if req == nil || req.GroupID == "" { return nil, errors.New("missing group id") } @@ -287,9 +287,9 @@ func (s notificationReaderStub) ReadStreamMessages(_ context.Context, streamKey, func TestOrchestratorServerSubmitExecution(t *testing.T) { server := &orchestratorServer{ - execution: executionSubmitterStub{resp: &executionmodule.SubmitExecutionResp{ + execution: executionSubmitterStub{resp: &execution.SubmitExecutionResp{ GroupID: "group-1", - Items: []executionmodule.SubmitExecutionItem{{ + Items: []execution.SubmitExecutionItem{{ Index: 0, TraceID: "trace-1", TaskID: "task-1", @@ -338,15 +338,15 @@ func TestOrchestratorServerSubmitExecution(t *testing.T) { func TestOrchestratorServerSubmitFaultInjection(t *testing.T) { server := &orchestratorServer{ execution: executionSubmitterStub{}, - injection: injectionSubmitterStub{injectionResp: &injectionmodule.SubmitInjectionResp{ + injection: injectionSubmitterStub{injectionResp: &injection.SubmitInjectionResp{ GroupID: "group-2", OriginalCount: 1, - Items: []injectionmodule.SubmitInjectionItem{{ + Items: []injection.SubmitInjectionItem{{ Index: 0, TraceID: "trace-2", TaskID: "task-2", }}, - Warnings: &injectionmodule.InjectionWarnings{ + Warnings: &injection.InjectionWarnings{ DuplicateServicesInBatch: []string{"svc-a"}, }, }}, @@ -402,20 +402,20 @@ func TestOrchestratorServerRuntimeMutations(t *testing.T) { server := &orchestratorServer{ execution: executionSubmitterStub{ id: 22, - item: &executionmodule.ExecutionDetailResp{ - ExecutionResp: executionmodule.ExecutionResp{ID: 22}, + item: &execution.ExecutionDetailResp{ + ExecutionResp: execution.ExecutionResp{ID: 22}, }, - evaluationItems: []executionmodule.EvaluationExecutionItem{{ + evaluationItems: []execution.EvaluationExecutionItem{{ Datapack: "dp-1", - ExecutionRef: executionmodule.ExecutionRef{ + ExecutionRef: execution.ExecutionRef{ ExecutionID: 22, }, }}, }, injection: injectionSubmitterStub{item: injectionItem}, metrics: metricsReaderStub{ - injection: &metricmodule.InjectionMetrics{TotalCount: 3}, - execution: &metricmodule.ExecutionMetrics{TotalCount: 4}, + injection: &metric.InjectionMetrics{TotalCount: 3}, + execution: &metric.ExecutionMetrics{TotalCount: 4}, }, tasks: taskControllerStub{}, taskRead: taskReaderStub{}, @@ -605,8 +605,8 @@ func TestOrchestratorServerGetTask(t *testing.T) { injection: injectionSubmitterStub{}, metrics: metricsReaderStub{}, tasks: taskControllerStub{}, - taskRead: taskReaderStub{detail: &taskmodule.TaskDetailResp{ - TaskResp: taskmodule.TaskResp{ + taskRead: taskReaderStub{detail: &task.TaskDetailResp{ + TaskResp: task.TaskResp{ ID: "task-1", Type: consts.GetTaskTypeName(consts.TaskTypeRunAlgorithm), State: consts.GetTaskStateName(consts.TaskPending), @@ -654,8 +654,8 @@ func TestOrchestratorServerListTasks(t *testing.T) { injection: injectionSubmitterStub{}, metrics: metricsReaderStub{}, tasks: taskControllerStub{}, - taskRead: taskReaderStub{list: &dto.ListResp[taskmodule.TaskResp]{ - Items: []taskmodule.TaskResp{{ID: "task-1"}}, + taskRead: taskReaderStub{list: &dto.ListResp[task.TaskResp]{ + Items: []task.TaskResp{{ID: "task-1"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -687,8 +687,8 @@ func TestOrchestratorServerGetTrace(t *testing.T) { metrics: metricsReaderStub{}, tasks: taskControllerStub{}, taskRead: taskReaderStub{}, - traceRead: traceReaderStub{detail: &tracemodule.TraceDetailResp{ - TraceResp: tracemodule.TraceResp{ + traceRead: traceReaderStub{detail: &trace.TraceDetailResp{ + TraceResp: trace.TraceResp{ ID: "trace-1", Type: "full_pipeline", GroupID: "group-1", @@ -718,8 +718,8 @@ func TestOrchestratorServerListTraces(t *testing.T) { metrics: metricsReaderStub{}, tasks: taskControllerStub{}, taskRead: taskReaderStub{}, - traceRead: traceReaderStub{list: &dto.ListResp[tracemodule.TraceResp]{ - Items: []tracemodule.TraceResp{{ID: "trace-1"}}, + traceRead: traceReaderStub{list: &dto.ListResp[trace.TraceResp]{ + Items: []trace.TraceResp{{ID: "trace-1"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -751,7 +751,7 @@ func TestOrchestratorServerGetGroupStats(t *testing.T) { tasks: taskControllerStub{}, taskRead: taskReaderStub{}, traceRead: traceReaderStub{}, - groupRead: groupReaderStub{stats: &groupmodule.GroupStats{ + groupRead: groupReaderStub{stats: &group.GroupStats{ TotalTraces: 3, AvgDuration: 4.5, }}, diff --git a/src/interface/grpcresource/lifecycle.go b/src/interface/grpc/resource/lifecycle.go similarity index 98% rename from src/interface/grpcresource/lifecycle.go rename to src/interface/grpc/resource/lifecycle.go index d18b02a3..dcf5ea7a 100644 --- a/src/interface/grpcresource/lifecycle.go +++ b/src/interface/grpc/resource/lifecycle.go @@ -1,4 +1,4 @@ -package grpcresourceinterface +package grpcresource import ( "context" diff --git a/src/interface/grpcresource/module.go b/src/interface/grpc/resource/module.go similarity index 83% rename from src/interface/grpcresource/module.go rename to src/interface/grpc/resource/module.go index ffa1859e..6b32ec3a 100644 --- a/src/interface/grpcresource/module.go +++ b/src/interface/grpc/resource/module.go @@ -1,4 +1,4 @@ -package grpcresourceinterface +package grpcresource import "go.uber.org/fx" diff --git a/src/interface/grpcresource/service.go b/src/interface/grpc/resource/service.go similarity index 80% rename from src/interface/grpcresource/service.go rename to src/interface/grpc/resource/service.go index 422911a8..0969a239 100644 --- a/src/interface/grpcresource/service.go +++ b/src/interface/grpc/resource/service.go @@ -1,4 +1,4 @@ -package grpcresourceinterface +package grpcresource import ( "context" @@ -8,12 +8,12 @@ import ( "aegis/consts" "aegis/dto" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - labelmodule "aegis/module/label" - projectmodule "aegis/module/project" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + label "aegis/module/label" + project "aegis/module/project" resourcev1 "aegis/proto/resource/v1" "google.golang.org/grpc/codes" @@ -25,49 +25,49 @@ import ( const resourceServiceName = "resource-service" type projectReader interface { - GetProjectDetail(context.Context, int) (*projectmodule.ProjectDetailResp, error) - ListProjects(context.Context, *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) + GetProjectDetail(context.Context, int) (*project.ProjectDetailResp, error) + ListProjects(context.Context, *project.ListProjectReq) (*dto.ListResp[project.ProjectResp], error) } type containerReader interface { - GetContainer(context.Context, int) (*containermodule.ContainerDetailResp, error) - ListContainers(context.Context, *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) + GetContainer(context.Context, int) (*container.ContainerDetailResp, error) + ListContainers(context.Context, *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) } type datasetReader interface { - GetDataset(context.Context, int) (*datasetmodule.DatasetDetailResp, error) - ListDatasets(context.Context, *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) + GetDataset(context.Context, int) (*dataset.DatasetDetailResp, error) + ListDatasets(context.Context, *dataset.ListDatasetReq) (*dto.ListResp[dataset.DatasetResp], error) } type evaluationReader interface { - ListDatapackEvaluationResults(context.Context, *evaluationmodule.BatchEvaluateDatapackReq, int) (*evaluationmodule.BatchEvaluateDatapackResp, error) - ListDatasetEvaluationResults(context.Context, *evaluationmodule.BatchEvaluateDatasetReq, int) (*evaluationmodule.BatchEvaluateDatasetResp, error) - ListEvaluations(context.Context, *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) - GetEvaluation(context.Context, int) (*evaluationmodule.EvaluationResp, error) + ListDatapackEvaluationResults(context.Context, *evaluation.BatchEvaluateDatapackReq, int) (*evaluation.BatchEvaluateDatapackResp, error) + ListDatasetEvaluationResults(context.Context, *evaluation.BatchEvaluateDatasetReq, int) (*evaluation.BatchEvaluateDatasetResp, error) + ListEvaluations(context.Context, *evaluation.ListEvaluationReq) (*dto.ListResp[evaluation.EvaluationResp], error) + GetEvaluation(context.Context, int) (*evaluation.EvaluationResp, error) DeleteEvaluation(context.Context, int) error } type labelReader interface { BatchDelete(context.Context, []int) error - Create(context.Context, *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) + Create(context.Context, *label.CreateLabelReq) (*label.LabelResp, error) Delete(context.Context, int) error - GetDetail(context.Context, int) (*labelmodule.LabelDetailResp, error) - List(context.Context, *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) - Update(context.Context, *labelmodule.UpdateLabelReq, int) (*labelmodule.LabelResp, error) + GetDetail(context.Context, int) (*label.LabelDetailResp, error) + List(context.Context, *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) + Update(context.Context, *label.UpdateLabelReq, int) (*label.LabelResp, error) } type chaosSystemReader interface { - ListSystems(context.Context, *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) - GetSystem(context.Context, int) (*chaossystemmodule.ChaosSystemResp, error) - CreateSystem(context.Context, *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) - UpdateSystem(context.Context, int, *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) + ListSystems(context.Context, *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) + GetSystem(context.Context, int) (*chaossystem.ChaosSystemResp, error) + CreateSystem(context.Context, *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) + UpdateSystem(context.Context, int, *chaossystem.UpdateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) DeleteSystem(context.Context, int) error - UpsertMetadata(context.Context, int, *chaossystemmodule.BulkUpsertSystemMetadataReq) error - ListMetadata(context.Context, int, string) ([]chaossystemmodule.SystemMetadataResp, error) + UpsertMetadata(context.Context, int, *chaossystem.BulkUpsertSystemMetadataReq) error + ListMetadata(context.Context, int, string) ([]chaossystem.SystemMetadataResp, error) } type chaosSystemMetadataListResponse struct { - Items []chaossystemmodule.SystemMetadataResp `json:"items"` + Items []chaossystem.SystemMetadataResp `json:"items"` } type resourceServer struct { @@ -81,12 +81,12 @@ type resourceServer struct { } func newResourceServer( - projects *projectmodule.Service, - containers *containermodule.Service, - datasets *datasetmodule.Service, - labels labelmodule.HandlerService, - chaosSystems chaossystemmodule.HandlerService, - evaluations *evaluationmodule.Service, + projects *project.Service, + containers *container.Service, + datasets *dataset.Service, + labels label.HandlerService, + chaosSystems chaossystem.HandlerService, + evaluations *evaluation.Service, ) *resourceServer { return &resourceServer{ projects: projects, @@ -108,7 +108,7 @@ func (s *resourceServer) Ping(context.Context, *resourcev1.PingRequest) (*resour } func (s *resourceServer) ListProjects(ctx context.Context, req *resourcev1.ListProjectsRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[projectmodule.ListProjectReq](req.GetQuery()) + query, err := decodeQuery[project.ListProjectReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -136,7 +136,7 @@ func (s *resourceServer) GetProject(ctx context.Context, req *resourcev1.GetReso } func (s *resourceServer) ListContainers(ctx context.Context, req *resourcev1.ListContainersRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[containermodule.ListContainerReq](req.GetQuery()) + query, err := decodeQuery[container.ListContainerReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -164,7 +164,7 @@ func (s *resourceServer) GetContainer(ctx context.Context, req *resourcev1.GetRe } func (s *resourceServer) ListDatasets(ctx context.Context, req *resourcev1.ListDatasetsRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[datasetmodule.ListDatasetReq](req.GetQuery()) + query, err := decodeQuery[dataset.ListDatasetReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -192,7 +192,7 @@ func (s *resourceServer) GetDataset(ctx context.Context, req *resourcev1.GetReso } func (s *resourceServer) CreateLabel(ctx context.Context, req *resourcev1.MutationRequest) (*resourcev1.ResourceItemResponse, error) { - body, err := decodeQuery[labelmodule.CreateLabelReq](req.GetBody()) + body, err := decodeQuery[label.CreateLabelReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -220,7 +220,7 @@ func (s *resourceServer) GetLabel(ctx context.Context, req *resourcev1.GetResour } func (s *resourceServer) ListLabels(ctx context.Context, req *resourcev1.QueryRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[labelmodule.ListLabelReq](req.GetQuery()) + query, err := decodeQuery[label.ListLabelReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -240,7 +240,7 @@ func (s *resourceServer) UpdateLabel(ctx context.Context, req *resourcev1.Update return nil, status.Error(codes.InvalidArgument, "id is required") } - body, err := decodeQuery[labelmodule.UpdateLabelReq](req.GetBody()) + body, err := decodeQuery[label.UpdateLabelReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -276,7 +276,7 @@ func (s *resourceServer) BatchDeleteLabels(ctx context.Context, req *resourcev1. } func (s *resourceServer) ListChaosSystems(ctx context.Context, req *resourcev1.QueryRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[chaossystemmodule.ListChaosSystemReq](req.GetQuery()) + query, err := decodeQuery[chaossystem.ListChaosSystemReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -304,7 +304,7 @@ func (s *resourceServer) GetChaosSystem(ctx context.Context, req *resourcev1.Get } func (s *resourceServer) CreateChaosSystem(ctx context.Context, req *resourcev1.MutationRequest) (*resourcev1.ResourceItemResponse, error) { - body, err := decodeQuery[chaossystemmodule.CreateChaosSystemReq](req.GetBody()) + body, err := decodeQuery[chaossystem.CreateChaosSystemReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -321,7 +321,7 @@ func (s *resourceServer) UpdateChaosSystem(ctx context.Context, req *resourcev1. return nil, status.Error(codes.InvalidArgument, "id is required") } - body, err := decodeQuery[chaossystemmodule.UpdateChaosSystemReq](req.GetBody()) + body, err := decodeQuery[chaossystem.UpdateChaosSystemReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -348,7 +348,7 @@ func (s *resourceServer) UpsertChaosSystemMetadata(ctx context.Context, req *res return nil, status.Error(codes.InvalidArgument, "id is required") } - body, err := decodeQuery[chaossystemmodule.BulkUpsertSystemMetadataReq](req.GetBody()) + body, err := decodeQuery[chaossystem.BulkUpsertSystemMetadataReq](req.GetBody()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -382,7 +382,7 @@ func (s *resourceServer) ListDatapackEvaluationResults(ctx context.Context, req return nil, status.Error(codes.InvalidArgument, "user_id is required") } - query, err := decodeQuery[evaluationmodule.BatchEvaluateDatapackReq](req.GetQuery()) + query, err := decodeQuery[evaluation.BatchEvaluateDatapackReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -402,7 +402,7 @@ func (s *resourceServer) ListDatasetEvaluationResults(ctx context.Context, req * return nil, status.Error(codes.InvalidArgument, "user_id is required") } - query, err := decodeQuery[evaluationmodule.BatchEvaluateDatasetReq](req.GetQuery()) + query, err := decodeQuery[evaluation.BatchEvaluateDatasetReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -418,7 +418,7 @@ func (s *resourceServer) ListDatasetEvaluationResults(ctx context.Context, req * } func (s *resourceServer) ListEvaluations(ctx context.Context, req *resourcev1.ListEvaluationsRequest) (*resourcev1.ResourceListResponse, error) { - query, err := decodeQuery[evaluationmodule.ListEvaluationReq](req.GetQuery()) + query, err := decodeQuery[evaluation.ListEvaluationReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } diff --git a/src/interface/grpcresource/service_test.go b/src/interface/grpc/resource/service_test.go similarity index 77% rename from src/interface/grpcresource/service_test.go rename to src/interface/grpc/resource/service_test.go index c3353083..c3bec150 100644 --- a/src/interface/grpcresource/service_test.go +++ b/src/interface/grpc/resource/service_test.go @@ -1,4 +1,4 @@ -package grpcresourceinterface +package grpcresource import ( "context" @@ -7,12 +7,12 @@ import ( "aegis/consts" "aegis/dto" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - labelmodule "aegis/module/label" - projectmodule "aegis/module/project" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + label "aegis/module/label" + project "aegis/module/project" resourcev1 "aegis/proto/resource/v1" "google.golang.org/grpc/codes" @@ -21,19 +21,19 @@ import ( ) type projectReaderStub struct { - listResp *dto.ListResp[projectmodule.ProjectResp] - getResp *projectmodule.ProjectDetailResp + listResp *dto.ListResp[project.ProjectResp] + getResp *project.ProjectDetailResp err error } -func (s projectReaderStub) GetProjectDetail(_ context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { +func (s projectReaderStub) GetProjectDetail(_ context.Context, projectID int) (*project.ProjectDetailResp, error) { if projectID <= 0 { return nil, errors.New("invalid id") } return s.getResp, s.err } -func (s projectReaderStub) ListProjects(_ context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { +func (s projectReaderStub) ListProjects(_ context.Context, req *project.ListProjectReq) (*dto.ListResp[project.ProjectResp], error) { if req == nil { return nil, errors.New("nil request") } @@ -41,19 +41,19 @@ func (s projectReaderStub) ListProjects(_ context.Context, req *projectmodule.Li } type containerReaderStub struct { - listResp *dto.ListResp[containermodule.ContainerResp] - getResp *containermodule.ContainerDetailResp + listResp *dto.ListResp[container.ContainerResp] + getResp *container.ContainerDetailResp err error } -func (s containerReaderStub) GetContainer(_ context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { +func (s containerReaderStub) GetContainer(_ context.Context, containerID int) (*container.ContainerDetailResp, error) { if containerID <= 0 { return nil, errors.New("invalid id") } return s.getResp, s.err } -func (s containerReaderStub) ListContainers(_ context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { +func (s containerReaderStub) ListContainers(_ context.Context, req *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) { if req == nil { return nil, errors.New("nil request") } @@ -61,19 +61,19 @@ func (s containerReaderStub) ListContainers(_ context.Context, req *containermod } type datasetReaderStub struct { - listResp *dto.ListResp[datasetmodule.DatasetResp] - getResp *datasetmodule.DatasetDetailResp + listResp *dto.ListResp[dataset.DatasetResp] + getResp *dataset.DatasetDetailResp err error } -func (s datasetReaderStub) GetDataset(_ context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { +func (s datasetReaderStub) GetDataset(_ context.Context, datasetID int) (*dataset.DatasetDetailResp, error) { if datasetID <= 0 { return nil, errors.New("invalid id") } return s.getResp, s.err } -func (s datasetReaderStub) ListDatasets(_ context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { +func (s datasetReaderStub) ListDatasets(_ context.Context, req *dataset.ListDatasetReq) (*dto.ListResp[dataset.DatasetResp], error) { if req == nil { return nil, errors.New("nil request") } @@ -81,35 +81,35 @@ func (s datasetReaderStub) ListDatasets(_ context.Context, req *datasetmodule.Li } type evaluationReaderStub struct { - datapackResp *evaluationmodule.BatchEvaluateDatapackResp - datasetResp *evaluationmodule.BatchEvaluateDatasetResp - listResp *dto.ListResp[evaluationmodule.EvaluationResp] - getResp *evaluationmodule.EvaluationResp + datapackResp *evaluation.BatchEvaluateDatapackResp + datasetResp *evaluation.BatchEvaluateDatasetResp + listResp *dto.ListResp[evaluation.EvaluationResp] + getResp *evaluation.EvaluationResp err error } -func (s evaluationReaderStub) ListDatapackEvaluationResults(_ context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { +func (s evaluationReaderStub) ListDatapackEvaluationResults(_ context.Context, req *evaluation.BatchEvaluateDatapackReq, userID int) (*evaluation.BatchEvaluateDatapackResp, error) { if req == nil || userID <= 0 { return nil, errors.New("invalid request") } return s.datapackResp, s.err } -func (s evaluationReaderStub) ListDatasetEvaluationResults(_ context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { +func (s evaluationReaderStub) ListDatasetEvaluationResults(_ context.Context, req *evaluation.BatchEvaluateDatasetReq, userID int) (*evaluation.BatchEvaluateDatasetResp, error) { if req == nil || userID <= 0 { return nil, errors.New("invalid request") } return s.datasetResp, s.err } -func (s evaluationReaderStub) ListEvaluations(_ context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { +func (s evaluationReaderStub) ListEvaluations(_ context.Context, req *evaluation.ListEvaluationReq) (*dto.ListResp[evaluation.EvaluationResp], error) { if req == nil { return nil, errors.New("nil request") } return s.listResp, s.err } -func (s evaluationReaderStub) GetEvaluation(_ context.Context, id int) (*evaluationmodule.EvaluationResp, error) { +func (s evaluationReaderStub) GetEvaluation(_ context.Context, id int) (*evaluation.EvaluationResp, error) { if id <= 0 { return nil, errors.New("invalid id") } @@ -124,9 +124,9 @@ func (s evaluationReaderStub) DeleteEvaluation(_ context.Context, id int) error } type labelReaderStub struct { - listResp *dto.ListResp[labelmodule.LabelResp] - getResp *labelmodule.LabelDetailResp - itemResp *labelmodule.LabelResp + listResp *dto.ListResp[label.LabelResp] + getResp *label.LabelDetailResp + itemResp *label.LabelResp err error } @@ -137,7 +137,7 @@ func (s labelReaderStub) BatchDelete(_ context.Context, ids []int) error { return s.err } -func (s labelReaderStub) Create(_ context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { +func (s labelReaderStub) Create(_ context.Context, req *label.CreateLabelReq) (*label.LabelResp, error) { if req == nil { return nil, errors.New("nil request") } @@ -151,21 +151,21 @@ func (s labelReaderStub) Delete(_ context.Context, id int) error { return s.err } -func (s labelReaderStub) GetDetail(_ context.Context, id int) (*labelmodule.LabelDetailResp, error) { +func (s labelReaderStub) GetDetail(_ context.Context, id int) (*label.LabelDetailResp, error) { if id <= 0 { return nil, errors.New("invalid id") } return s.getResp, s.err } -func (s labelReaderStub) List(_ context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { +func (s labelReaderStub) List(_ context.Context, req *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) { if req == nil { return nil, errors.New("nil request") } return s.listResp, s.err } -func (s labelReaderStub) Update(_ context.Context, req *labelmodule.UpdateLabelReq, id int) (*labelmodule.LabelResp, error) { +func (s labelReaderStub) Update(_ context.Context, req *label.UpdateLabelReq, id int) (*label.LabelResp, error) { if req == nil || id <= 0 { return nil, errors.New("invalid request") } @@ -173,34 +173,34 @@ func (s labelReaderStub) Update(_ context.Context, req *labelmodule.UpdateLabelR } type chaosSystemReaderStub struct { - listResp *dto.ListResp[chaossystemmodule.ChaosSystemResp] - getResp *chaossystemmodule.ChaosSystemResp - metadataResp []chaossystemmodule.SystemMetadataResp + listResp *dto.ListResp[chaossystem.ChaosSystemResp] + getResp *chaossystem.ChaosSystemResp + metadataResp []chaossystem.SystemMetadataResp err error } -func (s chaosSystemReaderStub) ListSystems(_ context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { +func (s chaosSystemReaderStub) ListSystems(_ context.Context, req *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) { if req == nil { return nil, errors.New("nil request") } return s.listResp, s.err } -func (s chaosSystemReaderStub) GetSystem(_ context.Context, id int) (*chaossystemmodule.ChaosSystemResp, error) { +func (s chaosSystemReaderStub) GetSystem(_ context.Context, id int) (*chaossystem.ChaosSystemResp, error) { if id <= 0 { return nil, errors.New("invalid id") } return s.getResp, s.err } -func (s chaosSystemReaderStub) CreateSystem(_ context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { +func (s chaosSystemReaderStub) CreateSystem(_ context.Context, req *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { if req == nil { return nil, errors.New("nil request") } return s.getResp, s.err } -func (s chaosSystemReaderStub) UpdateSystem(_ context.Context, id int, req *chaossystemmodule.UpdateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { +func (s chaosSystemReaderStub) UpdateSystem(_ context.Context, id int, req *chaossystem.UpdateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { if id <= 0 || req == nil { return nil, errors.New("invalid request") } @@ -214,14 +214,14 @@ func (s chaosSystemReaderStub) DeleteSystem(_ context.Context, id int) error { return s.err } -func (s chaosSystemReaderStub) UpsertMetadata(_ context.Context, id int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { +func (s chaosSystemReaderStub) UpsertMetadata(_ context.Context, id int, req *chaossystem.BulkUpsertSystemMetadataReq) error { if id <= 0 || req == nil { return errors.New("invalid request") } return s.err } -func (s chaosSystemReaderStub) ListMetadata(_ context.Context, id int, _ string) ([]chaossystemmodule.SystemMetadataResp, error) { +func (s chaosSystemReaderStub) ListMetadata(_ context.Context, id int, _ string) ([]chaossystem.SystemMetadataResp, error) { if id <= 0 { return nil, errors.New("invalid id") } @@ -230,8 +230,8 @@ func (s chaosSystemReaderStub) ListMetadata(_ context.Context, id int, _ string) func TestResourceServerListProjects(t *testing.T) { server := &resourceServer{ - projects: projectReaderStub{listResp: &dto.ListResp[projectmodule.ProjectResp]{ - Items: []projectmodule.ProjectResp{{ID: 1, Name: "demo"}}, + projects: projectReaderStub{listResp: &dto.ListResp[project.ProjectResp]{ + Items: []project.ProjectResp{{ID: 1, Name: "demo"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -338,8 +338,8 @@ func TestResourceServerListEvaluations(t *testing.T) { datasets: datasetReaderStub{}, labels: labelReaderStub{}, chaosSystems: chaosSystemReaderStub{}, - evaluations: evaluationReaderStub{listResp: &dto.ListResp[evaluationmodule.EvaluationResp]{ - Items: []evaluationmodule.EvaluationResp{{ID: 3, EvalType: consts.EvalTypeDataset}}, + evaluations: evaluationReaderStub{listResp: &dto.ListResp[evaluation.EvaluationResp]{ + Items: []evaluation.EvaluationResp{{ID: 3, EvalType: consts.EvalTypeDataset}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -365,8 +365,8 @@ func TestResourceServerListLabels(t *testing.T) { projects: projectReaderStub{}, containers: containerReaderStub{}, datasets: datasetReaderStub{}, - labels: labelReaderStub{listResp: &dto.ListResp[labelmodule.LabelResp]{ - Items: []labelmodule.LabelResp{{ID: 9, Key: "env", Value: "prod"}}, + labels: labelReaderStub{listResp: &dto.ListResp[label.LabelResp]{ + Items: []label.LabelResp{{ID: 9, Key: "env", Value: "prod"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -414,8 +414,8 @@ func TestResourceServerListChaosSystems(t *testing.T) { containers: containerReaderStub{}, datasets: datasetReaderStub{}, labels: labelReaderStub{}, - chaosSystems: chaosSystemReaderStub{listResp: &dto.ListResp[chaossystemmodule.ChaosSystemResp]{ - Items: []chaossystemmodule.ChaosSystemResp{{ID: 4, Name: "k8s"}}, + chaosSystems: chaosSystemReaderStub{listResp: &dto.ListResp[chaossystem.ChaosSystemResp]{ + Items: []chaossystem.ChaosSystemResp{{ID: 4, Name: "k8s"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, @@ -443,7 +443,7 @@ func TestResourceServerListChaosSystemMetadata(t *testing.T) { containers: containerReaderStub{}, datasets: datasetReaderStub{}, labels: labelReaderStub{}, - chaosSystems: chaosSystemReaderStub{metadataResp: []chaossystemmodule.SystemMetadataResp{ + chaosSystems: chaosSystemReaderStub{metadataResp: []chaossystem.SystemMetadataResp{ {ID: 1, SystemName: "k8s", MetadataType: "service", ServiceName: "api"}, }}, evaluations: evaluationReaderStub{}, diff --git a/src/interface/grpcruntime/lifecycle.go b/src/interface/grpc/runtime/lifecycle.go similarity index 98% rename from src/interface/grpcruntime/lifecycle.go rename to src/interface/grpc/runtime/lifecycle.go index 331364d7..52e90274 100644 --- a/src/interface/grpcruntime/lifecycle.go +++ b/src/interface/grpc/runtime/lifecycle.go @@ -1,4 +1,4 @@ -package grpcruntimeinterface +package grpcruntime import ( "context" diff --git a/src/interface/grpcruntime/module.go b/src/interface/grpc/runtime/module.go similarity index 83% rename from src/interface/grpcruntime/module.go rename to src/interface/grpc/runtime/module.go index 4e22597e..0aecb71c 100644 --- a/src/interface/grpcruntime/module.go +++ b/src/interface/grpc/runtime/module.go @@ -1,4 +1,4 @@ -package grpcruntimeinterface +package grpcruntime import "go.uber.org/fx" diff --git a/src/interface/grpcruntime/service.go b/src/interface/grpc/runtime/service.go similarity index 83% rename from src/interface/grpcruntime/service.go rename to src/interface/grpc/runtime/service.go index 93e52b0b..0ebf4648 100644 --- a/src/interface/grpcruntime/service.go +++ b/src/interface/grpc/runtime/service.go @@ -1,4 +1,4 @@ -package grpcruntimeinterface +package grpcruntime import ( "context" @@ -8,11 +8,11 @@ import ( "aegis/consts" "aegis/dto" - buildkitinfra "aegis/infra/buildkit" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" - taskmodule "aegis/module/task" + buildkit "aegis/infra/buildkit" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" + task "aegis/module/task" runtimev1 "aegis/proto/runtime/v1" "aegis/service/consumer" @@ -27,10 +27,10 @@ type runtimeServerParams struct { fx.In DB *gorm.DB - RedisGateway *redisinfra.Gateway - K8sGateway *k8sinfra.Gateway - BuildKit *buildkitinfra.Gateway - Helm *helminfra.Gateway + RedisGateway *redis.Gateway + K8sGateway *k8s.Gateway + BuildKit *buildkit.Gateway + Helm *helm.Gateway RestartLimiter *consumer.TokenBucketRateLimiter `name:"restart_limiter"` BuildLimiter *consumer.TokenBucketRateLimiter `name:"build_limiter"` AlgoLimiter *consumer.TokenBucketRateLimiter `name:"algo_limiter"` @@ -39,7 +39,7 @@ type runtimeServerParams struct { type runtimeServer struct { runtimev1.UnimplementedRuntimeServiceServer snapshots *consumer.RuntimeSnapshotService - redis *redisinfra.Gateway + redis *redis.Gateway } func newRuntimeServer(params runtimeServerParams) *runtimeServer { @@ -143,7 +143,7 @@ func (s *runtimeServer) GetQueuedTasks(ctx context.Context, _ *runtimev1.PingReq return encodeStruct(items) } -func listNamespaceLocks(ctx context.Context, redis *redisinfra.Gateway) (map[string]map[string]any, error) { +func listNamespaceLocks(ctx context.Context, redis *redis.Gateway) (map[string]map[string]any, error) { namespaces, err := redis.SetMembers(ctx, consts.NamespacesKey) if err != nil { return nil, err @@ -165,7 +165,7 @@ func listNamespaceLocks(ctx context.Context, redis *redisinfra.Gateway) (map[str return items, nil } -func listQueuedTasks(ctx context.Context, redis *redisinfra.Gateway) (map[string]any, error) { +func listQueuedTasks(ctx context.Context, redis *redis.Gateway) (map[string]any, error) { readyItems, err := redis.ListReadyTasks(ctx) if err != nil { return nil, err @@ -190,24 +190,24 @@ func listQueuedTasks(ctx context.Context, redis *redisinfra.Gateway) (map[string }, nil } -func decodeQueuedTasks(items []string) ([]taskmodule.TaskResp, error) { - result := make([]taskmodule.TaskResp, 0, len(items)) +func decodeQueuedTasks(items []string) ([]task.TaskResp, error) { + result := make([]task.TaskResp, 0, len(items)) for _, item := range items { - var task dto.UnifiedTask - if err := json.Unmarshal([]byte(item), &task); err != nil { + var queuedTask dto.UnifiedTask + if err := json.Unmarshal([]byte(item), &queuedTask); err != nil { return nil, err } - result = append(result, taskmodule.TaskResp{ - ID: task.TaskID, - Type: consts.GetTaskTypeName(task.Type), - Immediate: task.Immediate, - ExecuteTime: task.ExecuteTime, - CronExpr: task.CronExpr, - TraceID: task.TraceID, - GroupID: task.GroupID, - State: consts.GetTaskStateName(task.State), + result = append(result, task.TaskResp{ + ID: queuedTask.TaskID, + Type: consts.GetTaskTypeName(queuedTask.Type), + Immediate: queuedTask.Immediate, + ExecuteTime: queuedTask.ExecuteTime, + CronExpr: queuedTask.CronExpr, + TraceID: queuedTask.TraceID, + GroupID: queuedTask.GroupID, + State: consts.GetTaskStateName(queuedTask.State), Status: consts.GetStatusTypeName(consts.CommonEnabled), - ProjectID: task.ProjectID, + ProjectID: queuedTask.ProjectID, }) } return result, nil diff --git a/src/interface/grpcruntime/service_test.go b/src/interface/grpc/runtime/service_test.go similarity index 98% rename from src/interface/grpcruntime/service_test.go rename to src/interface/grpc/runtime/service_test.go index 9f1a8529..2f283d26 100644 --- a/src/interface/grpcruntime/service_test.go +++ b/src/interface/grpc/runtime/service_test.go @@ -1,4 +1,4 @@ -package grpcruntimeinterface +package grpcruntime import ( "context" diff --git a/src/interface/grpcsystem/lifecycle.go b/src/interface/grpc/system/lifecycle.go similarity index 98% rename from src/interface/grpcsystem/lifecycle.go rename to src/interface/grpc/system/lifecycle.go index ec2afc42..cfc0f865 100644 --- a/src/interface/grpcsystem/lifecycle.go +++ b/src/interface/grpc/system/lifecycle.go @@ -1,4 +1,4 @@ -package grpcsysteminterface +package grpcsystem import ( "context" diff --git a/src/interface/grpcsystem/module.go b/src/interface/grpc/system/module.go similarity index 84% rename from src/interface/grpcsystem/module.go rename to src/interface/grpc/system/module.go index 3a9316ea..1e89a851 100644 --- a/src/interface/grpcsystem/module.go +++ b/src/interface/grpc/system/module.go @@ -1,4 +1,4 @@ -package grpcsysteminterface +package grpcsystem import "go.uber.org/fx" diff --git a/src/interface/grpcsystem/service.go b/src/interface/grpc/system/service.go similarity index 82% rename from src/interface/grpcsystem/service.go rename to src/interface/grpc/system/service.go index d30d86d4..4468127a 100644 --- a/src/interface/grpcsystem/service.go +++ b/src/interface/grpc/system/service.go @@ -1,4 +1,4 @@ -package grpcsysteminterface +package grpcsystem import ( "context" @@ -8,8 +8,8 @@ import ( "aegis/consts" "aegis/dto" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" systemv1 "aegis/proto/system/v1" "google.golang.org/grpc/codes" @@ -20,20 +20,20 @@ import ( const systemServiceName = "system-service" type systemReader interface { - GetHealth(context.Context) (*systemmodule.HealthCheckResp, error) - GetMetrics(context.Context) (*systemmodule.MonitoringMetricsResp, error) - GetSystemInfo(context.Context) (*systemmodule.SystemInfo, error) - ListNamespaceLocks(context.Context) (*systemmodule.ListNamespaceLockResp, error) - ListQueuedTasks(context.Context) (*systemmodule.QueuedTasksResp, error) - GetAuditLog(context.Context, int) (*systemmodule.AuditLogDetailResp, error) - ListAuditLogs(context.Context, *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) - GetConfig(context.Context, int) (*systemmodule.ConfigDetailResp, error) - ListConfigs(context.Context, *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) + GetHealth(context.Context) (*system.HealthCheckResp, error) + GetMetrics(context.Context) (*system.MonitoringMetricsResp, error) + GetSystemInfo(context.Context) (*system.SystemInfo, error) + ListNamespaceLocks(context.Context) (*system.ListNamespaceLockResp, error) + ListQueuedTasks(context.Context) (*system.QueuedTasksResp, error) + GetAuditLog(context.Context, int) (*system.AuditLogDetailResp, error) + ListAuditLogs(context.Context, *system.ListAuditLogReq) (*dto.ListResp[system.AuditLogResp], error) + GetConfig(context.Context, int) (*system.ConfigDetailResp, error) + ListConfigs(context.Context, *system.ListConfigReq) (*dto.ListResp[system.ConfigResp], error) } type metricsReader interface { - GetSystemMetrics(context.Context) (*systemmetricmodule.SystemMetricsResp, error) - GetSystemMetricsHistory(context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) + GetSystemMetrics(context.Context) (*systemmetric.SystemMetricsResp, error) + GetSystemMetricsHistory(context.Context) (*systemmetric.SystemMetricsHistoryResp, error) } type systemServer struct { @@ -42,7 +42,7 @@ type systemServer struct { metrics metricsReader } -func newSystemServer(system *systemmodule.Service, metrics *systemmetricmodule.Service) *systemServer { +func newSystemServer(system *system.Service, metrics *systemmetric.Service) *systemServer { return &systemServer{ system: system, metrics: metrics, @@ -83,7 +83,7 @@ func (s *systemServer) GetSystemInfo(ctx context.Context, _ *systemv1.PingReques } func (s *systemServer) ListConfigs(ctx context.Context, req *systemv1.ListConfigsRequest) (*systemv1.ResourceListResponse, error) { - query, err := decodeQuery[systemmodule.ListConfigReq](req.GetQuery()) + query, err := decodeQuery[system.ListConfigReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } @@ -110,7 +110,7 @@ func (s *systemServer) GetConfig(ctx context.Context, req *systemv1.GetResourceR } func (s *systemServer) ListAuditLogs(ctx context.Context, req *systemv1.ListAuditLogsRequest) (*systemv1.ResourceListResponse, error) { - query, err := decodeQuery[systemmodule.ListAuditLogReq](req.GetQuery()) + query, err := decodeQuery[system.ListAuditLogReq](req.GetQuery()) if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } diff --git a/src/interface/grpcsystem/service_test.go b/src/interface/grpc/system/service_test.go similarity index 64% rename from src/interface/grpcsystem/service_test.go rename to src/interface/grpc/system/service_test.go index 82412ec9..82571e01 100644 --- a/src/interface/grpcsystem/service_test.go +++ b/src/interface/grpc/system/service_test.go @@ -1,4 +1,4 @@ -package grpcsysteminterface +package grpcsystem import ( "context" @@ -8,8 +8,8 @@ import ( "aegis/consts" "aegis/dto" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" systemv1 "aegis/proto/system/v1" "google.golang.org/grpc/codes" @@ -18,79 +18,79 @@ import ( ) type systemReaderStub struct { - health *systemmodule.HealthCheckResp - metrics *systemmodule.MonitoringMetricsResp - info *systemmodule.SystemInfo - locks *systemmodule.ListNamespaceLockResp - queued *systemmodule.QueuedTasksResp - audit *systemmodule.AuditLogDetailResp - audits *dto.ListResp[systemmodule.AuditLogResp] - config *systemmodule.ConfigDetailResp - configs *dto.ListResp[systemmodule.ConfigResp] + health *system.HealthCheckResp + metrics *system.MonitoringMetricsResp + info *system.SystemInfo + locks *system.ListNamespaceLockResp + queued *system.QueuedTasksResp + audit *system.AuditLogDetailResp + audits *dto.ListResp[system.AuditLogResp] + config *system.ConfigDetailResp + configs *dto.ListResp[system.ConfigResp] err error } -func (s systemReaderStub) GetHealth(context.Context) (*systemmodule.HealthCheckResp, error) { +func (s systemReaderStub) GetHealth(context.Context) (*system.HealthCheckResp, error) { return s.health, s.err } -func (s systemReaderStub) GetMetrics(context.Context) (*systemmodule.MonitoringMetricsResp, error) { +func (s systemReaderStub) GetMetrics(context.Context) (*system.MonitoringMetricsResp, error) { return s.metrics, s.err } -func (s systemReaderStub) GetSystemInfo(context.Context) (*systemmodule.SystemInfo, error) { +func (s systemReaderStub) GetSystemInfo(context.Context) (*system.SystemInfo, error) { return s.info, s.err } -func (s systemReaderStub) ListNamespaceLocks(context.Context) (*systemmodule.ListNamespaceLockResp, error) { +func (s systemReaderStub) ListNamespaceLocks(context.Context) (*system.ListNamespaceLockResp, error) { return s.locks, s.err } -func (s systemReaderStub) ListQueuedTasks(context.Context) (*systemmodule.QueuedTasksResp, error) { +func (s systemReaderStub) ListQueuedTasks(context.Context) (*system.QueuedTasksResp, error) { return s.queued, s.err } -func (s systemReaderStub) GetAuditLog(_ context.Context, id int) (*systemmodule.AuditLogDetailResp, error) { +func (s systemReaderStub) GetAuditLog(_ context.Context, id int) (*system.AuditLogDetailResp, error) { if id <= 0 { return nil, errors.New("invalid id") } return s.audit, s.err } -func (s systemReaderStub) ListAuditLogs(context.Context, *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { +func (s systemReaderStub) ListAuditLogs(context.Context, *system.ListAuditLogReq) (*dto.ListResp[system.AuditLogResp], error) { return s.audits, s.err } -func (s systemReaderStub) GetConfig(_ context.Context, id int) (*systemmodule.ConfigDetailResp, error) { +func (s systemReaderStub) GetConfig(_ context.Context, id int) (*system.ConfigDetailResp, error) { if id <= 0 { return nil, errors.New("invalid id") } return s.config, s.err } -func (s systemReaderStub) ListConfigs(context.Context, *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { +func (s systemReaderStub) ListConfigs(context.Context, *system.ListConfigReq) (*dto.ListResp[system.ConfigResp], error) { return s.configs, s.err } type metricsReaderStub struct { - current *systemmetricmodule.SystemMetricsResp - history *systemmetricmodule.SystemMetricsHistoryResp + current *systemmetric.SystemMetricsResp + history *systemmetric.SystemMetricsHistoryResp err error } -func (s metricsReaderStub) GetSystemMetrics(context.Context) (*systemmetricmodule.SystemMetricsResp, error) { +func (s metricsReaderStub) GetSystemMetrics(context.Context) (*systemmetric.SystemMetricsResp, error) { return s.current, s.err } -func (s metricsReaderStub) GetSystemMetricsHistory(context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { +func (s metricsReaderStub) GetSystemMetricsHistory(context.Context) (*systemmetric.SystemMetricsHistoryResp, error) { return s.history, s.err } func TestSystemServerGetHealth(t *testing.T) { server := &systemServer{ system: systemReaderStub{ - health: &systemmodule.HealthCheckResp{ + health: &system.HealthCheckResp{ Status: "healthy", Timestamp: time.Now(), Version: "v1", Uptime: "1m", - Services: map[string]systemmodule.ServiceInfo{ + Services: map[string]system.ServiceInfo{ "redis": {Status: "healthy"}, }, }, - metrics: &systemmodule.MonitoringMetricsResp{}, - info: &systemmodule.SystemInfo{}, + metrics: &system.MonitoringMetricsResp{}, + info: &system.SystemInfo{}, }, metrics: metricsReaderStub{}, } @@ -107,14 +107,14 @@ func TestSystemServerGetHealth(t *testing.T) { func TestSystemServerListConfigs(t *testing.T) { server := &systemServer{ system: systemReaderStub{ - configs: &dto.ListResp[systemmodule.ConfigResp]{ - Items: []systemmodule.ConfigResp{{ID: 1, Key: "demo.key"}}, + configs: &dto.ListResp[system.ConfigResp]{ + Items: []system.ConfigResp{{ID: 1, Key: "demo.key"}}, Pagination: &dto.PaginationInfo{ Page: 1, Size: 20, Total: 1, TotalPages: 1, }, }, - metrics: &systemmodule.MonitoringMetricsResp{}, - info: &systemmodule.SystemInfo{}, + metrics: &system.MonitoringMetricsResp{}, + info: &system.SystemInfo{}, }, metrics: metricsReaderStub{}, } @@ -151,12 +151,12 @@ func TestSystemServerGetAuditLogNotFound(t *testing.T) { func TestSystemServerGetSystemMetricsHistory(t *testing.T) { server := &systemServer{ system: systemReaderStub{ - metrics: &systemmodule.MonitoringMetricsResp{}, - info: &systemmodule.SystemInfo{}, + metrics: &system.MonitoringMetricsResp{}, + info: &system.SystemInfo{}, }, metrics: metricsReaderStub{ - history: &systemmetricmodule.SystemMetricsHistoryResp{ - CPU: []systemmetricmodule.MetricValue{{Value: 1}}, + history: &systemmetric.SystemMetricsHistoryResp{ + CPU: []systemmetric.MetricValue{{Value: 1}}, }, }, } diff --git a/src/interface/http/module.go b/src/interface/http/module.go index b52faad2..d9cc5ba5 100644 --- a/src/interface/http/module.go +++ b/src/interface/http/module.go @@ -1,7 +1,8 @@ -package httpinterface +package httpapi import ( "aegis/middleware" + "aegis/router" "go.uber.org/fx" ) @@ -9,7 +10,7 @@ import ( var Module = fx.Module("http", fx.Provide( middleware.NewService, - NewGinEngine, + router.New, NewServer, ), fx.Invoke(registerServerLifecycle), diff --git a/src/interface/http/router.go b/src/interface/http/router.go deleted file mode 100644 index a56725d6..00000000 --- a/src/interface/http/router.go +++ /dev/null @@ -1,12 +0,0 @@ -package httpinterface - -import ( - "aegis/middleware" - "aegis/router" - - "github.com/gin-gonic/gin" -) - -func NewGinEngine(handlers *router.Handlers, middlewareService middleware.Service) *gin.Engine { - return router.New(handlers, middlewareService) -} diff --git a/src/interface/http/server.go b/src/interface/http/server.go index 1dadeeae..fc54863b 100644 --- a/src/interface/http/server.go +++ b/src/interface/http/server.go @@ -1,4 +1,4 @@ -package httpinterface +package httpapi import ( "context" diff --git a/src/interface/receiver/module.go b/src/interface/receiver/module.go index 9149955a..868f3b45 100644 --- a/src/interface/receiver/module.go +++ b/src/interface/receiver/module.go @@ -1,10 +1,10 @@ -package receiverinterface +package receiver import ( "context" "aegis/config" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/service/logreceiver" "github.com/sirupsen/logrus" @@ -22,7 +22,7 @@ type Lifecycle struct { StopFunc func() } -func newLifecycle(redisGateway *redisinfra.Gateway) *Lifecycle { +func newLifecycle(redisGateway *redis.Gateway) *Lifecycle { otlpPort := config.GetInt("otlp_receiver.port") if otlpPort == 0 { otlpPort = logreceiver.DefaultPort diff --git a/src/interface/worker/module.go b/src/interface/worker/module.go index 1a28f66a..66f3ad00 100644 --- a/src/interface/worker/module.go +++ b/src/interface/worker/module.go @@ -1,13 +1,13 @@ -package workerinterface +package worker import ( "context" - buildkitinfra "aegis/infra/buildkit" - etcdinfra "aegis/infra/etcd" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + buildkit "aegis/infra/buildkit" + etcd "aegis/infra/etcd" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" commonservice "aegis/service/common" "aegis/service/consumer" "aegis/service/initialization" @@ -25,12 +25,12 @@ type Params struct { fx.In DB *gorm.DB - RedisGateway *redisinfra.Gateway - BuildKit *buildkitinfra.Gateway - Helm *helminfra.Gateway - K8sGateway *k8sinfra.Gateway - Controller *k8sinfra.Controller - Etcd *etcdinfra.Gateway + RedisGateway *redis.Gateway + BuildKit *buildkit.Gateway + Helm *helm.Gateway + K8sGateway *k8s.Gateway + Controller *k8s.Controller + Etcd *etcd.Gateway Monitor consumer.NamespaceMonitor RestartLimiter *consumer.TokenBucketRateLimiter `name:"restart_limiter"` BuildLimiter *consumer.TokenBucketRateLimiter `name:"build_limiter"` diff --git a/src/internalclient/iamclient/client.go b/src/internalclient/iamclient/client.go index 03628fd3..1733a74b 100644 --- a/src/internalclient/iamclient/client.go +++ b/src/internalclient/iamclient/client.go @@ -11,10 +11,10 @@ import ( "aegis/dto" "aegis/httpx" "aegis/middleware" - authmodule "aegis/module/auth" - rbacmodule "aegis/module/rbac" - teammodule "aegis/module/team" - usermodule "aegis/module/user" + auth "aegis/module/auth" + rbac "aegis/module/rbac" + team "aegis/module/team" + user "aegis/module/user" iamv1 "aegis/proto/iam/v1" "aegis/utils" @@ -224,7 +224,7 @@ func (c *Client) IsUserInProject(ctx context.Context, userID, projectID int) (bo return resp.GetValue(), nil } -func (c *Client) Login(ctx context.Context, req *authmodule.LoginReq) (*authmodule.LoginResp, error) { +func (c *Client) Login(ctx context.Context, req *auth.LoginReq) (*auth.LoginResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -236,10 +236,10 @@ func (c *Client) Login(ctx context.Context, req *authmodule.LoginReq) (*authmodu if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.LoginResp](resp.GetData()) + return decodeStruct[auth.LoginResp](resp.GetData()) } -func (c *Client) Register(ctx context.Context, req *authmodule.RegisterReq) (*authmodule.UserInfo, error) { +func (c *Client) Register(ctx context.Context, req *auth.RegisterReq) (*auth.UserInfo, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -251,10 +251,10 @@ func (c *Client) Register(ctx context.Context, req *authmodule.RegisterReq) (*au if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.UserInfo](resp.GetData()) + return decodeStruct[auth.UserInfo](resp.GetData()) } -func (c *Client) RefreshToken(ctx context.Context, req *authmodule.TokenRefreshReq) (*authmodule.TokenRefreshResp, error) { +func (c *Client) RefreshToken(ctx context.Context, req *auth.TokenRefreshReq) (*auth.TokenRefreshResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -266,7 +266,7 @@ func (c *Client) RefreshToken(ctx context.Context, req *authmodule.TokenRefreshR if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.TokenRefreshResp](resp.GetData()) + return decodeStruct[auth.TokenRefreshResp](resp.GetData()) } func (c *Client) Logout(ctx context.Context, claims *utils.Claims) error { @@ -284,7 +284,7 @@ func (c *Client) Logout(ctx context.Context, claims *utils.Claims) error { return mapRPCError(err) } -func (c *Client) ChangePassword(ctx context.Context, req *authmodule.ChangePasswordReq, userID int) error { +func (c *Client) ChangePassword(ctx context.Context, req *auth.ChangePasswordReq, userID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } @@ -299,7 +299,7 @@ func (c *Client) ChangePassword(ctx context.Context, req *authmodule.ChangePassw return mapRPCError(err) } -func (c *Client) GetProfile(ctx context.Context, userID int) (*authmodule.UserProfileResp, error) { +func (c *Client) GetProfile(ctx context.Context, userID int) (*auth.UserProfileResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -307,10 +307,10 @@ func (c *Client) GetProfile(ctx context.Context, userID int) (*authmodule.UserPr if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.UserProfileResp](resp.GetData()) + return decodeStruct[auth.UserProfileResp](resp.GetData()) } -func (c *Client) CreateAPIKey(ctx context.Context, userID int, req *authmodule.CreateAPIKeyReq) (*authmodule.APIKeyWithSecretResp, error) { +func (c *Client) CreateAPIKey(ctx context.Context, userID int, req *auth.CreateAPIKeyReq) (*auth.APIKeyWithSecretResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -325,10 +325,10 @@ func (c *Client) CreateAPIKey(ctx context.Context, userID int, req *authmodule.C if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.APIKeyWithSecretResp](resp.GetData()) + return decodeStruct[auth.APIKeyWithSecretResp](resp.GetData()) } -func (c *Client) ListAPIKeys(ctx context.Context, userID int, req *authmodule.ListAPIKeyReq) (*authmodule.ListAPIKeyResp, error) { +func (c *Client) ListAPIKeys(ctx context.Context, userID int, req *auth.ListAPIKeyReq) (*auth.ListAPIKeyResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -343,10 +343,10 @@ func (c *Client) ListAPIKeys(ctx context.Context, userID int, req *authmodule.Li if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.ListAPIKeyResp](resp.GetData()) + return decodeStruct[auth.ListAPIKeyResp](resp.GetData()) } -func (c *Client) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyInfo, error) { +func (c *Client) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*auth.APIKeyInfo, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -357,7 +357,7 @@ func (c *Client) GetAPIKey(ctx context.Context, userID, accessKeyID int) (*authm if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.APIKeyInfo](resp.GetData()) + return decodeStruct[auth.APIKeyInfo](resp.GetData()) } func (c *Client) DeleteAPIKey(ctx context.Context, userID, accessKeyID int) error { @@ -404,7 +404,7 @@ func (c *Client) RevokeAPIKey(ctx context.Context, userID, accessKeyID int) erro return mapRPCError(err) } -func (c *Client) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*authmodule.APIKeyWithSecretResp, error) { +func (c *Client) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*auth.APIKeyWithSecretResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -415,10 +415,10 @@ func (c *Client) RotateAPIKey(ctx context.Context, userID, accessKeyID int) (*au if err != nil { return nil, mapRPCError(err) } - return decodeStruct[authmodule.APIKeyWithSecretResp](resp.GetData()) + return decodeStruct[auth.APIKeyWithSecretResp](resp.GetData()) } -func (c *Client) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKeyTokenReq, method, path string) (*authmodule.APIKeyTokenResp, error) { +func (c *Client) ExchangeAPIKeyToken(ctx context.Context, req *auth.APIKeyTokenReq, method, path string) (*auth.APIKeyTokenResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -433,7 +433,7 @@ func (c *Client) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKey if err != nil { return nil, mapRPCError(err) } - return &authmodule.APIKeyTokenResp{ + return &auth.APIKeyTokenResp{ Token: resp.GetToken(), TokenType: resp.GetTokenType(), ExpiresAt: time.Unix(resp.GetExpiresAtUnix(), 0), @@ -442,7 +442,7 @@ func (c *Client) ExchangeAPIKeyToken(ctx context.Context, req *authmodule.APIKey }, nil } -func (c *Client) CreateUser(ctx context.Context, req *usermodule.CreateUserReq) (*usermodule.UserResp, error) { +func (c *Client) CreateUser(ctx context.Context, req *user.CreateUserReq) (*user.UserResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -454,7 +454,7 @@ func (c *Client) CreateUser(ctx context.Context, req *usermodule.CreateUserReq) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[usermodule.UserResp](resp.GetData()) + return decodeStruct[user.UserResp](resp.GetData()) } func (c *Client) DeleteUser(ctx context.Context, userID int) error { @@ -465,7 +465,7 @@ func (c *Client) DeleteUser(ctx context.Context, userID int) error { return mapRPCError(err) } -func (c *Client) GetUser(ctx context.Context, userID int) (*usermodule.UserDetailResp, error) { +func (c *Client) GetUser(ctx context.Context, userID int) (*user.UserDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -473,10 +473,10 @@ func (c *Client) GetUser(ctx context.Context, userID int) (*usermodule.UserDetai if err != nil { return nil, mapRPCError(err) } - return decodeStruct[usermodule.UserDetailResp](resp.GetData()) + return decodeStruct[user.UserDetailResp](resp.GetData()) } -func (c *Client) ListUsers(ctx context.Context, req *usermodule.ListUserReq) (*dto.ListResp[usermodule.UserResp], error) { +func (c *Client) ListUsers(ctx context.Context, req *user.ListUserReq) (*dto.ListResp[user.UserResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -488,10 +488,10 @@ func (c *Client) ListUsers(ctx context.Context, req *usermodule.ListUserReq) (*d if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[usermodule.UserResp]](resp.GetData()) + return decodeStruct[dto.ListResp[user.UserResp]](resp.GetData()) } -func (c *Client) UpdateUser(ctx context.Context, req *usermodule.UpdateUserReq, userID int) (*usermodule.UserResp, error) { +func (c *Client) UpdateUser(ctx context.Context, req *user.UpdateUserReq, userID int) (*user.UserResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -506,7 +506,7 @@ func (c *Client) UpdateUser(ctx context.Context, req *usermodule.UpdateUserReq, if err != nil { return nil, mapRPCError(err) } - return decodeStruct[usermodule.UserResp](resp.GetData()) + return decodeStruct[user.UserResp](resp.GetData()) } func (c *Client) AssignUserRole(ctx context.Context, userID, roleID int) error { @@ -531,7 +531,7 @@ func (c *Client) RemoveUserRole(ctx context.Context, userID, roleID int) error { return mapRPCError(err) } -func (c *Client) AssignUserPermissions(ctx context.Context, userID int, req *usermodule.AssignUserPermissionReq) error { +func (c *Client) AssignUserPermissions(ctx context.Context, userID int, req *user.AssignUserPermissionReq) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } @@ -546,7 +546,7 @@ func (c *Client) AssignUserPermissions(ctx context.Context, userID int, req *use return mapRPCError(err) } -func (c *Client) RemoveUserPermissions(ctx context.Context, userID int, req *usermodule.RemoveUserPermissionReq) error { +func (c *Client) RemoveUserPermissions(ctx context.Context, userID int, req *user.RemoveUserPermissionReq) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } @@ -630,7 +630,7 @@ func (c *Client) RemoveUserProject(ctx context.Context, userID, projectID int) e return mapRPCError(err) } -func (c *Client) CreateRole(ctx context.Context, req *rbacmodule.CreateRoleReq) (*rbacmodule.RoleResp, error) { +func (c *Client) CreateRole(ctx context.Context, req *rbac.CreateRoleReq) (*rbac.RoleResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -642,7 +642,7 @@ func (c *Client) CreateRole(ctx context.Context, req *rbacmodule.CreateRoleReq) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[rbacmodule.RoleResp](resp.GetData()) + return decodeStruct[rbac.RoleResp](resp.GetData()) } func (c *Client) DeleteRole(ctx context.Context, roleID int) error { @@ -653,7 +653,7 @@ func (c *Client) DeleteRole(ctx context.Context, roleID int) error { return mapRPCError(err) } -func (c *Client) GetRole(ctx context.Context, roleID int) (*rbacmodule.RoleDetailResp, error) { +func (c *Client) GetRole(ctx context.Context, roleID int) (*rbac.RoleDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -661,10 +661,10 @@ func (c *Client) GetRole(ctx context.Context, roleID int) (*rbacmodule.RoleDetai if err != nil { return nil, mapRPCError(err) } - return decodeStruct[rbacmodule.RoleDetailResp](resp.GetData()) + return decodeStruct[rbac.RoleDetailResp](resp.GetData()) } -func (c *Client) ListRoles(ctx context.Context, req *rbacmodule.ListRoleReq) (*dto.ListResp[rbacmodule.RoleResp], error) { +func (c *Client) ListRoles(ctx context.Context, req *rbac.ListRoleReq) (*dto.ListResp[rbac.RoleResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -676,10 +676,10 @@ func (c *Client) ListRoles(ctx context.Context, req *rbacmodule.ListRoleReq) (*d if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[rbacmodule.RoleResp]](resp.GetData()) + return decodeStruct[dto.ListResp[rbac.RoleResp]](resp.GetData()) } -func (c *Client) UpdateRole(ctx context.Context, req *rbacmodule.UpdateRoleReq, roleID int) (*rbacmodule.RoleResp, error) { +func (c *Client) UpdateRole(ctx context.Context, req *rbac.UpdateRoleReq, roleID int) (*rbac.RoleResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -694,7 +694,7 @@ func (c *Client) UpdateRole(ctx context.Context, req *rbacmodule.UpdateRoleReq, if err != nil { return nil, mapRPCError(err) } - return decodeStruct[rbacmodule.RoleResp](resp.GetData()) + return decodeStruct[rbac.RoleResp](resp.GetData()) } func (c *Client) AssignRolePermissions(ctx context.Context, roleID int, permissionIDs []int) error { @@ -719,7 +719,7 @@ func (c *Client) RemoveRolePermissions(ctx context.Context, roleID int, permissi return mapRPCError(err) } -func (c *Client) ListUsersFromRole(ctx context.Context, roleID int) ([]rbacmodule.UserListItem, error) { +func (c *Client) ListUsersFromRole(ctx context.Context, roleID int) ([]rbac.UserListItem, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -727,14 +727,14 @@ func (c *Client) ListUsersFromRole(ctx context.Context, roleID int) ([]rbacmodul if err != nil { return nil, mapRPCError(err) } - data, err := decodeStruct[[]rbacmodule.UserListItem](resp.GetData()) + data, err := decodeStruct[[]rbac.UserListItem](resp.GetData()) if err != nil { return nil, err } return *data, nil } -func (c *Client) GetPermission(ctx context.Context, permissionID int) (*rbacmodule.PermissionDetailResp, error) { +func (c *Client) GetPermission(ctx context.Context, permissionID int) (*rbac.PermissionDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -742,10 +742,10 @@ func (c *Client) GetPermission(ctx context.Context, permissionID int) (*rbacmodu if err != nil { return nil, mapRPCError(err) } - return decodeStruct[rbacmodule.PermissionDetailResp](resp.GetData()) + return decodeStruct[rbac.PermissionDetailResp](resp.GetData()) } -func (c *Client) ListPermissions(ctx context.Context, req *rbacmodule.ListPermissionReq) (*dto.ListResp[rbacmodule.PermissionResp], error) { +func (c *Client) ListPermissions(ctx context.Context, req *rbac.ListPermissionReq) (*dto.ListResp[rbac.PermissionResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -757,10 +757,10 @@ func (c *Client) ListPermissions(ctx context.Context, req *rbacmodule.ListPermis if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[rbacmodule.PermissionResp]](resp.GetData()) + return decodeStruct[dto.ListResp[rbac.PermissionResp]](resp.GetData()) } -func (c *Client) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbacmodule.RoleResp, error) { +func (c *Client) ListRolesFromPermission(ctx context.Context, permissionID int) ([]rbac.RoleResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -768,14 +768,14 @@ func (c *Client) ListRolesFromPermission(ctx context.Context, permissionID int) if err != nil { return nil, mapRPCError(err) } - data, err := decodeStruct[[]rbacmodule.RoleResp](resp.GetData()) + data, err := decodeStruct[[]rbac.RoleResp](resp.GetData()) if err != nil { return nil, err } return *data, nil } -func (c *Client) GetResource(ctx context.Context, resourceID int) (*rbacmodule.ResourceResp, error) { +func (c *Client) GetResource(ctx context.Context, resourceID int) (*rbac.ResourceResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -783,10 +783,10 @@ func (c *Client) GetResource(ctx context.Context, resourceID int) (*rbacmodule.R if err != nil { return nil, mapRPCError(err) } - return decodeStruct[rbacmodule.ResourceResp](resp.GetData()) + return decodeStruct[rbac.ResourceResp](resp.GetData()) } -func (c *Client) ListResources(ctx context.Context, req *rbacmodule.ListResourceReq) (*dto.ListResp[rbacmodule.ResourceResp], error) { +func (c *Client) ListResources(ctx context.Context, req *rbac.ListResourceReq) (*dto.ListResp[rbac.ResourceResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -798,10 +798,10 @@ func (c *Client) ListResources(ctx context.Context, req *rbacmodule.ListResource if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[rbacmodule.ResourceResp]](resp.GetData()) + return decodeStruct[dto.ListResp[rbac.ResourceResp]](resp.GetData()) } -func (c *Client) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbacmodule.PermissionResp, error) { +func (c *Client) ListResourcePermissions(ctx context.Context, resourceID int) ([]rbac.PermissionResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -809,14 +809,14 @@ func (c *Client) ListResourcePermissions(ctx context.Context, resourceID int) ([ if err != nil { return nil, mapRPCError(err) } - data, err := decodeStruct[[]rbacmodule.PermissionResp](resp.GetData()) + data, err := decodeStruct[[]rbac.PermissionResp](resp.GetData()) if err != nil { return nil, err } return *data, nil } -func (c *Client) CreateTeam(ctx context.Context, req *teammodule.CreateTeamReq, userID int) (*teammodule.TeamResp, error) { +func (c *Client) CreateTeam(ctx context.Context, req *team.CreateTeamReq, userID int) (*team.TeamResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -831,7 +831,7 @@ func (c *Client) CreateTeam(ctx context.Context, req *teammodule.CreateTeamReq, if err != nil { return nil, mapRPCError(err) } - return decodeStruct[teammodule.TeamResp](resp.GetData()) + return decodeStruct[team.TeamResp](resp.GetData()) } func (c *Client) DeleteTeam(ctx context.Context, teamID int) error { @@ -842,7 +842,7 @@ func (c *Client) DeleteTeam(ctx context.Context, teamID int) error { return mapRPCError(err) } -func (c *Client) GetTeam(ctx context.Context, teamID int) (*teammodule.TeamDetailResp, error) { +func (c *Client) GetTeam(ctx context.Context, teamID int) (*team.TeamDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -850,10 +850,10 @@ func (c *Client) GetTeam(ctx context.Context, teamID int) (*teammodule.TeamDetai if err != nil { return nil, mapRPCError(err) } - return decodeStruct[teammodule.TeamDetailResp](resp.GetData()) + return decodeStruct[team.TeamDetailResp](resp.GetData()) } -func (c *Client) ListTeams(ctx context.Context, req *teammodule.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[teammodule.TeamResp], error) { +func (c *Client) ListTeams(ctx context.Context, req *team.ListTeamReq, userID int, isAdmin bool) (*dto.ListResp[team.TeamResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -869,10 +869,10 @@ func (c *Client) ListTeams(ctx context.Context, req *teammodule.ListTeamReq, use if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[teammodule.TeamResp]](resp.GetData()) + return decodeStruct[dto.ListResp[team.TeamResp]](resp.GetData()) } -func (c *Client) UpdateTeam(ctx context.Context, req *teammodule.UpdateTeamReq, teamID int) (*teammodule.TeamResp, error) { +func (c *Client) UpdateTeam(ctx context.Context, req *team.UpdateTeamReq, teamID int) (*team.TeamResp, error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -887,10 +887,10 @@ func (c *Client) UpdateTeam(ctx context.Context, req *teammodule.UpdateTeamReq, if err != nil { return nil, mapRPCError(err) } - return decodeStruct[teammodule.TeamResp](resp.GetData()) + return decodeStruct[team.TeamResp](resp.GetData()) } -func (c *Client) ListTeamProjects(ctx context.Context, req *teammodule.TeamProjectListReq, teamID int) (*dto.ListResp[teammodule.TeamProjectItem], error) { +func (c *Client) ListTeamProjects(ctx context.Context, req *team.TeamProjectListReq, teamID int) (*dto.ListResp[team.TeamProjectItem], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -905,10 +905,10 @@ func (c *Client) ListTeamProjects(ctx context.Context, req *teammodule.TeamProje if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[teammodule.TeamProjectItem]](resp.GetData()) + return decodeStruct[dto.ListResp[team.TeamProjectItem]](resp.GetData()) } -func (c *Client) AddTeamMember(ctx context.Context, req *teammodule.AddTeamMemberReq, teamID int) error { +func (c *Client) AddTeamMember(ctx context.Context, req *team.AddTeamMemberReq, teamID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } @@ -935,7 +935,7 @@ func (c *Client) RemoveTeamMember(ctx context.Context, teamID, currentUserID, ta return mapRPCError(err) } -func (c *Client) UpdateTeamMemberRole(ctx context.Context, req *teammodule.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { +func (c *Client) UpdateTeamMemberRole(ctx context.Context, req *team.UpdateTeamMemberRoleReq, teamID, targetUserID, currentUserID int) error { if !c.Enabled() { return fmt.Errorf("iam grpc client is not configured") } @@ -952,7 +952,7 @@ func (c *Client) UpdateTeamMemberRole(ctx context.Context, req *teammodule.Updat return mapRPCError(err) } -func (c *Client) ListTeamMembers(ctx context.Context, req *teammodule.ListTeamMemberReq, teamID int) (*dto.ListResp[teammodule.TeamMemberResp], error) { +func (c *Client) ListTeamMembers(ctx context.Context, req *team.ListTeamMemberReq, teamID int) (*dto.ListResp[team.TeamMemberResp], error) { if !c.Enabled() { return nil, fmt.Errorf("iam grpc client is not configured") } @@ -967,7 +967,7 @@ func (c *Client) ListTeamMembers(ctx context.Context, req *teammodule.ListTeamMe if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[teammodule.TeamMemberResp]](resp.GetData()) + return decodeStruct[dto.ListResp[team.TeamMemberResp]](resp.GetData()) } var _ middleware.TokenVerifier = (*Client)(nil) diff --git a/src/internalclient/orchestratorclient/client.go b/src/internalclient/orchestratorclient/client.go index 60b04e8a..a4cadb5a 100644 --- a/src/internalclient/orchestratorclient/client.go +++ b/src/internalclient/orchestratorclient/client.go @@ -10,12 +10,12 @@ import ( "aegis/consts" "aegis/dto" "aegis/httpx" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - metricmodule "aegis/module/metric" - taskmodule "aegis/module/task" - tracemodule "aegis/module/trace" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + metric "aegis/module/metric" + task "aegis/module/task" + trace "aegis/module/trace" orchestratorv1 "aegis/proto/orchestrator/v1" "github.com/redis/go-redis/v9" @@ -70,7 +70,7 @@ func (c *Client) Enabled() bool { return c != nil && c.rpc != nil } -func (c *Client) SubmitExecution(ctx context.Context, req *executionmodule.SubmitExecutionReq, groupID string, userID int) (*executionmodule.SubmitExecutionResp, error) { +func (c *Client) SubmitExecution(ctx context.Context, req *execution.SubmitExecutionReq, groupID string, userID int) (*execution.SubmitExecutionResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -88,9 +88,9 @@ func (c *Client) SubmitExecution(ctx context.Context, req *executionmodule.Submi return nil, mapRPCError(err) } - items := make([]executionmodule.SubmitExecutionItem, 0, len(resp.GetItems())) + items := make([]execution.SubmitExecutionItem, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { - mapped := executionmodule.SubmitExecutionItem{ + mapped := execution.SubmitExecutionItem{ Index: int(item.GetIndex()), TraceID: item.GetTraceId(), TaskID: item.GetTaskId(), @@ -108,13 +108,13 @@ func (c *Client) SubmitExecution(ctx context.Context, req *executionmodule.Submi items = append(items, mapped) } - return &executionmodule.SubmitExecutionResp{ + return &execution.SubmitExecutionResp{ GroupID: resp.GetGroupId(), Items: items, }, nil } -func (c *Client) SubmitFaultInjection(ctx context.Context, req *injectionmodule.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitInjectionResp, error) { +func (c *Client) SubmitFaultInjection(ctx context.Context, req *injection.SubmitInjectionReq, groupID string, userID int, projectID *int) (*injection.SubmitInjectionResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -137,22 +137,22 @@ func (c *Client) SubmitFaultInjection(ctx context.Context, req *injectionmodule. return nil, mapRPCError(err) } - items := make([]injectionmodule.SubmitInjectionItem, 0, len(resp.GetItems())) + items := make([]injection.SubmitInjectionItem, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { - items = append(items, injectionmodule.SubmitInjectionItem{ + items = append(items, injection.SubmitInjectionItem{ Index: int(item.GetIndex()), TraceID: item.GetTraceId(), TaskID: item.GetTaskId(), }) } - result := &injectionmodule.SubmitInjectionResp{ + result := &injection.SubmitInjectionResp{ GroupID: resp.GetGroupId(), Items: items, OriginalCount: int(resp.GetOriginalCount()), } if warnings := resp.GetWarnings(); warnings != nil { - result.Warnings = &injectionmodule.InjectionWarnings{ + result.Warnings = &injection.InjectionWarnings{ DuplicateServicesInBatch: warnings.GetDuplicateServicesInBatch(), DuplicateBatchesInRequest: int64sToInts(warnings.GetDuplicateBatchesInRequest()), BatchesExistInDatabase: int64sToInts(warnings.GetBatchesExistInDatabase()), @@ -161,7 +161,7 @@ func (c *Client) SubmitFaultInjection(ctx context.Context, req *injectionmodule. return result, nil } -func (c *Client) SubmitDatapackBuilding(ctx context.Context, req *injectionmodule.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injectionmodule.SubmitDatapackBuildingResp, error) { +func (c *Client) SubmitDatapackBuilding(ctx context.Context, req *injection.SubmitDatapackBuildingReq, groupID string, userID int, projectID *int) (*injection.SubmitDatapackBuildingResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -184,22 +184,22 @@ func (c *Client) SubmitDatapackBuilding(ctx context.Context, req *injectionmodul return nil, mapRPCError(err) } - items := make([]injectionmodule.SubmitBuildingItem, 0, len(resp.GetItems())) + items := make([]injection.SubmitBuildingItem, 0, len(resp.GetItems())) for _, item := range resp.GetItems() { - items = append(items, injectionmodule.SubmitBuildingItem{ + items = append(items, injection.SubmitBuildingItem{ Index: int(item.GetIndex()), TraceID: item.GetTraceId(), TaskID: item.GetTaskId(), }) } - return &injectionmodule.SubmitDatapackBuildingResp{ + return &injection.SubmitDatapackBuildingResp{ GroupID: resp.GetGroupId(), Items: items, }, nil } -func (c *Client) CreateExecution(ctx context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { +func (c *Client) CreateExecution(ctx context.Context, req *execution.RuntimeCreateExecutionReq) (int, error) { if !c.Enabled() { return 0, fmt.Errorf("orchestrator grpc client is not configured") } @@ -219,7 +219,7 @@ func (c *Client) CreateExecution(ctx context.Context, req *executionmodule.Runti return int(executionID), nil } -func (c *Client) CreateInjection(ctx context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { +func (c *Client) CreateInjection(ctx context.Context, req *injection.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -234,7 +234,7 @@ func (c *Client) CreateInjection(ctx context.Context, req *injectionmodule.Runti return decodeStruct[dto.InjectionItem](resp.GetData()) } -func (c *Client) UpdateExecutionState(ctx context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { +func (c *Client) UpdateExecutionState(ctx context.Context, req *execution.RuntimeUpdateExecutionStateReq) error { if !c.Enabled() { return fmt.Errorf("orchestrator grpc client is not configured") } @@ -249,7 +249,7 @@ func (c *Client) UpdateExecutionState(ctx context.Context, req *executionmodule. return nil } -func (c *Client) UpdateInjectionState(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { +func (c *Client) UpdateInjectionState(ctx context.Context, req *injection.RuntimeUpdateInjectionStateReq) error { if !c.Enabled() { return fmt.Errorf("orchestrator grpc client is not configured") } @@ -264,7 +264,7 @@ func (c *Client) UpdateInjectionState(ctx context.Context, req *injectionmodule. return nil } -func (c *Client) UpdateInjectionTimestamps(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { +func (c *Client) UpdateInjectionTimestamps(ctx context.Context, req *injection.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -279,7 +279,7 @@ func (c *Client) UpdateInjectionTimestamps(ctx context.Context, req *injectionmo return decodeStruct[dto.InjectionItem](resp.GetData()) } -func (c *Client) GetExecution(ctx context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { +func (c *Client) GetExecution(ctx context.Context, executionID int) (*execution.ExecutionDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -287,10 +287,10 @@ func (c *Client) GetExecution(ctx context.Context, executionID int) (*executionm if err != nil { return nil, mapRPCError(err) } - return decodeStruct[executionmodule.ExecutionDetailResp](resp.GetData()) + return decodeStruct[execution.ExecutionDetailResp](resp.GetData()) } -func (c *Client) GetInjectionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.InjectionMetrics, error) { +func (c *Client) GetInjectionMetrics(ctx context.Context, req *metric.GetMetricsReq) (*metric.InjectionMetrics, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -302,10 +302,10 @@ func (c *Client) GetInjectionMetrics(ctx context.Context, req *metricmodule.GetM if err != nil { return nil, mapRPCError(err) } - return decodeStruct[metricmodule.InjectionMetrics](resp.GetData()) + return decodeStruct[metric.InjectionMetrics](resp.GetData()) } -func (c *Client) GetExecutionMetrics(ctx context.Context, req *metricmodule.GetMetricsReq) (*metricmodule.ExecutionMetrics, error) { +func (c *Client) GetExecutionMetrics(ctx context.Context, req *metric.GetMetricsReq) (*metric.ExecutionMetrics, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -317,7 +317,7 @@ func (c *Client) GetExecutionMetrics(ctx context.Context, req *metricmodule.GetM if err != nil { return nil, mapRPCError(err) } - return decodeStruct[metricmodule.ExecutionMetrics](resp.GetData()) + return decodeStruct[metric.ExecutionMetrics](resp.GetData()) } func (c *Client) ListProjectStatistics(ctx context.Context, projectIDs []int) (map[int]*dto.ProjectStatistics, error) { @@ -333,7 +333,7 @@ func (c *Client) ListProjectStatistics(ctx context.Context, projectIDs []int) (m return decodeProjectStatisticsMap(resp.GetData()) } -func (c *Client) ListEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (c *Client) ListEvaluationExecutionsByDatapack(ctx context.Context, req *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -345,10 +345,10 @@ func (c *Client) ListEvaluationExecutionsByDatapack(ctx context.Context, req *ex if err != nil { return nil, mapRPCError(err) } - return decodeStructItems[executionmodule.EvaluationExecutionItem](resp.GetData()) + return decodeStructItems[execution.EvaluationExecutionItem](resp.GetData()) } -func (c *Client) ListEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (c *Client) ListEvaluationExecutionsByDataset(ctx context.Context, req *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -360,10 +360,10 @@ func (c *Client) ListEvaluationExecutionsByDataset(ctx context.Context, req *exe if err != nil { return nil, mapRPCError(err) } - return decodeStructItems[executionmodule.EvaluationExecutionItem](resp.GetData()) + return decodeStructItems[execution.EvaluationExecutionItem](resp.GetData()) } -func (c *Client) GetTask(ctx context.Context, taskID string) (*taskmodule.TaskDetailResp, error) { +func (c *Client) GetTask(ctx context.Context, taskID string) (*task.TaskDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -371,10 +371,10 @@ func (c *Client) GetTask(ctx context.Context, taskID string) (*taskmodule.TaskDe if err != nil { return nil, mapRPCError(err) } - return decodeStruct[taskmodule.TaskDetailResp](resp.GetData()) + return decodeStruct[task.TaskDetailResp](resp.GetData()) } -func (c *Client) PollTaskLogs(ctx context.Context, taskID string, after time.Time) (*taskmodule.TaskLogPollResp, error) { +func (c *Client) PollTaskLogs(ctx context.Context, taskID string, after time.Time) (*task.TaskLogPollResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -386,10 +386,10 @@ func (c *Client) PollTaskLogs(ctx context.Context, taskID string, after time.Tim if err != nil { return nil, mapRPCError(err) } - return decodeStruct[taskmodule.TaskLogPollResp](resp.GetData()) + return decodeStruct[task.TaskLogPollResp](resp.GetData()) } -func (c *Client) ListTasks(ctx context.Context, req *taskmodule.ListTaskReq) (*dto.ListResp[taskmodule.TaskResp], error) { +func (c *Client) ListTasks(ctx context.Context, req *task.ListTaskReq) (*dto.ListResp[task.TaskResp], error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -401,10 +401,10 @@ func (c *Client) ListTasks(ctx context.Context, req *taskmodule.ListTaskReq) (*d if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[taskmodule.TaskResp]](resp.GetData()) + return decodeStruct[dto.ListResp[task.TaskResp]](resp.GetData()) } -func (c *Client) GetTrace(ctx context.Context, traceID string) (*tracemodule.TraceDetailResp, error) { +func (c *Client) GetTrace(ctx context.Context, traceID string) (*trace.TraceDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -412,10 +412,10 @@ func (c *Client) GetTrace(ctx context.Context, traceID string) (*tracemodule.Tra if err != nil { return nil, mapRPCError(err) } - return decodeStruct[tracemodule.TraceDetailResp](resp.GetData()) + return decodeStruct[trace.TraceDetailResp](resp.GetData()) } -func (c *Client) ListTraces(ctx context.Context, req *tracemodule.ListTraceReq) (*dto.ListResp[tracemodule.TraceResp], error) { +func (c *Client) ListTraces(ctx context.Context, req *trace.ListTraceReq) (*dto.ListResp[trace.TraceResp], error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -427,10 +427,10 @@ func (c *Client) ListTraces(ctx context.Context, req *tracemodule.ListTraceReq) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[tracemodule.TraceResp]](resp.GetData()) + return decodeStruct[dto.ListResp[trace.TraceResp]](resp.GetData()) } -func (c *Client) GetGroupStats(ctx context.Context, groupID string) (*groupmodule.GroupStats, error) { +func (c *Client) GetGroupStats(ctx context.Context, groupID string) (*group.GroupStats, error) { if !c.Enabled() { return nil, fmt.Errorf("orchestrator grpc client is not configured") } @@ -438,7 +438,7 @@ func (c *Client) GetGroupStats(ctx context.Context, groupID string) (*groupmodul if err != nil { return nil, mapRPCError(err) } - return decodeStruct[groupmodule.GroupStats](resp.GetData()) + return decodeStruct[group.GroupStats](resp.GetData()) } func (c *Client) GetTraceStreamAlgorithms(ctx context.Context, traceID string) ([]dto.ContainerVersionItem, error) { diff --git a/src/internalclient/resourceclient/client.go b/src/internalclient/resourceclient/client.go index 2dd5f7de..2e162d1e 100644 --- a/src/internalclient/resourceclient/client.go +++ b/src/internalclient/resourceclient/client.go @@ -9,12 +9,12 @@ import ( "aegis/consts" "aegis/dto" "aegis/httpx" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - labelmodule "aegis/module/label" - projectmodule "aegis/module/project" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + label "aegis/module/label" + project "aegis/module/project" resourcev1 "aegis/proto/resource/v1" "go.uber.org/fx" @@ -68,7 +68,7 @@ func (c *Client) Enabled() bool { return c != nil && c.rpc != nil } -func (c *Client) ListProjects(ctx context.Context, req *projectmodule.ListProjectReq) (*dto.ListResp[projectmodule.ProjectResp], error) { +func (c *Client) ListProjects(ctx context.Context, req *project.ListProjectReq) (*dto.ListResp[project.ProjectResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -80,10 +80,10 @@ func (c *Client) ListProjects(ctx context.Context, req *projectmodule.ListProjec if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[projectmodule.ProjectResp]](resp.GetData()) + return decodeStruct[dto.ListResp[project.ProjectResp]](resp.GetData()) } -func (c *Client) GetProject(ctx context.Context, projectID int) (*projectmodule.ProjectDetailResp, error) { +func (c *Client) GetProject(ctx context.Context, projectID int) (*project.ProjectDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -91,10 +91,10 @@ func (c *Client) GetProject(ctx context.Context, projectID int) (*projectmodule. if err != nil { return nil, mapRPCError(err) } - return decodeStruct[projectmodule.ProjectDetailResp](resp.GetData()) + return decodeStruct[project.ProjectDetailResp](resp.GetData()) } -func (c *Client) ListContainers(ctx context.Context, req *containermodule.ListContainerReq) (*dto.ListResp[containermodule.ContainerResp], error) { +func (c *Client) ListContainers(ctx context.Context, req *container.ListContainerReq) (*dto.ListResp[container.ContainerResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -106,10 +106,10 @@ func (c *Client) ListContainers(ctx context.Context, req *containermodule.ListCo if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[containermodule.ContainerResp]](resp.GetData()) + return decodeStruct[dto.ListResp[container.ContainerResp]](resp.GetData()) } -func (c *Client) GetContainer(ctx context.Context, containerID int) (*containermodule.ContainerDetailResp, error) { +func (c *Client) GetContainer(ctx context.Context, containerID int) (*container.ContainerDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -117,10 +117,10 @@ func (c *Client) GetContainer(ctx context.Context, containerID int) (*containerm if err != nil { return nil, mapRPCError(err) } - return decodeStruct[containermodule.ContainerDetailResp](resp.GetData()) + return decodeStruct[container.ContainerDetailResp](resp.GetData()) } -func (c *Client) ListDatasets(ctx context.Context, req *datasetmodule.ListDatasetReq) (*dto.ListResp[datasetmodule.DatasetResp], error) { +func (c *Client) ListDatasets(ctx context.Context, req *dataset.ListDatasetReq) (*dto.ListResp[dataset.DatasetResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -132,10 +132,10 @@ func (c *Client) ListDatasets(ctx context.Context, req *datasetmodule.ListDatase if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[datasetmodule.DatasetResp]](resp.GetData()) + return decodeStruct[dto.ListResp[dataset.DatasetResp]](resp.GetData()) } -func (c *Client) GetDataset(ctx context.Context, datasetID int) (*datasetmodule.DatasetDetailResp, error) { +func (c *Client) GetDataset(ctx context.Context, datasetID int) (*dataset.DatasetDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -143,10 +143,10 @@ func (c *Client) GetDataset(ctx context.Context, datasetID int) (*datasetmodule. if err != nil { return nil, mapRPCError(err) } - return decodeStruct[datasetmodule.DatasetDetailResp](resp.GetData()) + return decodeStruct[dataset.DatasetDetailResp](resp.GetData()) } -func (c *Client) CreateLabel(ctx context.Context, req *labelmodule.CreateLabelReq) (*labelmodule.LabelResp, error) { +func (c *Client) CreateLabel(ctx context.Context, req *label.CreateLabelReq) (*label.LabelResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -158,10 +158,10 @@ func (c *Client) CreateLabel(ctx context.Context, req *labelmodule.CreateLabelRe if err != nil { return nil, mapRPCError(err) } - return decodeStruct[labelmodule.LabelResp](resp.GetData()) + return decodeStruct[label.LabelResp](resp.GetData()) } -func (c *Client) GetLabel(ctx context.Context, labelID int) (*labelmodule.LabelDetailResp, error) { +func (c *Client) GetLabel(ctx context.Context, labelID int) (*label.LabelDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -169,10 +169,10 @@ func (c *Client) GetLabel(ctx context.Context, labelID int) (*labelmodule.LabelD if err != nil { return nil, mapRPCError(err) } - return decodeStruct[labelmodule.LabelDetailResp](resp.GetData()) + return decodeStruct[label.LabelDetailResp](resp.GetData()) } -func (c *Client) ListLabels(ctx context.Context, req *labelmodule.ListLabelReq) (*dto.ListResp[labelmodule.LabelResp], error) { +func (c *Client) ListLabels(ctx context.Context, req *label.ListLabelReq) (*dto.ListResp[label.LabelResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -184,10 +184,10 @@ func (c *Client) ListLabels(ctx context.Context, req *labelmodule.ListLabelReq) if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[labelmodule.LabelResp]](resp.GetData()) + return decodeStruct[dto.ListResp[label.LabelResp]](resp.GetData()) } -func (c *Client) UpdateLabel(ctx context.Context, req *labelmodule.UpdateLabelReq, labelID int) (*labelmodule.LabelResp, error) { +func (c *Client) UpdateLabel(ctx context.Context, req *label.UpdateLabelReq, labelID int) (*label.LabelResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -202,7 +202,7 @@ func (c *Client) UpdateLabel(ctx context.Context, req *labelmodule.UpdateLabelRe if err != nil { return nil, mapRPCError(err) } - return decodeStruct[labelmodule.LabelResp](resp.GetData()) + return decodeStruct[label.LabelResp](resp.GetData()) } func (c *Client) DeleteLabel(ctx context.Context, labelID int) error { @@ -227,7 +227,7 @@ func (c *Client) BatchDeleteLabels(ctx context.Context, ids []int) error { return nil } -func (c *Client) ListChaosSystems(ctx context.Context, req *chaossystemmodule.ListChaosSystemReq) (*dto.ListResp[chaossystemmodule.ChaosSystemResp], error) { +func (c *Client) ListChaosSystems(ctx context.Context, req *chaossystem.ListChaosSystemReq) (*dto.ListResp[chaossystem.ChaosSystemResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -239,10 +239,10 @@ func (c *Client) ListChaosSystems(ctx context.Context, req *chaossystemmodule.Li if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[chaossystemmodule.ChaosSystemResp]](resp.GetData()) + return decodeStruct[dto.ListResp[chaossystem.ChaosSystemResp]](resp.GetData()) } -func (c *Client) GetChaosSystem(ctx context.Context, systemID int) (*chaossystemmodule.ChaosSystemResp, error) { +func (c *Client) GetChaosSystem(ctx context.Context, systemID int) (*chaossystem.ChaosSystemResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -250,10 +250,10 @@ func (c *Client) GetChaosSystem(ctx context.Context, systemID int) (*chaossystem if err != nil { return nil, mapRPCError(err) } - return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) + return decodeStruct[chaossystem.ChaosSystemResp](resp.GetData()) } -func (c *Client) CreateChaosSystem(ctx context.Context, req *chaossystemmodule.CreateChaosSystemReq) (*chaossystemmodule.ChaosSystemResp, error) { +func (c *Client) CreateChaosSystem(ctx context.Context, req *chaossystem.CreateChaosSystemReq) (*chaossystem.ChaosSystemResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -265,10 +265,10 @@ func (c *Client) CreateChaosSystem(ctx context.Context, req *chaossystemmodule.C if err != nil { return nil, mapRPCError(err) } - return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) + return decodeStruct[chaossystem.ChaosSystemResp](resp.GetData()) } -func (c *Client) UpdateChaosSystem(ctx context.Context, req *chaossystemmodule.UpdateChaosSystemReq, systemID int) (*chaossystemmodule.ChaosSystemResp, error) { +func (c *Client) UpdateChaosSystem(ctx context.Context, req *chaossystem.UpdateChaosSystemReq, systemID int) (*chaossystem.ChaosSystemResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -283,7 +283,7 @@ func (c *Client) UpdateChaosSystem(ctx context.Context, req *chaossystemmodule.U if err != nil { return nil, mapRPCError(err) } - return decodeStruct[chaossystemmodule.ChaosSystemResp](resp.GetData()) + return decodeStruct[chaossystem.ChaosSystemResp](resp.GetData()) } func (c *Client) DeleteChaosSystem(ctx context.Context, systemID int) error { @@ -297,7 +297,7 @@ func (c *Client) DeleteChaosSystem(ctx context.Context, systemID int) error { return nil } -func (c *Client) UpsertChaosSystemMetadata(ctx context.Context, systemID int, req *chaossystemmodule.BulkUpsertSystemMetadataReq) error { +func (c *Client) UpsertChaosSystemMetadata(ctx context.Context, systemID int, req *chaossystem.BulkUpsertSystemMetadataReq) error { if !c.Enabled() { return fmt.Errorf("resource grpc client is not configured") } @@ -315,7 +315,7 @@ func (c *Client) UpsertChaosSystemMetadata(ctx context.Context, systemID int, re return nil } -func (c *Client) ListChaosSystemMetadata(ctx context.Context, systemID int, metadataType string) ([]chaossystemmodule.SystemMetadataResp, error) { +func (c *Client) ListChaosSystemMetadata(ctx context.Context, systemID int, metadataType string) ([]chaossystem.SystemMetadataResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -331,7 +331,7 @@ func (c *Client) ListChaosSystemMetadata(ctx context.Context, systemID int, meta return nil, mapRPCError(err) } items, err := decodeStruct[struct { - Items []chaossystemmodule.SystemMetadataResp `json:"items"` + Items []chaossystem.SystemMetadataResp `json:"items"` }](resp.GetData()) if err != nil { return nil, err @@ -339,7 +339,7 @@ func (c *Client) ListChaosSystemMetadata(ctx context.Context, systemID int, meta return items.Items, nil } -func (c *Client) ListDatapackEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatapackReq, userID int) (*evaluationmodule.BatchEvaluateDatapackResp, error) { +func (c *Client) ListDatapackEvaluationResults(ctx context.Context, req *evaluation.BatchEvaluateDatapackReq, userID int) (*evaluation.BatchEvaluateDatapackResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -354,10 +354,10 @@ func (c *Client) ListDatapackEvaluationResults(ctx context.Context, req *evaluat if err != nil { return nil, mapRPCError(err) } - return decodeStruct[evaluationmodule.BatchEvaluateDatapackResp](resp.GetData()) + return decodeStruct[evaluation.BatchEvaluateDatapackResp](resp.GetData()) } -func (c *Client) ListDatasetEvaluationResults(ctx context.Context, req *evaluationmodule.BatchEvaluateDatasetReq, userID int) (*evaluationmodule.BatchEvaluateDatasetResp, error) { +func (c *Client) ListDatasetEvaluationResults(ctx context.Context, req *evaluation.BatchEvaluateDatasetReq, userID int) (*evaluation.BatchEvaluateDatasetResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -372,10 +372,10 @@ func (c *Client) ListDatasetEvaluationResults(ctx context.Context, req *evaluati if err != nil { return nil, mapRPCError(err) } - return decodeStruct[evaluationmodule.BatchEvaluateDatasetResp](resp.GetData()) + return decodeStruct[evaluation.BatchEvaluateDatasetResp](resp.GetData()) } -func (c *Client) ListEvaluations(ctx context.Context, req *evaluationmodule.ListEvaluationReq) (*dto.ListResp[evaluationmodule.EvaluationResp], error) { +func (c *Client) ListEvaluations(ctx context.Context, req *evaluation.ListEvaluationReq) (*dto.ListResp[evaluation.EvaluationResp], error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -387,10 +387,10 @@ func (c *Client) ListEvaluations(ctx context.Context, req *evaluationmodule.List if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[evaluationmodule.EvaluationResp]](resp.GetData()) + return decodeStruct[dto.ListResp[evaluation.EvaluationResp]](resp.GetData()) } -func (c *Client) GetEvaluation(ctx context.Context, evaluationID int) (*evaluationmodule.EvaluationResp, error) { +func (c *Client) GetEvaluation(ctx context.Context, evaluationID int) (*evaluation.EvaluationResp, error) { if !c.Enabled() { return nil, fmt.Errorf("resource grpc client is not configured") } @@ -398,7 +398,7 @@ func (c *Client) GetEvaluation(ctx context.Context, evaluationID int) (*evaluati if err != nil { return nil, mapRPCError(err) } - return decodeStruct[evaluationmodule.EvaluationResp](resp.GetData()) + return decodeStruct[evaluation.EvaluationResp](resp.GetData()) } func (c *Client) DeleteEvaluation(ctx context.Context, evaluationID int) error { diff --git a/src/internalclient/runtimeclient/client.go b/src/internalclient/runtimeclient/client.go index 40a79e0d..31031cd8 100644 --- a/src/internalclient/runtimeclient/client.go +++ b/src/internalclient/runtimeclient/client.go @@ -8,8 +8,8 @@ import ( "aegis/config" "aegis/consts" "aegis/httpx" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" runtimev1 "aegis/proto/runtime/v1" "go.uber.org/fx" @@ -63,7 +63,7 @@ func (c *Client) Enabled() bool { return c != nil && c.rpc != nil } -func (c *Client) GetNamespaceLocks(ctx context.Context) (*systemmetricmodule.ListNamespaceLockResp, error) { +func (c *Client) GetNamespaceLocks(ctx context.Context) (*systemmetric.ListNamespaceLockResp, error) { if !c.Enabled() { return nil, fmt.Errorf("runtime grpc client is not configured") } @@ -71,10 +71,10 @@ func (c *Client) GetNamespaceLocks(ctx context.Context) (*systemmetricmodule.Lis if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmetricmodule.ListNamespaceLockResp](resp.GetData()) + return decodeStruct[systemmetric.ListNamespaceLockResp](resp.GetData()) } -func (c *Client) GetQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { +func (c *Client) GetQueuedTasks(ctx context.Context) (*task.QueuedTasksResp, error) { if !c.Enabled() { return nil, fmt.Errorf("runtime grpc client is not configured") } @@ -82,7 +82,7 @@ func (c *Client) GetQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksRes if err != nil { return nil, mapRPCError(err) } - return decodeStruct[taskmodule.QueuedTasksResp](resp.GetData()) + return decodeStruct[task.QueuedTasksResp](resp.GetData()) } func decodeStruct[T any](payload *structpb.Struct) (*T, error) { diff --git a/src/internalclient/systemclient/client.go b/src/internalclient/systemclient/client.go index cee5f356..bee6e5ae 100644 --- a/src/internalclient/systemclient/client.go +++ b/src/internalclient/systemclient/client.go @@ -9,8 +9,8 @@ import ( "aegis/consts" "aegis/dto" "aegis/httpx" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" systemv1 "aegis/proto/system/v1" "go.uber.org/fx" @@ -64,7 +64,7 @@ func (c *Client) Enabled() bool { return c != nil && c.rpc != nil } -func (c *Client) GetHealth(ctx context.Context) (*systemmodule.HealthCheckResp, error) { +func (c *Client) GetHealth(ctx context.Context) (*system.HealthCheckResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -72,10 +72,10 @@ func (c *Client) GetHealth(ctx context.Context) (*systemmodule.HealthCheckResp, if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.HealthCheckResp](resp.GetData()) + return decodeStruct[system.HealthCheckResp](resp.GetData()) } -func (c *Client) GetMetrics(ctx context.Context) (*systemmodule.MonitoringMetricsResp, error) { +func (c *Client) GetMetrics(ctx context.Context) (*system.MonitoringMetricsResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -83,10 +83,10 @@ func (c *Client) GetMetrics(ctx context.Context) (*systemmodule.MonitoringMetric if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.MonitoringMetricsResp](resp.GetData()) + return decodeStruct[system.MonitoringMetricsResp](resp.GetData()) } -func (c *Client) GetSystemInfo(ctx context.Context) (*systemmodule.SystemInfo, error) { +func (c *Client) GetSystemInfo(ctx context.Context) (*system.SystemInfo, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -94,10 +94,10 @@ func (c *Client) GetSystemInfo(ctx context.Context) (*systemmodule.SystemInfo, e if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.SystemInfo](resp.GetData()) + return decodeStruct[system.SystemInfo](resp.GetData()) } -func (c *Client) ListConfigs(ctx context.Context, req *systemmodule.ListConfigReq) (*dto.ListResp[systemmodule.ConfigResp], error) { +func (c *Client) ListConfigs(ctx context.Context, req *system.ListConfigReq) (*dto.ListResp[system.ConfigResp], error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -109,10 +109,10 @@ func (c *Client) ListConfigs(ctx context.Context, req *systemmodule.ListConfigRe if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[systemmodule.ConfigResp]](resp.GetData()) + return decodeStruct[dto.ListResp[system.ConfigResp]](resp.GetData()) } -func (c *Client) GetConfig(ctx context.Context, configID int) (*systemmodule.ConfigDetailResp, error) { +func (c *Client) GetConfig(ctx context.Context, configID int) (*system.ConfigDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -120,10 +120,10 @@ func (c *Client) GetConfig(ctx context.Context, configID int) (*systemmodule.Con if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.ConfigDetailResp](resp.GetData()) + return decodeStruct[system.ConfigDetailResp](resp.GetData()) } -func (c *Client) ListAuditLogs(ctx context.Context, req *systemmodule.ListAuditLogReq) (*dto.ListResp[systemmodule.AuditLogResp], error) { +func (c *Client) ListAuditLogs(ctx context.Context, req *system.ListAuditLogReq) (*dto.ListResp[system.AuditLogResp], error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -135,10 +135,10 @@ func (c *Client) ListAuditLogs(ctx context.Context, req *systemmodule.ListAuditL if err != nil { return nil, mapRPCError(err) } - return decodeStruct[dto.ListResp[systemmodule.AuditLogResp]](resp.GetData()) + return decodeStruct[dto.ListResp[system.AuditLogResp]](resp.GetData()) } -func (c *Client) GetAuditLog(ctx context.Context, auditLogID int) (*systemmodule.AuditLogDetailResp, error) { +func (c *Client) GetAuditLog(ctx context.Context, auditLogID int) (*system.AuditLogDetailResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -146,10 +146,10 @@ func (c *Client) GetAuditLog(ctx context.Context, auditLogID int) (*systemmodule if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.AuditLogDetailResp](resp.GetData()) + return decodeStruct[system.AuditLogDetailResp](resp.GetData()) } -func (c *Client) ListNamespaceLocks(ctx context.Context) (*systemmodule.ListNamespaceLockResp, error) { +func (c *Client) ListNamespaceLocks(ctx context.Context) (*system.ListNamespaceLockResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -157,10 +157,10 @@ func (c *Client) ListNamespaceLocks(ctx context.Context) (*systemmodule.ListName if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.ListNamespaceLockResp](resp.GetData()) + return decodeStruct[system.ListNamespaceLockResp](resp.GetData()) } -func (c *Client) ListQueuedTasks(ctx context.Context) (*systemmodule.QueuedTasksResp, error) { +func (c *Client) ListQueuedTasks(ctx context.Context) (*system.QueuedTasksResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -168,10 +168,10 @@ func (c *Client) ListQueuedTasks(ctx context.Context) (*systemmodule.QueuedTasks if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmodule.QueuedTasksResp](resp.GetData()) + return decodeStruct[system.QueuedTasksResp](resp.GetData()) } -func (c *Client) GetSystemMetrics(ctx context.Context) (*systemmetricmodule.SystemMetricsResp, error) { +func (c *Client) GetSystemMetrics(ctx context.Context) (*systemmetric.SystemMetricsResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -179,10 +179,10 @@ func (c *Client) GetSystemMetrics(ctx context.Context) (*systemmetricmodule.Syst if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmetricmodule.SystemMetricsResp](resp.GetData()) + return decodeStruct[systemmetric.SystemMetricsResp](resp.GetData()) } -func (c *Client) GetSystemMetricsHistory(ctx context.Context) (*systemmetricmodule.SystemMetricsHistoryResp, error) { +func (c *Client) GetSystemMetricsHistory(ctx context.Context) (*systemmetric.SystemMetricsHistoryResp, error) { if !c.Enabled() { return nil, fmt.Errorf("system grpc client is not configured") } @@ -190,7 +190,7 @@ func (c *Client) GetSystemMetricsHistory(ctx context.Context) (*systemmetricmodu if err != nil { return nil, mapRPCError(err) } - return decodeStruct[systemmetricmodule.SystemMetricsHistoryResp](resp.GetData()) + return decodeStruct[systemmetric.SystemMetricsHistoryResp](resp.GetData()) } func toStructPB(value any) (*structpb.Struct, error) { diff --git a/src/main.go b/src/main.go index 81f5062e..cef7ba4a 100644 --- a/src/main.go +++ b/src/main.go @@ -21,12 +21,12 @@ import ( "os" "aegis/app" - gatewayapp "aegis/app/gateway" - iamapp "aegis/app/iam" - orchestratorapp "aegis/app/orchestrator" - resourceapp "aegis/app/resource" + gateway "aegis/app/gateway" + iam "aegis/app/iam" + orchestrator "aegis/app/orchestrator" + resource "aegis/app/resource" runtimeapp "aegis/app/runtime" - systemapp "aegis/app/system" + system "aegis/app/system" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -75,22 +75,22 @@ func main() { fx.New(app.BothOptions(viper.GetString("conf"), viper.GetString("port"))).Run() }) apiGatewayCmd := newModeCommand("api-gateway", "Run as the API gateway", func() { - fx.New(gatewayapp.Options(viper.GetString("conf"), viper.GetString("port"))).Run() + fx.New(gateway.Options(viper.GetString("conf"), viper.GetString("port"))).Run() }) iamServiceCmd := newModeCommand("iam-service", "Run as the IAM service", func() { - fx.New(iamapp.Options(viper.GetString("conf"))).Run() + fx.New(iam.Options(viper.GetString("conf"))).Run() }) orchestratorServiceCmd := newModeCommand("orchestrator-service", "Run as the orchestrator service", func() { - fx.New(orchestratorapp.Options(viper.GetString("conf"))).Run() + fx.New(orchestrator.Options(viper.GetString("conf"))).Run() }) resourceServiceCmd := newModeCommand("resource-service", "Run as the resource service", func() { - fx.New(resourceapp.Options(viper.GetString("conf"))).Run() + fx.New(resource.Options(viper.GetString("conf"))).Run() }) runtimeWorkerServiceCmd := newModeCommand("runtime-worker-service", "Run as the runtime worker service", func() { fx.New(runtimeapp.Options(viper.GetString("conf"))).Run() }) systemServiceCmd := newModeCommand("system-service", "Run as the system service", func() { - fx.New(systemapp.Options(viper.GetString("conf"))).Run() + fx.New(system.Options(viper.GetString("conf"))).Run() }) rootCmd.AddCommand( diff --git a/src/module/auth/api_types.go b/src/module/auth/api_types.go index 76dc242c..1f0e42de 100644 --- a/src/module/auth/api_types.go +++ b/src/module/auth/api_types.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "fmt" @@ -10,7 +10,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - usermodule "aegis/module/user" + user "aegis/module/user" "aegis/utils" ) @@ -265,9 +265,9 @@ type UserProfileResp struct { LastLoginAt *time.Time `json:"last_login_at,omitempty"` CreatedAt time.Time `json:"created_at"` - ContainerRoles []usermodule.UserContainerInfo `json:"container_roles,omitempty"` - DatasetRoles []usermodule.UserDatasetInfo `json:"dataset_roles,omitempty"` - ProjectRoles []usermodule.UserProjectInfo `json:"project_roles,omitempty"` + ContainerRoles []user.UserContainerInfo `json:"container_roles,omitempty"` + DatasetRoles []user.UserDatasetInfo `json:"dataset_roles,omitempty"` + ProjectRoles []user.UserProjectInfo `json:"project_roles,omitempty"` } func NewUserProfileResp(user *model.User) *UserProfileResp { diff --git a/src/module/auth/handler.go b/src/module/auth/handler.go index 1292ca48..676ef5fc 100644 --- a/src/module/auth/handler.go +++ b/src/module/auth/handler.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "aegis/httpx" @@ -28,11 +28,11 @@ func NewHandler(service HandlerService) *Handler { // @ID login // @Accept json // @Produce json -// @Param request body LoginReq true "Login credentials" -// @Success 200 {object} dto.GenericResponse[LoginResp] "Login successful" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid user name or password" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body LoginReq true "Login credentials" +// @Success 200 {object} dto.GenericResponse[LoginResp] "Login successful" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid user name or password" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/auth/login [post] // @x-api-type {"portal":"true","admin":"true"} func (h *Handler) Login(c *gin.Context) { @@ -63,11 +63,11 @@ func (h *Handler) Login(c *gin.Context) { // @ID register_user // @Accept json // @Produce json -// @Param request body RegisterReq true "Registration details" -// @Success 201 {object} dto.GenericResponse[UserInfo] "Registration successful" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 409 {object} dto.GenericResponse[any] "User already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body RegisterReq true "Registration details" +// @Success 201 {object} dto.GenericResponse[UserInfo] "Registration successful" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 409 {object} dto.GenericResponse[any] "User already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/auth/register [post] // @x-api-type {"portal":"true","admin":"true"} func (h *Handler) Register(c *gin.Context) { @@ -98,11 +98,11 @@ func (h *Handler) Register(c *gin.Context) { // @ID refresh_auth_token // @Accept json // @Produce json -// @Param request body TokenRefreshReq true "Token refresh request" -// @Success 200 {object} dto.GenericResponse[TokenRefreshResp] "Token refreshed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body TokenRefreshReq true "Token refresh request" +// @Success 200 {object} dto.GenericResponse[TokenRefreshResp] "Token refreshed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid token" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/auth/refresh [post] // @x-api-type {"portal":"true","admin":"true"} func (h *Handler) RefreshToken(c *gin.Context) { @@ -241,9 +241,9 @@ func (h *Handler) GetProfile(c *gin.Context) { // @Security BearerAuth // @Param request body CreateAPIKeyReq true "API key create request" // @Success 201 {object} dto.GenericResponse[APIKeyWithSecretResp] "API key created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys [post] // @x-api-type {"portal":"true"} func (h *Handler) CreateAPIKey(c *gin.Context) { @@ -279,12 +279,12 @@ func (h *Handler) CreateAPIKey(c *gin.Context) { // @ID list_api_keys // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" -// @Param size query int false "Page size" +// @Param page query int false "Page number" +// @Param size query int false "Page size" // @Success 200 {object} dto.GenericResponse[ListAPIKeyResp] "API keys listed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys [get] // @x-api-type {"portal":"true"} func (h *Handler) ListAPIKeys(c *gin.Context) { @@ -320,11 +320,11 @@ func (h *Handler) ListAPIKeys(c *gin.Context) { // @ID get_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" -// @Success 200 {object} dto.GenericResponse[APIKeyInfo] "API key detail retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "API key not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[APIKeyInfo] "API key detail retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys/{id} [get] // @x-api-type {"portal":"true"} func (h *Handler) GetAPIKey(c *gin.Context) { @@ -349,11 +349,11 @@ func (h *Handler) GetAPIKey(c *gin.Context) { // @ID delete_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" -// @Success 204 {object} dto.GenericResponse[any] "API key deleted successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "API key not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "API key record ID" +// @Success 204 {object} dto.GenericResponse[any] "API key deleted successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys/{id} [delete] // @x-api-type {"portal":"true"} func (h *Handler) DeleteAPIKey(c *gin.Context) { @@ -377,11 +377,11 @@ func (h *Handler) DeleteAPIKey(c *gin.Context) { // @ID disable_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" -// @Success 200 {object} dto.GenericResponse[any] "API key disabled successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "API key not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[any] "API key disabled successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys/{id}/disable [post] // @x-api-type {"portal":"true"} func (h *Handler) DisableAPIKey(c *gin.Context) { @@ -405,11 +405,11 @@ func (h *Handler) DisableAPIKey(c *gin.Context) { // @ID enable_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" -// @Success 200 {object} dto.GenericResponse[any] "API key enabled successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "API key not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[any] "API key enabled successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys/{id}/enable [post] // @x-api-type {"portal":"true"} func (h *Handler) EnableAPIKey(c *gin.Context) { @@ -433,7 +433,7 @@ func (h *Handler) EnableAPIKey(c *gin.Context) { // @ID revoke_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" +// @Param id path int true "API key record ID" // @Success 200 {object} dto.GenericResponse[any] "API key revoked successfully" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 404 {object} dto.GenericResponse[any] "API key not found" @@ -461,11 +461,11 @@ func (h *Handler) RevokeAPIKey(c *gin.Context) { // @ID rotate_api_key // @Produce json // @Security BearerAuth -// @Param id path int true "API key record ID" -// @Success 200 {object} dto.GenericResponse[APIKeyWithSecretResp] "API key rotated successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "API key not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "API key record ID" +// @Success 200 {object} dto.GenericResponse[APIKeyWithSecretResp] "API key rotated successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "API key not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/api-keys/{id}/rotate [post] // @x-api-type {"portal":"true"} func (h *Handler) RotateAPIKey(c *gin.Context) { @@ -489,14 +489,14 @@ func (h *Handler) RotateAPIKey(c *gin.Context) { // @Tags Authentication // @ID exchange_api_key_token // @Produce json -// @Param X-Key-Id header string true "Public key identifier" -// @Param X-Timestamp header string true "Unix timestamp in seconds" -// @Param X-Nonce header string true "Unique request nonce" -// @Param X-Signature header string true "Hex encoded HMAC-SHA256 signature of METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)" -// @Success 200 {object} dto.GenericResponse[APIKeyTokenResp] "API key token issued successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Invalid signature or replayed request" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param X-Key-Id header string true "Public key identifier" +// @Param X-Timestamp header string true "Unix timestamp in seconds" +// @Param X-Nonce header string true "Unique request nonce" +// @Param X-Signature header string true "Hex encoded HMAC-SHA256 signature of METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nSHA256(BODY)" +// @Success 200 {object} dto.GenericResponse[APIKeyTokenResp] "API key token issued successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Invalid signature or replayed request" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/auth/api-key/token [post] // @x-api-type {"sdk":"true"} func (h *Handler) ExchangeAPIKeyToken(c *gin.Context) { diff --git a/src/module/auth/handler_service.go b/src/module/auth/handler_service.go index 923930bc..55827e46 100644 --- a/src/module/auth/handler_service.go +++ b/src/module/auth/handler_service.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "context" diff --git a/src/module/auth/middleware_adapter.go b/src/module/auth/middleware_adapter.go index 9b5131be..ce98623f 100644 --- a/src/module/auth/middleware_adapter.go +++ b/src/module/auth/middleware_adapter.go @@ -1,4 +1,4 @@ -package authmodule +package auth import "aegis/middleware" diff --git a/src/module/auth/module.go b/src/module/auth/module.go index dfe1e4a1..fcf705e0 100644 --- a/src/module/auth/module.go +++ b/src/module/auth/module.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "go.uber.org/fx" diff --git a/src/module/auth/repository.go b/src/module/auth/repository.go index 791e4b28..bc06fb1d 100644 --- a/src/module/auth/repository.go +++ b/src/module/auth/repository.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "aegis/consts" diff --git a/src/module/auth/service.go b/src/module/auth/service.go index b73340ca..3f04f90f 100644 --- a/src/module/auth/service.go +++ b/src/module/auth/service.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "context" @@ -10,7 +10,7 @@ import ( "aegis/consts" "aegis/model" - usermodule "aegis/module/user" + user "aegis/module/user" "aegis/utils" "github.com/sirupsen/logrus" @@ -523,32 +523,32 @@ func (s *Service) generateAPIKeyTokenWithRoles(roleRepo *RoleRepository, user *m return token, expiresAt, nil } -func (s *Service) getAllUserResourceRoles(userID int) ([]usermodule.UserContainerInfo, []usermodule.UserDatasetInfo, []usermodule.UserProjectInfo, error) { +func (s *Service) getAllUserResourceRoles(userID int) ([]user.UserContainerInfo, []user.UserDatasetInfo, []user.UserProjectInfo, error) { userContainers, err := s.userRepo.ListContainerRoles(userID) if err != nil { return nil, nil, nil, fmt.Errorf("failed to list user-container roles: %w", err) } - containerRoles := make([]usermodule.UserContainerInfo, 0, len(userContainers)) + containerRoles := make([]user.UserContainerInfo, 0, len(userContainers)) for _, uc := range userContainers { - containerRoles = append(containerRoles, *usermodule.NewUserContainerInfo(&uc)) + containerRoles = append(containerRoles, *user.NewUserContainerInfo(&uc)) } userDatasets, err := s.userRepo.ListDatasetRoles(userID) if err != nil { return nil, nil, nil, fmt.Errorf("failed to list user-dataset roles: %w", err) } - datasetRoles := make([]usermodule.UserDatasetInfo, 0, len(userDatasets)) + datasetRoles := make([]user.UserDatasetInfo, 0, len(userDatasets)) for _, ud := range userDatasets { - datasetRoles = append(datasetRoles, *usermodule.NewUserDatasetInfo(&ud)) + datasetRoles = append(datasetRoles, *user.NewUserDatasetInfo(&ud)) } userProjects, err := s.userRepo.ListProjectRoles(userID) if err != nil { return nil, nil, nil, fmt.Errorf("failed to list user-project roles: %w", err) } - projectRoles := make([]usermodule.UserProjectInfo, 0, len(userProjects)) + projectRoles := make([]user.UserProjectInfo, 0, len(userProjects)) for _, up := range userProjects { - projectRoles = append(projectRoles, *usermodule.NewUserProjectInfo(&up)) + projectRoles = append(projectRoles, *user.NewUserProjectInfo(&up)) } return containerRoles, datasetRoles, projectRoles, nil diff --git a/src/module/auth/service_test.go b/src/module/auth/service_test.go index 17fb4aad..88cd6f0e 100644 --- a/src/module/auth/service_test.go +++ b/src/module/auth/service_test.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "database/sql/driver" diff --git a/src/module/auth/token_store.go b/src/module/auth/token_store.go index f24f14ab..bee97d12 100644 --- a/src/module/auth/token_store.go +++ b/src/module/auth/token_store.go @@ -1,4 +1,4 @@ -package authmodule +package auth import ( "context" @@ -7,17 +7,17 @@ import ( "time" "aegis/consts" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" ) const tokenBlacklistPrefix = "blacklist:token:%s" const apiKeyNoncePrefix = "api_key:nonce:%s:%s" type TokenStore struct { - redis *redisinfra.Gateway + redis *redis.Gateway } -func NewTokenStore(redis *redisinfra.Gateway) *TokenStore { +func NewTokenStore(redis *redis.Gateway) *TokenStore { return &TokenStore{redis: redis} } diff --git a/src/module/chaossystem/api_types.go b/src/module/chaossystem/api_types.go index 68a795ab..c809b82f 100644 --- a/src/module/chaossystem/api_types.go +++ b/src/module/chaossystem/api_types.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import ( "encoding/json" diff --git a/src/module/chaossystem/handler.go b/src/module/chaossystem/handler.go index 7f9b3e88..286c5072 100644 --- a/src/module/chaossystem/handler.go +++ b/src/module/chaossystem/handler.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import ( "aegis/httpx" @@ -29,9 +29,9 @@ func NewHandler(service HandlerService) *Handler { // @Param page query int false "Page number" default(1) // @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[ChaosSystemResp]] "Systems retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems [get] // @x-api-type {"admin":"true"} func (h *Handler) ListSystems(c *gin.Context) { @@ -177,12 +177,12 @@ func (h *Handler) DeleteSystem(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "System ID" -// @Param request body BulkUpsertSystemMetadataReq true "Metadata upsert request" -// @Success 200 {object} dto.GenericResponse[any] "Metadata upserted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 404 {object} dto.GenericResponse[any] "System not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "System ID" +// @Param request body BulkUpsertSystemMetadataReq true "Metadata upsert request" +// @Success 200 {object} dto.GenericResponse[any] "Metadata upserted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 404 {object} dto.GenericResponse[any] "System not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id}/metadata [post] // @x-api-type {"admin":"true"} func (h *Handler) UpsertMetadata(c *gin.Context) { @@ -209,12 +209,12 @@ func (h *Handler) UpsertMetadata(c *gin.Context) { // @ID list_chaos_system_metadata // @Produce json // @Security BearerAuth -// @Param id path int true "System ID" -// @Param type query string false "Metadata type filter" +// @Param id path int true "System ID" +// @Param type query string false "Metadata type filter" // @Success 200 {object} dto.GenericResponse[[]SystemMetadataResp] "Metadata retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid system ID" -// @Failure 404 {object} dto.GenericResponse[any] "System not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid system ID" +// @Failure 404 {object} dto.GenericResponse[any] "System not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/systems/{id}/metadata [get] // @x-api-type {"admin":"true"} func (h *Handler) ListMetadata(c *gin.Context) { diff --git a/src/module/chaossystem/handler_service.go b/src/module/chaossystem/handler_service.go index 23b3c9a0..d1b8b911 100644 --- a/src/module/chaossystem/handler_service.go +++ b/src/module/chaossystem/handler_service.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import ( "context" diff --git a/src/module/chaossystem/module.go b/src/module/chaossystem/module.go index 2a75c3f9..cb9e440c 100644 --- a/src/module/chaossystem/module.go +++ b/src/module/chaossystem/module.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import "go.uber.org/fx" diff --git a/src/module/chaossystem/repository.go b/src/module/chaossystem/repository.go index f8c18994..de1e2518 100644 --- a/src/module/chaossystem/repository.go +++ b/src/module/chaossystem/repository.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import ( "aegis/consts" diff --git a/src/module/chaossystem/service.go b/src/module/chaossystem/service.go index 3dc5d7bc..394e000c 100644 --- a/src/module/chaossystem/service.go +++ b/src/module/chaossystem/service.go @@ -1,4 +1,4 @@ -package chaossystemmodule +package chaossystem import ( "context" diff --git a/src/module/container/api_types.go b/src/module/container/api_types.go index 70185560..c573753b 100644 --- a/src/module/container/api_types.go +++ b/src/module/container/api_types.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "fmt" diff --git a/src/module/container/build_gateway.go b/src/module/container/build_gateway.go index f879dcbb..a0b9753a 100644 --- a/src/module/container/build_gateway.go +++ b/src/module/container/build_gateway.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "fmt" diff --git a/src/module/container/build_gateway_test.go b/src/module/container/build_gateway_test.go index 3211565f..f20a85f9 100644 --- a/src/module/container/build_gateway_test.go +++ b/src/module/container/build_gateway_test.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "os" diff --git a/src/module/container/core.go b/src/module/container/core.go index 300c9e09..1f72abba 100644 --- a/src/module/container/core.go +++ b/src/module/container/core.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "aegis/model" diff --git a/src/module/container/file_store.go b/src/module/container/file_store.go index 5bc2dc13..fe217aae 100644 --- a/src/module/container/file_store.go +++ b/src/module/container/file_store.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "fmt" diff --git a/src/module/container/file_store_test.go b/src/module/container/file_store_test.go index 1fada913..eee4f1b7 100644 --- a/src/module/container/file_store_test.go +++ b/src/module/container/file_store_test.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "bytes" diff --git a/src/module/container/handler.go b/src/module/container/handler.go index 05c34e85..01b45b74 100644 --- a/src/module/container/handler.go +++ b/src/module/container/handler.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "aegis/httpx" @@ -31,15 +31,15 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateContainerReq true "Container creation request" -// @Success 201 {object} dto.GenericResponse[ContainerResp] "Container created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body CreateContainerReq true "Container creation request" +// @Success 201 {object} dto.GenericResponse[ContainerResp] "Container created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) CreateContainer(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -82,7 +82,7 @@ func (h *Handler) CreateContainer(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Container not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [delete] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DeleteContainer(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -104,15 +104,15 @@ func (h *Handler) DeleteContainer(c *gin.Context) { // @ID get_container_by_id // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" +// @Param container_id path int true "Container ID" // @Success 200 {object} dto.GenericResponse[ContainerDetailResp] "Container retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetContainer(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -135,18 +135,18 @@ func (h *Handler) GetContainer(c *gin.Context) { // @ID list_containers // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query consts.PageSize false "Page size" default(20) -// @Param type query consts.ContainerType false "Container type filter" -// @Param is_public query bool false "Container public visibility filter" -// @Param status query consts.StatusType false "Container status filter" +// @Param page query int false "Page number" default(1) +// @Param size query consts.PageSize false "Page size" default(20) +// @Param type query consts.ContainerType false "Container type filter" +// @Param is_public query bool false "Container public visibility filter" +// @Param status query consts.StatusType false "Container status filter" // @Success 200 {object} dto.GenericResponse[dto.ListResp[ContainerResp]] "Containers retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListContainers(c *gin.Context) { var req ListContainerReq if err := c.ShouldBindQuery(&req); err != nil { @@ -176,16 +176,16 @@ func (h *Handler) ListContainers(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param request body UpdateContainerReq true "Container update request" -// @Success 202 {object} dto.GenericResponse[ContainerResp] "Container updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param container_id path int true "Container ID" +// @Param request body UpdateContainerReq true "Container update request" +// @Success 202 {object} dto.GenericResponse[ContainerResp] "Container updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id} [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UpdateContainer(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -215,16 +215,16 @@ func (h *Handler) UpdateContainer(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param manage body ManageContainerLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[ContainerResp] "Labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param container_id path int true "Container ID" +// @Param manage body ManageContainerLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[ContainerResp] "Labels managed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/labels [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ManageContainerCustomLabels(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -259,16 +259,16 @@ func (h *Handler) ManageContainerCustomLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" +// @Param container_id path int true "Container ID" // @Param request body CreateContainerVersionReq true "Container version creation request" // @Success 201 {object} dto.GenericResponse[ContainerVersionResp] "Container version created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) CreateContainerVersion(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -317,7 +317,7 @@ func (h *Handler) CreateContainerVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [delete] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DeleteContainerVersion(c *gin.Context) { versionID, ok := parseVersionID(c, "Invalid container version ID") if !ok { @@ -339,16 +339,16 @@ func (h *Handler) DeleteContainerVersion(c *gin.Context) { // @ID get_container_version_by_id // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param version_id path int true "Container Version ID" +// @Param container_id path int true "Container ID" +// @Param version_id path int true "Container Version ID" // @Success 200 {object} dto.GenericResponse[ContainerVersionDetailResp] "Container version retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetContainerVersion(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -375,17 +375,17 @@ func (h *Handler) GetContainerVersion(c *gin.Context) { // @ID list_container_versions // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param status query consts.StatusType false "Container version status filter" +// @Param container_id path int true "Container ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param status query consts.StatusType false "Container version status filter" // @Success 200 {object} dto.GenericResponse[dto.ListResp[ContainerVersionResp]] "Container versions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListContainerVersions(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -420,17 +420,17 @@ func (h *Handler) ListContainerVersions(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param version_id path int true "Container Version ID" +// @Param container_id path int true "Container ID" +// @Param version_id path int true "Container Version ID" // @Param request body UpdateContainerVersionReq true "Container version update request" // @Success 202 {object} dto.GenericResponse[ContainerVersionResp] "Container version updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid container ID/container version ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id} [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UpdateContainerVersion(c *gin.Context) { containerID, ok := parseContainerID(c) if !ok { @@ -466,13 +466,13 @@ func (h *Handler) UpdateContainerVersion(c *gin.Context) { // @Security BearerAuth // @Param request body SubmitBuildContainerReq true "Container build request" // @Success 200 {object} dto.GenericResponse[SubmitContainerBuildResp] "Container build task submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Required files not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Required files not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/build [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) SubmitContainerBuilding(c *gin.Context) { groupID := c.GetString("groupID") userID, exists := middleware.GetCurrentUserID(c) @@ -509,17 +509,17 @@ func (h *Handler) SubmitContainerBuilding(c *gin.Context) { // @Accept multipart/form-data // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param version_id path int true "Container Version ID" -// @Param file formData file true "Helm chart package (.tgz)" +// @Param container_id path int true "Container ID" +// @Param version_id path int true "Container Version ID" +// @Param file formData file true "Helm chart package (.tgz)" // @Success 200 {object} dto.GenericResponse[UploadHelmChartResp] "Chart uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id}/helm-chart [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UploadHelmChart(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -565,17 +565,17 @@ func (h *Handler) UploadHelmChart(c *gin.Context) { // @Accept multipart/form-data // @Produce json // @Security BearerAuth -// @Param container_id path int true "Container ID" -// @Param version_id path int true "Container Version ID" -// @Param file formData file true "Helm values YAML file" +// @Param container_id path int true "Container ID" +// @Param version_id path int true "Container Version ID" +// @Param file formData file true "Helm values YAML file" // @Success 200 {object} dto.GenericResponse[UploadHelmValueFileResp] "File uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request or file" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Container or version not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/containers/{container_id}/versions/{version_id}/helm-values [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UploadHelmValueFile(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { diff --git a/src/module/container/handler_service.go b/src/module/container/handler_service.go index ba892342..a129958d 100644 --- a/src/module/container/handler_service.go +++ b/src/module/container/handler_service.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "context" diff --git a/src/module/container/module.go b/src/module/container/module.go index f7d19b5b..16ab6eb0 100644 --- a/src/module/container/module.go +++ b/src/module/container/module.go @@ -1,4 +1,4 @@ -package containermodule +package container import "go.uber.org/fx" diff --git a/src/module/container/repository.go b/src/module/container/repository.go index 989eb2c2..cf95197f 100644 --- a/src/module/container/repository.go +++ b/src/module/container/repository.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "aegis/consts" diff --git a/src/module/container/resolve.go b/src/module/container/resolve.go index 573a45ba..b85e49c5 100644 --- a/src/module/container/resolve.go +++ b/src/module/container/resolve.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "aegis/consts" diff --git a/src/module/container/service.go b/src/module/container/service.go index fe1d31e1..9d2657a5 100644 --- a/src/module/container/service.go +++ b/src/module/container/service.go @@ -1,4 +1,4 @@ -package containermodule +package container import ( "context" @@ -8,9 +8,9 @@ import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/model" - labelmodule "aegis/module/label" + label "aegis/module/label" "aegis/service/common" "gorm.io/gorm" @@ -20,10 +20,10 @@ type Service struct { repo *Repository build *BuildGateway helmFiles *HelmFileStore - redis *redisinfra.Gateway + redis *redis.Gateway } -func NewService(repo *Repository, build *BuildGateway, helmFiles *HelmFileStore, redis *redisinfra.Gateway) *Service { +func NewService(repo *Repository, build *BuildGateway, helmFiles *HelmFileStore, redis *redis.Gateway) *Service { return &Service{repo: repo, build: build, helmFiles: helmFiles, redis: redis} } @@ -169,7 +169,7 @@ func (s *Service) ManageContainerLabels(_ context.Context, req *ManageContainerL } if len(req.AddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ContainerCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/dataset/api_types.go b/src/module/dataset/api_types.go index 928dc1f4..8af8589f 100644 --- a/src/module/dataset/api_types.go +++ b/src/module/dataset/api_types.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "encoding/json" diff --git a/src/module/dataset/core.go b/src/module/dataset/core.go index 3aea12d9..77c7a638 100644 --- a/src/module/dataset/core.go +++ b/src/module/dataset/core.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "aegis/model" diff --git a/src/module/dataset/file_store.go b/src/module/dataset/file_store.go index b26c06dc..4d13a414 100644 --- a/src/module/dataset/file_store.go +++ b/src/module/dataset/file_store.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "archive/zip" diff --git a/src/module/dataset/file_store_test.go b/src/module/dataset/file_store_test.go index 4e10a3f3..6d662142 100644 --- a/src/module/dataset/file_store_test.go +++ b/src/module/dataset/file_store_test.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "archive/zip" diff --git a/src/module/dataset/handler.go b/src/module/dataset/handler.go index 86821551..c19ce5af 100644 --- a/src/module/dataset/handler.go +++ b/src/module/dataset/handler.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "aegis/httpx" @@ -32,15 +32,15 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateDatasetReq true "Dataset creation request" -// @Success 201 {object} dto.GenericResponse[DatasetResp] "Dataset created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body CreateDatasetReq true "Dataset creation request" +// @Success 201 {object} dto.GenericResponse[DatasetResp] "Dataset created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) CreateDataset(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { @@ -83,7 +83,7 @@ func (h *Handler) CreateDataset(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [delete] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DeleteDataset(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -105,15 +105,15 @@ func (h *Handler) DeleteDataset(c *gin.Context) { // @ID get_dataset_by_id // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" +// @Param dataset_id path int true "Dataset ID" // @Success 200 {object} dto.GenericResponse[DatasetDetailResp] "Dataset retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetDataset(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -136,18 +136,18 @@ func (h *Handler) GetDataset(c *gin.Context) { // @ID list_datasets // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param type query string false "Dataset type filter" -// @Param is_public query bool false "Dataset public visibility filter" -// @Param status query consts.StatusType false "Dataset status filter" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param type query string false "Dataset type filter" +// @Param is_public query bool false "Dataset public visibility filter" +// @Param status query consts.StatusType false "Dataset status filter" // @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetResp]] "Datasets retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListDatasets(c *gin.Context) { var req ListDatasetReq if err := c.ShouldBindQuery(&req); err != nil { @@ -179,12 +179,12 @@ func (h *Handler) ListDatasets(c *gin.Context) { // @Security BearerAuth // @Param request body SearchDatasetReq true "Dataset search request" // @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetDetailResp]] "Datasets retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/search [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) SearchDataset(c *gin.Context) { var req SearchDatasetReq if err := c.ShouldBindJSON(&req); err != nil { @@ -214,16 +214,16 @@ func (h *Handler) SearchDataset(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param request body UpdateDatasetReq true "Dataset update request" -// @Success 202 {object} dto.GenericResponse[DatasetResp] "Dataset updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param dataset_id path int true "Dataset ID" +// @Param request body UpdateDatasetReq true "Dataset update request" +// @Success 202 {object} dto.GenericResponse[DatasetResp] "Dataset updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id} [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UpdateDataset(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -258,16 +258,16 @@ func (h *Handler) UpdateDataset(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param manage body ManageDatasetLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[DatasetResp] "Labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param dataset_id path int true "Dataset ID" +// @Param manage body ManageDatasetLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[DatasetResp] "Labels managed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/labels [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ManageDatasetCustomLabels(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -302,16 +302,16 @@ func (h *Handler) ManageDatasetCustomLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" +// @Param dataset_id path int true "Dataset ID" // @Param request body CreateDatasetVersionReq true "Dataset version creation request" // @Success 201 {object} dto.GenericResponse[DatasetVersionResp] "Dataset version created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Conflict error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) CreateDatasetVersion(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists { @@ -360,7 +360,7 @@ func (h *Handler) CreateDatasetVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset or version not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [delete] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DeleteDatasetVersion(c *gin.Context) { versionID, ok := parseDatasetVersionID(c) if !ok { @@ -382,16 +382,16 @@ func (h *Handler) DeleteDatasetVersion(c *gin.Context) { // @ID get_dataset_version_by_id // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param version_id path int true "Dataset Version ID" +// @Param dataset_id path int true "Dataset ID" +// @Param version_id path int true "Dataset Version ID" // @Success 200 {object} dto.GenericResponse[DatasetVersionDetailResp] "Dataset version retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset or version not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset or version not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetDatasetVersion(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -418,17 +418,17 @@ func (h *Handler) GetDatasetVersion(c *gin.Context) { // @ID list_dataset_versions // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param status query consts.StatusType false "Dataset version status filter" +// @Param dataset_id path int true "Dataset ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param status query consts.StatusType false "Dataset version status filter" // @Success 200 {object} dto.GenericResponse[dto.ListResp[DatasetVersionResp]] "Dataset versions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListDatasetVersions(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -463,17 +463,17 @@ func (h *Handler) ListDatasetVersions(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param version_id path int true "Dataset Version ID" +// @Param dataset_id path int true "Dataset ID" +// @Param version_id path int true "Dataset Version ID" // @Param request body UpdateDatasetVersionReq true "Dataset version update request" // @Success 202 {object} dto.GenericResponse[DatasetVersionResp] "Dataset version updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID/request format/request parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID/dataset version ID/request format/request parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id} [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UpdateDatasetVersion(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -519,7 +519,7 @@ func (h *Handler) UpdateDatasetVersion(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/versions/{version_id}/download [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) DownloadDatasetVersion(c *gin.Context) { datasetID, ok := parseDatasetID(c) if !ok { @@ -557,17 +557,17 @@ func (h *Handler) DownloadDatasetVersion(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param dataset_id path int true "Dataset ID" -// @Param version_id path int true "Dataset Version ID" +// @Param dataset_id path int true "Dataset ID" +// @Param version_id path int true "Dataset Version ID" // @Param manage body ManageDatasetVersionInjectionReq true "Injection management request" // @Success 200 {object} dto.GenericResponse[DatasetVersionDetailResp] "Injections managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid dataset ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/datasets/{dataset_id}/version/{version_id}/injections [patch] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ManageDatasetVersionInjections(c *gin.Context) { _, ok := parseDatasetID(c) if !ok { diff --git a/src/module/dataset/handler_service.go b/src/module/dataset/handler_service.go index 5b6842e6..53fcad78 100644 --- a/src/module/dataset/handler_service.go +++ b/src/module/dataset/handler_service.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "archive/zip" diff --git a/src/module/dataset/module.go b/src/module/dataset/module.go index d4856ee8..bfa18f77 100644 --- a/src/module/dataset/module.go +++ b/src/module/dataset/module.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import "go.uber.org/fx" diff --git a/src/module/dataset/repository.go b/src/module/dataset/repository.go index 656e2a29..4fe7366f 100644 --- a/src/module/dataset/repository.go +++ b/src/module/dataset/repository.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "aegis/consts" diff --git a/src/module/dataset/resolve.go b/src/module/dataset/resolve.go index 357f9fef..96d19826 100644 --- a/src/module/dataset/resolve.go +++ b/src/module/dataset/resolve.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "aegis/dto" diff --git a/src/module/dataset/service.go b/src/module/dataset/service.go index a5046832..aad5fc53 100644 --- a/src/module/dataset/service.go +++ b/src/module/dataset/service.go @@ -1,4 +1,4 @@ -package datasetmodule +package dataset import ( "archive/zip" @@ -9,7 +9,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - labelmodule "aegis/module/label" + label "aegis/module/label" "aegis/utils" "gorm.io/gorm" @@ -189,7 +189,7 @@ func (s *Service) ManageDatasetLabels(_ context.Context, req *ManageDatasetLabel } if len(req.AddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.DatasetCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/docs/swagger_models.go b/src/module/docs/swagger_models.go index e2db65ca..c9e45787 100644 --- a/src/module/docs/swagger_models.go +++ b/src/module/docs/swagger_models.go @@ -1,12 +1,12 @@ -package docsmodule +package docs import ( - groupmodule "aegis/module/group" + group "aegis/module/group" "github.com/gin-gonic/gin" ) -type GroupStreamEvent = groupmodule.GroupStreamEvent +type GroupStreamEvent = group.GroupStreamEvent // SwaggerModelsDoc is a documentation-only endpoint that ensures all DTO models are included in Swagger. // This endpoint should NEVER be registered in the actual router. @@ -38,5 +38,5 @@ type GroupStreamEvent = groupmodule.GroupStreamEvent // @Success 200 {object} consts.TaskType "Task type constants" // @Success 200 {object} consts.SSEEventName "SSE event name constants" // @Router /api/_docs/models [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func SwaggerModelsDoc(c *gin.Context) {} diff --git a/src/module/evaluation/api_types.go b/src/module/evaluation/api_types.go index c766e735..ca1bc3be 100644 --- a/src/module/evaluation/api_types.go +++ b/src/module/evaluation/api_types.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import ( "fmt" @@ -7,7 +7,7 @@ import ( "aegis/config" "aegis/dto" "aegis/model" - executionmodule "aegis/module/execution" + execution "aegis/module/execution" chaos "github.com/OperationsPAI/chaos-experiment/handler" ) @@ -58,7 +58,7 @@ func NewEvaluationResp(eval *model.Evaluation) *EvaluationResp { // Execution represents execution data for evaluation. type Execution struct { - Items []executionmodule.GranularityResultItem `json:"items"` + Items []execution.GranularityResultItem `json:"items"` } // Conclusion represents evaluation conclusion. @@ -107,9 +107,9 @@ func (req *BatchEvaluateDatapackReq) Validate() error { } type EvaluateDatapackRef struct { - Datapack string `json:"datapack"` - Groundtruths []chaos.Groundtruth `json:"groundtruths"` - ExecutionRefs []executionmodule.ExecutionRef `json:"execution_refs"` + Datapack string `json:"datapack"` + Groundtruths []chaos.Groundtruth `json:"groundtruths"` + ExecutionRefs []execution.ExecutionRef `json:"execution_refs"` } type EvaluateDatapackItem struct { diff --git a/src/module/evaluation/execution_query.go b/src/module/evaluation/execution_query.go index 0e718648..6782cf7e 100644 --- a/src/module/evaluation/execution_query.go +++ b/src/module/evaluation/execution_query.go @@ -1,23 +1,23 @@ -package evaluationmodule +package evaluation import ( "context" "fmt" "aegis/internalclient/orchestratorclient" - executionmodule "aegis/module/execution" + execution "aegis/module/execution" "go.uber.org/fx" ) type executionQuerySource interface { - ListEvaluationExecutionsByDatapack(context.Context, *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) - ListEvaluationExecutionsByDataset(context.Context, *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) + ListEvaluationExecutionsByDatapack(context.Context, *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) + ListEvaluationExecutionsByDataset(context.Context, *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) } type executionQueryAdapter struct { orchestrator *orchestratorclient.Client - local *executionmodule.Service + local *execution.Service requireRemote bool } @@ -25,7 +25,7 @@ type executionQuerySourceParams struct { fx.In Orchestrator *orchestratorclient.Client `optional:"true"` - Local *executionmodule.Service `optional:"true"` + Local *execution.Service `optional:"true"` } func newExecutionQuerySource(params executionQuerySourceParams) executionQuerySource { @@ -44,7 +44,7 @@ func newRemoteExecutionQuerySource(params executionQuerySourceParams) executionQ } } -func (a executionQueryAdapter) ListEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (a executionQueryAdapter) ListEvaluationExecutionsByDatapack(ctx context.Context, req *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.ListEvaluationExecutionsByDatapack(ctx, req) } @@ -57,7 +57,7 @@ func (a executionQueryAdapter) ListEvaluationExecutionsByDatapack(ctx context.Co return a.local.ListEvaluationExecutionsByDatapack(ctx, req) } -func (a executionQueryAdapter) ListEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (a executionQueryAdapter) ListEvaluationExecutionsByDataset(ctx context.Context, req *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.ListEvaluationExecutionsByDataset(ctx, req) } diff --git a/src/module/evaluation/handler.go b/src/module/evaluation/handler.go index 8ed01bdf..b2ab6ff0 100644 --- a/src/module/evaluation/handler.go +++ b/src/module/evaluation/handler.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import ( "aegis/httpx" @@ -28,14 +28,14 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body BatchEvaluateDatapackReq true "Batch evaluation request containing multiple algorithm-datapack pairs" -// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatapackResp] "Batch algorithm datapack evaluation data retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body BatchEvaluateDatapackReq true "Batch evaluation request containing multiple algorithm-datapack pairs" +// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatapackResp] "Batch algorithm datapack evaluation data retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/datapacks [post] -// @x-api-type {} +// @x-api-type {"sdk":"true"} func (h *Handler) ListDatapackEvaluationResults(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -71,14 +71,14 @@ func (h *Handler) ListDatapackEvaluationResults(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body BatchEvaluateDatasetReq true "Batch evaluation request containing multiple algorithm-dataset pairs" -// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatasetResp] "Batch algorithm dataset evaluation data retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body BatchEvaluateDatasetReq true "Batch evaluation request containing multiple algorithm-dataset pairs" +// @Success 200 {object} dto.GenericResponse[BatchEvaluateDatasetResp] "Batch algorithm dataset evaluation data retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/datasets [post] -// @x-api-type {} +// @x-api-type {"sdk":"true"} func (h *Handler) ListDatasetEvaluationResults(c *gin.Context) { userID, exists := middleware.GetCurrentUserID(c) if !exists || userID <= 0 { @@ -113,14 +113,14 @@ func (h *Handler) ListDatasetEvaluationResults(c *gin.Context) { // @ID list_evaluations // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Success 200 {object} dto.GenericResponse[dto.ListResp[EvaluationResp]] "Evaluations retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Success 200 {object} dto.GenericResponse[dto.ListResp[EvaluationResp]] "Evaluations retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations [get] -// @x-api-type {} +// @x-api-type {"sdk":"true"} func (h *Handler) ListEvaluations(c *gin.Context) { var req ListEvaluationReq if err := c.ShouldBindQuery(&req); err != nil { @@ -149,13 +149,13 @@ func (h *Handler) ListEvaluations(c *gin.Context) { // @ID get_evaluation_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Evaluation ID" +// @Param id path int true "Evaluation ID" // @Success 200 {object} dto.GenericResponse[EvaluationResp] "Evaluation retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" -// @Failure 404 {object} dto.GenericResponse[any] "Evaluation not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" +// @Failure 404 {object} dto.GenericResponse[any] "Evaluation not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/{id} [get] -// @x-api-type {} +// @x-api-type {"sdk":"true"} func (h *Handler) GetEvaluation(c *gin.Context) { id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") if !ok { @@ -184,7 +184,7 @@ func (h *Handler) GetEvaluation(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Evaluation not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/evaluations/{id} [delete] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DeleteEvaluation(c *gin.Context) { id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathID), "evaluation ID") if !ok { diff --git a/src/module/evaluation/handler_service.go b/src/module/evaluation/handler_service.go index 0800d053..1fc6fdbd 100644 --- a/src/module/evaluation/handler_service.go +++ b/src/module/evaluation/handler_service.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import ( "context" diff --git a/src/module/evaluation/module.go b/src/module/evaluation/module.go index 07d21d53..3b88adf9 100644 --- a/src/module/evaluation/module.go +++ b/src/module/evaluation/module.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import "go.uber.org/fx" diff --git a/src/module/evaluation/repository.go b/src/module/evaluation/repository.go index f1524042..2a737e27 100644 --- a/src/module/evaluation/repository.go +++ b/src/module/evaluation/repository.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import ( "aegis/consts" diff --git a/src/module/evaluation/service.go b/src/module/evaluation/service.go index 77f903ef..c00cee0d 100644 --- a/src/module/evaluation/service.go +++ b/src/module/evaluation/service.go @@ -1,4 +1,4 @@ -package evaluationmodule +package evaluation import ( "context" @@ -8,9 +8,9 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - executionmodule "aegis/module/execution" + container "aegis/module/container" + dataset "aegis/module/dataset" + execution "aegis/module/execution" "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -38,7 +38,7 @@ func (s *Service) ListDatapackEvaluationResults(ctx context.Context, req *BatchE algorithms = append(algorithms, &req.Specs[i].Algorithm) } - algorithmVersionResults, err := containermodule.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := container.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -56,7 +56,7 @@ func (s *Service) ListDatapackEvaluationResults(ctx context.Context, req *BatchE continue } - executions, err := s.listEvaluationExecutionsByDatapack(ctx, &executionmodule.EvaluationExecutionsByDatapackReq{ + executions, err := s.listEvaluationExecutionsByDatapack(ctx, &execution.EvaluationExecutionsByDatapackReq{ AlgorithmVersionID: algorithmVersion.ID, DatapackName: spec.Datapack, FilterLabels: spec.FilterLabels, @@ -70,7 +70,7 @@ func (s *Service) ListDatapackEvaluationResults(ctx context.Context, req *BatchE continue } - refs := make([]executionmodule.ExecutionRef, 0, len(executions)) + refs := make([]execution.ExecutionRef, 0, len(executions)) for _, execution := range executions { refs = append(refs, execution.ExecutionRef) } @@ -121,12 +121,12 @@ func (s *Service) ListDatasetEvaluationResults(ctx context.Context, req *BatchEv datasets = append(datasets, &req.Specs[i].Dataset) } - algorithmVersionResults, err := containermodule.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := container.NewRepository(s.repo.db).ResolveContainerVersions(algorithms, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } - datasetVersionResults, err := datasetmodule.NewRepository(s.repo.db).ResolveDatasetVersions(datasets, userID) + datasetVersionResults, err := dataset.NewRepository(s.repo.db).ResolveDatasetVersions(datasets, userID) if err != nil { return nil, fmt.Errorf("failed to map dataset refs to versions: %w", err) } @@ -150,7 +150,7 @@ func (s *Service) ListDatasetEvaluationResults(ctx context.Context, req *BatchEv continue } - executions, err := s.listEvaluationExecutionsByDataset(ctx, &executionmodule.EvaluationExecutionsByDatasetReq{ + executions, err := s.listEvaluationExecutionsByDataset(ctx, &execution.EvaluationExecutionsByDatasetReq{ AlgorithmVersionID: algorithmVersion.ID, DatasetVersionID: datasetVersion.ID, FilterLabels: spec.FilterLabels, @@ -164,13 +164,13 @@ func (s *Service) ListDatasetEvaluationResults(ctx context.Context, req *BatchEv continue } - executionMap := make(map[string][]executionmodule.EvaluationExecutionItem) - for _, execution := range executions { - name := execution.Datapack + executionMap := make(map[string][]execution.EvaluationExecutionItem) + for _, executionItem := range executions { + name := executionItem.Datapack if _, exists := executionMap[name]; !exists { - executionMap[name] = make([]executionmodule.EvaluationExecutionItem, 0) + executionMap[name] = make([]execution.EvaluationExecutionItem, 0) } - executionMap[name] = append(executionMap[name], execution) + executionMap[name] = append(executionMap[name], executionItem) } notExecutedDatapacks := make([]string, 0) @@ -182,9 +182,9 @@ func (s *Service) ListDatasetEvaluationResults(ctx context.Context, req *BatchEv evaluateRefs := make([]EvaluateDatapackRef, 0, len(executionMap)) for datapackName, groupedExecutions := range executionMap { - refs := make([]executionmodule.ExecutionRef, 0, len(groupedExecutions)) - for _, execution := range groupedExecutions { - refs = append(refs, execution.ExecutionRef) + refs := make([]execution.ExecutionRef, 0, len(groupedExecutions)) + for _, executionItem := range groupedExecutions { + refs = append(refs, executionItem.ExecutionRef) } evaluateRef := EvaluateDatapackRef{ @@ -259,14 +259,14 @@ func (s *Service) DeleteEvaluation(_ context.Context, id int) error { return s.repo.DeleteEvaluation(id) } -func (s *Service) listEvaluationExecutionsByDatapack(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatapackReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (s *Service) listEvaluationExecutionsByDatapack(ctx context.Context, req *execution.EvaluationExecutionsByDatapackReq) ([]execution.EvaluationExecutionItem, error) { if s.query == nil { return nil, fmt.Errorf("evaluation execution query source is not configured") } return s.query.ListEvaluationExecutionsByDatapack(ctx, req) } -func (s *Service) listEvaluationExecutionsByDataset(ctx context.Context, req *executionmodule.EvaluationExecutionsByDatasetReq) ([]executionmodule.EvaluationExecutionItem, error) { +func (s *Service) listEvaluationExecutionsByDataset(ctx context.Context, req *execution.EvaluationExecutionsByDatasetReq) ([]execution.EvaluationExecutionItem, error) { if s.query == nil { return nil, fmt.Errorf("evaluation execution query source is not configured") } diff --git a/src/module/evaluation/service_test.go b/src/module/evaluation/service_test.go index 920163ef..4f261924 100644 --- a/src/module/evaluation/service_test.go +++ b/src/module/evaluation/service_test.go @@ -1,20 +1,20 @@ -package evaluationmodule +package evaluation import ( "testing" - executionmodule "aegis/module/execution" + execution "aegis/module/execution" ) func TestListEvaluationExecutionsRequiresQuerySource(t *testing.T) { service := &Service{} - _, err := service.listEvaluationExecutionsByDatapack(t.Context(), &executionmodule.EvaluationExecutionsByDatapackReq{}) + _, err := service.listEvaluationExecutionsByDatapack(t.Context(), &execution.EvaluationExecutionsByDatapackReq{}) if err == nil { t.Fatalf("expected datapack query to fail without orchestrator or execution service") } - _, err = service.listEvaluationExecutionsByDataset(t.Context(), &executionmodule.EvaluationExecutionsByDatasetReq{}) + _, err = service.listEvaluationExecutionsByDataset(t.Context(), &execution.EvaluationExecutionsByDatasetReq{}) if err == nil { t.Fatalf("expected dataset query to fail without orchestrator or execution service") } diff --git a/src/module/execution/api_types.go b/src/module/execution/api_types.go index 0c649554..0e6c35e3 100644 --- a/src/module/execution/api_types.go +++ b/src/module/execution/api_types.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "fmt" diff --git a/src/module/execution/handler.go b/src/module/execution/handler.go index 7dae9053..f668147b 100644 --- a/src/module/execution/handler.go +++ b/src/module/execution/handler.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "aegis/httpx" @@ -32,15 +32,15 @@ func NewHandler(service HandlerService) *Handler { // @ID list_project_executions // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param project_id path int true "Project ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[ExecutionResp]] "Executions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/executions [get] // @x-api-type {"portal":"true"} func (h *Handler) ListProjectExecutions(c *gin.Context) { @@ -77,16 +77,16 @@ func (h *Handler) ListProjectExecutions(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param request body SubmitExecutionReq true "Algorithm execution request" -// @Success 200 {object} dto.GenericResponse[SubmitExecutionResp] "Algorithm execution submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project, algorithm, datapack or dataset not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param project_id path int true "Project ID" +// @Param request body SubmitExecutionReq true "Algorithm execution request" +// @Success 200 {object} dto.GenericResponse[SubmitExecutionResp] "Algorithm execution submitted successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project, algorithm, datapack or dataset not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/executions/execute [post] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) SubmitAlgorithmExecution(c *gin.Context) { groupID := c.GetString("groupID") userID, exists := middleware.GetCurrentUserID(c) @@ -125,43 +125,6 @@ func (h *Handler) SubmitAlgorithmExecution(c *gin.Context) { dto.SuccessResponse(c, resp) } -// ListExecutions handles listing executions with pagination and filtering -// -// @Summary List executions -// @Description Get a paginated list of executions with pagination and filtering -// @Tags Executions -// @ID list_executions -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param state query consts.ExecutionState false "Filter by execution state" -// @Param status query consts.StatusType false "Filter by status" -// @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:test')" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[ExecutionResp]] "Executions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/executions [get] -// @x-api-type {} -func (h *Handler) ListExecutions(c *gin.Context) { - var req ListExecutionReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - resp, err := h.service.ListExecutions(c.Request.Context(), &req) - if httpx.HandleServiceError(c, err) { - return - } - dto.SuccessResponse(c, resp) -} - // GetExecution handles getting a single execution by ID // // @Summary Get execution by ID @@ -170,15 +133,15 @@ func (h *Handler) ListExecutions(c *gin.Context) { // @ID get_execution_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Execution ID" +// @Param id path int true "Execution ID" // @Success 200 {object} dto.GenericResponse[ExecutionDetailResp] "Execution retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetExecution(c *gin.Context) { id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { @@ -204,7 +167,7 @@ func (h *Handler) GetExecution(c *gin.Context) { // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/labels [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListAvailableExecutionLabels(c *gin.Context) { labels, err := h.service.ListAvailableLabels(c.Request.Context()) if httpx.HandleServiceError(c, err) { @@ -222,16 +185,16 @@ func (h *Handler) ListAvailableExecutionLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Execution ID" +// @Param id path int true "Execution ID" // @Param manage body ManageExecutionLabelReq true "Custom label management request" // @Success 200 {object} dto.GenericResponse[ExecutionResp] "Custom labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID or request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid execution ID or request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{id}/labels [patch] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ManageExecutionCustomLabels(c *gin.Context) { id, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { @@ -262,14 +225,14 @@ func (h *Handler) ManageExecutionCustomLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body BatchDeleteExecutionReq true "Batch delete request" +// @Param request body BatchDeleteExecutionReq true "Batch delete request" // @Success 200 {object} dto.GenericResponse[any] "Executions deleted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/batch-delete [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) BatchDeleteExecutions(c *gin.Context) { var req BatchDeleteExecutionReq if err := c.ShouldBindJSON(&req); err != nil { @@ -295,16 +258,16 @@ func (h *Handler) BatchDeleteExecutions(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param execution_id path int true "Execution ID" -// @Param request body UploadDetectorResultReq true "Detector results" -// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid executionID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param execution_id path int true "Execution ID" +// @Param request body UploadDetectorResultReq true "Detector results" +// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid executionID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/detector_results [post] -// @x-api-type {"runtime":"true"} +// @x-api-type {"sdk":"true","runtime":"true"} func (h *Handler) UploadDetectorResults(c *gin.Context) { executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { @@ -335,16 +298,16 @@ func (h *Handler) UploadDetectorResults(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param execution_id path int true "Execution ID" -// @Param request body UploadGranularityResultReq true "Granularity results" -// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid exeuction ID or invalid request form or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param execution_id path int true "Execution ID" +// @Param request body UploadGranularityResultReq true "Granularity results" +// @Success 200 {object} dto.GenericResponse[UploadExecutionResultResp] "Results uploaded successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid exeuction ID or invalid request form or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Execution not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/executions/{execution_id}/granularity_results [post] -// @x-api-type {"runtime":"true"} +// @x-api-type {"sdk":"true","runtime":"true"} func (h *Handler) UploadGranularityResults(c *gin.Context) { executionID, ok := httpx.ParsePositiveID(c, c.Param(consts.URLPathExecutionID), "execution ID") if !ok { diff --git a/src/module/execution/handler_service.go b/src/module/execution/handler_service.go index f27c665a..282f7bf5 100644 --- a/src/module/execution/handler_service.go +++ b/src/module/execution/handler_service.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "context" diff --git a/src/module/execution/module.go b/src/module/execution/module.go index 02411c83..673255cc 100644 --- a/src/module/execution/module.go +++ b/src/module/execution/module.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import "go.uber.org/fx" diff --git a/src/module/execution/repository.go b/src/module/execution/repository.go index 1da0684e..36bc4314 100644 --- a/src/module/execution/repository.go +++ b/src/module/execution/repository.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "aegis/config" diff --git a/src/module/execution/result_types.go b/src/module/execution/result_types.go index a51c07ee..1c7620f0 100644 --- a/src/module/execution/result_types.go +++ b/src/module/execution/result_types.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "aegis/model" diff --git a/src/module/execution/runtime_types.go b/src/module/execution/runtime_types.go index 4d1631b0..cd2392c6 100644 --- a/src/module/execution/runtime_types.go +++ b/src/module/execution/runtime_types.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "aegis/consts" diff --git a/src/module/execution/service.go b/src/module/execution/service.go index 5cc1c191..a0893512 100644 --- a/src/module/execution/service.go +++ b/src/module/execution/service.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "context" @@ -8,11 +8,11 @@ import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/model" - containermodule "aegis/module/container" - injectionmodule "aegis/module/injection" - labelmodule "aegis/module/label" + container "aegis/module/container" + injection "aegis/module/injection" + label "aegis/module/label" "aegis/service/common" "aegis/utils" @@ -22,10 +22,10 @@ import ( type Service struct { repo *Repository - redis *redisinfra.Gateway + redis *redis.Gateway } -func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { +func NewService(repo *Repository, redis *redis.Gateway) *Service { return &Service{repo: repo, redis: redis} } @@ -71,7 +71,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu refs = append(refs, &req.Specs[i].Algorithm.ContainerRef) } - algorithmVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) + algorithmVersionResults, err := container.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -81,7 +81,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu var allExecutionItems []SubmitExecutionItem for idx, spec := range req.Specs { - datapacks, datasetID, err := injectionmodule.NewRepository(s.repo.db).ResolveDatapacks(spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) + datapacks, datasetID, err := injection.NewRepository(s.repo.db).ResolveDatapacks(spec.Datapack, spec.Dataset, userID, consts.TaskTypeRunAlgorithm) if err != nil { return nil, fmt.Errorf("failed to extract datapacks: %w", err) } @@ -97,7 +97,7 @@ func (s *Service) SubmitAlgorithmExecution(ctx context.Context, req *SubmitExecu } algorithmItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.Algorithm.EnvVars, &algorithmVersion) + envVars, err := container.NewRepository(db).ListContainerVersionEnvVars(spec.Algorithm.EnvVars, &algorithmVersion) if err != nil { return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) } @@ -242,7 +242,7 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageExecutionLabelReq, } if len(req.AddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ExecutionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -403,7 +403,7 @@ func (s *Service) CreateExecutionRecord(_ context.Context, req *RuntimeCreateExe } if len(req.Labels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.ExecutionCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.ExecutionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/execution/service_test.go b/src/module/execution/service_test.go index c0b5441e..14abf2d0 100644 --- a/src/module/execution/service_test.go +++ b/src/module/execution/service_test.go @@ -1,4 +1,4 @@ -package executionmodule +package execution import ( "regexp" @@ -7,7 +7,7 @@ import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/testutil" "github.com/DATA-DOG/go-sqlmock" @@ -32,7 +32,7 @@ func newExecutionService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { }), &gorm.Config{}) require.NoError(t, err) - return NewService(NewRepository(db), redisinfra.NewGateway(nil)), mock, func() { + return NewService(NewRepository(db), redis.NewGateway(nil)), mock, func() { cleanupRedis() _ = sqlDB.Close() } diff --git a/src/module/group/api_types.go b/src/module/group/api_types.go index 6e66e205..d04515e6 100644 --- a/src/module/group/api_types.go +++ b/src/module/group/api_types.go @@ -1,4 +1,4 @@ -package groupmodule +package group import ( "fmt" diff --git a/src/module/group/handler.go b/src/module/group/handler.go index 7ae2fa0d..dd64e8e5 100644 --- a/src/module/group/handler.go +++ b/src/module/group/handler.go @@ -1,4 +1,4 @@ -package groupmodule +package group import ( "aegis/httpx" @@ -34,14 +34,14 @@ func NewHandler(service HandlerService) *Handler { // @ID get_group_stats // @Produce json // @Security BearerAuth -// @Param group_id path string true "Group ID (UUID)" -// @Success 200 {object} dto.GenericResponse[GroupStats] "Group trace statistics" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param group_id path string true "Group ID (UUID)" +// @Success 200 {object} dto.GenericResponse[GroupStats] "Group trace statistics" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/groups/{group_id}/stats [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetGroupStats(c *gin.Context) { groupID := c.Param(consts.URLPathGroupID) if !utils.IsValidUUID(groupID) { @@ -80,8 +80,8 @@ func (h *Handler) GetGroupStats(c *gin.Context) { // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/groups/{group_id}/stream [get] +// @x-api-type {"portal":"true"} // @x-request-type {"stream":"true"} -// @x-api-type {} func (h *Handler) GetGroupStream(c *gin.Context) { groupID := c.Param(consts.URLPathGroupID) if !utils.IsValidUUID(groupID) { diff --git a/src/module/group/handler_service.go b/src/module/group/handler_service.go index 5154dcdc..021555dc 100644 --- a/src/module/group/handler_service.go +++ b/src/module/group/handler_service.go @@ -1,4 +1,4 @@ -package groupmodule +package group import ( "context" diff --git a/src/module/group/module.go b/src/module/group/module.go index ce600619..bf806ee3 100644 --- a/src/module/group/module.go +++ b/src/module/group/module.go @@ -1,4 +1,4 @@ -package groupmodule +package group import "go.uber.org/fx" diff --git a/src/module/group/repository.go b/src/module/group/repository.go index 101736fb..a2e9ebc6 100644 --- a/src/module/group/repository.go +++ b/src/module/group/repository.go @@ -1,4 +1,4 @@ -package groupmodule +package group import ( "aegis/consts" diff --git a/src/module/group/service.go b/src/module/group/service.go index 9f4a79e3..ae9a9f7a 100644 --- a/src/module/group/service.go +++ b/src/module/group/service.go @@ -1,4 +1,4 @@ -package groupmodule +package group import ( "context" @@ -10,7 +10,7 @@ import ( "aegis/consts" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type Service struct { @@ -79,7 +79,7 @@ func (s *Service) GetGroupTraceCount(groupID string) (int64, error) { return total, nil } -func (s *Service) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { +func (s *Service) ReadGroupStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]goredis.XStream, error) { if lastID == "" { lastID = "0" } @@ -103,7 +103,7 @@ func NewGroupStreamProcessor(totalTraces int) *GroupStreamProcessor { } } -func (p *GroupStreamProcessor) ProcessGroupMessage(msg redis.XMessage) (*GroupStreamEvent, error) { +func (p *GroupStreamProcessor) ProcessGroupMessage(msg goredis.XMessage) (*GroupStreamEvent, error) { traceID, ok := msg.Values[consts.RdbEventTraceID].(string) if !ok || traceID == "" { return nil, fmt.Errorf("missing or invalid %s in group stream message", consts.RdbEventTraceID) diff --git a/src/module/injection/api_types.go b/src/module/injection/api_types.go index dafe9a64..ec3f3d0f 100644 --- a/src/module/injection/api_types.go +++ b/src/module/injection/api_types.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "encoding/json" diff --git a/src/module/injection/archive.go b/src/module/injection/archive.go index 6dc5dfc2..de7c7362 100644 --- a/src/module/injection/archive.go +++ b/src/module/injection/archive.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "archive/zip" diff --git a/src/module/injection/datapack_store.go b/src/module/injection/datapack_store.go index b2c8503e..a9e35b8d 100644 --- a/src/module/injection/datapack_store.go +++ b/src/module/injection/datapack_store.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "archive/zip" diff --git a/src/module/injection/datapack_store_test.go b/src/module/injection/datapack_store_test.go index f652f7f8..8d98f277 100644 --- a/src/module/injection/datapack_store_test.go +++ b/src/module/injection/datapack_store_test.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "archive/zip" diff --git a/src/module/injection/handler.go b/src/module/injection/handler.go index 9dc26122..66e6e063 100644 --- a/src/module/injection/handler.go +++ b/src/module/injection/handler.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "aegis/httpx" @@ -39,17 +39,17 @@ func NewHandler(service HandlerService) *Handler { // @ID list_project_injections // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param project_id path int true "Project ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[InjectionResp]] "Fault injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections [get] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ListProjectInjections(c *gin.Context) { projectID, ok := parseProjectID(c) if !ok { @@ -84,14 +84,14 @@ func (h *Handler) ListProjectInjections(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" +// @Param project_id path int true "Project ID" // @Param search body SearchInjectionReq true "Search criteria" // @Success 200 {object} dto.GenericResponse[dto.SearchResp[InjectionDetailResp]] "Search results" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections/search [post] // @x-api-type {"portal":"true"} func (h *Handler) SearchProjectInjections(c *gin.Context) { @@ -100,7 +100,23 @@ func (h *Handler) SearchProjectInjections(c *gin.Context) { return } - h.searchInjections(c, &projectID) + var req SearchInjectionReq + if err := c.ShouldBindJSON(&req); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + + if err := req.Validate(); err != nil { + dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) + return + } + + resp, err := h.service.Search(c.Request.Context(), &req, &projectID) + if httpx.HandleServiceError(c, err) { + return + } + + dto.SuccessResponse(c, resp) } // ListProjectFaultInjectionNoIssues lists fault injections without issues for a project @@ -111,19 +127,19 @@ func (h *Handler) SearchProjectInjections(c *gin.Context) { // @ID list_project_injections_no_issues // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param labels query []string false "Filter by labels" -// @Param lookback query string false "Time range query" -// @Param custom_start_time query string false "Custom start time" -// @Param custom_end_time query string false "Custom end time" +// @Param project_id path int true "Project ID" +// @Param labels query []string false "Filter by labels" +// @Param lookback query string false "Time range query" +// @Param custom_start_time query string false "Custom start time" +// @Param custom_end_time query string false "Custom end time" // @Success 200 {object} dto.GenericResponse[[]InjectionNoIssuesResp] "Injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections/analysis/no-issues [get] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ListProjectFaultInjectionNoIssues(c *gin.Context) { projectID, ok := parseProjectID(c) if !ok { @@ -141,19 +157,19 @@ func (h *Handler) ListProjectFaultInjectionNoIssues(c *gin.Context) { // @ID list_project_injections_with_issues // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param labels query []string false "Filter by labels" -// @Param lookback query string false "Time range query" -// @Param custom_start_time query string false "Custom start time" -// @Param custom_end_time query string false "Custom end time" +// @Param project_id path int true "Project ID" +// @Param labels query []string false "Filter by labels" +// @Param lookback query string false "Time range query" +// @Param custom_start_time query string false "Custom start time" +// @Param custom_end_time query string false "Custom end time" // @Success 200 {object} dto.GenericResponse[[]InjectionWithIssuesResp] "Injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections/analysis/with-issues [get] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ListProjectFaultInjectionWithIssues(c *gin.Context) { projectID, ok := parseProjectID(c) if !ok { @@ -172,16 +188,16 @@ func (h *Handler) ListProjectFaultInjectionWithIssues(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" +// @Param project_id path int true "Project ID" // @Param body body SubmitInjectionReq true "Fault injection request" // @Success 200 {object} dto.GenericResponse[SubmitInjectionResp] "Injections submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections/inject [post] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) SubmitProjectFaultInjection(c *gin.Context) { projectID, ok := parseProjectID(c) if !ok { @@ -200,16 +216,16 @@ func (h *Handler) SubmitProjectFaultInjection(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" +// @Param project_id path int true "Project ID" // @Param body body SubmitDatapackBuildingReq true "Datapack building request" // @Success 202 {object} dto.GenericResponse[SubmitDatapackBuildingResp] "Datapack buildings submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/injections/build [post] -// @x-api-type {"portal":"true"} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) SubmitProjectDatapackBuilding(c *gin.Context) { projectID, ok := parseProjectID(c) if !ok { @@ -219,141 +235,6 @@ func (h *Handler) SubmitProjectDatapackBuilding(c *gin.Context) { h.submitDatapackBuilding(c, &projectID) } -// ListInjections handles listing injections with pagination and filtering -// -// @Summary List injections -// @Description Get a paginated list of injections with pagination and filtering -// @Tags Injections -// @ID list_injections -// @Produce json -// @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param type query chaos.ChaosType false "Filter by fault type" -// @Param benchmark query string false "Filter by benchmark" -// @Param state query consts.DatapackState false "Filter by injection state" -// @Param status query int false "Filter by status" -// @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:chaos')" -// @Success 200 {object} dto.GenericResponse[dto.ListResp[InjectionResp]] "Injections retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections [get] -// @x-api-type {} -func (h *Handler) ListInjections(c *gin.Context) { - var req ListInjectionReq - if err := c.ShouldBindQuery(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format: "+err.Error()) - return - } - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request parameters: "+err.Error()) - return - } - resp, err := h.service.ListInjections(c.Request.Context(), &req) - if httpx.HandleServiceError(c, err) { - return - } - dto.SuccessResponse(c, resp) -} - -// SearchInjections -// -// @Summary Search injections -// @Description Advanced search for injections with complex filtering including name search, custom labels, tags, and time ranges -// @Tags Injections -// @ID search_injections -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param search body SearchInjectionReq true "Search criteria" -// @Success 200 {object} dto.GenericResponse[dto.SearchResp[InjectionDetailResp]] "Search results" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/search [post] -// @x-api-type {} -func (h *Handler) SearchInjections(c *gin.Context) { h.searchInjections(c, nil) } - -// SubmitFaultInjection submits batch fault injections -// -// @Summary Submit batch fault injections -// @Description Submit multiple fault injection tasks in batch -// @Tags Injections -// @ID inject_fault -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param body body SubmitInjectionReq true "Fault injection request body" -// @Success 200 {object} dto.GenericResponse[SubmitInjectionResp] "Fault injection submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/inject [post] -// @x-api-type {} -func (h *Handler) SubmitFaultInjection(c *gin.Context) { h.submitFaultInjection(c, nil) } - -// SubmitDatapackBuilding submits batch datapack buildings -// -// @Summary Submit batch datapack buildings -// @Description. Submit multiple datapack building tasks in batch -// @Tags Injections -// @ID build_datapack -// @Accept json -// @Produce json -// @Param body body SubmitDatapackBuildingReq true "Datapack building request body" -// @Success 202 {object} dto.GenericResponse[SubmitDatapackBuildingResp] "Datapack building submitted successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/build [post] -// @x-api-type {} -func (h *Handler) SubmitDatapackBuilding(c *gin.Context) { h.submitDatapackBuilding(c, nil) } - -// ListFaultInjectionNoIssues -// -// @Summary Query Fault Injection Records Without Issues -// @Description Query all fault injection records without issues based on time range, returning detailed records including configuration information -// @Tags Injections -// @ID list_failed_injections -// @Produce json -// @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:chaos')" -// @Param lookback query string false "Time range query, supports custom relative time (1h/24h/7d) or custom, default not set" -// @Param custom_start_time query string false "Custom start time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Param custom_end_time query string false "Custom end time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Success 200 {object} dto.GenericResponse[[]InjectionNoIssuesResp] "Successfully returned fault injection records without issues" -// @Failure 400 {object} dto.GenericResponse[any] "Request parameter error, such as incorrect time format or parameter validation failure, etc." -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/analysis/no-issues [get] -// @x-api-type {} -func (h *Handler) ListFaultInjectionNoIssues(c *gin.Context) { h.listFaultInjectionNoIssues(c, nil) } - -// ListFaultInjectionWithIssues -// -// @Summary Query Fault Injection Records With Issues -// @Description Query all fault injection records with issues based on time range -// @Tags Injections -// @ID list_successful_injections -// @Produce json -// @Param labels query []string false "Filter by labels (array of key:value strings, e.g., 'type:chaos')" -// @Param lookback query string false "Time range query, supports custom relative time (1h/24h/7d) or custom, default not set" -// @Param custom_start_time query string false "Custom start time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Param custom_end_time query string false "Custom end time, RFC3339 format, required when lookback=custom" Format(date-time) -// @Success 200 {object} dto.GenericResponse[[]InjectionWithIssuesResp] -// @Failure 400 {object} dto.GenericResponse[any] "Request parameter error, such as incorrect time format or parameter validation failure, etc." -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/analysis/with-issues [get] -// @x-api-type {} -func (h *Handler) ListFaultInjectionWithIssues(c *gin.Context) { - h.listFaultInjectionWithIssues(c, nil) -} - // GetInjection handles getting a single injection by ID // // @Summary Get injection by ID @@ -362,15 +243,15 @@ func (h *Handler) ListFaultInjectionWithIssues(c *gin.Context) { // @ID get_injection_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" +// @Param id path int true "Injection ID" // @Success 200 {object} dto.GenericResponse[InjectionDetailResp] "Injection retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetInjection(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { @@ -391,15 +272,15 @@ func (h *Handler) GetInjection(c *gin.Context) { // @ID get_injection_metadata // @Produce json // @Security BearerAuth -// @Param system query chaos.SystemType true "System for config and resources metadata" +// @Param system query chaos.SystemType true "System for config and resources metadata" // @Success 200 {object} dto.GenericResponse[InjectionMetadataResp] "Successfully returned metadata" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid system" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid system" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/metadata [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetInjectionMetadata(c *gin.Context) { systemStr := c.Query("system") ctx := c.Request.Context() @@ -439,16 +320,16 @@ func (h *Handler) GetInjectionMetadata(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" +// @Param id path int true "Injection ID" // @Param manage body ManageInjectionLabelReq true "Custom label management request" // @Success 200 {object} dto.GenericResponse[InjectionResp] "Custom labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID or request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID or request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/labels [patch] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) ManageInjectionCustomLabels(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { @@ -481,12 +362,12 @@ func (h *Handler) ManageInjectionCustomLabels(c *gin.Context) { // @Security BearerAuth // @Param batch_manage body BatchManageInjectionLabelReq true "Batch manage label request" // @Success 200 {object} dto.GenericResponse[BatchManageInjectionLabelResp] "Injection labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/labels/batch [patch] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) BatchManageInjectionLabels(c *gin.Context) { var req BatchManageInjectionLabelReq if err := c.ShouldBindJSON(&req); err != nil { @@ -513,14 +394,14 @@ func (h *Handler) BatchManageInjectionLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param batch_delete body BatchDeleteInjectionReq true "Batch delete request" +// @Param batch_delete body BatchDeleteInjectionReq true "Batch delete request" // @Success 200 {object} dto.GenericResponse[any] "Injections deleted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/batch-delete [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) BatchDeleteInjections(c *gin.Context) { var req BatchDeleteInjectionReq if err := c.ShouldBindJSON(&req); err != nil { @@ -546,15 +427,15 @@ func (h *Handler) BatchDeleteInjections(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" +// @Param id path int true "Injection ID" // @Param body body CloneInjectionReq true "Clone request" // @Success 201 {object} dto.GenericResponse[InjectionDetailResp] "Injection cloned successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/clone [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) CloneInjection(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { @@ -572,34 +453,6 @@ func (h *Handler) CloneInjection(c *gin.Context) { dto.JSONResponse(c, http.StatusCreated, "Injection cloned successfully", resp) } -// GetInjectionLogs handles getting injection execution logs -// -// @Summary Get injection logs -// @Description Get execution logs for a specific injection -// @Tags Injections -// @ID get_injection_logs -// @Produce json -// @Security BearerAuth -// @Param id path int true "Injection ID" -// @Success 200 {object} dto.GenericResponse[InjectionLogsResp] "Logs retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /api/v2/injections/{id}/logs [get] -// @x-api-type {} -func (h *Handler) GetInjectionLogs(c *gin.Context) { - id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") - if !ok { - return - } - resp, err := h.service.GetLogs(c.Request.Context(), id) - if httpx.HandleServiceError(c, err) { - return - } - dto.JSONResponse(c, http.StatusOK, "Logs retrieved successfully", resp) -} - // DownloadDatapack handles datapack file download // // @Summary Download datapack @@ -615,7 +468,7 @@ func (h *Handler) GetInjectionLogs(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/download [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DownloadDatapack(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { @@ -644,14 +497,14 @@ func (h *Handler) DownloadDatapack(c *gin.Context) { // @ID list_datapack_files // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" +// @Param id path int true "Injection ID" // @Success 200 {object} dto.GenericResponse[DatapackFilesResp] "Files retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 404 {object} dto.GenericResponse[any] "Datapack not found or not ready" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid injection ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 404 {object} dto.GenericResponse[any] "Datapack not found or not ready" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListDatapackFiles(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { @@ -689,7 +542,7 @@ func (h *Handler) ListDatapackFiles(c *gin.Context) { // @Failure 416 {object} dto.GenericResponse[any] "Range not satisfiable" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files/download [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) DownloadDatapackFile(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { @@ -743,7 +596,7 @@ func (h *Handler) DownloadDatapackFile(c *gin.Context) { // @Failure 404 {object} dto.GenericResponse[any] "Datapack or file not found" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/{id}/files/query [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) QueryDatapackFile(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "datapack ID") if !ok { @@ -779,13 +632,13 @@ func (h *Handler) QueryDatapackFile(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Injection ID" +// @Param id path int true "Injection ID" // @Param request body UpdateGroundtruthReq true "Ground truth data" -// @Success 200 {object} dto.GenericResponse[any] "Ground truth updated" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" +// @Success 200 {object} dto.GenericResponse[any] "Ground truth updated" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 404 {object} dto.GenericResponse[any] "Injection not found" // @Router /api/v2/injections/{id}/groundtruth [put] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UpdateGroundtruth(c *gin.Context) { id, ok := parsePositiveID(c, consts.URLPathID, "injection ID") if !ok { @@ -815,18 +668,18 @@ func (h *Handler) UpdateGroundtruth(c *gin.Context) { // @Accept multipart/form-data // @Produce json // @Security BearerAuth -// @Param name formData string true "Datapack name" -// @Param description formData string false "Description" -// @Param category formData string false "Category" -// @Param labels formData string false "JSON-encoded labels" -// @Param ground_truths formData string false "JSON-encoded ground truths" -// @Param file formData file true "Zip archive file" +// @Param name formData string true "Datapack name" +// @Param description formData string false "Description" +// @Param category formData string false "Category" +// @Param labels formData string false "JSON-encoded labels" +// @Param ground_truths formData string false "JSON-encoded ground truths" +// @Param file formData file true "Zip archive file" // @Success 201 {object} dto.GenericResponse[UploadDatapackResp] "Datapack uploaded successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/injections/upload [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) UploadDatapack(c *gin.Context) { fileHeader, err := c.FormFile("file") if err != nil { @@ -858,26 +711,6 @@ func (h *Handler) UploadDatapack(c *gin.Context) { dto.JSONResponse(c, http.StatusCreated, "Datapack uploaded successfully", resp) } -func (h *Handler) searchInjections(c *gin.Context, projectID *int) { - var req SearchInjectionReq - if err := c.ShouldBindJSON(&req); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request: "+err.Error()) - return - } - - if err := req.Validate(); err != nil { - dto.ErrorResponse(c, http.StatusBadRequest, "Validation failed: "+err.Error()) - return - } - - resp, err := h.service.Search(c.Request.Context(), &req, projectID) - if httpx.HandleServiceError(c, err) { - return - } - - dto.SuccessResponse(c, resp) -} - func (h *Handler) listFaultInjectionNoIssues(c *gin.Context, projectID *int) { var req ListInjectionNoIssuesReq if err := c.BindQuery(&req); err != nil { diff --git a/src/module/injection/handler_service.go b/src/module/injection/handler_service.go index aaa2b831..20dc227c 100644 --- a/src/module/injection/handler_service.go +++ b/src/module/injection/handler_service.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "archive/zip" diff --git a/src/module/injection/module.go b/src/module/injection/module.go index bd28c7b6..48141cf6 100644 --- a/src/module/injection/module.go +++ b/src/module/injection/module.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import "go.uber.org/fx" diff --git a/src/module/injection/query_datapack_arrow.go b/src/module/injection/query_datapack_arrow.go index cc16c717..b69aad4b 100644 --- a/src/module/injection/query_datapack_arrow.go +++ b/src/module/injection/query_datapack_arrow.go @@ -1,6 +1,6 @@ //go:build duckdb_arrow -package injectionmodule +package injection import ( "context" diff --git a/src/module/injection/query_datapack_noarrow.go b/src/module/injection/query_datapack_noarrow.go index 7d87b881..c75f131a 100644 --- a/src/module/injection/query_datapack_noarrow.go +++ b/src/module/injection/query_datapack_noarrow.go @@ -1,6 +1,6 @@ //go:build !duckdb_arrow -package injectionmodule +package injection import ( "context" diff --git a/src/module/injection/repository.go b/src/module/injection/repository.go index 46484ccc..1ab848f4 100644 --- a/src/module/injection/repository.go +++ b/src/module/injection/repository.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "aegis/consts" diff --git a/src/module/injection/resolve.go b/src/module/injection/resolve.go index f61874ad..1f83e415 100644 --- a/src/module/injection/resolve.go +++ b/src/module/injection/resolve.go @@ -1,10 +1,10 @@ -package injectionmodule +package injection import ( "aegis/consts" "aegis/dto" "aegis/model" - datasetmodule "aegis/module/dataset" + dataset "aegis/module/dataset" "fmt" ) @@ -55,7 +55,7 @@ func (r *Repository) ResolveDatapacks(datapackName *string, datasetRef *dto.Data } if datasetRef != nil { - datasetVersionResults, err := datasetmodule.NewRepository(r.db).ResolveDatasetVersions([]*dto.DatasetRef{datasetRef}, userID) + datasetVersionResults, err := dataset.NewRepository(r.db).ResolveDatasetVersions([]*dto.DatasetRef{datasetRef}, userID) if err != nil { return nil, nil, fmt.Errorf("failed to get dataset versions: %w", err) } @@ -65,7 +65,7 @@ func (r *Repository) ResolveDatapacks(datapackName *string, datasetRef *dto.Data return nil, nil, fmt.Errorf("dataset version not found for %v", datasetRef) } - datapacks, err := datasetmodule.NewRepository(r.db).ListInjectionsByDatasetVersionID(version.ID, true) + datapacks, err := dataset.NewRepository(r.db).ListInjectionsByDatasetVersionID(version.ID, true) if err != nil { return nil, nil, fmt.Errorf("failed to get dataset datapacks: %s", err.Error()) } diff --git a/src/module/injection/runtime_types.go b/src/module/injection/runtime_types.go index 6f2a3f11..f97fc279 100644 --- a/src/module/injection/runtime_types.go +++ b/src/module/injection/runtime_types.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "time" diff --git a/src/module/injection/service.go b/src/module/injection/service.go index fbe8010c..225ca805 100644 --- a/src/module/injection/service.go +++ b/src/module/injection/service.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "archive/zip" @@ -12,11 +12,11 @@ import ( "aegis/consts" "aegis/dto" - lokiinfra "aegis/infra/loki" - redisinfra "aegis/infra/redis" + loki "aegis/infra/loki" + redis "aegis/infra/redis" "aegis/model" - containermodule "aegis/module/container" - labelmodule "aegis/module/label" + container "aegis/module/container" + label "aegis/module/label" "aegis/service/common" "aegis/utils" @@ -27,11 +27,11 @@ import ( type Service struct { repo *Repository store *DatapackStore - lokiClient *lokiinfra.Client - redis *redisinfra.Gateway + lokiClient *loki.Client + redis *redis.Gateway } -func NewService(repo *Repository, store *DatapackStore, lokiClient *lokiinfra.Client, redis *redisinfra.Gateway) *Service { +func NewService(repo *Repository, store *DatapackStore, lokiClient *loki.Client, redis *redis.Gateway) *Service { return &Service{repo: repo, store: store, lokiClient: lokiClient, redis: redis} } @@ -166,7 +166,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection projectID = &project.ID } - pedestalVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) + pedestalVersionResults, err := container.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Pedestal.ContainerRef}, consts.ContainerTypePedestal, userID) if err != nil { return nil, fmt.Errorf("failed to map pedestal container ref to version: %w", err) } @@ -184,7 +184,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } params := flattenYAMLToParameters(req.Pedestal.Payload, "") - helmValues, err := containermodule.NewRepository(db).ListHelmConfigValues(params, helmConfig) + helmValues, err := container.NewRepository(db).ListHelmConfigValues(params, helmConfig) if err != nil { return nil, fmt.Errorf("failed to render pedestal helm values: %w", err) } @@ -195,7 +195,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection pedestalItem := dto.NewContainerVersionItem(&pedestalVersion) pedestalItem.Extra = helmConfigItem - benchmarkVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) + benchmarkVersionResults, err := container.NewRepository(db).ResolveContainerVersions([]*dto.ContainerRef{&req.Benchmark.ContainerRef}, consts.ContainerTypeBenchmark, userID) if err != nil { return nil, fmt.Errorf("failed to map benchmark container ref to version: %w", err) } @@ -205,7 +205,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(req.Benchmark.EnvVars, &benchmarkVersion) + envVars, err := container.NewRepository(db).ListContainerVersionEnvVars(req.Benchmark.EnvVars, &benchmarkVersion) if err != nil { return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) } @@ -245,7 +245,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection refs = append(refs, &req.Algorithms[i].ContainerRef) } - algorithmVersionsResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) + algorithmVersionsResults, err := container.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeAlgorithm, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -259,7 +259,7 @@ func (s *Service) SubmitFaultInjection(ctx context.Context, req *SubmitInjection } algorithmVersionItem := dto.NewContainerVersionItem(&algorithmVersion) - envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.EnvVars, &algorithmVersion) + envVars, err := container.NewRepository(db).ListContainerVersionEnvVars(spec.EnvVars, &algorithmVersion) if err != nil { return nil, fmt.Errorf("failed to list algorithm env vars: %w", err) } @@ -348,7 +348,7 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac refs = append(refs, &req.Specs[i].Benchmark.ContainerRef) } - benchmarkVersionResults, err := containermodule.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeBenchmark, userID) + benchmarkVersionResults, err := container.NewRepository(db).ResolveContainerVersions(refs, consts.ContainerTypeBenchmark, userID) if err != nil { return nil, fmt.Errorf("failed to map container refs to versions: %w", err) } @@ -366,7 +366,7 @@ func (s *Service) SubmitDatapackBuilding(ctx context.Context, req *SubmitDatapac } benchmarkVersionItem := dto.NewContainerVersionItem(&benchmarkVersion) - envVars, err := containermodule.NewRepository(db).ListContainerVersionEnvVars(spec.Benchmark.EnvVars, &benchmarkVersion) + envVars, err := container.NewRepository(db).ListContainerVersionEnvVars(spec.Benchmark.EnvVars, &benchmarkVersion) if err != nil { return nil, fmt.Errorf("failed to list benchmark env vars: %w", err) } @@ -459,7 +459,7 @@ func (s *Service) ManageLabels(_ context.Context, req *ManageInjectionLabelReq, } if len(req.AddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -559,7 +559,7 @@ func (s *Service) BatchManageLabels(_ context.Context, req *BatchManageInjection var labelMap map[string]int if len(allAddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, allAddLabels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -680,7 +680,7 @@ func (s *Service) Clone(_ context.Context, id int, req *CloneInjectionReq) (*Inj return fmt.Errorf("failed to create injection: %w", err) } if len(req.Labels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -727,7 +727,7 @@ func (s *Service) GetLogs(ctx context.Context, id int) (*InjectionLogsResp, erro lokiCtx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - logEntries, lokiErr := s.lokiClient.QueryJobLogs(lokiCtx, *injection.TaskID, lokiinfra.QueryOpts{ + logEntries, lokiErr := s.lokiClient.QueryJobLogs(lokiCtx, *injection.TaskID, loki.QueryOpts{ Start: task.CreatedAt, Direction: "forward", }) @@ -836,7 +836,7 @@ func (s *Service) CreateInjectionRecord(_ context.Context, req *RuntimeCreateInj } if len(req.Labels) > 0 { - createdLabels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) + createdLabels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.Labels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } @@ -1007,7 +1007,7 @@ func (s *Service) UploadDatapack(_ context.Context, req *UploadDatapackReq, file } if len(labels) > 0 { - createdLabels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, labels, consts.InjectionCategory) + createdLabels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, labels, consts.InjectionCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/injection/service_test.go b/src/module/injection/service_test.go index c43da5eb..261993fe 100644 --- a/src/module/injection/service_test.go +++ b/src/module/injection/service_test.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "regexp" @@ -7,7 +7,7 @@ import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/testutil" "github.com/DATA-DOG/go-sqlmock" @@ -32,7 +32,7 @@ func newInjectionService(t *testing.T) (*Service, sqlmock.Sqlmock, func()) { }), &gorm.Config{}) require.NoError(t, err) - return NewService(NewRepository(db), nil, nil, redisinfra.NewGateway(nil)), mock, func() { + return NewService(NewRepository(db), nil, nil, redis.NewGateway(nil)), mock, func() { cleanupRedis() _ = sqlDB.Close() } diff --git a/src/module/injection/submit.go b/src/module/injection/submit.go index 1f922c58..45a0f2d7 100644 --- a/src/module/injection/submit.go +++ b/src/module/injection/submit.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "aegis/consts" diff --git a/src/module/injection/time_range.go b/src/module/injection/time_range.go index 5f8b3872..c3e66818 100644 --- a/src/module/injection/time_range.go +++ b/src/module/injection/time_range.go @@ -1,4 +1,4 @@ -package injectionmodule +package injection import ( "fmt" diff --git a/src/module/label/api_types.go b/src/module/label/api_types.go index 77fc19e3..5a66f4fa 100644 --- a/src/module/label/api_types.go +++ b/src/module/label/api_types.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "fmt" diff --git a/src/module/label/core.go b/src/module/label/core.go index e11ace30..8f96bad6 100644 --- a/src/module/label/core.go +++ b/src/module/label/core.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "aegis/consts" diff --git a/src/module/label/handler.go b/src/module/label/handler.go index 0c96f8f8..d9a82bf7 100644 --- a/src/module/label/handler.go +++ b/src/module/label/handler.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "aegis/httpx" @@ -26,7 +26,7 @@ func NewHandler(service HandlerService) *Handler { return &Handler{service: serv // @Accept json // @Produce json // @Security BearerAuth -// @Param request body BatchDeleteLabelReq true "Batch delete request" +// @Param request body BatchDeleteLabelReq true "Batch delete request" // @Success 200 {object} dto.GenericResponse[any] "Labels deleted successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -59,13 +59,13 @@ func (h *Handler) BatchDeleteLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param label body CreateLabelReq true "Label creation request" -// @Success 201 {object} dto.GenericResponse[LabelResp] "Label created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Label already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param label body CreateLabelReq true "Label creation request" +// @Success 201 {object} dto.GenericResponse[LabelResp] "Label created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Label already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels [post] // @x-api-type {"portal":"true"} func (h *Handler) CreateLabel(c *gin.Context) { @@ -121,13 +121,13 @@ func (h *Handler) DeleteLabel(c *gin.Context) { // @ID get_label_by_id // @Produce json // @Security BearerAuth -// @Param label_id path int true "Label ID" +// @Param label_id path int true "Label ID" // @Success 200 {object} dto.GenericResponse[LabelDetailResp] "Label retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Label not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Label not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/{label_id} [get] // @x-api-type {"portal":"true"} func (h *Handler) GetLabelDetail(c *gin.Context) { @@ -150,18 +150,18 @@ func (h *Handler) GetLabelDetail(c *gin.Context) { // @ID list_labels // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param key query string false "Filter by label key" -// @Param value query string false "Filter by label value" -// @Param category query consts.LabelCategory false "Filter by category" -// @Param is_system query bool false "Filter by system label" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param key query string false "Filter by label key" +// @Param value query string false "Filter by label value" +// @Param category query consts.LabelCategory false "Filter by category" +// @Param is_system query bool false "Filter by system label" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[LabelResp]] "Labels retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels [get] // @x-api-type {"portal":"true"} func (h *Handler) ListLabels(c *gin.Context) { @@ -190,14 +190,14 @@ func (h *Handler) ListLabels(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param label_id path int true "Label ID" -// @Param request body UpdateLabelReq true "Label update request" -// @Success 202 {object} dto.GenericResponse[LabelResp] "Label updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Label not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param label_id path int true "Label ID" +// @Param request body UpdateLabelReq true "Label update request" +// @Success 202 {object} dto.GenericResponse[LabelResp] "Label updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid label ID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Label not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/labels/{label_id} [patch] // @x-api-type {"portal":"true"} func (h *Handler) UpdateLabel(c *gin.Context) { diff --git a/src/module/label/handler_service.go b/src/module/label/handler_service.go index 733d7d93..e80ca0b1 100644 --- a/src/module/label/handler_service.go +++ b/src/module/label/handler_service.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "context" diff --git a/src/module/label/module.go b/src/module/label/module.go index 46c7b518..df04d5d4 100644 --- a/src/module/label/module.go +++ b/src/module/label/module.go @@ -1,4 +1,4 @@ -package labelmodule +package label import "go.uber.org/fx" diff --git a/src/module/label/repository.go b/src/module/label/repository.go index 9baea135..9b1fcc83 100644 --- a/src/module/label/repository.go +++ b/src/module/label/repository.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "aegis/consts" diff --git a/src/module/label/service.go b/src/module/label/service.go index 3cb26410..ae364921 100644 --- a/src/module/label/service.go +++ b/src/module/label/service.go @@ -1,4 +1,4 @@ -package labelmodule +package label import ( "context" diff --git a/src/module/metric/api_types.go b/src/module/metric/api_types.go index d553d05a..f0f102e1 100644 --- a/src/module/metric/api_types.go +++ b/src/module/metric/api_types.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import ( "fmt" diff --git a/src/module/metric/handler.go b/src/module/metric/handler.go index 624b8137..92c20078 100644 --- a/src/module/metric/handler.go +++ b/src/module/metric/handler.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import ( "aegis/httpx" @@ -25,15 +25,15 @@ func NewHandler(service HandlerService) *Handler { // @ID get_injection_metrics // @Produce json // @Security BearerAuth -// @Param start_time query string false "Start time (RFC3339)" -// @Param end_time query string false "End time (RFC3339)" -// @Param fault_type query string false "Filter by fault type" -// @Success 200 {object} dto.GenericResponse[InjectionMetrics] "Injection metrics" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param start_time query string false "Start time (RFC3339)" +// @Param end_time query string false "End time (RFC3339)" +// @Param fault_type query string false "Filter by fault type" +// @Success 200 {object} dto.GenericResponse[InjectionMetrics] "Injection metrics" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/injections [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetInjectionMetrics(c *gin.Context) { var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { @@ -57,15 +57,15 @@ func (h *Handler) GetInjectionMetrics(c *gin.Context) { // @ID get_execution_metrics // @Produce json // @Security BearerAuth -// @Param start_time query string false "Start time (RFC3339)" -// @Param end_time query string false "End time (RFC3339)" -// @Param algorithm_id query int false "Filter by algorithm ID" -// @Success 200 {object} dto.GenericResponse[ExecutionMetrics] "Execution metrics" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param start_time query string false "Start time (RFC3339)" +// @Param end_time query string false "End time (RFC3339)" +// @Param algorithm_id query int false "Filter by algorithm ID" +// @Success 200 {object} dto.GenericResponse[ExecutionMetrics] "Execution metrics" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/executions [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetExecutionMetrics(c *gin.Context) { var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { @@ -89,15 +89,15 @@ func (h *Handler) GetExecutionMetrics(c *gin.Context) { // @ID get_algorithm_metrics // @Produce json // @Security BearerAuth -// @Param algorithm_ids query string false "Comma-separated algorithm IDs" -// @Param start_time query string false "Start time (RFC3339)" -// @Param end_time query string false "End time (RFC3339)" -// @Success 200 {object} dto.GenericResponse[AlgorithmMetrics] "Algorithm metrics" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param algorithm_ids query string false "Comma-separated algorithm IDs" +// @Param start_time query string false "Start time (RFC3339)" +// @Param end_time query string false "End time (RFC3339)" +// @Success 200 {object} dto.GenericResponse[AlgorithmMetrics] "Algorithm metrics" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/metrics/algorithms [get] -// @x-api-type {} +// @x-api-type {"portal":"true","sdk":"true"} func (h *Handler) GetAlgorithmMetrics(c *gin.Context) { var req GetMetricsReq if err := c.ShouldBindQuery(&req); err != nil { diff --git a/src/module/metric/handler_service.go b/src/module/metric/handler_service.go index faa0aefa..25ce3201 100644 --- a/src/module/metric/handler_service.go +++ b/src/module/metric/handler_service.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import "context" diff --git a/src/module/metric/module.go b/src/module/metric/module.go index 3efa27a2..356bb015 100644 --- a/src/module/metric/module.go +++ b/src/module/metric/module.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import "go.uber.org/fx" diff --git a/src/module/metric/repository.go b/src/module/metric/repository.go index 99039fa3..eef9f458 100644 --- a/src/module/metric/repository.go +++ b/src/module/metric/repository.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import ( "aegis/model" diff --git a/src/module/metric/service.go b/src/module/metric/service.go index 4c8add1d..057eb2f7 100644 --- a/src/module/metric/service.go +++ b/src/module/metric/service.go @@ -1,4 +1,4 @@ -package metricmodule +package metric import ( "context" diff --git a/src/module/notification/api_types.go b/src/module/notification/api_types.go index dd44e524..6fd83c50 100644 --- a/src/module/notification/api_types.go +++ b/src/module/notification/api_types.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import "time" diff --git a/src/module/notification/handler.go b/src/module/notification/handler.go index 92f99142..8cc3b6f8 100644 --- a/src/module/notification/handler.go +++ b/src/module/notification/handler.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import ( "context" @@ -39,7 +39,7 @@ func NewHandler(service HandlerService) *Handler { // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/notifications/stream [get] // @x-request-type {"stream":"true"} -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetStream(c *gin.Context) { var req GetNotificationStreamReq if err := c.ShouldBindQuery(&req); err != nil { diff --git a/src/module/notification/handler_service.go b/src/module/notification/handler_service.go index 725492d2..6c838a52 100644 --- a/src/module/notification/handler_service.go +++ b/src/module/notification/handler_service.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import ( "context" diff --git a/src/module/notification/module.go b/src/module/notification/module.go index 2c4c8fc2..21a1d42f 100644 --- a/src/module/notification/module.go +++ b/src/module/notification/module.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import "go.uber.org/fx" diff --git a/src/module/notification/repository.go b/src/module/notification/repository.go index 6188f578..48e5426b 100644 --- a/src/module/notification/repository.go +++ b/src/module/notification/repository.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import "gorm.io/gorm" diff --git a/src/module/notification/service.go b/src/module/notification/service.go index a76a5142..40c01716 100644 --- a/src/module/notification/service.go +++ b/src/module/notification/service.go @@ -1,4 +1,4 @@ -package notificationmodule +package notification import ( "context" @@ -7,7 +7,7 @@ import ( redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type Service struct { @@ -19,7 +19,7 @@ func NewService(repo *Repository, redis *redisinfra.Gateway) *Service { return &Service{repo: repo, redis: redis} } -func (s *Service) ReadStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { +func (s *Service) ReadStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]goredis.XStream, error) { if lastID == "" { lastID = "0" } diff --git a/src/module/project/api_types.go b/src/module/project/api_types.go index 3e72b992..1cea634f 100644 --- a/src/module/project/api_types.go +++ b/src/module/project/api_types.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "fmt" @@ -8,13 +8,13 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - injectionmodule "aegis/module/injection" + container "aegis/module/container" + dataset "aegis/module/dataset" + injection "aegis/module/injection" ) -type ProjectContainerItem = containermodule.ContainerResp -type ProjectDatasetItem = datasetmodule.DatasetResp +type ProjectContainerItem = container.ContainerResp +type ProjectDatasetItem = dataset.DatasetResp // CreateProjectReq represents project creation request. type CreateProjectReq struct { @@ -161,10 +161,10 @@ func NewProjectResp(project *model.Project, stats *dto.ProjectStatistics) *Proje type ProjectDetailResp struct { ProjectResp - Containers []ProjectContainerItem `json:"containers,omitempty"` - Datapacks []injectionmodule.InjectionResp `json:"datapacks,omitempty"` - Datasets []ProjectDatasetItem `json:"datasets,omitempty"` - UserCount int `json:"user_count"` + Containers []ProjectContainerItem `json:"containers,omitempty"` + Datapacks []injection.InjectionResp `json:"datapacks,omitempty"` + Datasets []ProjectDatasetItem `json:"datasets,omitempty"` + UserCount int `json:"user_count"` } func NewProjectDetailResp(project *model.Project, stats *dto.ProjectStatistics) *ProjectDetailResp { diff --git a/src/module/project/handler.go b/src/module/project/handler.go index 7fd1e109..0b3f6534 100644 --- a/src/module/project/handler.go +++ b/src/module/project/handler.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "aegis/httpx" @@ -29,13 +29,13 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateProjectReq true "Project creation request" -// @Success 201 {object} dto.GenericResponse[ProjectResp] "Project created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Project already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body CreateProjectReq true "Project creation request" +// @Success 201 {object} dto.GenericResponse[ProjectResp] "Project created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Project already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects [post] // @x-api-type {"portal":"true"} func (h *Handler) CreateProject(c *gin.Context) { @@ -103,13 +103,13 @@ func (h *Handler) DeleteProject(c *gin.Context) { // @ID get_project_by_id // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" +// @Param project_id path int true "Project ID" // @Success 200 {object} dto.GenericResponse[ProjectDetailResp] "Project retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id} [get] // @x-api-type {"portal":"true"} func (h *Handler) GetProjectDetail(c *gin.Context) { @@ -134,15 +134,15 @@ func (h *Handler) GetProjectDetail(c *gin.Context) { // @ID list_projects // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param is_public query bool false "Filter by public status" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param is_public query bool false "Filter by public status" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[ProjectResp]] "Projects retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects [get] // @x-api-type {"portal":"true"} func (h *Handler) ListProjects(c *gin.Context) { @@ -174,14 +174,14 @@ func (h *Handler) ListProjects(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param request body UpdateProjectReq true "Project update request" -// @Success 202 {object} dto.GenericResponse[ProjectResp] "Project updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param project_id path int true "Project ID" +// @Param request body UpdateProjectReq true "Project update request" +// @Success 202 {object} dto.GenericResponse[ProjectResp] "Project updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id} [patch] // @x-api-type {"portal":"true"} func (h *Handler) UpdateProject(c *gin.Context) { @@ -218,14 +218,14 @@ func (h *Handler) UpdateProject(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param project_id path int true "Project ID" -// @Param manage body ManageProjectLabelReq true "Label management request" -// @Success 200 {object} dto.GenericResponse[ProjectResp] "Labels managed successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Project not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param project_id path int true "Project ID" +// @Param manage body ManageProjectLabelReq true "Label management request" +// @Success 200 {object} dto.GenericResponse[ProjectResp] "Labels managed successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid project ID or invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Project not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/projects/{project_id}/labels [patch] // @x-api-type {"portal":"true"} func (h *Handler) ManageProjectCustomLabels(c *gin.Context) { diff --git a/src/module/project/handler_service.go b/src/module/project/handler_service.go index 83987f93..2463064d 100644 --- a/src/module/project/handler_service.go +++ b/src/module/project/handler_service.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "context" diff --git a/src/module/project/module.go b/src/module/project/module.go index 5acbdeae..48d8eeac 100644 --- a/src/module/project/module.go +++ b/src/module/project/module.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "go.uber.org/fx" diff --git a/src/module/project/project_statistics.go b/src/module/project/project_statistics.go index 1a6689d9..997892df 100644 --- a/src/module/project/project_statistics.go +++ b/src/module/project/project_statistics.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "context" diff --git a/src/module/project/repository.go b/src/module/project/repository.go index 28ed5b9d..7255ade8 100644 --- a/src/module/project/repository.go +++ b/src/module/project/repository.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "aegis/consts" diff --git a/src/module/project/service.go b/src/module/project/service.go index 24ea94ed..a282d28d 100644 --- a/src/module/project/service.go +++ b/src/module/project/service.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "context" @@ -8,7 +8,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - labelmodule "aegis/module/label" + label "aegis/module/label" "gorm.io/gorm" ) @@ -178,7 +178,7 @@ func (s *Service) ManageProjectLabels(ctx context.Context, req *ManageProjectLab repo := NewRepository(tx) addLabelIDs := make([]int, 0, len(req.AddLabels)) if len(req.AddLabels) > 0 { - labels, err := labelmodule.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) + labels, err := label.NewRepository(tx).CreateOrUpdateLabelsFromItems(tx, req.AddLabels, consts.ProjectCategory) if err != nil { return fmt.Errorf("failed to create or update labels: %w", err) } diff --git a/src/module/project/service_test.go b/src/module/project/service_test.go index 613cea25..33414002 100644 --- a/src/module/project/service_test.go +++ b/src/module/project/service_test.go @@ -1,4 +1,4 @@ -package projectmodule +package project import ( "regexp" diff --git a/src/module/rbac/api_types.go b/src/module/rbac/api_types.go index 92cc57ba..141b5afe 100644 --- a/src/module/rbac/api_types.go +++ b/src/module/rbac/api_types.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "fmt" diff --git a/src/module/rbac/handler.go b/src/module/rbac/handler.go index 1d436700..3d63bdc5 100644 --- a/src/module/rbac/handler.go +++ b/src/module/rbac/handler.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "aegis/httpx" @@ -28,13 +28,13 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateRoleReq true "Role creation request" +// @Param request body CreateRoleReq true "Role creation request" // @Success 201 {object} dto.GenericResponse[RoleResp] "Role created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Role already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Role already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/roles [post] // @x-api-type {"admin":"true"} func (h *Handler) CreateRole(c *gin.Context) { @@ -86,13 +86,13 @@ func (h *Handler) DeleteRole(c *gin.Context) { // @ID get_role_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Role ID" +// @Param id path int true "Role ID" // @Success 200 {object} dto.GenericResponse[RoleDetailResp] "Role retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid role ID" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/roles/{id} [get] // @x-api-type {"admin":"true"} func (h *Handler) GetRole(c *gin.Context) { @@ -115,15 +115,15 @@ func (h *Handler) GetRole(c *gin.Context) { // @ID list_roles // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param is_system query bool false "Filter by system role" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param is_system query bool false "Filter by system role" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[RoleResp]] "Roles retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/roles [get] // @x-api-type {"admin":"true"} func (h *Handler) ListRoles(c *gin.Context) { @@ -148,14 +148,14 @@ func (h *Handler) ListRoles(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "Role ID" -// @Param request body UpdateRoleReq true "Role update request" +// @Param id path int true "Role ID" +// @Param request body UpdateRoleReq true "Role update request" // @Success 202 {object} dto.GenericResponse[RoleResp] "Role updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Role not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Role not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/roles/{id} [patch] // @x-api-type {"admin":"true"} func (h *Handler) UpdateRole(c *gin.Context) { @@ -286,13 +286,13 @@ func (h *Handler) ListUsersFromRole(c *gin.Context) { // @ID get_permission_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Permission ID" +// @Param id path int true "Permission ID" // @Success 200 {object} dto.GenericResponse[PermissionDetailResp] "Permission retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/permissions/{id} [get] // @x-api-type {"admin":"true"} func (h *Handler) GetPermission(c *gin.Context) { @@ -315,16 +315,16 @@ func (h *Handler) GetPermission(c *gin.Context) { // @ID list_permissions // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param action query string false "Filter by action" -// @Param is_system query bool false "Filter by system permission" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param action query string false "Filter by action" +// @Param is_system query bool false "Filter by system permission" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[PermissionResp]] "Permissions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/permissions [get] // @x-api-type {"admin":"true"} func (h *Handler) ListPermissions(c *gin.Context) { @@ -352,13 +352,13 @@ func (h *Handler) ListPermissions(c *gin.Context) { // @ID list_roles_with_permission // @Produce json // @Security BearerAuth -// @Param permission_id path int true "Permission ID" +// @Param permission_id path int true "Permission ID" // @Success 200 {object} dto.GenericResponse[[]RoleResp] "Roles retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid permission ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Permission not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/permissions/{permission_id}/roles [get] // @x-api-type {"admin":"true"} func (h *Handler) ListRolesFromPermission(c *gin.Context) { @@ -381,13 +381,13 @@ func (h *Handler) ListRolesFromPermission(c *gin.Context) { // @ID get_resource_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "Resource ID" +// @Param id path int true "Resource ID" // @Success 200 {object} dto.GenericResponse[ResourceResp] "Resource retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/resources/{id} [get] // @x-api-type {"admin":"true"} func (h *Handler) GetResource(c *gin.Context) { @@ -410,15 +410,15 @@ func (h *Handler) GetResource(c *gin.Context) { // @ID list_resources // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param type query consts.ResourceType false "Filter by resource type" -// @Param category query consts.ResourceCategory false "Filter by resource category" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param type query consts.ResourceType false "Filter by resource type" +// @Param category query consts.ResourceCategory false "Filter by resource category" // @Success 200 {object} dto.GenericResponse[dto.ListResp[ResourceResp]] "Resources retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/resources [get] // @x-api-type {"admin":"true"} func (h *Handler) ListResources(c *gin.Context) { @@ -446,13 +446,13 @@ func (h *Handler) ListResources(c *gin.Context) { // @ID list_resource_permissions // @Produce json // @Security BearerAuth -// @Param id path int true "Resource ID" +// @Param id path int true "Resource ID" // @Success 200 {object} dto.GenericResponse[[]PermissionResp] "Permissions retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID or request form" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid resource ID or request form" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Resource not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/resources/{id}/permissions [get] // @x-api-type {"admin":"true"} func (h *Handler) ListResourcePermissions(c *gin.Context) { diff --git a/src/module/rbac/handler_service.go b/src/module/rbac/handler_service.go index df23835e..1d66277f 100644 --- a/src/module/rbac/handler_service.go +++ b/src/module/rbac/handler_service.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "context" diff --git a/src/module/rbac/module.go b/src/module/rbac/module.go index e4a268bd..5e29782f 100644 --- a/src/module/rbac/module.go +++ b/src/module/rbac/module.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import "go.uber.org/fx" diff --git a/src/module/rbac/repository.go b/src/module/rbac/repository.go index 6bcda444..5544e1a5 100644 --- a/src/module/rbac/repository.go +++ b/src/module/rbac/repository.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "aegis/consts" diff --git a/src/module/rbac/service.go b/src/module/rbac/service.go index 91ff744e..3ae88583 100644 --- a/src/module/rbac/service.go +++ b/src/module/rbac/service.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "context" diff --git a/src/module/rbac/service_test.go b/src/module/rbac/service_test.go index 5423e8ff..3d99a853 100644 --- a/src/module/rbac/service_test.go +++ b/src/module/rbac/service_test.go @@ -1,4 +1,4 @@ -package rbacmodule +package rbac import ( "regexp" diff --git a/src/module/sdk/api_types.go b/src/module/sdk/api_types.go index e655dfcb..6da3f4ad 100644 --- a/src/module/sdk/api_types.go +++ b/src/module/sdk/api_types.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import ( "fmt" diff --git a/src/module/sdk/handler.go b/src/module/sdk/handler.go index e6aa5f2a..a67c947b 100644 --- a/src/module/sdk/handler.go +++ b/src/module/sdk/handler.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import ( "aegis/httpx" @@ -26,13 +26,13 @@ func NewHandler(service *Service) *Handler { // @ID list_sdk_evaluations // @Produce json // @Security BearerAuth -// @Param exp_id query string false "Experiment ID filter" -// @Param stage query string false "Stage filter (init, rollout, judged)" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param exp_id query string false "Experiment ID filter" +// @Param stage query string false "Stage filter (init, rollout, judged)" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[SDKEvaluationSample]] "SDK evaluations retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations [get] // @x-api-type {"sdk":"true"} func (h *Handler) ListEvaluations(c *gin.Context) { @@ -60,11 +60,11 @@ func (h *Handler) ListEvaluations(c *gin.Context) { // @ID get_sdk_evaluation // @Produce json // @Security BearerAuth -// @Param id path int true "SDK Evaluation Sample ID" +// @Param id path int true "SDK Evaluation Sample ID" // @Success 200 {object} dto.GenericResponse[SDKEvaluationSample] "SDK evaluation sample retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" -// @Failure 404 {object} dto.GenericResponse[any] "SDK evaluation sample not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid evaluation ID" +// @Failure 404 {object} dto.GenericResponse[any] "SDK evaluation sample not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations/{id} [get] // @x-api-type {"sdk":"true"} func (h *Handler) GetEvaluation(c *gin.Context) { @@ -88,7 +88,7 @@ func (h *Handler) GetEvaluation(c *gin.Context) { // @Produce json // @Security BearerAuth // @Success 200 {object} dto.GenericResponse[SDKExperimentListResp] "SDK experiments retrieved successfully" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/evaluations/experiments [get] // @x-api-type {"sdk":"true"} func (h *Handler) ListExperiments(c *gin.Context) { @@ -107,12 +107,12 @@ func (h *Handler) ListExperiments(c *gin.Context) { // @ID list_sdk_dataset_samples // @Produce json // @Security BearerAuth -// @Param dataset query string false "Dataset name filter" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param dataset query string false "Dataset name filter" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[SDKDatasetSample]] "SDK dataset samples retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/sdk/datasets [get] // @x-api-type {"sdk":"true"} func (h *Handler) ListDatasetSamples(c *gin.Context) { diff --git a/src/module/sdk/models.go b/src/module/sdk/models.go index 0cdb7de2..78ba51e5 100644 --- a/src/module/sdk/models.go +++ b/src/module/sdk/models.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import "time" diff --git a/src/module/sdk/module.go b/src/module/sdk/module.go index 8f42df2f..57301321 100644 --- a/src/module/sdk/module.go +++ b/src/module/sdk/module.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import "go.uber.org/fx" diff --git a/src/module/sdk/repository.go b/src/module/sdk/repository.go index f60d28b9..9c77de2a 100644 --- a/src/module/sdk/repository.go +++ b/src/module/sdk/repository.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import ( "fmt" diff --git a/src/module/sdk/service.go b/src/module/sdk/service.go index 92136a5d..97b56885 100644 --- a/src/module/sdk/service.go +++ b/src/module/sdk/service.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import ( "context" diff --git a/src/module/sdk/service_test.go b/src/module/sdk/service_test.go index e4b6976e..3a525136 100644 --- a/src/module/sdk/service_test.go +++ b/src/module/sdk/service_test.go @@ -1,4 +1,4 @@ -package sdkmodule +package sdk import ( "regexp" diff --git a/src/module/system/api_types.go b/src/module/system/api_types.go index 805bfc07..cd726509 100644 --- a/src/module/system/api_types.go +++ b/src/module/system/api_types.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "fmt" @@ -7,8 +7,8 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" ) // HealthCheckResp represents system health check response. @@ -52,9 +52,9 @@ type MonitoringMetricsResp struct { Labels map[string]string `json:"labels,omitempty"` } -type MetricValue = systemmetricmodule.MetricValue -type ListNamespaceLockResp = systemmetricmodule.ListNamespaceLockResp -type QueuedTasksResp = taskmodule.QueuedTasksResp +type MetricValue = systemmetric.MetricValue +type ListNamespaceLockResp = systemmetric.ListNamespaceLockResp +type QueuedTasksResp = task.QueuedTasksResp type ListAuditLogFilters struct { Action string diff --git a/src/module/system/handler.go b/src/module/system/handler.go index 193c460b..68b9eabc 100644 --- a/src/module/system/handler.go +++ b/src/module/system/handler.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "aegis/httpx" @@ -28,7 +28,7 @@ func NewHandler(service HandlerService) *Handler { // @ID get_system_health // @Produce json // @Success 200 {object} dto.GenericResponse[HealthCheckResp] "Health check successful" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/health [get] // @x-api-type {"admin":"true"} func (h *Handler) GetHealth(c *gin.Context) { @@ -45,18 +45,18 @@ func (h *Handler) GetHealth(c *gin.Context) { // @Summary Get monitoring metrics // @Description Deprecated: This endpoint returns hardcoded/fabricated data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. // @Deprecated -// @Tags System -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body MonitoringQueryReq true "Metrics query request" -// @Success 200 {object} dto.GenericResponse[MonitoringMetricsResp] "Metrics retrieved successfully" -// @Success 400 {object} dto.GenericResponse[any] "Invalid request format" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/monitor/metrics [post] -// @x-api-type {"admin":"true"} +// @Tags System +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body MonitoringQueryReq true "Metrics query request" +// @Success 200 {object} dto.GenericResponse[MonitoringMetricsResp] "Metrics retrieved successfully" +// @Success 400 {object} dto.GenericResponse[any] "Invalid request format" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/monitor/metrics [post] +// @x-api-type {"admin":"true"} func (h *Handler) GetMetrics(c *gin.Context) { var req MonitoringQueryReq if err := c.ShouldBindJSON(&req); err != nil { @@ -77,15 +77,15 @@ func (h *Handler) GetMetrics(c *gin.Context) { // @Summary Get system information // @Description Deprecated: This endpoint returns partially hardcoded data. Use the v2 equivalent GET /api/v2/system/metrics which provides real system metrics via gopsutil. // @Deprecated -// @Tags System -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.GenericResponse[SystemInfo] "System info retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" -// @Router /system/monitor/info [get] -// @x-api-type {"admin":"true"} +// @Tags System +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.GenericResponse[SystemInfo] "System info retrieved successfully" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Router /system/monitor/info [get] +// @x-api-type {"admin":"true"} func (h *Handler) GetSystemInfo(c *gin.Context) { c.Header("Deprecation", "true") c.Header("Link", `; rel="successor-version"`) @@ -104,9 +104,9 @@ func (h *Handler) GetSystemInfo(c *gin.Context) { // @Produce json // @Security BearerAuth // @Success 200 {object} dto.GenericResponse[ListNamespaceLockResp] "Successfully retrieved the list of locks" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal Server Error" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal Server Error" // @Router /system/monitor/namespaces/locks [get] // @x-api-type {"admin":"true"} func (h *Handler) ListNamespaceLocks(c *gin.Context) { @@ -125,10 +125,10 @@ func (h *Handler) ListNamespaceLocks(c *gin.Context) { // @Produce json // @Security BearerAuth // @Success 200 {object} dto.GenericResponse[QueuedTasksResp] "Queued tasks retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "No queued tasks found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "No queued tasks found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/monitor/tasks/queue [post] // @x-api-type {"admin":"true"} func (h *Handler) ListQueuedTasks(c *gin.Context) { @@ -146,13 +146,13 @@ func (h *Handler) ListQueuedTasks(c *gin.Context) { // @Tags System // @Produce json // @Security BearerAuth -// @Param id path int true "Audit log ID" +// @Param id path int true "Audit log ID" // @Success 200 {object} dto.GenericResponse[AuditLogDetailResp] "Audit log retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Audit log not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Audit log not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/audit/{id} [get] // @x-api-type {"admin":"true"} func (h *Handler) GetAuditLog(c *gin.Context) { @@ -175,20 +175,20 @@ func (h *Handler) GetAuditLog(c *gin.Context) { // @Tags System // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param action query string false "Filter by action" -// @Param user_id query int false "Filter by user ID" -// @Param resource_id query int false "Filter by resource ID" -// @Param state query int false "Filter by state" -// @Param status query int false "Filter by status" -// @Param start_date query string false "Filter from date (YYYY-MM-DD)" -// @Param end_date query string false "Filter to date (YYYY-MM-DD)" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param action query string false "Filter by action" +// @Param user_id query int false "Filter by user ID" +// @Param resource_id query int false "Filter by resource ID" +// @Param state query int false "Filter by state" +// @Param status query int false "Filter by status" +// @Param start_date query string false "Filter from date (YYYY-MM-DD)" +// @Param end_date query string false "Filter to date (YYYY-MM-DD)" // @Success 200 {object} dto.GenericResponse[dto.ListResp[AuditLogResp]] "Audit logs retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format/parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/audit [get] // @x-api-type {"admin":"true"} func (h *Handler) ListAuditLogs(c *gin.Context) { @@ -217,13 +217,13 @@ func (h *Handler) ListAuditLogs(c *gin.Context) { // @ID get_config_by_id // @Produce json // @Security BearerAuth -// @Param config_id path int true "Configuration ID" +// @Param config_id path int true "Configuration ID" // @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Config not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Config not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/configs/{config_id} [get] // @x-api-type {"admin":"true"} func (h *Handler) GetConfig(c *gin.Context) { @@ -247,17 +247,17 @@ func (h *Handler) GetConfig(c *gin.Context) { // @ID list_configs // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param page_size query int false "Page size" default(20) -// @Param category query string false "Filter by configuration category" -// @Param value_type query consts.ConfigValueType false "Filter by configuration value type" -// @Param is_secret query bool false "Filter by secret status" -// @Param updated_by query int false "Filter by ID of the user who last updated the config" +// @Param page query int false "Page number" default(1) +// @Param page_size query int false "Page size" default(20) +// @Param category query string false "Filter by configuration category" +// @Param value_type query consts.ConfigValueType false "Filter by configuration value type" +// @Param is_secret query bool false "Filter by secret status" +// @Param updated_by query int false "Filter by ID of the user who last updated the config" // @Success 200 {object} dto.GenericResponse[dto.ListResp[ConfigResp]] "Configurations retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/configs [get] // @x-api-type {"admin":"true"} func (h *Handler) ListConfigs(c *gin.Context) { @@ -288,7 +288,7 @@ func (h *Handler) ListConfigs(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param config_id path int true "Configuration ID" -// @Param rollback body RollbackConfigReq true "Rollback request with history_id and reason" +// @Param rollback body RollbackConfigReq true "Rollback request with history_id and reason" // @Success 202 {object} dto.GenericResponse[any] "Configuration value rolled back successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is not a value change" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -330,14 +330,14 @@ func (h *Handler) RollbackConfigValue(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param config_id path int true "Configuration ID" +// @Param config_id path int true "Configuration ID" // @Param rollback body RollbackConfigReq true "Rollback request with history_id and reason" // @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration metadata rolled back successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is a value change" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request format/history is a value change" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration or history not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/configs/{config_id}/metadata/rollback [post] // @x-api-type {"admin":"true"} func (h *Handler) RollbackConfigMetadata(c *gin.Context) { @@ -375,7 +375,7 @@ func (h *Handler) RollbackConfigMetadata(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param config_id path int true "Configuration ID" -// @Param request body UpdateConfigValueReq true "Configuration value update request" +// @Param request body UpdateConfigValueReq true "Configuration value update request" // @Success 202 {object} dto.GenericResponse[any] "Configuration value updated successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -417,14 +417,14 @@ func (h *Handler) UpdateConfigValue(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param config_id path int true "Configuration ID" +// @Param config_id path int true "Configuration ID" // @Param request body UpdateConfigMetadataReq true "Configuration metadata update request" // @Success 200 {object} dto.GenericResponse[ConfigResp] "Configuration metadata updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" -// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid config ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied - admin only" +// @Failure 404 {object} dto.GenericResponse[any] "Configuration not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/configs/{config_id}/metadata [put] // @x-api-type {"admin":"true"} func (h *Handler) UpdateConfigMetadata(c *gin.Context) { @@ -464,14 +464,14 @@ func (h *Handler) UpdateConfigMetadata(c *gin.Context) { // @ID list_config_histories // @Produce json // @Security BearerAuth -// @Param config_id path int true "Configuration ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param config_id path int true "Configuration ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[ConfigHistoryResp]] "Config histories retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /system/configs/{config_id}/histories [get] // @x-api-type {"admin":"true"} func (h *Handler) ListConfigHistories(c *gin.Context) { diff --git a/src/module/system/handler_service.go b/src/module/system/handler_service.go index 5860a9ff..21b3816c 100644 --- a/src/module/system/handler_service.go +++ b/src/module/system/handler_service.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "context" diff --git a/src/module/system/handler_test.go b/src/module/system/handler_test.go index f1251f50..5e0e1969 100644 --- a/src/module/system/handler_test.go +++ b/src/module/system/handler_test.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "bytes" diff --git a/src/module/system/module.go b/src/module/system/module.go index a3976b00..d1bc05c9 100644 --- a/src/module/system/module.go +++ b/src/module/system/module.go @@ -1,4 +1,4 @@ -package systemmodule +package system import "go.uber.org/fx" diff --git a/src/module/system/repository.go b/src/module/system/repository.go index 88ccf041..72def364 100644 --- a/src/module/system/repository.go +++ b/src/module/system/repository.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "aegis/consts" diff --git a/src/module/system/runtime_query.go b/src/module/system/runtime_query.go index ba38d4b8..13fe579a 100644 --- a/src/module/system/runtime_query.go +++ b/src/module/system/runtime_query.go @@ -1,24 +1,24 @@ -package systemmodule +package system import ( "context" "fmt" "aegis/internalclient/runtimeclient" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" "go.uber.org/fx" ) type runtimeQuerySource interface { ListNamespaceLocks(context.Context) (*ListNamespaceLockResp, error) - ListQueuedTasks(context.Context) (*taskmodule.QueuedTasksResp, error) + ListQueuedTasks(context.Context) (*task.QueuedTasksResp, error) } type runtimeQueryAdapter struct { runtime *runtimeclient.Client - local *systemmetricmodule.Service + local *systemmetric.Service requireRemote bool } @@ -26,7 +26,7 @@ type runtimeQuerySourceParams struct { fx.In Runtime *runtimeclient.Client `optional:"true"` - Local *systemmetricmodule.Service + Local *systemmetric.Service } func newRuntimeQuerySource(params runtimeQuerySourceParams) runtimeQuerySource { @@ -55,7 +55,7 @@ func (a runtimeQueryAdapter) ListNamespaceLocks(ctx context.Context) (*ListNames return a.local.ListNamespaceLocks(ctx) } -func (a runtimeQueryAdapter) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { +func (a runtimeQueryAdapter) ListQueuedTasks(ctx context.Context) (*task.QueuedTasksResp, error) { if a.runtime != nil && a.runtime.Enabled() { return a.runtime.GetQueuedTasks(ctx) } diff --git a/src/module/system/service.go b/src/module/system/service.go index 25f06288..d82b1214 100644 --- a/src/module/system/service.go +++ b/src/module/system/service.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "context" @@ -12,10 +12,10 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - buildkitinfra "aegis/infra/buildkit" - etcdinfra "aegis/infra/etcd" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + buildkit "aegis/infra/buildkit" + etcd "aegis/infra/etcd" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" "aegis/model" "aegis/service/common" "aegis/utils" @@ -49,10 +49,10 @@ type configHistoryWriter interface { type Service struct { repo *Repository - buildkit *buildkitinfra.Gateway - etcd *etcdinfra.Gateway - k8s *k8sinfra.Gateway - redis *redisinfra.Gateway + buildkit *buildkit.Gateway + etcd *etcd.Gateway + k8s *k8s.Gateway + redis *redis.Gateway runtimeQuery runtimeQuerySource } @@ -60,10 +60,10 @@ type serviceParams struct { fx.In Repo *Repository - Buildkit *buildkitinfra.Gateway - Etcd *etcdinfra.Gateway - K8s *k8sinfra.Gateway - Redis *redisinfra.Gateway + Buildkit *buildkit.Gateway + Etcd *etcd.Gateway + K8s *k8s.Gateway + Redis *redis.Gateway RuntimeQuery runtimeQuerySource } diff --git a/src/module/system/service_test.go b/src/module/system/service_test.go index 2b2d61e6..e1871391 100644 --- a/src/module/system/service_test.go +++ b/src/module/system/service_test.go @@ -1,4 +1,4 @@ -package systemmodule +package system import ( "context" diff --git a/src/module/systemmetric/api_types.go b/src/module/systemmetric/api_types.go index 7ed384f7..de1eca3d 100644 --- a/src/module/systemmetric/api_types.go +++ b/src/module/systemmetric/api_types.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import "time" diff --git a/src/module/systemmetric/collector.go b/src/module/systemmetric/collector.go index 51082120..2ec13354 100644 --- a/src/module/systemmetric/collector.go +++ b/src/module/systemmetric/collector.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import ( "context" diff --git a/src/module/systemmetric/handler.go b/src/module/systemmetric/handler.go index e7c3f356..8600b7e2 100644 --- a/src/module/systemmetric/handler.go +++ b/src/module/systemmetric/handler.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import ( "net/http" @@ -25,8 +25,8 @@ func NewHandler(service HandlerService) *Handler { // @Produce json // @Security BearerAuth // @Success 200 {object} dto.GenericResponse[SystemMetricsResp] "System metrics retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/system/metrics [get] // @x-api-type {"admin":"true"} func (h *Handler) GetSystemMetrics(c *gin.Context) { @@ -48,8 +48,8 @@ func (h *Handler) GetSystemMetrics(c *gin.Context) { // @Produce json // @Security BearerAuth // @Success 200 {object} dto.GenericResponse[SystemMetricsHistoryResp] "System metrics history retrieved successfully" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/system/metrics/history [get] // @x-api-type {"admin":"true"} func (h *Handler) GetSystemMetricsHistory(c *gin.Context) { diff --git a/src/module/systemmetric/handler_service.go b/src/module/systemmetric/handler_service.go index d2d3cc7f..13e338c6 100644 --- a/src/module/systemmetric/handler_service.go +++ b/src/module/systemmetric/handler_service.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import "context" diff --git a/src/module/systemmetric/module.go b/src/module/systemmetric/module.go index 49e3986a..827e81b5 100644 --- a/src/module/systemmetric/module.go +++ b/src/module/systemmetric/module.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import "go.uber.org/fx" diff --git a/src/module/systemmetric/repository.go b/src/module/systemmetric/repository.go index 5a2552ac..65fc8d53 100644 --- a/src/module/systemmetric/repository.go +++ b/src/module/systemmetric/repository.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import "gorm.io/gorm" diff --git a/src/module/systemmetric/service.go b/src/module/systemmetric/service.go index 6f2c391d..80cff2f3 100644 --- a/src/module/systemmetric/service.go +++ b/src/module/systemmetric/service.go @@ -1,4 +1,4 @@ -package systemmetricmodule +package systemmetric import ( "context" @@ -11,9 +11,9 @@ import ( "aegis/consts" "aegis/dto" redisinfra "aegis/infra/redis" - taskmodule "aegis/module/task" + task "aegis/module/task" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" "github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/disk" "github.com/shirou/gopsutil/v3/mem" @@ -75,12 +75,12 @@ func (s *Service) GetSystemMetricsHistory(ctx context.Context) (*SystemMetricsHi endTime := now.Unix() cpuData, err := s.redis.ZRangeByScore(ctx, "system:metrics:cpu", fmt.Sprintf("%d", startTime), fmt.Sprintf("%d", endTime)) - if err != nil && !errors.Is(err, redis.Nil) { + if err != nil && !errors.Is(err, goredis.Nil) { return nil, fmt.Errorf("failed to get CPU history: %v", err) } memData, err := s.redis.ZRangeByScore(ctx, "system:metrics:memory", fmt.Sprintf("%d", startTime), fmt.Sprintf("%d", endTime)) - if err != nil && !errors.Is(err, redis.Nil) { + if err != nil && !errors.Is(err, goredis.Nil) { return nil, fmt.Errorf("failed to get memory history: %v", err) } @@ -143,16 +143,16 @@ func (s *Service) ListNamespaceLocks(ctx context.Context) (*ListNamespaceLockRes return &ListNamespaceLockResp{Items: items}, nil } -func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksResp, error) { +func (s *Service) ListQueuedTasks(ctx context.Context) (*task.QueuedTasksResp, error) { readyTaskDatas, err := s.redis.ListReadyTasks(ctx) if err != nil { - if errors.Is(err, redis.Nil) { + if errors.Is(err, goredis.Nil) { return nil, fmt.Errorf("%w: no ready tasks found", consts.ErrNotFound) } return nil, err } - readyTasks := make([]taskmodule.TaskResp, 0, len(readyTaskDatas)) + readyTasks := make([]task.TaskResp, 0, len(readyTaskDatas)) for _, taskData := range readyTaskDatas { taskResp, err := decodeQueuedTask(taskData) if err != nil { @@ -163,13 +163,13 @@ func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksR delayedTaskDatas, err := s.redis.ListDelayedTasks(ctx, 1000) if err != nil { - if errors.Is(err, redis.Nil) { + if errors.Is(err, goredis.Nil) { return nil, fmt.Errorf("%w: no delayed tasks found", consts.ErrNotFound) } return nil, err } - delayedTasks := make([]taskmodule.TaskResp, 0, len(delayedTaskDatas)) + delayedTasks := make([]task.TaskResp, 0, len(delayedTaskDatas)) for _, taskData := range delayedTaskDatas { taskResp, err := decodeQueuedTask(taskData) if err != nil { @@ -178,29 +178,29 @@ func (s *Service) ListQueuedTasks(ctx context.Context) (*taskmodule.QueuedTasksR delayedTasks = append(delayedTasks, taskResp) } - return &taskmodule.QueuedTasksResp{ + return &task.QueuedTasksResp{ ReadyTasks: readyTasks, DelayedTasks: delayedTasks, }, nil } -func decodeQueuedTask(taskData string) (taskmodule.TaskResp, error) { - var task dto.UnifiedTask - if err := json.Unmarshal([]byte(taskData), &task); err != nil { - return taskmodule.TaskResp{}, err - } - - return taskmodule.TaskResp{ - ID: task.TaskID, - Type: consts.GetTaskTypeName(task.Type), - Immediate: task.Immediate, - ExecuteTime: task.ExecuteTime, - CronExpr: task.CronExpr, - TraceID: task.TraceID, - GroupID: task.GroupID, - State: consts.GetTaskStateName(task.State), +func decodeQueuedTask(taskData string) (task.TaskResp, error) { + var queuedTask dto.UnifiedTask + if err := json.Unmarshal([]byte(taskData), &queuedTask); err != nil { + return task.TaskResp{}, err + } + + return task.TaskResp{ + ID: queuedTask.TaskID, + Type: consts.GetTaskTypeName(queuedTask.Type), + Immediate: queuedTask.Immediate, + ExecuteTime: queuedTask.ExecuteTime, + CronExpr: queuedTask.CronExpr, + TraceID: queuedTask.TraceID, + GroupID: queuedTask.GroupID, + State: consts.GetTaskStateName(queuedTask.State), Status: consts.GetStatusTypeName(consts.CommonEnabled), - ProjectID: task.ProjectID, + ProjectID: queuedTask.ProjectID, }, nil } @@ -224,7 +224,7 @@ func (s *Service) StoreSystemMetrics(ctx context.Context) error { now := time.Now().Unix() cpuData, _ := json.Marshal(metrics.CPU) - if err := s.redis.ZAdd(ctx, "system:metrics:cpu", redis.Z{ + if err := s.redis.ZAdd(ctx, "system:metrics:cpu", goredis.Z{ Score: float64(now), Member: cpuData, }); err != nil { @@ -232,7 +232,7 @@ func (s *Service) StoreSystemMetrics(ctx context.Context) error { } memData, _ := json.Marshal(metrics.Memory) - if err := s.redis.ZAdd(ctx, "system:metrics:memory", redis.Z{ + if err := s.redis.ZAdd(ctx, "system:metrics:memory", goredis.Z{ Score: float64(now), Member: memData, }); err != nil { diff --git a/src/module/task/api_types.go b/src/module/task/api_types.go index fc1a60cc..cdcbe1fd 100644 --- a/src/module/task/api_types.go +++ b/src/module/task/api_types.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "encoding/json" diff --git a/src/module/task/handler.go b/src/module/task/handler.go index 99c7c993..0b9bb0e0 100644 --- a/src/module/task/handler.go +++ b/src/module/task/handler.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "aegis/httpx" @@ -46,7 +46,7 @@ func NewHandler(service HandlerService) *Handler { // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks/batch-delete [post] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) BatchDelete(c *gin.Context) { var req BatchDeleteTaskReq if err := c.ShouldBindJSON(&req); err != nil { @@ -75,16 +75,16 @@ func (h *Handler) BatchDelete(c *gin.Context) { // @ID get_task_by_id // @Produce json // @Security BearerAuth -// @Param task_id path string true "Task ID" -// @Success 200 {object} dto.GenericResponse[TaskDetailResp] "Task retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid task ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Task not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param task_id path string true "Task ID" +// @Success 200 {object} dto.GenericResponse[TaskDetailResp] "Task retrieved successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid task ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Task not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks/{task_id} [get] -// @x-api-type {} -func (h *Handler) Get(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) GetTask(c *gin.Context) { taskID := c.Param(consts.URLPathTaskID) if !utils.IsValidUUID(taskID) { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid task ID") @@ -107,23 +107,23 @@ func (h *Handler) Get(c *gin.Context) { // @ID list_tasks // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param task_type query consts.TaskType false "Filter by task type" -// @Param immediate query bool false "Filter by immediate execution" -// @Param trace_id query string false "Filter by trace ID (uuid format)" -// @Param group_id query string false "Filter by group ID (uuid format)" -// @Param project_id query int false "Filter by project ID" -// @Param state query consts.TaskState false "Filter by state" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param task_type query consts.TaskType false "Filter by task type" +// @Param immediate query bool false "Filter by immediate execution" +// @Param trace_id query string false "Filter by trace ID (uuid format)" +// @Param group_id query string false "Filter by group ID (uuid format)" +// @Param project_id query int false "Filter by project ID" +// @Param state query consts.TaskState false "Filter by state" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[TaskResp]] "Tasks retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/tasks [get] -// @x-api-type {} -func (h *Handler) List(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) ListTasks(c *gin.Context) { var req ListTaskReq if err := c.ShouldBindQuery(&req); err != nil { dto.ErrorResponse(c, http.StatusBadRequest, "Invalid request format : "+err.Error()) @@ -150,15 +150,15 @@ func (h *Handler) List(c *gin.Context) { // @Description Process: 1. Validate Token -> 2. Push historical logs from Loki -> 3. Subscribe to Redis for real-time updates -> 4. Close on task completion. // @Tags Tasks // @ID get_task_logs_ws -// @Param task_id path string true "Task ID" -// @Param token query string true "JWT authentication token" +// @Param task_id path string true "Task ID" +// @Param token query string true "JWT authentication token" // @Success 101 {object} WSLogMessage "WebSocket connection established" // @Failure 400 "Invalid task ID" // @Failure 401 "Authentication failed" // @Failure 404 "Task not found" // @Router /api/v2/tasks/{task_id}/logs/ws [get] -// @x-api-type {} -func (h *Handler) LogsWS(c *gin.Context) { +// @x-api-type {"portal":"true"} +func (h *Handler) GetTaskLogsWS(c *gin.Context) { taskID := c.Param(consts.URLPathTaskID) if taskID == "" { dto.ErrorResponse(c, http.StatusBadRequest, "task_id is required") diff --git a/src/module/task/handler_service.go b/src/module/task/handler_service.go index 6bc034bc..383bbc91 100644 --- a/src/module/task/handler_service.go +++ b/src/module/task/handler_service.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "context" diff --git a/src/module/task/log_service.go b/src/module/task/log_service.go index 43013cde..94c86334 100644 --- a/src/module/task/log_service.go +++ b/src/module/task/log_service.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "context" diff --git a/src/module/task/log_types.go b/src/module/task/log_types.go index 3c643124..6a72ab59 100644 --- a/src/module/task/log_types.go +++ b/src/module/task/log_types.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "aegis/consts" diff --git a/src/module/task/loki_gateway.go b/src/module/task/loki_gateway.go index 20f5cea4..86914fa5 100644 --- a/src/module/task/loki_gateway.go +++ b/src/module/task/loki_gateway.go @@ -1,23 +1,23 @@ -package taskmodule +package task import ( "context" "time" "aegis/dto" - lokiinfra "aegis/infra/loki" + loki "aegis/infra/loki" ) type LokiGateway struct { - client *lokiinfra.Client + client *loki.Client } -func NewLokiGateway(client *lokiinfra.Client) *LokiGateway { +func NewLokiGateway(client *loki.Client) *LokiGateway { return &LokiGateway{client: client} } func (g *LokiGateway) QueryJobLogs(ctx context.Context, taskID string, start time.Time) ([]dto.LogEntry, error) { - return g.client.QueryJobLogs(ctx, taskID, lokiinfra.QueryOpts{ + return g.client.QueryJobLogs(ctx, taskID, loki.QueryOpts{ Start: start, Direction: "forward", }) diff --git a/src/module/task/module.go b/src/module/task/module.go index 0df7fe63..bc7dd39c 100644 --- a/src/module/task/module.go +++ b/src/module/task/module.go @@ -1,4 +1,4 @@ -package taskmodule +package task import "go.uber.org/fx" diff --git a/src/module/task/queue_store.go b/src/module/task/queue_store.go index f9ecc2b2..373f3a9d 100644 --- a/src/module/task/queue_store.go +++ b/src/module/task/queue_store.go @@ -1,11 +1,11 @@ -package taskmodule +package task import ( "context" "fmt" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) const jobLogsChannelPrefix = "joblogs" @@ -18,7 +18,7 @@ func NewTaskQueueStore(redis *redisinfra.Gateway) *TaskQueueStore { return &TaskQueueStore{redis: redis} } -func (s *TaskQueueStore) SubscribeJobLogs(ctx context.Context, taskID string) (*redis.PubSub, error) { +func (s *TaskQueueStore) SubscribeJobLogs(ctx context.Context, taskID string) (*goredis.PubSub, error) { channel := fmt.Sprintf("%s:%s", jobLogsChannelPrefix, taskID) return s.redis.Subscribe(ctx, channel) } diff --git a/src/module/task/repository.go b/src/module/task/repository.go index eb367b99..8621fd1d 100644 --- a/src/module/task/repository.go +++ b/src/module/task/repository.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "aegis/consts" diff --git a/src/module/task/service.go b/src/module/task/service.go index 24a5f45c..4612113e 100644 --- a/src/module/task/service.go +++ b/src/module/task/service.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "context" diff --git a/src/module/task/service_test.go b/src/module/task/service_test.go index 4db329ea..9d07f387 100644 --- a/src/module/task/service_test.go +++ b/src/module/task/service_test.go @@ -1,4 +1,4 @@ -package taskmodule +package task import ( "context" @@ -20,7 +20,7 @@ import ( "gorm.io/gorm" ) -func newTaskService(t *testing.T, loki *LokiGateway) (*Service, sqlmock.Sqlmock, func()) { +func newTaskService(t *testing.T, gateway *LokiGateway) (*Service, sqlmock.Sqlmock, func()) { t.Helper() sqlDB, mock, err := sqlmock.New() @@ -32,11 +32,11 @@ func newTaskService(t *testing.T, loki *LokiGateway) (*Service, sqlmock.Sqlmock, }), &gorm.Config{}) require.NoError(t, err) - if loki == nil { - loki = NewLokiGateway(&lokiinfra.Client{}) + if gateway == nil { + gateway = NewLokiGateway(&lokiinfra.Client{}) } - service := NewService(NewRepository(db), NewTaskLogService(NewRepository(db), nil, loki), loki) + service := NewService(NewRepository(db), NewTaskLogService(NewRepository(db), nil, gateway), gateway) return service, mock, func() { _ = sqlDB.Close() } @@ -92,8 +92,8 @@ func TestTaskServiceQueryHistoricalLogsSuccess(t *testing.T) { viper.Set("loki.address", server.URL) viper.Set("loki.max_entries", 100) - loki := NewLokiGateway(lokiinfra.NewClient()) - service, _, cleanup := newTaskService(t, loki) + gateway := NewLokiGateway(lokiinfra.NewClient()) + service, _, cleanup := newTaskService(t, gateway) defer cleanup() logs := service.queryHistoricalLogs(context.Background(), &model.Task{ diff --git a/src/module/team/api_types.go b/src/module/team/api_types.go index d7f46605..644ca405 100644 --- a/src/module/team/api_types.go +++ b/src/module/team/api_types.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "fmt" @@ -8,11 +8,11 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - projectmodule "aegis/module/project" + project "aegis/module/project" ) -type TeamProjectListReq = projectmodule.ListProjectReq -type TeamProjectItem = projectmodule.ProjectResp +type TeamProjectListReq = project.ListProjectReq +type TeamProjectItem = project.ProjectResp // CreateTeamReq represents team creation request. type CreateTeamReq struct { diff --git a/src/module/team/handler.go b/src/module/team/handler.go index 536346e7..87861c3c 100644 --- a/src/module/team/handler.go +++ b/src/module/team/handler.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "aegis/httpx" @@ -29,13 +29,13 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateTeamReq true "Team creation request" -// @Success 201 {object} dto.GenericResponse[TeamResp] "Team created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 409 {object} dto.GenericResponse[any] "Team already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body CreateTeamReq true "Team creation request" +// @Success 201 {object} dto.GenericResponse[TeamResp] "Team created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 409 {object} dto.GenericResponse[any] "Team already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams [post] // @x-api-type {"portal":"true"} func (h *Handler) CreateTeam(c *gin.Context) { @@ -96,13 +96,13 @@ func (h *Handler) DeleteTeam(c *gin.Context) { // @ID get_team_by_id // @Produce json // @Security BearerAuth -// @Param team_id path int true "Team ID" +// @Param team_id path int true "Team ID" // @Success 200 {object} dto.GenericResponse[TeamDetailResp] "Team retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Team not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Team not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id} [get] // @x-api-type {"portal":"true"} func (h *Handler) GetTeamDetail(c *gin.Context) { @@ -125,15 +125,15 @@ func (h *Handler) GetTeamDetail(c *gin.Context) { // @ID list_teams // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param is_public query bool false "Filter by public status" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param is_public query bool false "Filter by public status" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[TeamResp]] "Teams retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams [get] // @x-api-type {"portal":"true"} func (h *Handler) ListTeams(c *gin.Context) { @@ -167,14 +167,14 @@ func (h *Handler) ListTeams(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param team_id path int true "Team ID" -// @Param request body UpdateTeamReq true "Team update request" -// @Success 202 {object} dto.GenericResponse[TeamResp] "Team updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Team not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param team_id path int true "Team ID" +// @Param request body UpdateTeamReq true "Team update request" +// @Success 202 {object} dto.GenericResponse[TeamResp] "Team updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Team not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id} [patch] // @x-api-type {"portal":"true"} func (h *Handler) UpdateTeam(c *gin.Context) { @@ -379,15 +379,15 @@ func (h *Handler) UpdateTeamMemberRole(c *gin.Context) { // @ID list_team_members // @Produce json // @Security BearerAuth -// @Param team_id path int true "Team ID" -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) +// @Param team_id path int true "Team ID" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) // @Success 200 {object} dto.GenericResponse[dto.ListResp[TeamMemberResp]] "Members retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or request parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Team not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid team ID or request parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Team not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/teams/{team_id}/members [get] // @x-api-type {"portal":"true"} func (h *Handler) ListTeamMembers(c *gin.Context) { diff --git a/src/module/team/handler_service.go b/src/module/team/handler_service.go index 88882617..cbfc3d22 100644 --- a/src/module/team/handler_service.go +++ b/src/module/team/handler_service.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "context" diff --git a/src/module/team/module.go b/src/module/team/module.go index 77874c6d..af60d319 100644 --- a/src/module/team/module.go +++ b/src/module/team/module.go @@ -1,4 +1,4 @@ -package teammodule +package team import "go.uber.org/fx" diff --git a/src/module/team/project_reader.go b/src/module/team/project_reader.go index abea64db..7d822749 100644 --- a/src/module/team/project_reader.go +++ b/src/module/team/project_reader.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "context" @@ -8,7 +8,7 @@ import ( "aegis/dto" "aegis/internalclient/resourceclient" "aegis/model" - projectmodule "aegis/module/project" + project "aegis/module/project" "go.uber.org/fx" ) @@ -49,7 +49,7 @@ func newRemoteProjectReader(params projectReaderParams) projectReader { func (r projectReaderAdapter) CountProjects(ctx context.Context, teamID int) (int, error) { if r.resource != nil && r.resource.Enabled() { includeStatistics := false - resp, err := r.resource.ListProjects(ctx, &projectmodule.ListProjectReq{ + resp, err := r.resource.ListProjects(ctx, &project.ListProjectReq{ PaginationReq: dto.PaginationReq{Page: 1, Size: 10}, TeamID: &teamID, IncludeStatistics: &includeStatistics, @@ -110,7 +110,7 @@ func (r projectReaderAdapter) ListProjects(ctx context.Context, req *TeamProject items := make([]TeamProjectItem, 0, len(projects)) for i := range projects { - items = append(items, *projectmodule.NewProjectResp(&projects[i], statsMap[projects[i].ID])) + items = append(items, *project.NewProjectResp(&projects[i], statsMap[projects[i].ID])) } return &dto.ListResp[TeamProjectItem]{ diff --git a/src/module/team/repository.go b/src/module/team/repository.go index 67c5ebcf..2ad0a1bf 100644 --- a/src/module/team/repository.go +++ b/src/module/team/repository.go @@ -1,10 +1,10 @@ -package teammodule +package team import ( "aegis/consts" "aegis/dto" "aegis/model" - projectmodule "aegis/module/project" + project "aegis/module/project" "errors" "fmt" @@ -131,7 +131,7 @@ func (r *Repository) listTeamProjectViews(teamID, limit, offset int, isPublic *b projectIDs = append(projectIDs, project.ID) } - statsMap, err := projectmodule.NewRepository(r.db).ListProjectStatistics(projectIDs) + statsMap, err := project.NewRepository(r.db).ListProjectStatistics(projectIDs) if err != nil { return nil, nil, 0, err } diff --git a/src/module/team/service.go b/src/module/team/service.go index 276bf733..27ae902d 100644 --- a/src/module/team/service.go +++ b/src/module/team/service.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "context" diff --git a/src/module/team/service_test.go b/src/module/team/service_test.go index 46653f9c..d7dbadf3 100644 --- a/src/module/team/service_test.go +++ b/src/module/team/service_test.go @@ -1,4 +1,4 @@ -package teammodule +package team import ( "context" diff --git a/src/module/trace/api_types.go b/src/module/trace/api_types.go index 5fae70aa..3069f466 100644 --- a/src/module/trace/api_types.go +++ b/src/module/trace/api_types.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "fmt" @@ -7,7 +7,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - taskmodule "aegis/module/task" + task "aegis/module/task" "aegis/utils" ) @@ -127,16 +127,16 @@ func NewTraceResp(trace *model.Trace) *TraceResp { type TraceDetailResp struct { TraceResp - Tasks []taskmodule.TaskResp `json:"tasks"` + Tasks []task.TaskResp `json:"tasks"` } func NewTraceDetailResp(trace *model.Trace) *TraceDetailResp { resp := &TraceDetailResp{ TraceResp: *NewTraceResp(trace), - Tasks: make([]taskmodule.TaskResp, 0, len(trace.Tasks)), + Tasks: make([]task.TaskResp, 0, len(trace.Tasks)), } for i := range trace.Tasks { - resp.Tasks = append(resp.Tasks, *taskmodule.NewTaskResp(&trace.Tasks[i])) + resp.Tasks = append(resp.Tasks, *task.NewTaskResp(&trace.Tasks[i])) } return resp } diff --git a/src/module/trace/handler.go b/src/module/trace/handler.go index 3be294b6..b7ba62ed 100644 --- a/src/module/trace/handler.go +++ b/src/module/trace/handler.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "aegis/httpx" @@ -34,15 +34,15 @@ func NewHandler(service HandlerService) *Handler { // @ID get_trace_by_id // @Produce json // @Security BearerAuth -// @Param trace_id path string true "Trace ID" +// @Param trace_id path string true "Trace ID" // @Success 200 {object} dto.GenericResponse[TraceDetailResp] "Trace retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid trace ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "Trace not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid trace ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "Trace not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces/{trace_id} [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) GetTrace(c *gin.Context) { traceID := c.Param(consts.URLPathTraceID) if !utils.IsValidUUID(traceID) { @@ -66,20 +66,20 @@ func (h *Handler) GetTrace(c *gin.Context) { // @ID list_traces // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param trace_type query consts.TraceType false "Filter by trace type" -// @Param group_id query string false "Filter by group ID (uuid format)" -// @Param project_id query int false "Filter by project ID" -// @Param state query consts.TraceState false "Filter by state" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param trace_type query consts.TraceType false "Filter by trace type" +// @Param group_id query string false "Filter by group ID (uuid format)" +// @Param project_id query int false "Filter by project ID" +// @Param state query consts.TraceState false "Filter by state" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[TraceResp]] "Traces retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces [get] -// @x-api-type {} +// @x-api-type {"portal":"true"} func (h *Handler) ListTraces(c *gin.Context) { var req ListTraceReq if err := c.ShouldBindQuery(&req); err != nil { @@ -116,8 +116,8 @@ func (h *Handler) ListTraces(c *gin.Context) { // @Failure 403 {object} dto.GenericResponse[any] "Permission denied" // @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/traces/{trace_id}/stream [get] -// @x-api-type {} // @x-request-type {"stream":"true"} +// @x-api-type {"portal":"true"} func (h *Handler) GetTraceStream(c *gin.Context) { traceID := c.Param(consts.URLPathTraceID) if !utils.IsValidUUID(traceID) { diff --git a/src/module/trace/handler_service.go b/src/module/trace/handler_service.go index 9fa133a2..998d7edc 100644 --- a/src/module/trace/handler_service.go +++ b/src/module/trace/handler_service.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "context" diff --git a/src/module/trace/module.go b/src/module/trace/module.go index 09d32c85..0bfbfad5 100644 --- a/src/module/trace/module.go +++ b/src/module/trace/module.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import "go.uber.org/fx" diff --git a/src/module/trace/repository.go b/src/module/trace/repository.go index 06fb739f..4e32c047 100644 --- a/src/module/trace/repository.go +++ b/src/module/trace/repository.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "aegis/consts" diff --git a/src/module/trace/service.go b/src/module/trace/service.go index f072f0a7..6af2c1d3 100644 --- a/src/module/trace/service.go +++ b/src/module/trace/service.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "context" @@ -10,7 +10,7 @@ import ( "aegis/dto" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type Service struct { @@ -84,7 +84,7 @@ func (s *Service) GetTraceStreamAlgorithms(ctx context.Context, traceID string) return filtered, nil } -func (s *Service) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]redis.XStream, error) { +func (s *Service) ReadTraceStreamMessages(ctx context.Context, streamKey, lastID string, count int64, block time.Duration) ([]goredis.XStream, error) { if lastID == "" { lastID = "0" } diff --git a/src/module/trace/stream.go b/src/module/trace/stream.go index b6b23114..c4ab77f3 100644 --- a/src/module/trace/stream.go +++ b/src/module/trace/stream.go @@ -1,4 +1,4 @@ -package tracemodule +package trace import ( "encoding/json" diff --git a/src/module/user/api_types.go b/src/module/user/api_types.go index 4352014d..d205d1cc 100644 --- a/src/module/user/api_types.go +++ b/src/module/user/api_types.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "fmt" @@ -8,7 +8,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - rbacmodule "aegis/module/rbac" + rbac "aegis/module/rbac" ) // CreateUserReq represents user creation request. @@ -121,11 +121,11 @@ func NewUserResp(user *model.User) *UserResp { type UserDetailResp struct { UserResp - GlobalRoles []rbacmodule.RoleResp `json:"global_roles,omitempty"` - Permissions []rbacmodule.PermissionResp `json:"permissions,omitempty"` - ContainerRoles []UserContainerInfo `json:"container_roles,omitempty"` - DatasetRoles []UserDatasetInfo `json:"dataset_roles,omitempty"` - ProjectRoles []UserProjectInfo `json:"project_roles,omitempty"` + GlobalRoles []rbac.RoleResp `json:"global_roles,omitempty"` + Permissions []rbac.PermissionResp `json:"permissions,omitempty"` + ContainerRoles []UserContainerInfo `json:"container_roles,omitempty"` + DatasetRoles []UserDatasetInfo `json:"dataset_roles,omitempty"` + ProjectRoles []UserProjectInfo `json:"project_roles,omitempty"` } func NewUserDetailResp(user *model.User) *UserDetailResp { diff --git a/src/module/user/handler.go b/src/module/user/handler.go index 13742a47..1f4379e3 100644 --- a/src/module/user/handler.go +++ b/src/module/user/handler.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "aegis/httpx" @@ -28,11 +28,11 @@ func NewHandler(service HandlerService) *Handler { // @Accept json // @Produce json // @Security BearerAuth -// @Param request body CreateUserReq true "User creation request" -// @Success 201 {object} dto.GenericResponse[UserResp] "User created successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 409 {object} dto.GenericResponse[any] "User already exists" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param request body CreateUserReq true "User creation request" +// @Success 201 {object} dto.GenericResponse[UserResp] "User created successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 409 {object} dto.GenericResponse[any] "User already exists" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users [post] // @x-api-type {"admin":"true"} func (h *Handler) CreateUser(c *gin.Context) { @@ -91,13 +91,13 @@ func (h *Handler) DeleteUser(c *gin.Context) { // @ID get_user_by_id // @Produce json // @Security BearerAuth -// @Param id path int true "User ID" +// @Param id path int true "User ID" // @Success 200 {object} dto.GenericResponse[UserDetailResp] "User retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "User not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "User not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{id}/detail [get] // @x-api-type {"admin":"true"} func (h *Handler) GetUserDetail(c *gin.Context) { @@ -120,17 +120,17 @@ func (h *Handler) GetUserDetail(c *gin.Context) { // @ID list_users // @Produce json // @Security BearerAuth -// @Param page query int false "Page number" default(1) -// @Param size query int false "Page size" default(20) -// @Param username query string false "Filter by username" -// @Param email query string false "Filter by email" -// @Param is_active query bool false "Filter by active status" -// @Param status query consts.StatusType false "Filter by status" +// @Param page query int false "Page number" default(1) +// @Param size query int false "Page size" default(20) +// @Param username query string false "Filter by username" +// @Param email query string false "Filter by email" +// @Param is_active query bool false "Filter by active status" +// @Param status query consts.StatusType false "Filter by status" // @Success 200 {object} dto.GenericResponse[dto.ListResp[UserResp]] "Users retrieved successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid request format or parameters" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users [get] // @x-api-type {"admin":"true"} func (h *Handler) ListUsers(c *gin.Context) { @@ -159,14 +159,14 @@ func (h *Handler) ListUsers(c *gin.Context) { // @Accept json // @Produce json // @Security BearerAuth -// @Param id path int true "User ID" -// @Param request body UpdateUserReq true "User update request" -// @Success 202 {object} dto.GenericResponse[UserResp] "User updated successfully" -// @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID/request" -// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" -// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" -// @Failure 404 {object} dto.GenericResponse[any] "User not found" -// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" +// @Param id path int true "User ID" +// @Param request body UpdateUserReq true "User update request" +// @Success 202 {object} dto.GenericResponse[UserResp] "User updated successfully" +// @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID/request" +// @Failure 401 {object} dto.GenericResponse[any] "Authentication required" +// @Failure 403 {object} dto.GenericResponse[any] "Permission denied" +// @Failure 404 {object} dto.GenericResponse[any] "User not found" +// @Failure 500 {object} dto.GenericResponse[any] "Internal server error" // @Router /api/v2/users/{id} [patch] // @x-api-type {"admin":"true"} func (h *Handler) UpdateUser(c *gin.Context) { @@ -254,7 +254,7 @@ func (h *Handler) RemoveRole(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param user_id path int true "User ID" -// @Param request body AssignUserPermissionReq true "User permission assignment request" +// @Param request body AssignUserPermissionReq true "User permission assignment request" // @Success 200 {object} dto.GenericResponse[any] "Permission assigned successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user ID or invalid request format or parameters" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" @@ -293,7 +293,7 @@ func (h *Handler) AssignPermissions(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param user_id path int true "User ID" -// @Param request body RemoveUserPermissionReq true "User permission removal request" +// @Param request body RemoveUserPermissionReq true "User permission removal request" // @Success 200 {object} dto.GenericResponse[any] "Permission removed successfully" // @Failure 400 {object} dto.GenericResponse[any] "Invalid user or permission ID" // @Failure 401 {object} dto.GenericResponse[any] "Authentication required" diff --git a/src/module/user/handler_service.go b/src/module/user/handler_service.go index 34fcb4d0..484929a3 100644 --- a/src/module/user/handler_service.go +++ b/src/module/user/handler_service.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "context" diff --git a/src/module/user/module.go b/src/module/user/module.go index 08c04c8d..2b55f7a8 100644 --- a/src/module/user/module.go +++ b/src/module/user/module.go @@ -1,4 +1,4 @@ -package usermodule +package user import "go.uber.org/fx" diff --git a/src/module/user/repository.go b/src/module/user/repository.go index 518cb59b..2af653b0 100644 --- a/src/module/user/repository.go +++ b/src/module/user/repository.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "aegis/consts" diff --git a/src/module/user/service.go b/src/module/user/service.go index b187a2af..cf6565b7 100644 --- a/src/module/user/service.go +++ b/src/module/user/service.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "context" @@ -8,7 +8,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - rbacmodule "aegis/module/rbac" + rbac "aegis/module/rbac" "gorm.io/gorm" ) @@ -83,14 +83,14 @@ func (s *Service) GetUserDetail(_ context.Context, userID int) (*UserDetailResp, if err != nil { return nil, fmt.Errorf("failed to get user detail relations: %w", err) } - resp.GlobalRoles = make([]rbacmodule.RoleResp, len(globalRoles)) + resp.GlobalRoles = make([]rbac.RoleResp, len(globalRoles)) for i, role := range globalRoles { - resp.GlobalRoles[i] = *rbacmodule.NewRoleResp(&role) + resp.GlobalRoles[i] = *rbac.NewRoleResp(&role) } - resp.Permissions = make([]rbacmodule.PermissionResp, len(permissions)) + resp.Permissions = make([]rbac.PermissionResp, len(permissions)) for i, permission := range permissions { - resp.Permissions[i] = *rbacmodule.NewPermissionResp(&permission) + resp.Permissions[i] = *rbac.NewPermissionResp(&permission) } containerRoles, datasetRoles, projectRoles := buildUserResourceRoles(userContainers, userDatasets, userProjects) diff --git a/src/module/user/service_test.go b/src/module/user/service_test.go index 44f7bde6..b25ed074 100644 --- a/src/module/user/service_test.go +++ b/src/module/user/service_test.go @@ -1,4 +1,4 @@ -package usermodule +package user import ( "database/sql/driver" diff --git a/src/router/admin.go b/src/router/admin.go index 28eee692..85c45d8a 100644 --- a/src/router/admin.go +++ b/src/router/admin.go @@ -121,4 +121,38 @@ func SetupAdminV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { systems.DELETE("/:id", middleware.RequirePermission(consts.PermSystemManage), handlers.ChaosSystem.DeleteSystem) } + + system := v2.Group("/system", middleware.JWTAuth(), middleware.RequireSystemRead) + { + system.GET("/metrics", handlers.SystemMetric.GetSystemMetrics) + system.GET("/metrics/history", handlers.SystemMetric.GetSystemMetricsHistory) + audit := system.Group("/audit", middleware.RequireAuditRead) + { + audit.GET("", handlers.System.ListAuditLogs) + audit.GET("/:id", handlers.System.GetAuditLog) + } + + configs := system.Group("/configs") + { + configsRead := configs.Group("", middleware.RequireConfigurationRead) + { + configsRead.GET("", handlers.System.ListConfigs) + configsRead.GET("/:config_id", handlers.System.GetConfig) + configsRead.GET("/:config_id/histories", handlers.System.ListConfigHistories) + } + + configs.PATCH("/:config_id", middleware.RequireConfigurationUpdate, handlers.System.UpdateConfigValue) + configs.POST("/:config_id/value/rollback", middleware.RequireConfigurationUpdate, handlers.System.RollbackConfigValue) + configs.PUT("/:config_id/metadata", middleware.RequireConfigurationConfigure, handlers.System.UpdateConfigMetadata) + configs.POST("/:config_id/metadata/rollback", middleware.RequireConfigurationConfigure, handlers.System.RollbackConfigMetadata) + } + + system.GET("/health", handlers.System.GetHealth) + + monitor := system.Group("/monitor") + monitor.POST("/metrics", handlers.System.GetMetrics) + monitor.GET("/info", handlers.System.GetSystemInfo) + monitor.GET("/namespaces/locks", handlers.System.ListNamespaceLocks) + monitor.GET("/tasks/queue", handlers.System.ListQueuedTasks) + } } diff --git a/src/router/handlers.go b/src/router/handlers.go index 15ae5320..6e299756 100644 --- a/src/router/handlers.go +++ b/src/router/handlers.go @@ -1,72 +1,72 @@ package router import ( - authmodule "aegis/module/auth" - chaossystemmodule "aegis/module/chaossystem" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - evaluationmodule "aegis/module/evaluation" - executionmodule "aegis/module/execution" - groupmodule "aegis/module/group" - injectionmodule "aegis/module/injection" - labelmodule "aegis/module/label" - metricmodule "aegis/module/metric" - notificationmodule "aegis/module/notification" - projectmodule "aegis/module/project" - rbacmodule "aegis/module/rbac" - sdkmodule "aegis/module/sdk" - systemmodule "aegis/module/system" - systemmetricmodule "aegis/module/systemmetric" - taskmodule "aegis/module/task" - teammodule "aegis/module/team" - tracemodule "aegis/module/trace" - usermodule "aegis/module/user" + auth "aegis/module/auth" + chaossystem "aegis/module/chaossystem" + container "aegis/module/container" + dataset "aegis/module/dataset" + evaluation "aegis/module/evaluation" + execution "aegis/module/execution" + group "aegis/module/group" + injection "aegis/module/injection" + label "aegis/module/label" + metric "aegis/module/metric" + notification "aegis/module/notification" + project "aegis/module/project" + rbac "aegis/module/rbac" + sdk "aegis/module/sdk" + system "aegis/module/system" + systemmetric "aegis/module/systemmetric" + task "aegis/module/task" + team "aegis/module/team" + trace "aegis/module/trace" + user "aegis/module/user" ) type Handlers struct { - Auth *authmodule.Handler - Project *projectmodule.Handler - Task *taskmodule.Handler - Injection *injectionmodule.Handler - Execution *executionmodule.Handler - Container *containermodule.Handler - Dataset *datasetmodule.Handler - Evaluation *evaluationmodule.Handler - Trace *tracemodule.Handler - Group *groupmodule.Handler - Metric *metricmodule.Handler - User *usermodule.Handler - RBAC *rbacmodule.Handler - SDK *sdkmodule.Handler - System *systemmodule.Handler - Notification *notificationmodule.Handler - ChaosSystem *chaossystemmodule.Handler - Team *teammodule.Handler - Label *labelmodule.Handler - SystemMetric *systemmetricmodule.Handler + Auth *auth.Handler + Project *project.Handler + Task *task.Handler + Injection *injection.Handler + Execution *execution.Handler + Container *container.Handler + Dataset *dataset.Handler + Evaluation *evaluation.Handler + Trace *trace.Handler + Group *group.Handler + Metric *metric.Handler + User *user.Handler + RBAC *rbac.Handler + SDK *sdk.Handler + System *system.Handler + Notification *notification.Handler + ChaosSystem *chaossystem.Handler + Team *team.Handler + Label *label.Handler + SystemMetric *systemmetric.Handler } func NewHandlers( - auth *authmodule.Handler, - project *projectmodule.Handler, - task *taskmodule.Handler, - injection *injectionmodule.Handler, - execution *executionmodule.Handler, - container *containermodule.Handler, - dataset *datasetmodule.Handler, - evaluation *evaluationmodule.Handler, - trace *tracemodule.Handler, - group *groupmodule.Handler, - metric *metricmodule.Handler, - user *usermodule.Handler, - rbac *rbacmodule.Handler, - sdk *sdkmodule.Handler, - system *systemmodule.Handler, - notification *notificationmodule.Handler, - chaosSystem *chaossystemmodule.Handler, - team *teammodule.Handler, - label *labelmodule.Handler, - systemMetric *systemmetricmodule.Handler, + auth *auth.Handler, + project *project.Handler, + task *task.Handler, + injection *injection.Handler, + execution *execution.Handler, + container *container.Handler, + dataset *dataset.Handler, + evaluation *evaluation.Handler, + trace *trace.Handler, + group *group.Handler, + metric *metric.Handler, + user *user.Handler, + rbac *rbac.Handler, + sdk *sdk.Handler, + system *system.Handler, + notification *notification.Handler, + chaosSystem *chaossystem.Handler, + team *team.Handler, + label *label.Handler, + systemMetric *systemmetric.Handler, ) *Handlers { return &Handlers{ Auth: auth, diff --git a/src/router/portal.go b/src/router/portal.go index 83df8396..8a23bc85 100644 --- a/src/router/portal.go +++ b/src/router/portal.go @@ -7,41 +7,67 @@ import ( ) func SetupPortalV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { - projects := v2.Group("/projects", middleware.JWTAuth()) + containers := v2.Group("/containers", middleware.JWTAuth()) { - injections := projects.Group("/:project_id/injections") + containerRead := containers.Group("", middleware.RequireContainerRead) { - injectionRead := injections.Group("", middleware.RequireProjectRead) - { - analysis := injectionRead.Group("/analysis") - { - analysis.GET("/no-issues", handlers.Injection.ListProjectFaultInjectionNoIssues) - analysis.GET("/with-issues", handlers.Injection.ListProjectFaultInjectionWithIssues) - } - - injectionRead.GET("", handlers.Injection.ListProjectInjections) - injectionRead.POST("/search", handlers.Injection.SearchProjectInjections) - } + containerRead.GET("", handlers.Container.ListContainers) + containerRead.GET("/:container_id", handlers.Container.GetContainer) + } + + containers.POST("", middleware.RequireContainerCreate, handlers.Container.CreateContainer) + containers.PATCH("/:container_id", middleware.RequireContainerUpdate, handlers.Container.UpdateContainer) + containers.PATCH("/:container_id/labels", middleware.RequireContainerUpdate, handlers.Container.ManageContainerCustomLabels) + containers.DELETE("/:container_id", middleware.RequireContainerDelete, handlers.Container.DeleteContainer) + containers.POST("/build", middleware.RequireContainerExecute, handlers.Container.SubmitContainerBuilding) - injectionExecute := injections.Group("", middleware.RequireProjectInjectionExecute) + containerVersions := containers.Group("/:container_id/versions") + { + containerVersionRead := containerVersions.Group("", middleware.RequireContainerVersionRead) { - injectionExecute.POST("/inject", handlers.Injection.SubmitProjectFaultInjection) - injectionExecute.POST("/build", handlers.Injection.SubmitProjectDatapackBuilding) + containerVersionRead.GET("", handlers.Container.ListContainerVersions) + containerVersionRead.GET("/:version_id", handlers.Container.GetContainerVersion) } + + containerVersions.POST("", middleware.RequireContainerVersionCreate, handlers.Container.CreateContainerVersion) + containerVersions.PATCH("/:version_id", middleware.RequireContainerVersionUpdate, handlers.Container.UpdateContainerVersion) + containerVersions.DELETE("/:version_id", middleware.RequireContainerVersionDelete, handlers.Container.DeleteContainerVersion) + containerVersions.POST("/:version_id/helm-chart", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmChart) + containerVersions.POST("/:version_id/helm-values", middleware.RequireContainerVersionUpload, handlers.Container.UploadHelmValueFile) } + } - executions := projects.Group("/:project_id/executions") + datasets := v2.Group("/datasets", middleware.JWTAuth()) + { + datasetRead := datasets.Group("", middleware.RequireDatasetRead) { - executionRead := executions.Group("", middleware.RequireProjectRead) - { - executionRead.GET("", handlers.Execution.ListProjectExecutions) - } + datasetRead.GET("", handlers.Dataset.ListDatasets) + datasetRead.GET("/:dataset_id", handlers.Dataset.GetDataset) + datasetRead.POST("/search", handlers.Dataset.SearchDataset) + } + + datasets.POST("", middleware.RequireDatasetCreate, handlers.Dataset.CreateDataset) + datasets.PATCH("/:dataset_id", middleware.RequireDatasetUpdate, handlers.Dataset.UpdateDataset) + datasets.PATCH("/:dataset_id/labels", middleware.RequireDatasetUpdate, handlers.Dataset.ManageDatasetCustomLabels) + datasets.DELETE("/:dataset_id", middleware.RequireDatasetDelete, handlers.Dataset.DeleteDataset) - executionExecute := executions.Group("", middleware.RequireProjectExecutionExecute) + datasetVersions := datasets.Group("/:dataset_id/versions") + { + datasetVersionRead := datasetVersions.Group("", middleware.RequireDatasetVersionRead) { - executionExecute.POST("/execute", handlers.Execution.SubmitAlgorithmExecution) + datasetVersionRead.GET("", handlers.Dataset.ListDatasetVersions) + datasetVersionRead.GET("/:version_id", handlers.Dataset.GetDatasetVersion) } + + datasetVersions.POST("", middleware.RequireDatasetVersionCreate, handlers.Dataset.CreateDatasetVersion) + datasetVersions.PATCH("/:version_id", middleware.RequireDatasetVersionUpdate, handlers.Dataset.UpdateDatasetVersion) + datasetVersions.DELETE("/:version_id", middleware.RequireDatasetVersionDelete, handlers.Dataset.DeleteDatasetVersion) } + } + + projects := v2.Group("/projects", middleware.JWTAuth()) + { + projects.POST("/:project_id/injections/search", middleware.RequireProjectRead, handlers.Injection.SearchProjectInjections) projectRead := projects.Group("", middleware.RequireProjectRead) { @@ -93,6 +119,55 @@ func SetupPortalV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { labels.POST("/batch-delete", middleware.RequireLabelDelete, handlers.Label.BatchDeleteLabels) } + evaluations := v2.Group("/evaluations", middleware.JWTAuth()) + { + evaluations.DELETE("/:id", handlers.Evaluation.DeleteEvaluation) + } + + executions := v2.Group("/executions", middleware.JWTAuth()) + { + executions.GET("/labels", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:executions:*", "sdk:executions:read"), handlers.Execution.ListAvailableExecutionLabels) + executions.POST("/batch-delete", handlers.Execution.BatchDeleteExecutions) + } + + injections := v2.Group("/injections", middleware.JWTAuth()) + { + injections.PATCH("/labels/batch", handlers.Injection.BatchManageInjectionLabels) + injections.POST("/batch-delete", handlers.Injection.BatchDeleteInjections) + injections.POST("/upload", handlers.Injection.UploadDatapack) + injections.PUT("/:id/groundtruth", handlers.Injection.UpdateGroundtruth) + } + + notifications := v2.Group("/notifications", middleware.JWTAuth()) + { + notifications.GET("/stream", handlers.Notification.GetStream) + } + + tasks := v2.Group("/tasks", middleware.JWTAuth()) + { + taskRead := tasks.Group("", middleware.RequireTaskRead) + { + taskRead.GET("", handlers.Task.ListTasks) + taskRead.GET("/:task_id", handlers.Task.GetTask) + taskRead.GET("/:task_id/logs/ws", handlers.Task.GetTaskLogsWS) + } + + tasks.POST("/batch-delete", middleware.RequireTaskDelete, handlers.Task.BatchDelete) + } + + groups := v2.Group("/groups", middleware.JWTAuth(), middleware.RequireTraceRead) + { + groups.GET("/:group_id/stats", handlers.Group.GetGroupStats) + groups.GET("/:group_id/stream", handlers.Group.GetGroupStream) + } + + traces := v2.Group("/traces", middleware.JWTAuth(), middleware.RequireTraceRead) + { + traces.GET("", handlers.Trace.ListTraces) + traces.GET("/:trace_id", handlers.Trace.GetTrace) + traces.GET("/:trace_id/stream", handlers.Trace.GetTraceStream) + } + accessKeys := v2.Group("/api-keys", middleware.JWTAuth(), middleware.RequireHumanUserAuth()) { accessKeys.GET("", handlers.Auth.ListAPIKeys) diff --git a/src/router/public.go b/src/router/public.go index 499e80c4..bcbdd123 100644 --- a/src/router/public.go +++ b/src/router/public.go @@ -12,7 +12,6 @@ func SetupPublicV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { auth.POST("/login", handlers.Auth.Login) // User login auth.POST("/register", handlers.Auth.Register) // User registration auth.POST("/refresh", handlers.Auth.RefreshToken) // Token refresh - auth.POST("/api-key/token", handlers.Auth.ExchangeAPIKeyToken) // These require authentication authProtected := auth.Group("", middleware.JWTAuth(), middleware.RequireHumanUserAuth()) diff --git a/src/router/router.go b/src/router/router.go index caf77df4..f7f20bea 100644 --- a/src/router/router.go +++ b/src/router/router.go @@ -10,12 +10,8 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -func New(handlers *Handlers, services ...middleware.Service) *gin.Engine { +func New(handlers *Handlers, middlewareService middleware.Service) *gin.Engine { router := gin.Default() - var middlewareService middleware.Service - if len(services) > 0 { - middlewareService = services[0] - } // CORS configuration config := cors.DefaultConfig() @@ -40,11 +36,8 @@ func New(handlers *Handlers, services ...middleware.Service) *gin.Engine { v2 := router.Group("/api/v2") SetupPublicV2Routes(v2, handlers) SetupSDKV2Routes(v2, handlers) - SetupRuntimeV2Routes(v2, handlers) SetupAdminV2Routes(v2, handlers) SetupPortalV2Routes(v2, handlers) - SetupSystemV2Routes(v2, handlers) - SetupSystemRoutes(router, handlers) // Swagger documentation router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) diff --git a/src/router/router_test.go b/src/router/router_test.go index b1470083..f4692910 100644 --- a/src/router/router_test.go +++ b/src/router/router_test.go @@ -10,7 +10,7 @@ import ( ) func TestRouterSeparatesRouteGroups(t *testing.T) { - engine := New(&Handlers{}) + engine := New(&Handlers{}, nil) routes := engine.Routes() requiredPrefixes := []string{ @@ -19,10 +19,10 @@ func TestRouterSeparatesRouteGroups(t *testing.T) { "/api/v2/executions", "/api/v2/users", "/api/v2/sdk", - "/system/audit", - "/system/configs", - "/system/monitor", - "/system/health", + "/api/v2/system/audit", + "/api/v2/system/configs", + "/api/v2/system/monitor", + "/api/v2/system/health", "/docs/", } @@ -43,7 +43,7 @@ func hasRoutePrefix(routes []gin.RouteInfo, prefix string) bool { } func TestSwaggerDocEndpointServesRegisteredSpec(t *testing.T) { - engine := New(&Handlers{}) + engine := New(&Handlers{}, nil) req := httptest.NewRequest(http.MethodGet, "/docs/doc.json", nil) w := httptest.NewRecorder() diff --git a/src/router/runtime.go b/src/router/runtime.go deleted file mode 100644 index c88d7a90..00000000 --- a/src/router/runtime.go +++ /dev/null @@ -1,15 +0,0 @@ -package router - -import ( - "aegis/middleware" - - "github.com/gin-gonic/gin" -) - -func SetupRuntimeV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { - runtime := v2.Group("/executions", middleware.JWTAuth(), middleware.RequireServiceTokenAuth()) - { - runtime.POST("/:execution_id/detector_results", handlers.Execution.UploadDetectorResults) - runtime.POST("/:execution_id/granularity_results", handlers.Execution.UploadGranularityResults) - } -} diff --git a/src/router/sdk.go b/src/router/sdk.go index e8e8a6da..df3c8769 100644 --- a/src/router/sdk.go +++ b/src/router/sdk.go @@ -7,6 +7,11 @@ import ( ) func SetupSDKV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { + auth := v2.Group("/auth") + { + auth.POST("/api-key/token", handlers.Auth.ExchangeAPIKeyToken) + } + sdkEval := v2.Group("/sdk/evaluations", middleware.JWTAuth(), middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read")) { sdkEval.GET("", handlers.SDK.ListEvaluations) @@ -18,4 +23,89 @@ func SetupSDKV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { { sdkData.GET("", handlers.SDK.ListDatasetSamples) } + + datasets := v2.Group("/datasets", middleware.JWTAuth()) + { + datasetVersions := datasets.Group("/:dataset_id/versions") + { + datasetVersions.GET("/:version_id/download", middleware.RequireDatasetVersionDownload, middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:datasets:*", "sdk:datasets:read"), handlers.Dataset.DownloadDatasetVersion) + } + + datasets.PATCH("/:dataset_id/version/:version_id/injections", middleware.RequireDatasetVersionUpdate, middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:datasets:*", "sdk:datasets:write"), handlers.Dataset.ManageDatasetVersionInjections) + } + + projects := v2.Group("/projects", middleware.JWTAuth()) + { + injections := projects.Group("/:project_id/injections") + { + injectionRead := injections.Group("", middleware.RequireProjectRead) + { + analysis := injectionRead.Group("/analysis") + { + analysis.GET("/no-issues", handlers.Injection.ListProjectFaultInjectionNoIssues) + analysis.GET("/with-issues", handlers.Injection.ListProjectFaultInjectionWithIssues) + } + + injectionRead.GET("", handlers.Injection.ListProjectInjections) + } + + injectionExecute := injections.Group("", middleware.RequireProjectInjectionExecute) + { + injectionExecute.POST("/inject", handlers.Injection.SubmitProjectFaultInjection) + injectionExecute.POST("/build", handlers.Injection.SubmitProjectDatapackBuilding) + } + } + + executions := projects.Group("/:project_id/executions") + { + executionRead := executions.Group("", middleware.RequireProjectRead) + { + executionRead.GET("", handlers.Execution.ListProjectExecutions) + } + + executionExecute := executions.Group("", middleware.RequireProjectExecutionExecute) + { + executionExecute.POST("/execute", handlers.Execution.SubmitAlgorithmExecution) + } + } + } + + evaluations := v2.Group("/evaluations", middleware.JWTAuth()) + { + evaluations.POST("/datapacks", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read"), handlers.Evaluation.ListDatapackEvaluationResults) + evaluations.POST("/datasets", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read"), handlers.Evaluation.ListDatasetEvaluationResults) + evaluations.GET("", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read"), handlers.Evaluation.ListEvaluations) + evaluations.GET("/:id", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:evaluations:*", "sdk:evaluations:read"), handlers.Evaluation.GetEvaluation) + } + + executions := v2.Group("/executions", middleware.JWTAuth()) + { + executions.GET("/:id", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:executions:*", "sdk:executions:read"), handlers.Execution.GetExecution) + executions.PATCH("/:id/labels", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:executions:*", "sdk:executions:write"), handlers.Execution.ManageExecutionCustomLabels) + } + + runtime := v2.Group("/executions", middleware.RequireServiceTokenAuth()) + { + runtime.POST("/:execution_id/detector_results", handlers.Execution.UploadDetectorResults) + runtime.POST("/:execution_id/granularity_results", handlers.Execution.UploadGranularityResults) + } + + injections := v2.Group("/injections", middleware.JWTAuth()) + { + injections.GET("/metadata", handlers.Injection.GetInjectionMetadata) + injections.GET("/:id", handlers.Injection.GetInjection) + injections.POST("/:id/clone", handlers.Injection.CloneInjection) + injections.GET("/:id/download", handlers.Injection.DownloadDatapack) + injections.GET("/:id/files", handlers.Injection.ListDatapackFiles) + injections.GET("/:id/files/download", handlers.Injection.DownloadDatapackFile) + injections.GET("/:id/files/query", handlers.Injection.QueryDatapackFile) + injections.PATCH("/:id/labels", handlers.Injection.ManageInjectionCustomLabels) + } + + metrics := v2.Group("/metrics", middleware.JWTAuth()) + { + metrics.GET("/algorithms", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:metrics:*", "sdk:metrics:read"), handlers.Metric.GetAlgorithmMetrics) + metrics.GET("/executions", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:metrics:*", "sdk:metrics:read"), handlers.Metric.GetExecutionMetrics) + metrics.GET("/injections", middleware.RequireAPIKeyScopesAny("sdk:*", "sdk:metrics:*", "sdk:metrics:read"), handlers.Metric.GetInjectionMetrics) + } } diff --git a/src/router/system.go b/src/router/system.go deleted file mode 100644 index 3ebf9124..00000000 --- a/src/router/system.go +++ /dev/null @@ -1,56 +0,0 @@ -package router - -import ( - "aegis/middleware" - - "github.com/gin-gonic/gin" -) - -// SetupSystemRoutes sets up system routes -func SetupSystemRoutes(router *gin.Engine, handlers *Handlers) { - audit := router.Group("/system/audit", middleware.JWTAuth(), middleware.RequireAuditRead) - { - audit.GET("", handlers.System.ListAuditLogs) - audit.GET("/:id", handlers.System.GetAuditLog) - } - - // Dynamic Configuration Management - configs := router.Group("/system/configs", middleware.JWTAuth()) - { - configsRead := configs.Group("", middleware.RequireConfigurationRead) - { - configsRead.GET("", handlers.System.ListConfigs) // Search configurations with filters - configsRead.GET("/:config_id", handlers.System.GetConfig) // Get configuration by ID - configsRead.GET("/:config_id/histories", handlers.System.ListConfigHistories) // Get configuration change history - } - - // Configuration Update operations - configs.PATCH("/:config_id", middleware.RequireConfigurationUpdate, handlers.System.UpdateConfigValue) // Update configuration value - configs.POST("/:config_id/value/rollback", middleware.RequireConfigurationUpdate, handlers.System.RollbackConfigValue) // Rollback configuration value - - // Configuration Configure operations (metadata management, higher privilege) - configs.PUT("/:config_id/metadata", middleware.RequireConfigurationConfigure, handlers.System.UpdateConfigMetadata) // Update configuration metadata (schema) - configs.POST("/:config_id/metadata/rollback", middleware.RequireConfigurationConfigure, handlers.System.RollbackConfigMetadata) // Rollback configuration metadata - } - - health := router.Group("/system/health") - { - health.GET("", handlers.System.GetHealth) - } - - monitor := router.Group("/system/monitor", middleware.JWTAuth(), middleware.RequireSystemRead) - { - monitor.POST("/metrics", handlers.System.GetMetrics) - monitor.GET("/info", handlers.System.GetSystemInfo) - monitor.GET("/namespaces/locks", handlers.System.ListNamespaceLocks) - monitor.GET("/tasks/queue", handlers.System.ListQueuedTasks) - } -} - -func SetupSystemV2Routes(v2 *gin.RouterGroup, handlers *Handlers) { - system := v2.Group("/system", middleware.JWTAuth(), middleware.RequireSystemRead) - { - system.GET("/metrics", handlers.SystemMetric.GetSystemMetrics) // Get current system metrics - system.GET("/metrics/history", handlers.SystemMetric.GetSystemMetricsHistory) // Get historical system metrics - } -} diff --git a/src/service/common/config_listener.go b/src/service/common/config_listener.go index d4fd0819..ccc2f7f0 100644 --- a/src/service/common/config_listener.go +++ b/src/service/common/config_listener.go @@ -8,7 +8,7 @@ import ( "aegis/config" "aegis/consts" - etcdinfra "aegis/infra/etcd" + etcd "aegis/infra/etcd" "github.com/sirupsen/logrus" clientv3 "go.etcd.io/etcd/client/v3" @@ -33,10 +33,10 @@ type ConfigUpdateListener struct { mu sync.Mutex active map[consts.ConfigScope]bool // scopes already loaded + watched db *gorm.DB - gateway *etcdinfra.Gateway + gateway *etcd.Gateway } -func NewConfigUpdateListener(ctx context.Context, db *gorm.DB, gateway *etcdinfra.Gateway) *ConfigUpdateListener { +func NewConfigUpdateListener(ctx context.Context, db *gorm.DB, gateway *etcd.Gateway) *ConfigUpdateListener { listenerCtx, cancel := context.WithCancel(ctx) listener := &ConfigUpdateListener{ ctx: listenerCtx, diff --git a/src/service/common/injection.go b/src/service/common/injection.go index 4e097507..df7cf1c9 100644 --- a/src/service/common/injection.go +++ b/src/service/common/injection.go @@ -3,7 +3,7 @@ package common import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/utils" "context" "fmt" @@ -12,7 +12,7 @@ import ( "gorm.io/gorm" ) -func ProduceFaultInjectionTasksWithDB(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, injectTime time.Time, payload map[string]any) error { +func ProduceFaultInjectionTasksWithDB(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, task *dto.UnifiedTask, injectTime time.Time, payload map[string]any) error { newTask := &dto.UnifiedTask{ Type: consts.TaskTypeFaultInjection, Immediate: false, diff --git a/src/service/common/task.go b/src/service/common/task.go index 0ea923e1..5638e345 100644 --- a/src/service/common/task.go +++ b/src/service/common/task.go @@ -3,7 +3,7 @@ package common import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/model" "context" "encoding/json" @@ -46,7 +46,7 @@ func CronNextTime(expr string) (time.Time, error) { // -> Task 3 // -> Task 4 // -> Task 5 -func SubmitTaskWithDB(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, t *dto.UnifiedTask) error { +func SubmitTaskWithDB(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, t *dto.UnifiedTask) error { if db == nil { return fmt.Errorf("task db is nil") } diff --git a/src/service/consumer/algo_execution.go b/src/service/consumer/algo_execution.go index 63baac35..53d1daa7 100644 --- a/src/service/consumer/algo_execution.go +++ b/src/service/consumer/algo_execution.go @@ -13,9 +13,9 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" - executionmodule "aegis/module/execution" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" + execution "aegis/module/execution" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -43,8 +43,8 @@ type algoJobCreationParams struct { payload *executionPayload } -func (p *algoJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, initContainers []corev1.Container, volumeMountconfigs []k8sinfra.VolumeMountConfig) *k8sinfra.JobConfig { - return &k8sinfra.JobConfig{ +func (p *algoJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, initContainers []corev1.Container, volumeMountconfigs []k8s.VolumeMountConfig) *k8s.JobConfig { + return &k8s.JobConfig{ JobName: p.jobName, Image: p.image, Command: strings.Split(p.payload.algorithm.Command, " "), @@ -155,7 +155,7 @@ func executeAlgorithm(ctx context.Context, task *dto.UnifiedTask, deps RuntimeDe } // rescheduleAlgoExecutionTask reschedules a algorithm execution task with a random delay between 1 to 5 minutes -func rescheduleAlgoExecutionTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { +func rescheduleAlgoExecutionTask(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) @@ -226,7 +226,7 @@ func parseExecutionPayload(payload map[string]any) (*executionPayload, error) { } // createAlgoJob creates and submits a Kubernetes job for algorithm execution -func createAlgoJob(ctx context.Context, gateway *k8sinfra.Gateway, params *algoJobCreationParams) error { +func createAlgoJob(ctx context.Context, gateway *k8s.Gateway, params *algoJobCreationParams) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ @@ -345,7 +345,7 @@ func createExecution(ctx context.Context, deps RuntimeDeps, taskID string, algor if deps.ExecutionOwner == nil { return 0, fmt.Errorf("execution owner service is nil") } - return deps.ExecutionOwner.CreateExecution(ctx, &executionmodule.RuntimeCreateExecutionReq{ + return deps.ExecutionOwner.CreateExecution(ctx, &execution.RuntimeCreateExecutionReq{ TaskID: taskID, AlgorithmVersionID: algorithmVersionID, DatapackID: datapackID, diff --git a/src/service/consumer/build_container.go b/src/service/consumer/build_container.go index 1839b80a..831c96e0 100644 --- a/src/service/consumer/build_container.go +++ b/src/service/consumer/build_container.go @@ -10,8 +10,8 @@ import ( "aegis/consts" "aegis/dto" - buildkitinfra "aegis/infra/buildkit" - redisinfra "aegis/infra/redis" + buildkit "aegis/infra/buildkit" + redis "aegis/infra/redis" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -120,7 +120,7 @@ func executeBuildContainer(ctx context.Context, task *dto.UnifiedTask, deps Runt } // rescheduleContainerBuildingTask reschedules a container building task with a random delay between 1 to 5 minutes -func rescheduleContainerBuildingTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { +func rescheduleContainerBuildingTask(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) @@ -185,7 +185,7 @@ func parseContainerPayload(payload map[string]any) (*containerPayload, error) { } // buildImageAndPush builds the container image using BuildKit and pushes it to the registry -func buildImageAndPush(ctx context.Context, buildKitGateway *buildkitinfra.Gateway, payload *containerPayload, logEntry *logrus.Entry) error { +func buildImageAndPush(ctx context.Context, buildKitGateway *buildkit.Gateway, payload *containerPayload, logEntry *logrus.Entry) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) diff --git a/src/service/consumer/build_datapack.go b/src/service/consumer/build_datapack.go index e73534d4..f5a60b04 100644 --- a/src/service/consumer/build_datapack.go +++ b/src/service/consumer/build_datapack.go @@ -16,8 +16,8 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - dbinfra "aegis/infra/db" - k8sinfra "aegis/infra/k8s" + db "aegis/infra/db" + k8s "aegis/infra/k8s" "aegis/tracing" "aegis/utils" ) @@ -35,11 +35,11 @@ type datapackJobCreationParams struct { annotations map[string]string labels map[string]string payload *datapackPayload - dbConfig *dbinfra.DatabaseConfig + dbConfig *db.DatabaseConfig } -func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volumeMountConfigs []k8sinfra.VolumeMountConfig) *k8sinfra.JobConfig { - return &k8sinfra.JobConfig{ +func (p *datapackJobCreationParams) toK8sJobConfig(envVars []corev1.EnvVar, volumeMountConfigs []k8s.VolumeMountConfig) *k8s.JobConfig { + return &k8s.JobConfig{ JobName: p.jobName, Image: p.image, Command: strings.Split(p.payload.benchmark.Command, " "), @@ -91,7 +91,7 @@ func executeBuildDatapackWithDeps(ctx context.Context, task *dto.UnifiedTask, de annotations: annotations, labels: jobLabels, payload: payload, - dbConfig: dbinfra.NewDatabaseConfig("clickhouse"), + dbConfig: db.NewDatabaseConfig("clickhouse"), } return createDatapackJob(childCtx, k8sGateway, params) }) @@ -127,7 +127,7 @@ func parseDatapackPayload(payload map[string]any) (*datapackPayload, error) { }, nil } -func createDatapackJob(ctx context.Context, gateway *k8sinfra.Gateway, params *datapackJobCreationParams) error { +func createDatapackJob(ctx context.Context, gateway *k8s.Gateway, params *datapackJobCreationParams) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ @@ -153,7 +153,7 @@ func createDatapackJob(ctx context.Context, gateway *k8sinfra.Gateway, params *d }) } -func getDatapackJobEnvVars(taskID string, datapackPathPrefix string, payload *datapackPayload, dbConfig *dbinfra.DatabaseConfig) ([]corev1.EnvVar, error) { +func getDatapackJobEnvVars(taskID string, datapackPathPrefix string, payload *datapackPayload, dbConfig *db.DatabaseConfig) ([]corev1.EnvVar, error) { tz := config.GetString("system.timezone") if tz == "" { tz = time.Local.String() diff --git a/src/service/consumer/collect_result.go b/src/service/consumer/collect_result.go index c189d3c8..83d4fedf 100644 --- a/src/service/consumer/collect_result.go +++ b/src/service/consumer/collect_result.go @@ -7,8 +7,8 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" - executionmodule "aegis/module/execution" + redis "aegis/infra/redis" + execution "aegis/module/execution" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -137,7 +137,7 @@ func executeCollectResult(ctx context.Context, task *dto.UnifiedTask, deps Runti }) } -func loadDetectorResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]executionmodule.DetectorResultItem, error) { +func loadDetectorResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]execution.DetectorResultItem, error) { if deps.ExecutionOwner == nil { return nil, fmt.Errorf("execution owner service is nil") } @@ -148,7 +148,7 @@ func loadDetectorResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, exec return resp.DetectorResults, nil } -func loadGranularityResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]executionmodule.GranularityResultItem, error) { +func loadGranularityResults(ctx context.Context, deps RuntimeDeps, _ *gorm.DB, executionID int) ([]execution.GranularityResultItem, error) { if deps.ExecutionOwner == nil { return nil, fmt.Errorf("execution owner service is nil") } @@ -185,7 +185,7 @@ func parseCollectPayload(payload map[string]any) (*collectionPayload, error) { } // produceAlgorithmExeuctionTask produces an algorithm execution task into Redis -func produceAlgorithmExeuctionTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, payload map[string]any, index int) error { +func produceAlgorithmExeuctionTask(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, task *dto.UnifiedTask, payload map[string]any, index int) error { newTask := &dto.UnifiedTask{ Type: consts.TaskTypeRunAlgorithm, Immediate: true, diff --git a/src/service/consumer/common.go b/src/service/consumer/common.go index 53f2b430..da3d8a40 100644 --- a/src/service/consumer/common.go +++ b/src/service/consumer/common.go @@ -2,7 +2,7 @@ package consumer import ( "aegis/consts" - k8sinfra "aegis/infra/k8s" + k8s "aegis/infra/k8s" "fmt" "github.com/sirupsen/logrus" @@ -16,7 +16,7 @@ const ( ) // getRequiredVolumeMountConfigs retrieves the volume mount configurations for the specified required keys -func getRequiredVolumeMountConfigs(gateway *k8sinfra.Gateway, requiredKeys []consts.VolumeMountName) ([]k8sinfra.VolumeMountConfig, error) { +func getRequiredVolumeMountConfigs(gateway *k8s.Gateway, requiredKeys []consts.VolumeMountName) ([]k8s.VolumeMountConfig, error) { if gateway == nil { return nil, fmt.Errorf("k8s gateway is nil") } @@ -26,7 +26,7 @@ func getRequiredVolumeMountConfigs(gateway *k8sinfra.Gateway, requiredKeys []con return nil, fmt.Errorf("failed to get volume mount configuration map: %w", err) } - volumeMountConfigs := make([]k8sinfra.VolumeMountConfig, 0, len(requiredKeys)) + volumeMountConfigs := make([]k8s.VolumeMountConfig, 0, len(requiredKeys)) for _, vmName := range requiredKeys { cfg, exists := volumeMountConfigMap[vmName] diff --git a/src/service/consumer/config_handlers.go b/src/service/consumer/config_handlers.go index 4ffbc558..55d9ead3 100644 --- a/src/service/consumer/config_handlers.go +++ b/src/service/consumer/config_handlers.go @@ -7,7 +7,7 @@ import ( "aegis/config" "aegis/consts" - k8sinfra "aegis/infra/k8s" + k8s "aegis/infra/k8s" "aegis/service/common" "github.com/sirupsen/logrus" @@ -16,7 +16,7 @@ import ( // RegisterConsumerHandlers registers all consumer-scoped configuration handlers. // Should be called during consumer initialization, after RegisterGlobalHandlers. func RegisterConsumerHandlers( - controller *k8sinfra.Controller, + controller *k8s.Controller, monitor NamespaceMonitor, publisher common.ConfigPublisher, restartLimiter *TokenBucketRateLimiter, @@ -35,7 +35,7 @@ func RegisterConsumerHandlers( } // UpdateK8sController updates K8s controller informers based on namespace changes. -func UpdateK8sController(controller *k8sinfra.Controller, toAdd, toRemove []string) error { +func UpdateK8sController(controller *k8s.Controller, toAdd, toRemove []string) error { if controller == nil { logrus.Warn("Controller not initialized, skipping informer update") return nil @@ -62,11 +62,11 @@ func UpdateK8sController(controller *k8sinfra.Controller, toAdd, toRemove []stri type chaosSystemCountHandler struct { monitor NamespaceMonitor - controller *k8sinfra.Controller + controller *k8s.Controller publisher common.ConfigPublisher } -func newChaosSystemCountHandler(m NamespaceMonitor, c *k8sinfra.Controller, publisher common.ConfigPublisher) *chaosSystemCountHandler { +func newChaosSystemCountHandler(m NamespaceMonitor, c *k8s.Controller, publisher common.ConfigPublisher) *chaosSystemCountHandler { return &chaosSystemCountHandler{monitor: m, controller: c, publisher: publisher} } diff --git a/src/service/consumer/fault_injection.go b/src/service/consumer/fault_injection.go index 4fc2e629..a7550dcf 100644 --- a/src/service/consumer/fault_injection.go +++ b/src/service/consumer/fault_injection.go @@ -11,7 +11,7 @@ import ( "aegis/consts" "aegis/dto" "aegis/model" - injectionmodule "aegis/module/injection" + injection "aegis/module/injection" "aegis/tracing" "aegis/utils" @@ -213,7 +213,7 @@ func executeFaultInjection(ctx context.Context, task *dto.UnifiedTask, deps Runt return handleExecutionError(span, logEntry, "injection owner service is nil", fmt.Errorf("missing injection owner service")) } - _, err = deps.InjectionOwner.CreateInjection(childCtx, &injectionmodule.RuntimeCreateInjectionReq{ + _, err = deps.InjectionOwner.CreateInjection(childCtx, &injection.RuntimeCreateInjectionReq{ Name: name, FaultType: faultType, Category: payload.pedestal, diff --git a/src/service/consumer/k8s_handler.go b/src/service/consumer/k8s_handler.go index 372f48de..0ffb2a6c 100644 --- a/src/service/consumer/k8s_handler.go +++ b/src/service/consumer/k8s_handler.go @@ -10,9 +10,9 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" - containermodule "aegis/module/container" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" + container "aegis/module/container" "aegis/service/common" "aegis/utils" @@ -37,11 +37,11 @@ type errorContext struct { logEntry *logrus.Entry labels *taskIdentifiers db *gorm.DB - redisGateway *redisinfra.Gateway + redisGateway *redis.Gateway } // NewErrorContext creates an ErrorContext from parsed labels -func NewErrorContext(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, span trace.Span, labels *taskIdentifiers) *errorContext { +func NewErrorContext(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, span trace.Span, labels *taskIdentifiers) *errorContext { return &errorContext{ ctx: ctx, span: span, @@ -131,12 +131,12 @@ type k8sHandler struct { store *stateStore monitor NamespaceMonitor algoLimiter *TokenBucketRateLimiter - k8sGateway *k8sinfra.Gateway - redisGateway *redisinfra.Gateway + k8sGateway *k8s.Gateway + redisGateway *redis.Gateway batchManager *FaultBatchManager } -func NewHandler(db *gorm.DB, monitor NamespaceMonitor, algoLimiter *TokenBucketRateLimiter, k8sGateway *k8sinfra.Gateway, redisGateway *redisinfra.Gateway, batchManager *FaultBatchManager, execution ExecutionOwner, injection InjectionOwner) *k8sHandler { +func NewHandler(db *gorm.DB, monitor NamespaceMonitor, algoLimiter *TokenBucketRateLimiter, k8sGateway *k8s.Gateway, redisGateway *redis.Gateway, batchManager *FaultBatchManager, execution ExecutionOwner, injection InjectionOwner) *k8sHandler { return &k8sHandler{ db: db, store: newStateStore(execution, injection), @@ -593,7 +593,7 @@ func (h *k8sHandler) HandleJobSucceeded(job *batchv1.Job, annotations map[string Name: config.GetDetectorName(), } - algorithmVersionResults, err := containermodule.NewRepository(h.db).ResolveContainerVersions([]*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) + algorithmVersionResults, err := container.NewRepository(h.db).ResolveContainerVersions([]*dto.ContainerRef{ref}, consts.ContainerTypeAlgorithm, parsedLabels.userID) if err != nil { errCtx.Fatal(nil, "failed to map container refs to versions", err) return diff --git a/src/service/consumer/monitor.go b/src/service/consumer/monitor.go index bedf8258..b4de5990 100644 --- a/src/service/consumer/monitor.go +++ b/src/service/consumer/monitor.go @@ -14,7 +14,7 @@ import ( redisinfra "aegis/infra/redis" "aegis/utils" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) @@ -170,7 +170,7 @@ func (m *monitor) AcquireLock(namespace string, endTime time.Time, traceID strin if err == nil { logEntry.Info("acquired namespace lock") - } else if err != redis.TxFailedErr { + } else if err != goredis.TxFailedErr { logEntry.Warn("failed to acquire namespace lock") } @@ -232,7 +232,7 @@ func (m *monitor) CheckNamespaceToInject(namespace string, executeTime time.Time // Try to acquire the lock - all availability checking is done inside acquireNamespaceLock err := m.AcquireLock(namespace, proposedEndTime, traceID, consts.TaskTypeFaultInjection) if err != nil { - if err == redis.TxFailedErr { + if err == goredis.TxFailedErr { return fmt.Errorf("cannot inject fault: namespace %s was concurrently acquired by another client", namespace) } return fmt.Errorf("cannot inject fault: %v", err) diff --git a/src/service/consumer/namespace_catalog_store.go b/src/service/consumer/namespace_catalog_store.go index 49b61017..6d934806 100644 --- a/src/service/consumer/namespace_catalog_store.go +++ b/src/service/consumer/namespace_catalog_store.go @@ -6,14 +6,14 @@ import ( "time" "aegis/consts" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" ) type namespaceCatalogStore struct { - client *redisinfra.Gateway + client *redis.Gateway } -func newNamespaceCatalogStore(client *redisinfra.Gateway) namespaceCatalogStore { +func newNamespaceCatalogStore(client *redis.Gateway) namespaceCatalogStore { return namespaceCatalogStore{client: client} } diff --git a/src/service/consumer/namespace_lock_store.go b/src/service/consumer/namespace_lock_store.go index f938db12..18854014 100644 --- a/src/service/consumer/namespace_lock_store.go +++ b/src/service/consumer/namespace_lock_store.go @@ -9,7 +9,7 @@ import ( "aegis/consts" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type namespaceLockState struct { @@ -31,12 +31,12 @@ func (s namespaceLockStore) key(namespace string) string { func (s namespaceLockStore) read(ctx context.Context, namespace string) (*namespaceLockState, error) { endTimeStr, err := s.client.HashGet(ctx, s.key(namespace), "end_time") - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { return nil, err } traceID, err := s.client.HashGet(ctx, s.key(namespace), "trace_id") - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { return nil, err } @@ -52,14 +52,14 @@ func (s namespaceLockStore) read(ctx context.Context, namespace string) (*namesp return &namespaceLockState{EndTime: endTime, TraceID: traceID}, nil } -func (s namespaceLockStore) readFromHash(reader redis.HashCmdable, ctx context.Context, namespace string) (*namespaceLockState, error) { +func (s namespaceLockStore) readFromHash(reader goredis.HashCmdable, ctx context.Context, namespace string) (*namespaceLockState, error) { endTimeStr, err := reader.HGet(ctx, s.key(namespace), "end_time").Result() - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { return nil, err } traceID, err := reader.HGet(ctx, s.key(namespace), "trace_id").Result() - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { return nil, err } @@ -83,7 +83,7 @@ func (s namespaceLockStore) write(ctx context.Context, namespace string, endTime } func (s namespaceLockStore) acquire(ctx context.Context, namespace string, endTime time.Time, traceID string, now time.Time) error { - return s.client.Watch(ctx, func(tx *redis.Tx) error { + return s.client.Watch(ctx, func(tx *goredis.Tx) error { state, err := s.readFromHash(tx, ctx, namespace) if err != nil { return err @@ -92,7 +92,7 @@ func (s namespaceLockStore) acquire(ctx context.Context, namespace string, endTi return fmt.Errorf("namespace %s is locked by %s until %v", namespace, state.TraceID, time.Unix(state.EndTime, 0).Format(time.RFC3339)) } - _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error { + _, err = tx.TxPipelined(ctx, func(pipe goredis.Pipeliner) error { pipe.HSet(ctx, s.key(namespace), "end_time", endTime.Unix()) pipe.HSet(ctx, s.key(namespace), "trace_id", traceID) return nil @@ -103,7 +103,7 @@ func (s namespaceLockStore) acquire(ctx context.Context, namespace string, endTi func (s namespaceLockStore) release(ctx context.Context, namespace, traceID string, releasedAt time.Time) error { state, err := s.read(ctx, namespace) - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { return fmt.Errorf("failed to get current trace_id: %v", err) } if state != nil && state.TraceID != traceID && state.TraceID != "" { @@ -115,7 +115,7 @@ func (s namespaceLockStore) release(ctx context.Context, namespace, traceID stri func (s namespaceLockStore) isActive(ctx context.Context, namespace string, now time.Time) (bool, error) { state, err := s.read(ctx, namespace) - if err == redis.Nil { + if err == goredis.Nil { return false, nil } if err != nil { diff --git a/src/service/consumer/namespace_status_store.go b/src/service/consumer/namespace_status_store.go index 6005c392..c38fe22d 100644 --- a/src/service/consumer/namespace_status_store.go +++ b/src/service/consumer/namespace_status_store.go @@ -7,7 +7,7 @@ import ( "aegis/consts" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type namespaceStatusStore struct { @@ -24,7 +24,7 @@ func (s namespaceStatusStore) key(namespace string) string { func (s namespaceStatusStore) get(ctx context.Context, namespace string) (consts.StatusType, error) { statusStr, err := s.client.HashGet(ctx, s.key(namespace), "status") - if err == redis.Nil { + if err == goredis.Nil { return consts.CommonEnabled, nil } if err != nil { diff --git a/src/service/consumer/owner_adapter.go b/src/service/consumer/owner_adapter.go index 66f61e0f..43e6def3 100644 --- a/src/service/consumer/owner_adapter.go +++ b/src/service/consumer/owner_adapter.go @@ -6,29 +6,29 @@ import ( "aegis/dto" "aegis/internalclient/orchestratorclient" - executionmodule "aegis/module/execution" - injectionmodule "aegis/module/injection" + execution "aegis/module/execution" + injection "aegis/module/injection" "go.uber.org/fx" ) // ExecutionOwner captures the execution owner operations used by runtime code. type ExecutionOwner interface { - CreateExecution(context.Context, *executionmodule.RuntimeCreateExecutionReq) (int, error) - GetExecution(context.Context, int) (*executionmodule.ExecutionDetailResp, error) - UpdateExecutionState(context.Context, *executionmodule.RuntimeUpdateExecutionStateReq) error + CreateExecution(context.Context, *execution.RuntimeCreateExecutionReq) (int, error) + GetExecution(context.Context, int) (*execution.ExecutionDetailResp, error) + UpdateExecutionState(context.Context, *execution.RuntimeUpdateExecutionStateReq) error } // InjectionOwner captures the injection owner operations used by runtime code. type InjectionOwner interface { - CreateInjection(context.Context, *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) - UpdateInjectionState(context.Context, *injectionmodule.RuntimeUpdateInjectionStateReq) error - UpdateInjectionTimestamps(context.Context, *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) + CreateInjection(context.Context, *injection.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) + UpdateInjectionState(context.Context, *injection.RuntimeUpdateInjectionStateReq) error + UpdateInjectionTimestamps(context.Context, *injection.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) } type executionOwnerAdapter struct { orchestrator *orchestratorclient.Client - local *executionmodule.Service + local *execution.Service requireRemote bool } @@ -36,14 +36,14 @@ type executionOwnerParams struct { fx.In Orchestrator *orchestratorclient.Client - Local *executionmodule.Service `optional:"true"` + Local *execution.Service `optional:"true"` } type injectionOwnerParams struct { fx.In Orchestrator *orchestratorclient.Client - Local *injectionmodule.Service `optional:"true"` + Local *injection.Service `optional:"true"` } func NewExecutionOwner(params executionOwnerParams) ExecutionOwner { @@ -78,7 +78,7 @@ func newRemoteInjectionOwner(params injectionOwnerParams) InjectionOwner { } } -func (a executionOwnerAdapter) CreateExecution(ctx context.Context, req *executionmodule.RuntimeCreateExecutionReq) (int, error) { +func (a executionOwnerAdapter) CreateExecution(ctx context.Context, req *execution.RuntimeCreateExecutionReq) (int, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.CreateExecution(ctx, req) } @@ -91,7 +91,7 @@ func (a executionOwnerAdapter) CreateExecution(ctx context.Context, req *executi return a.local.CreateExecutionRecord(ctx, req) } -func (a executionOwnerAdapter) GetExecution(ctx context.Context, executionID int) (*executionmodule.ExecutionDetailResp, error) { +func (a executionOwnerAdapter) GetExecution(ctx context.Context, executionID int) (*execution.ExecutionDetailResp, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.GetExecution(ctx, executionID) } @@ -104,7 +104,7 @@ func (a executionOwnerAdapter) GetExecution(ctx context.Context, executionID int return a.local.GetExecution(ctx, executionID) } -func (a executionOwnerAdapter) UpdateExecutionState(ctx context.Context, req *executionmodule.RuntimeUpdateExecutionStateReq) error { +func (a executionOwnerAdapter) UpdateExecutionState(ctx context.Context, req *execution.RuntimeUpdateExecutionStateReq) error { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.UpdateExecutionState(ctx, req) } @@ -119,11 +119,11 @@ func (a executionOwnerAdapter) UpdateExecutionState(ctx context.Context, req *ex type injectionOwnerAdapter struct { orchestrator *orchestratorclient.Client - local *injectionmodule.Service + local *injection.Service requireRemote bool } -func (a injectionOwnerAdapter) CreateInjection(ctx context.Context, req *injectionmodule.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { +func (a injectionOwnerAdapter) CreateInjection(ctx context.Context, req *injection.RuntimeCreateInjectionReq) (*dto.InjectionItem, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.CreateInjection(ctx, req) } @@ -136,7 +136,7 @@ func (a injectionOwnerAdapter) CreateInjection(ctx context.Context, req *injecti return a.local.CreateInjectionRecord(ctx, req) } -func (a injectionOwnerAdapter) UpdateInjectionState(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionStateReq) error { +func (a injectionOwnerAdapter) UpdateInjectionState(ctx context.Context, req *injection.RuntimeUpdateInjectionStateReq) error { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.UpdateInjectionState(ctx, req) } @@ -149,7 +149,7 @@ func (a injectionOwnerAdapter) UpdateInjectionState(ctx context.Context, req *in return a.local.UpdateInjectionState(ctx, req) } -func (a injectionOwnerAdapter) UpdateInjectionTimestamps(ctx context.Context, req *injectionmodule.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { +func (a injectionOwnerAdapter) UpdateInjectionTimestamps(ctx context.Context, req *injection.RuntimeUpdateInjectionTimestampReq) (*dto.InjectionItem, error) { if a.orchestrator != nil && a.orchestrator.Enabled() { return a.orchestrator.UpdateInjectionTimestamps(ctx, req) } diff --git a/src/service/consumer/rate_limiter.go b/src/service/consumer/rate_limiter.go index 093c65c1..317f51bd 100644 --- a/src/service/consumer/rate_limiter.go +++ b/src/service/consumer/rate_limiter.go @@ -7,7 +7,7 @@ import ( "aegis/config" "aegis/consts" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/trace" @@ -176,7 +176,7 @@ func (r *TokenBucketRateLimiter) WaitForToken(ctx context.Context, taskID, trace } } -func NewRestartPedestalRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { +func NewRestartPedestalRateLimiter(gateway *redis.Gateway) *TokenBucketRateLimiter { return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.RestartPedestalTokenBucket, MaxTokensKey: consts.MaxTokensKeyRestartPedestal, @@ -186,7 +186,7 @@ func NewRestartPedestalRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRate }) } -func NewBuildContainerRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { +func NewBuildContainerRateLimiter(gateway *redis.Gateway) *TokenBucketRateLimiter { return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.BuildContainerTokenBucket, MaxTokensKey: consts.MaxTokensKeyBuildContainer, @@ -196,7 +196,7 @@ func NewBuildContainerRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateL }) } -func NewAlgoExecutionRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLimiter { +func NewAlgoExecutionRateLimiter(gateway *redis.Gateway) *TokenBucketRateLimiter { return newTokenBucketRateLimiter(gateway, RateLimiterConfig{ TokenBucketKey: consts.AlgoExecutionTokenBucket, MaxTokensKey: consts.MaxTokensKeyAlgoExecution, @@ -207,7 +207,7 @@ func NewAlgoExecutionRateLimiter(gateway *redisinfra.Gateway) *TokenBucketRateLi } // newTokenBucketRateLimiter creates a new token bucket rate limiter -func newTokenBucketRateLimiter(gateway *redisinfra.Gateway, cfg RateLimiterConfig) *TokenBucketRateLimiter { +func newTokenBucketRateLimiter(gateway *redis.Gateway, cfg RateLimiterConfig) *TokenBucketRateLimiter { maxTokens := config.GetInt(cfg.MaxTokensKey) if maxTokens <= 0 { maxTokens = cfg.DefaultMaxTokens diff --git a/src/service/consumer/rate_limiter_store.go b/src/service/consumer/rate_limiter_store.go index 4802e2ab..2ff13dd9 100644 --- a/src/service/consumer/rate_limiter_store.go +++ b/src/service/consumer/rate_limiter_store.go @@ -5,7 +5,7 @@ import ( "fmt" redisinfra "aegis/infra/redis" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" ) type tokenBucketStore struct { @@ -18,7 +18,7 @@ func newTokenBucketStore(client *redisinfra.Gateway, bucketKey string) tokenBuck } func (s tokenBucketStore) acquire(ctx context.Context, maxTokens int, taskID, traceID string) (bool, error) { - script := redis.NewScript(` + script := goredis.NewScript(` local bucket_key = KEYS[1] local max_tokens = tonumber(ARGV[1]) local task_id = ARGV[2] diff --git a/src/service/consumer/redis.go b/src/service/consumer/redis.go index 0d8a9369..56f5563b 100644 --- a/src/service/consumer/redis.go +++ b/src/service/consumer/redis.go @@ -6,7 +6,7 @@ import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" ) func consumerDetachedContext() context.Context { @@ -17,7 +17,7 @@ type redisStreamEvent interface { ToRedisStream() map[string]any } -func publishRedisStreamEvent(gateway *redisinfra.Gateway, ctx context.Context, stream string, event redisStreamEvent) error { +func publishRedisStreamEvent(gateway *redis.Gateway, ctx context.Context, stream string, event redisStreamEvent) error { if gateway == nil { return fmt.Errorf("redis gateway is nil") } @@ -27,14 +27,14 @@ func publishRedisStreamEvent(gateway *redisinfra.Gateway, ctx context.Context, s return nil } -func publishTraceStreamEvent(gateway *redisinfra.Gateway, ctx context.Context, stream string, event *dto.TraceStreamEvent) error { +func publishTraceStreamEvent(gateway *redis.Gateway, ctx context.Context, stream string, event *dto.TraceStreamEvent) error { if event == nil { return nil } return publishRedisStreamEvent(gateway, ctx, stream, event) } -func loadCachedInjectionAlgorithms(gateway *redisinfra.Gateway, ctx context.Context, groupID string) ([]dto.ContainerVersionItem, bool, error) { +func loadCachedInjectionAlgorithms(gateway *redis.Gateway, ctx context.Context, groupID string) ([]dto.ContainerVersionItem, bool, error) { if gateway == nil { return nil, false, fmt.Errorf("redis gateway is nil") } diff --git a/src/service/consumer/restart_pedestal.go b/src/service/consumer/restart_pedestal.go index c466b9f0..1a34e1e0 100644 --- a/src/service/consumer/restart_pedestal.go +++ b/src/service/consumer/restart_pedestal.go @@ -4,8 +4,8 @@ import ( "aegis/config" "aegis/consts" "aegis/dto" - helminfra "aegis/infra/helm" - redisinfra "aegis/infra/redis" + helm "aegis/infra/helm" + redis "aegis/infra/redis" "aegis/service/common" "aegis/tracing" "aegis/utils" @@ -199,7 +199,7 @@ func executeRestartPedestal(ctx context.Context, task *dto.UnifiedTask, deps Run } // rescheduleRestartPedestalTask reschedules a pedestal restart task with exponential backoff and jitter -func rescheduleRestartPedestalTask(ctx context.Context, db *gorm.DB, redisGateway *redisinfra.Gateway, task *dto.UnifiedTask, reason string) error { +func rescheduleRestartPedestalTask(ctx context.Context, db *gorm.DB, redisGateway *redis.Gateway, task *dto.UnifiedTask, reason string) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(ctx) @@ -274,7 +274,7 @@ func parseRestartPayload(payload map[string]any) (*restartPayload, error) { // installPedestal installs or upgrades the pedestal using Helm // Priority: Remote (if configured) -> Local fallback (if remote fails and LocalPath is set) -func installPedestal(ctx context.Context, gateway *helminfra.Gateway, releaseName string, namespaceIdx int, item *dto.HelmConfigItem) error { +func installPedestal(ctx context.Context, gateway *helm.Gateway, releaseName string, namespaceIdx int, item *dto.HelmConfigItem) error { return tracing.WithSpan(ctx, func(childCtx context.Context) error { span := trace.SpanFromContext(childCtx) logEntry := logrus.WithFields(logrus.Fields{ diff --git a/src/service/consumer/runtime_deps.go b/src/service/consumer/runtime_deps.go index ed8cd3e2..daa9ebde 100644 --- a/src/service/consumer/runtime_deps.go +++ b/src/service/consumer/runtime_deps.go @@ -1,10 +1,10 @@ package consumer import ( - buildkitinfra "aegis/infra/buildkit" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + buildkit "aegis/infra/buildkit" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" "gorm.io/gorm" ) @@ -15,10 +15,10 @@ type RuntimeDeps struct { RestartRateLimiter *TokenBucketRateLimiter BuildRateLimiter *TokenBucketRateLimiter AlgorithmRateLimiter *TokenBucketRateLimiter - RedisGateway *redisinfra.Gateway - K8sGateway *k8sinfra.Gateway - BuildKitGateway *buildkitinfra.Gateway - HelmGateway *helminfra.Gateway + RedisGateway *redis.Gateway + K8sGateway *k8s.Gateway + BuildKitGateway *buildkit.Gateway + HelmGateway *helm.Gateway FaultBatchManager *FaultBatchManager ExecutionOwner ExecutionOwner InjectionOwner InjectionOwner diff --git a/src/service/consumer/runtime_snapshot.go b/src/service/consumer/runtime_snapshot.go index de997cb8..766450eb 100644 --- a/src/service/consumer/runtime_snapshot.go +++ b/src/service/consumer/runtime_snapshot.go @@ -6,10 +6,10 @@ import ( "time" "aegis/consts" - buildkitinfra "aegis/infra/buildkit" - helminfra "aegis/infra/helm" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + buildkit "aegis/infra/buildkit" + helm "aegis/infra/helm" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" "gorm.io/gorm" ) @@ -41,10 +41,10 @@ type RuntimeStatusSnapshot struct { type RuntimeSnapshotService struct { db *gorm.DB - redis *redisinfra.Gateway - k8s *k8sinfra.Gateway - buildkit *buildkitinfra.Gateway - helm *helminfra.Gateway + redis *redis.Gateway + k8s *k8s.Gateway + buildkit *buildkit.Gateway + helm *helm.Gateway restart *TokenBucketRateLimiter build *TokenBucketRateLimiter algorithm *TokenBucketRateLimiter @@ -52,10 +52,10 @@ type RuntimeSnapshotService struct { func NewRuntimeSnapshotService( db *gorm.DB, - redis *redisinfra.Gateway, - k8s *k8sinfra.Gateway, - buildkit *buildkitinfra.Gateway, - helm *helminfra.Gateway, + redis *redis.Gateway, + k8s *k8s.Gateway, + buildkit *buildkit.Gateway, + helm *helm.Gateway, restart *TokenBucketRateLimiter, build *TokenBucketRateLimiter, algorithm *TokenBucketRateLimiter, @@ -92,9 +92,9 @@ func (s *RuntimeSnapshotService) RuntimeStatus(ctx context.Context) RuntimeStatu } } -func (s *RuntimeSnapshotService) QueueStatus(ctx context.Context) (redisinfra.TaskQueueStats, error) { +func (s *RuntimeSnapshotService) QueueStatus(ctx context.Context) (redis.TaskQueueStats, error) { if s.redis == nil { - return redisinfra.TaskQueueStats{}, fmt.Errorf("redis gateway is nil") + return redis.TaskQueueStats{}, fmt.Errorf("redis gateway is nil") } return s.redis.GetTaskQueueStats(ctx) } diff --git a/src/service/consumer/state_store.go b/src/service/consumer/state_store.go index 52b0772a..7c672966 100644 --- a/src/service/consumer/state_store.go +++ b/src/service/consumer/state_store.go @@ -7,8 +7,8 @@ import ( "aegis/consts" "aegis/dto" - executionmodule "aegis/module/execution" - injectionmodule "aegis/module/injection" + execution "aegis/module/execution" + injection "aegis/module/injection" ) type stateStore struct { @@ -27,7 +27,7 @@ func (s *stateStore) updateExecutionState(ctx context.Context, executionID int, if s.execution == nil { return fmt.Errorf("execution owner service is nil") } - return s.execution.UpdateExecutionState(ctx, &executionmodule.RuntimeUpdateExecutionStateReq{ + return s.execution.UpdateExecutionState(ctx, &execution.RuntimeUpdateExecutionStateReq{ ExecutionID: executionID, State: newState, }) @@ -37,7 +37,7 @@ func (s *stateStore) updateInjectionState(ctx context.Context, injectionName str if s.injection == nil { return fmt.Errorf("injection owner service is nil") } - return s.injection.UpdateInjectionState(ctx, &injectionmodule.RuntimeUpdateInjectionStateReq{ + return s.injection.UpdateInjectionState(ctx, &injection.RuntimeUpdateInjectionStateReq{ Name: injectionName, State: newState, }) @@ -47,7 +47,7 @@ func (s *stateStore) updateInjectionTimestamp(ctx context.Context, injectionName if s.injection == nil { return nil, fmt.Errorf("injection owner service is nil") } - return s.injection.UpdateInjectionTimestamps(ctx, &injectionmodule.RuntimeUpdateInjectionTimestampReq{ + return s.injection.UpdateInjectionTimestamps(ctx, &injection.RuntimeUpdateInjectionTimestampReq{ Name: injectionName, StartTime: startTime, EndTime: endTime, diff --git a/src/service/consumer/task.go b/src/service/consumer/task.go index 4179d57c..e03b42d8 100644 --- a/src/service/consumer/task.go +++ b/src/service/consumer/task.go @@ -19,7 +19,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/redis/go-redis/v9" + goredis "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -169,7 +169,7 @@ func StartScheduler(ctx context.Context, redisGateway *redisinfra.Gateway) { func processDelayedTasks(ctx context.Context, redisGateway *redisinfra.Gateway) { result, err := redisGateway.ProcessDelayedTasks(ctx) - if err != nil && err != redis.Nil { + if err != nil && err != goredis.Nil { logrus.Errorf("scheduler error: %v", err) return } @@ -232,7 +232,7 @@ func ConsumeTasks(ctx context.Context, deps RuntimeDeps) { taskData, err := deps.RedisGateway.GetTask(ctx, 30*time.Second) if err != nil { deps.RedisGateway.ReleaseConcurrencyLock(ctx) - if err == redis.Nil { + if err == goredis.Nil { continue } logrus.Errorf("BRPop error: %v", err) @@ -443,16 +443,16 @@ func CancelTask(redisGateway *redisinfra.Gateway, taskID string) error { queueType, err := redisGateway.GetTaskQueue(ctx, taskID) if err == nil { switch queueType { - case redisinfra.ReadyQueueKey: - if _, err := redisGateway.RemoveFromList(ctx, redisinfra.ReadyQueueKey, taskID); err != nil { + case ReadyQueueKey: + if _, err := redisGateway.RemoveFromList(ctx, ReadyQueueKey, taskID); err != nil { logrus.Warnf("failed to remove from list: %v", err) } - case redisinfra.DelayedQueueKey: - if s := redisGateway.RemoveFromZSet(ctx, redisinfra.DelayedQueueKey, taskID); !s { + case DelayedQueueKey: + if s := redisGateway.RemoveFromZSet(ctx, DelayedQueueKey, taskID); !s { logrus.Warnf("failed to remove from delayed queue: %v", err) } - case redisinfra.DeadLetterKey: - if s := redisGateway.RemoveFromZSet(ctx, redisinfra.DeadLetterKey, taskID); !s { + case DeadLetterKey: + if s := redisGateway.RemoveFromZSet(ctx, DeadLetterKey, taskID); !s { logrus.Warnf("failed to remove from dead letter queue: %v", err) } } @@ -490,7 +490,7 @@ func publishEvent(gateway *redisinfra.Gateway, ctx context.Context, stream strin // Call repository layer for data access if err := publishTraceStreamEvent(gateway, ctx, stream, &event); err != nil { - if err == redis.Nil { + if err == goredis.Nil { logrus.Warnf("No new messages to publish to Redis stream %s", stream) return } diff --git a/src/service/consumer/trace.go b/src/service/consumer/trace.go index 591e853f..7400b3ee 100644 --- a/src/service/consumer/trace.go +++ b/src/service/consumer/trace.go @@ -3,9 +3,9 @@ package consumer import ( "aegis/consts" "aegis/dto" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/model" - groupmodule "aegis/module/group" + group "aegis/module/group" "context" "fmt" "time" @@ -82,7 +82,7 @@ func getEventTypeByTask(taskType consts.TaskType, taskState consts.TaskState) co // updateTraceState updates trace state based on task state change // This function is called after task state is persisted to ensure real-time sync -func updateTraceState(redisGateway *redisinfra.Gateway, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { +func updateTraceState(redisGateway *redis.Gateway, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { logEntry := logrus.WithField("trace_id", traceID).WithField("task_id", taskID) // Update trace state asynchronously to avoid blocking task processing @@ -98,7 +98,7 @@ func updateTraceState(redisGateway *redisinfra.Gateway, db *gorm.DB, traceID, ta } // performTraceStateUpdate performs the actual trace state update with retry logic -func performTraceStateUpdate(redisGateway *redisinfra.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { +func performTraceStateUpdate(redisGateway *redis.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, event *dto.TraceStreamEvent) error { const maxRetries = 3 logEntry := logrus.WithField("trace_id", traceID) @@ -122,7 +122,7 @@ func performTraceStateUpdate(redisGateway *redisinfra.Gateway, ctx context.Conte } // tryUpdateTraceStateCore attempts to update trace state once -func tryUpdateTraceStateCore(redisGateway *redisinfra.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, streamEvent *dto.TraceStreamEvent) error { +func tryUpdateTraceStateCore(redisGateway *redis.Gateway, ctx context.Context, db *gorm.DB, traceID, taskID string, newState consts.TaskState, streamEvent *dto.TraceStreamEvent) error { if db == nil { return fmt.Errorf("trace state update db is nil") } @@ -519,11 +519,11 @@ func isOptimisticLockError(err error) bool { // publishGroupStreamEvent publishes a lightweight event to the group-level Redis stream // when a trace reaches a terminal state (Completed/Failed). // This enables real-time SSE updates for group progress tracking on the frontend. -func publishGroupStreamEvent(redisGateway *redisinfra.Gateway, ctx context.Context, groupID, traceID string, state consts.TraceState, lastEvent consts.EventType) { +func publishGroupStreamEvent(redisGateway *redis.Gateway, ctx context.Context, groupID, traceID string, state consts.TraceState, lastEvent consts.EventType) { streamKey := fmt.Sprintf(consts.StreamGroupLogKey, groupID) logEntry := logrus.WithField("group_id", groupID).WithField("trace_id", traceID) - event := &groupmodule.GroupStreamEvent{ + event := &group.GroupStreamEvent{ TraceID: traceID, State: state, LastEvent: lastEvent, diff --git a/src/service/initialization/consumer.go b/src/service/initialization/consumer.go index 26f5667b..5a25fc3e 100644 --- a/src/service/initialization/consumer.go +++ b/src/service/initialization/consumer.go @@ -7,8 +7,8 @@ import ( "aegis/config" "aegis/consts" - k8sinfra "aegis/infra/k8s" - redisinfra "aegis/infra/redis" + k8s "aegis/infra/k8s" + redis "aegis/infra/redis" "aegis/model" "aegis/service/common" "aegis/service/consumer" @@ -20,9 +20,9 @@ import ( func InitializeConsumer( ctx context.Context, db *gorm.DB, - controller *k8sinfra.Controller, + controller *k8s.Controller, monitor consumer.NamespaceMonitor, - publisher *redisinfra.Gateway, + publisher *redis.Gateway, listener *common.ConfigUpdateListener, restartLimiter *consumer.TokenBucketRateLimiter, buildLimiter *consumer.TokenBucketRateLimiter, diff --git a/src/service/initialization/producer.go b/src/service/initialization/producer.go index dc6cafaf..c51a32f4 100644 --- a/src/service/initialization/producer.go +++ b/src/service/initialization/producer.go @@ -7,11 +7,11 @@ import ( "aegis/config" "aegis/consts" - redisinfra "aegis/infra/redis" + redis "aegis/infra/redis" "aegis/model" - containermodule "aegis/module/container" - datasetmodule "aegis/module/dataset" - labelmodule "aegis/module/label" + container "aegis/module/container" + dataset "aegis/module/dataset" + label "aegis/module/label" "aegis/service/common" "aegis/utils" @@ -32,7 +32,7 @@ func (r permMeta) String() string { return fmt.Sprintf("%v %v %v", r.action, r.resourceScope, r.resourceName) } -func InitializeProducer(db *gorm.DB, publisher *redisinfra.Gateway, listener *common.ConfigUpdateListener) error { +func InitializeProducer(db *gorm.DB, publisher *redis.Gateway, listener *common.ConfigUpdateListener) error { producerData, err := newConfigDataWithDB(db, consts.ConfigScopeProducer) if err != nil { return fmt.Errorf("failed to load producer config metadata: %w", err) @@ -349,11 +349,11 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { dataPath := config.GetString("initialization.data_path") for _, containerData := range data.Containers { - container := containerData.ConvertToDBContainer() - if container.Type == consts.ContainerTypePedestal { - system := chaos.SystemType(container.Name) + containerModel := containerData.ConvertToDBContainer() + if containerModel.Type == consts.ContainerTypePedestal { + system := chaos.SystemType(containerModel.Name) if !system.IsValid() { - return fmt.Errorf("invalid pedestal name: %s", container.Name) + return fmt.Errorf("invalid pedestal name: %s", containerModel.Name) } } @@ -387,17 +387,17 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { versions = append(versions, *version) } - container.Versions = versions + containerModel.Versions = versions - createdContainer, err := containermodule.NewRepository(tx).CreateContainerCore(container, userID) + createdContainer, err := container.NewRepository(tx).CreateContainerCore(containerModel, userID) if err != nil { return fmt.Errorf("failed to create container %s: %w", containerData.Name, err) } if createdContainer.Type == consts.ContainerTypePedestal { - if err := containermodule.NewRepository(tx).UploadHelmValueFileFromPath( + if err := container.NewRepository(tx).UploadHelmValueFileFromPath( containerData.Name, - container.Versions[0].HelmConfig, + containerModel.Versions[0].HelmConfig, filepath.Join(dataPath, fmt.Sprintf("%s.yaml", createdContainer.Name)), ); err != nil { return fmt.Errorf("failed to upload helm value file for container %s: %w", containerData.Name, err) @@ -410,7 +410,7 @@ func initializeContainers(tx *gorm.DB, data *InitialData, userID int) error { func initializeDatasets(tx *gorm.DB, data *InitialData, userID int) error { for _, datasetData := range data.Datasets { - dataset := datasetData.ConvertToDBDataset() + datasetModel := datasetData.ConvertToDBDataset() versions := make([]model.DatasetVersion, 0, len(datasetData.Versions)) for _, versionData := range datasetData.Versions { @@ -418,7 +418,7 @@ func initializeDatasets(tx *gorm.DB, data *InitialData, userID int) error { versions = append(versions, *version) } - _, err := datasetmodule.NewRepository(tx).CreateDatasetCore(dataset, versions, userID) + _, err := dataset.NewRepository(tx).CreateDatasetCore(datasetModel, versions, userID) if err != nil { return fmt.Errorf("failed to create dataset %s: %w", datasetData.Name, err) } @@ -437,7 +437,7 @@ func initializeExecutionLabels(tx *gorm.DB) error { } for _, labelInfo := range sourceLabels { - _, err := labelmodule.NewRepository(tx).CreateLabelCore(tx, &model.Label{ + _, err := label.NewRepository(tx).CreateLabelCore(tx, &model.Label{ Key: consts.ExecutionLabelSource, Value: labelInfo.value, Category: consts.ExecutionCategory,