openapi: 3.0.3
info:
  title: Workflows API
  version: "1.0.0"
  description: |
    REST API сервиса **workflows-api** для работы с workflow (пайплайнами задач):
    создание, получение, перезапуск задач, отмена запусков, приоритизация и получение
    позиции в очереди.

    ### Технологии
    - Язык: Go 1.24, HTTP-фреймворк **Fiber v2**.
    - JSON-сериализация: `bytedance/sonic`.
    - Хранилище: PostgreSQL (пул `pgx/v5`).
    - Трейсинг: OpenTelemetry (`otelfiber`), включается переменной `TRACER_USE`.

    ### Группы маршрутов
    Все обработчики регистрируются дважды — в двух группах с одинаковым набором путей
    (см. `internal/server/server.go` и `internal/controller/http/v1/workflows/routes.go`):

    - **`/api/v1/...`** — публичная группа. На префикс `/api` навешен middleware аутентификации
      (`auth.AuthMiddleware`): требуется валидный JWT. Для запросов выставляется `is_internal=false`
      и проверяется принадлежность пользователя к компании.
    - **`/internal/v1/...`** — внутренняя группа (сервис-сервис) без аутентификации; трактуется как
      доверенная (`is_internal=true`), проверки принадлежности к компании пропускаются.

    Отдельно, вне групп и без авторизации, доступен health-check **`GET /ping`**.

    В этом документе пути описаны относительно базового префикса группы `/api/v1`
    (см. `servers`). Те же пути доступны и под `/internal/v1`.

    ### Аутентификация
    Группа `/api` защищена middleware, который принимает один из двух токенов:
    - **`Authorization: Bearer <JWT>`** — токен Sarex, подпись проверяется публичным ключом
      из переменной `PUBLIC_KEY` (RS/PKIX). Из claims извлекаются `user_id`, `company_ids`,
      `is_superuser`.
    - **`Identity: Bearer <JWT>`** — токен Zitadel (проверяется без верификации подписи,
      `ParseUnverified`); данные пользователя берутся из claim
      `urn:zitadel:iam:user:metadata` (поля `id`, `company_ids`, `is_superuser`).

    Если присутствует заголовок `Identity`, используется он; иначе — `Authorization`.
    Часть операций (`prioritize`, `move_to_super_high_resources`) доступна только суперпользователю
    (`is_superuser=true`).

    ### Пагинация
    Метод `GET /workflows` поддерживает `limit` и `offset` (query-параметры, строки).
    Ответ содержит `items`, а также `limit`, `offset`, `total`.

    ### Обработка ошибок
    Ошибки возвращаются в JSON вида:
    ```json
    { "message": "human readable message", "error_code": "WF-0001" }
    ```
    Коды (`internal/app_errors`): `WF-0000` (system, 500), `WF-0001` (not found, 404),
    `WF-0002` (no auth, 401), `WF-0003` (no access), `WF-0004` (invalid, 400),
    `WF-0005` (workflow config error / forbidden, 400/403).
    HTTP-статус выбирается в `ErrorHandler` фреймворка по типу ошибки: 400 (ошибки парсинга/валидации/
    конфигурации workflow), 401 (нет/некорректный токен), 403 (forbidden), 404 (не найдено), 500 (прочее).

    ### Замечания (расхождения кода и существующей схемы)
    Документ приведён в соответствие с реальными маршрутами `routes.go`. Отличия от старого
    `openapi_schema.yaml`:
    - **Добавлены** отсутствовавшие маршруты: `POST /workflows/batch`, `GET /workflows/{workflow_id}/position`,
      `POST /workflows/{workflow_id}/prioritize`, `POST /tasks/{task_id}/move_to_super_high_resources`.
    - **Удалён** маршрут `GET /tasks-runs/{task_run_id}/logs` — в коде такого обработчика нет.
      (Внимание: `workflows-frontend` этот эндпоинт вызывает — см. `workflows-frontend.ENDPOINTS.md`.)
    - `POST /tasks-runs/{task_run_id}/cancel` фактически возвращает пустой объект `{}` (в коде
      `c.JSON(&struct{}{})`), а не объект task_run.
    - У задачи (`Task`) в модели есть поля `execution`, `services`, `layer`, `id`, `workflow_id`,
      а у workflow — `document_id`, которые в старой схеме отсутствовали.
servers:
  - url: 'http://localhost:8000/api/v1'
    description: Локальный сервер (дефолт HTTP_HOST / docker-compose)
  - url: 'http://workflows-service/api/v1'
    description: Внутрикластерный сервис (preprod/production; stage — workflows-api-service:8000)
  - url: 'https://stage-api.sarex.io/workflows/api/v1'
    description: Публичный stage (через ingress, префикс /workflows)
  - url: 'https://api.sarex.io/workflows/api/v1'
    description: Публичный production (через ingress, префикс /workflows)
tags:
  - name: workflows
    description: Операции с workflow
  - name: tasks
    description: Операции с задачами workflow
  - name: task-runs
    description: Операции с запусками задач
  - name: health
    description: Проверка доступности сервиса
paths:
  /ping:
    get:
      tags: [health]
      summary: Health-check
      description: >
        Проверка готовности сервиса. Зарегистрирован вне групп `/api` и `/internal`,
        без аутентификации (реальный путь — `/ping`, без префикса `/api/v1`).
      operationId: Ping
      responses:
        '200':
          description: Сервис готов
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ready

  /companies/{company_id}/workflows:
    post:
      tags: [workflows]
      summary: Создать workflow
      description: >
        Создаёт workflow для компании. Для группы `/api` пользователь должен принадлежать
        компании `company_id`. Задачи не должны содержать поле `services` — используется
        `service_requests`. Если у задачи не задан `backoff_limit`, он проставляется равным 5.
        Поле `valid_until` должно быть в будущем.
      operationId: CreateWorkflow
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: company_id
          in: path
          required: true
          description: Идентификатор компании
          schema:
            type: integer
            example: 1
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkflowRequest'
      responses:
        '200':
          description: Созданный workflow
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows:
    get:
      tags: [workflows]
      summary: Список workflow по компаниям
      description: >
        Возвращает список workflow для указанных компаний. Параметр `company_ids` —
        обязательная строка с идентификаторами через запятую. Для группы `/api`
        не-суперпользователю возвращаются только его компании.
      operationId: ListByCompanyID
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: company_ids
          in: query
          required: true
          description: Идентификаторы компаний через запятую
          schema:
            type: string
            example: "1,2,3"
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            example: 100
        - name: offset
          in: query
          required: false
          schema:
            type: integer
            example: 0
      responses:
        '200':
          description: Список workflow
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowsResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows/batch:
    post:
      tags: [workflows]
      summary: Получить workflow пачкой
      description: Возвращает workflow по списку идентификаторов.
      operationId: GetWorkflows
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetBatchWorkflowsRequest'
      responses:
        '200':
          description: Найденные workflow
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkflowsBatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows/{id}:
    get:
      tags: [workflows]
      summary: Получить workflow по ID
      operationId: GetWorkflow
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: id
          in: path
          required: true
          description: UUID workflow
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Workflow
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows/{workflows_ids}/state:
    get:
      tags: [workflows]
      summary: Состояния нескольких workflow
      description: >
        Возвращает отображение `workflow_id -> state` для списка workflow.
        `workflows_ids` — UUID через запятую.
      operationId: GetStates
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: workflows_ids
          in: path
          required: true
          description: UUID workflow через запятую
          schema:
            type: string
          example: "3fa85f64-5717-4562-b3fc-2c963f66afa6,4fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        '200':
          description: Отображение id -> состояние
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: '#/components/schemas/CompletenessState'
              example: { "3fa85f64-5717-4562-b3fc-2c963f66afa6": "done" }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows/{workflow_id}/position:
    get:
      tags: [workflows]
      summary: Позиция workflow в очереди
      description: Возвращает позицию workflow в очереди на выполнение (ограничение 100).
      operationId: GetWorkflowQueuePosition
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Позиция в очереди
          content:
            application/json:
              schema:
                type: object
                properties:
                  position:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /workflows/{workflow_id}/prioritize:
    post:
      tags: [workflows]
      summary: Приоритизировать workflow
      description: Повышает приоритет workflow. Доступно только суперпользователю.
      operationId: PrioritizeWorkflow
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: workflow_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Успешно, тело отсутствует
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/InternalError'

  /tasks/{task_id}/restart:
    post:
      tags: [tasks]
      summary: Перезапустить задачу
      description: >
        Перезапускает задачу, опционально переопределяя `inputs`, `outputs`, `parameters`,
        `valid_until`. Возвращает обновлённый workflow.
      operationId: RestartTask
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: task_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RestartTaskRequest'
      responses:
        '200':
          description: Обновлённый workflow
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workflow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /tasks/{task_id}/move_to_super_high_resources:
    post:
      tags: [tasks]
      summary: Перевести задачу на super-high-resources
      description: Перемещает задачу на пул ресурсов super-high-resources. Только суперпользователь.
      operationId: MoveTaskToSuperHighResources
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: task_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Успешно, тело отсутствует
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

  /tasks-runs/{task_run_id}/cancel:
    post:
      tags: [task-runs]
      summary: Отменить запуск задачи
      description: Отменяет запуск задачи (task run). Возвращает пустой объект.
      operationId: CancelTaskRun
      parameters:
        - $ref: '#/components/parameters/AuthorizationHeader'
        - name: task_run_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Успешно (пустой объект)
          content:
            application/json:
              schema:
                type: object
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  parameters:
    AuthorizationHeader:
      name: Authorization
      in: header
      required: true
      description: >
        `Bearer <JWT>` — токен Sarex. Альтернативно можно передать заголовок
        `Identity: Bearer <JWT>` (токен Zitadel). Не требуется для группы `/internal`.
      schema:
        type: string
        example: "Bearer eyJhbGciOi..."

  responses:
    BadRequest:
      description: Некорректный запрос (парсинг/валидация/конфигурация workflow)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AppError'
    Unauthorized:
      description: Не авторизован (нет/некорректный токен)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AppError'
    Forbidden:
      description: Доступ запрещён
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AppError'
    NotFound:
      description: Ресурс не найден
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AppError'
    InternalError:
      description: Внутренняя ошибка
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AppError'

  schemas:
    AppError:
      type: object
      properties:
        message:
          type: string
          example: "not found workflow"
        error_code:
          type: string
          description: "Код ошибки: WF-0000..WF-0005"
          example: "WF-0001"

    CompletenessState:
      type: string
      description: Состояние workflow
      enum: [done, running, error]

    ExecutionState:
      type: string
      description: Состояние запуска задачи (task run)
      enum: [pending, done, canceling, canceled, idle, running, error, lost]

    Description:
      type: object
      description: Описание входа/выхода задачи (источник/приёмник данных)
      required: [type, path]
      properties:
        type:
          type: string
          maxLength: 32
          description: "Тип хранилища (валидация: google, local, s3, s3v2, srx-tmp, url, pdm)"
          example: s3
        path:
          type: string
          maxLength: 512

    ServicePublicDescription:
      type: object
      properties:
        type:
          type: string
        kind:
          type: string

    Resources:
      type: object
      properties:
        cpu_limits:
          type: string
        memory_limits:
          type: string
        cpu_requests:
          type: string
        memory_requests:
          type: string

    Execution:
      type: object
      properties:
        executor:
          type: string
          description: "Исполнитель задачи (допустимые: k8s, amqp)"
          enum: [k8s, amqp]
        resources:
          $ref: '#/components/schemas/Resources'

    Task:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workflow_id:
          type: string
          format: uuid
        slug:
          type: string
        docker_image:
          type: string
        backoff_limit:
          type: integer
          format: int32
          description: "По умолчанию 5, если не задан"
        inputs:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Description'
        outputs:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Description'
        parameters:
          type: object
          additionalProperties: true
        service_requests:
          type: array
          items:
            type: string
        services:
          type: array
          items:
            $ref: '#/components/schemas/ServicePublicDescription'
        execution:
          $ref: '#/components/schemas/Execution'
        needs:
          type: array
          items:
            type: string
        layer:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TaskRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        task_id:
          type: string
          format: uuid
        workflow_id:
          type: string
          format: uuid
        state:
          $ref: '#/components/schemas/ExecutionState'
        reason:
          type: string
          nullable: true
        progress:
          type: integer
          format: int32
          nullable: true
        total_time:
          type: integer
          format: int32
          nullable: true
        logs_paths:
          type: array
          items:
            type: string
        logs_storage:
          type: string
          nullable: true
        logs_last_gathered:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Workflow:
      type: object
      properties:
        id:
          type: string
          format: uuid
        company_id:
          type: integer
        state:
          $ref: '#/components/schemas/CompletenessState'
        name:
          type: string
        valid_until:
          type: string
          format: date-time
        document_id:
          type: integer
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/Task'
        task_runs:
          type: array
          items:
            $ref: '#/components/schemas/TaskRun'

    CreateWorkflowRequest:
      type: object
      required: [name, valid_until]
      properties:
        name:
          type: string
        valid_until:
          type: string
          format: date-time
        document_id:
          type: integer
          nullable: true
        tasks:
          type: array
          items:
            $ref: '#/components/schemas/Task'

    RestartTaskRequest:
      type: object
      properties:
        inputs:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Description'
        outputs:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/Description'
        parameters:
          type: object
          additionalProperties: true
        valid_until:
          type: string
          format: date-time

    GetBatchWorkflowsRequest:
      type: object
      required: [workflows_ids]
      properties:
        workflows_ids:
          type: array
          items:
            type: string
            format: uuid

    WorkflowsBatchResponse:
      type: object
      properties:
        workflows:
          type: array
          items:
            $ref: '#/components/schemas/Workflow'

    WorkflowsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Workflow'
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
