diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index de9eaf2..5962678 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,35 +3,74 @@ on: pull_request: branches: [master] jobs: - build: + unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - uses: actions/setup-go@v5 with: - go-version: "^1.21" - - name: Extract branch name - id: extract_branch - run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV - - uses: paulhatch/semantic-version@v5.3.0 - id: version + go-version: "^1.24" + - run: make test + name: Unit tests + + integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - version_format: "${major}.${minor}.${patch}-${{ env.BRANCH_NAME }}${increment}" - #- name: Setup tmate session - # uses: mxschmitt/action-tmate@v3 + go-version: "^1.24" - run: make test-integration-docker name: Run integration tests inside Docker - #- name: Setup tmate session - # uses: mxschmitt/action-tmate@v3 - - run: make test - name: Unit tests + validate-api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "^1.24" + - run: make validate-api + name: Validate API + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "^1.24" - run: make build name: Build - run: make build-plugins name: Build Plugins + - uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + bin/druid + bin/druid_rcon + bin/druid_rcon_web_rust + .docker/entrypoint.sh + .docker/druid-install-command.sh + + prerelease: + runs-on: ubuntu-latest + needs: [unit-tests, integration-tests, validate-api, build] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Extract branch name + id: extract_branch + run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV + - uses: paulhatch/semantic-version@v5.3.0 + id: version + with: + version_format: "${major}.${minor}.${patch}-${{ env.BRANCH_NAME }}${increment}" + - uses: actions/download-artifact@v4 + with: + name: build-artifacts - name: Prerelease uses: softprops/action-gh-release@v2 with: diff --git a/.vscode/launch.json b/.vscode/launch.json index 70d1bb6..6b28b24 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -231,6 +231,20 @@ "9190" ], }, + { + "name": "Debug Daemon run install ark", + "type": "go", + "request": "launch", + "mode": "debug", + "console": "integratedTerminal", + "program": "${workspaceFolder}/main.go", + "args": [ + "run", + "install", + "--cwd", + "${workspaceFolder}/examples/ark" + ], + }, { "name": "Debug Daemon serve (emtpy dir)", "type": "go", diff --git a/Makefile b/Makefile index a3c621b..a677434 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,18 @@ VERSION ?= "dev" -build: ## Build Daemon +generate-api: ## Generate API types from OpenAPI spec + @echo "Generating API types from OpenAPI spec..." + @which oapi-codegen > /dev/null || (echo "Installing oapi-codegen..." && go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest) + oapi-codegen -config api/oapi-codegen.yaml api/openapi.yaml + +validate-api: ## Validate OpenAPI spec + @echo "Validating OpenAPI spec..." + @which oapi-codegen > /dev/null || (echo "Installing oapi-codegen..." && go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest) + oapi-codegen -config api/oapi-codegen.yaml api/openapi.yaml > /dev/null + @echo "✓ OpenAPI spec is valid" + +build: generate-api ## Build Daemon CGO_ENABLED=0 go build -ldflags "-X github.com/highcard-dev/daemon/internal.Version=$(VERSION)" -o ./bin/druid build-x86-docker: @@ -18,11 +29,6 @@ build-plugins: ## Build Plugins proto: protoc --go_out=paths=source_relative:./ --go-grpc_out=paths=source_relative:./ --go-grpc_opt=paths=source_relative plugin/proto/*.proto -generate-swagger: - swag init -g ./main.go --overridesFile override.swag - -serve-swagger: generate-swagger - npx serve ./docs generate-md-docs: go run ./docs_md/main.go diff --git a/api/oapi-codegen.yaml b/api/oapi-codegen.yaml new file mode 100644 index 0000000..3d4cf9c --- /dev/null +++ b/api/oapi-codegen.yaml @@ -0,0 +1,6 @@ +package: api +output: internal/api/generated.go +generate: + fiber-server: true + models: true + embedded-spec: true diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..fd4e8ab --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,964 @@ +openapi: 3.1.0 +info: + title: Druid CLI + version: 0.1.0 + description: | + Druid CLI is a process runner that launches and manages various sorts of + applications, like gameservers, databases or webservers. + contact: {} + +servers: + - url: / + description: Druid CLI API + +tags: + - name: scroll + description: Scroll management and command execution + - name: logs + description: Log streaming and retrieval + - name: metrics + description: Process metrics and monitoring + - name: process + description: Process management + - name: queue + description: Command queue management + - name: websocket + description: WebSocket connections and tokens + - name: port + description: Port information and status + - name: health + description: Health checks and status + - name: coldstarter + description: Cold start operations + - name: daemon + description: Daemon lifecycle management + - name: watch + description: File watching and development mode + - name: annotations + description: Annotation file access + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT token from identity provider + + tokenAuth: + type: apiKey + in: query + name: token + description: Short-lived query token for WebSocket connections + + schemas: + # Request Types + StartCommandRequest: + type: object + required: + - command + properties: + command: + type: string + description: The command ID to execute + example: "start" + sync: + type: boolean + default: false + description: Whether to run synchronously (wait for completion) + + StartProcedureRequest: + type: object + required: + - mode + - data + - process + properties: + mode: + type: string + description: The procedure mode (e.g., "stdin", or plugin mode) + example: "stdin" + data: + type: string + description: The data payload for the procedure + process: + type: string + description: The process name to run the procedure against + dependencies: + type: array + items: + type: string + description: List of dependency IDs this procedure depends on + sync: + type: boolean + default: false + description: Whether to run synchronously + + WatchModeRequest: + type: object + properties: + hotReloadCommands: + type: array + items: + type: string + description: Commands to run when files change + buildCommands: + type: array + items: + type: string + description: Build commands to run before hot reload + + # Response Types + TokenResponse: + type: object + required: + - token + properties: + token: + type: string + description: The generated authentication token + + ConsolesResponse: + type: object + required: + - consoles + properties: + consoles: + type: object + additionalProperties: + $ref: '#/components/schemas/Console' + + HealthResponse: + type: object + required: + - mode + properties: + mode: + type: string + description: Current health status mode + example: "ok" + progress: + type: number + format: float + minimum: 0 + maximum: 100 + description: Progress percentage for loading operations + start_date: + type: string + format: date-time + nullable: true + description: When the daemon started + + WatchStatusResponse: + type: object + required: + - enabled + - watchedPaths + properties: + enabled: + type: boolean + description: Whether watch mode is currently enabled + watchedPaths: + type: array + items: + type: string + description: List of currently watched file paths + + WatchModeResponse: + type: object + required: + - status + - enabled + properties: + status: + type: string + description: Result status of the operation + example: "success" + enabled: + type: boolean + description: Current watch mode state + + ErrorResponse: + type: object + required: + - status + - error + properties: + status: + type: string + example: "error" + error: + type: string + description: Error message + + ScrollLogStream: + type: object + required: + - key + - log + properties: + key: + type: string + description: The log stream identifier + log: + type: array + items: + type: string + description: Array of log lines + + ProcessesResponse: + type: object + required: + - processes + properties: + processes: + type: object + additionalProperties: + $ref: '#/components/schemas/Process' + + # Domain Types + ScrollFile: + type: object + description: Scroll configuration file structure + properties: + name: + type: string + description: Scroll name + desc: + type: string + description: Scroll description + version: + type: string + description: Scroll version (semver) + example: "1.0.0" + app_version: + type: string + description: Application version (not necessarily semver) + init: + type: string + description: Initialization command + ports: + type: array + items: + $ref: '#/components/schemas/Port' + keepAlivePPM: + type: integer + description: Keep alive packets per minute + commands: + type: object + additionalProperties: + $ref: '#/components/schemas/CommandInstructionSet' + plugins: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + cronjobs: + type: array + items: + $ref: '#/components/schemas/Cronjob' + + Port: + type: object + required: + - port + - protocol + - name + properties: + port: + type: integer + description: Port number + example: 8080 + protocol: + type: string + description: Network protocol + example: "tcp" + name: + type: string + description: Port name/identifier + sleep_handler: + type: string + nullable: true + description: Handler to call when port becomes inactive + mandatory: + type: boolean + description: Whether this port must be open for health check + vars: + type: array + items: + $ref: '#/components/schemas/ColdStarterVars' + start_delay: + type: integer + description: Delay in seconds before starting port check + finish_after_command: + type: string + description: Command to run after port is available + check_activity: + type: boolean + description: Whether to monitor port activity + description: + type: string + description: Port description + + AugmentedPort: + type: object + required: + - port + - protocol + - name + - inactive_since + - inactive_since_sec + - open + properties: + port: + type: integer + description: Port number + protocol: + type: string + description: Network protocol + name: + type: string + description: Port name/identifier + sleep_handler: + type: string + nullable: true + mandatory: + type: boolean + vars: + type: array + items: + $ref: '#/components/schemas/ColdStarterVars' + start_delay: + type: integer + finish_after_command: + type: string + check_activity: + type: boolean + description: + type: string + inactive_since: + type: string + format: date-time + description: When the port became inactive + inactive_since_sec: + type: integer + description: Seconds since port became inactive + open: + type: boolean + description: Whether the port is currently open + + Console: + type: object + required: + - type + - inputMode + properties: + type: + type: string + enum: [tty, process, plugin] + description: Console type + inputMode: + type: string + description: Input mode for the console + exit: + type: integer + nullable: true + description: Exit code if console has exited + + Process: + type: object + required: + - name + - type + properties: + name: + type: string + description: Process name/identifier + type: + type: string + description: Process type + + ProcessTreeRoot: + type: object + required: + - root + - total_memory_rss + - total_memory_vms + - total_memory_swap + - total_io_counters_read + - total_io_counters_write + - total_cpu_percent + - total_process_count + properties: + root: + $ref: '#/components/schemas/ProcessTreeNode' + total_memory_rss: + type: integer + format: int64 + total_memory_vms: + type: integer + format: int64 + total_memory_swap: + type: integer + format: int64 + total_io_counters_read: + type: integer + format: int64 + total_io_counters_write: + type: integer + format: int64 + total_cpu_percent: + type: number + format: double + total_process_count: + type: integer + + ProcessTreeNode: + type: object + properties: + process: + type: string + description: Process information (simplified from gopsutil) + memory: + type: string + description: Memory statistics + memory_ex: + type: string + description: Extended memory statistics + io_counters: + type: string + description: I/O counters + cpu_percent: + type: number + format: double + name: + type: string + gids: + type: array + items: + type: integer + username: + type: string + cmdline: + type: string + children: + type: array + items: + $ref: '#/components/schemas/ProcessTreeNode' + + ProcessMonitorMetrics: + type: object + required: + - cpu + - memory + - connections + - pid + properties: + cpu: + type: number + format: double + description: CPU usage percentage + memory: + type: integer + description: Memory usage in bytes + connections: + type: array + items: + type: string + description: Active network connections + pid: + type: integer + description: Process ID + + CommandInstructionSet: + type: object + required: + - procedures + properties: + dependencies: + type: array + items: + type: string + procedures: + type: array + items: + $ref: '#/components/schemas/Procedure' + needs: + type: array + items: + type: string + run: + type: string + enum: [always, once, restart, persistent] + description: Run mode for the command + + Procedure: + type: object + required: + - mode + properties: + mode: + type: string + description: Procedure execution mode + id: + type: string + nullable: true + description: Unique procedure identifier + wait: + oneOf: + - type: string + - type: integer + - type: boolean + description: Wait condition + data: + description: Procedure data payload + ignore_failure: + type: boolean + description: Whether to continue on failure + + Cronjob: + type: object + required: + - name + - schedule + - command + properties: + name: + type: string + schedule: + type: string + description: Cron schedule expression + example: "0 * * * *" + command: + type: string + + ColdStarterVars: + type: object + required: + - name + - value + properties: + name: + type: string + value: + type: string + + ScrollLockStatus: + type: string + enum: + - running + - done + - error + - waiting + description: Status of a command in the queue + + QueueResponse: + type: object + description: Map of command IDs to their execution status + additionalProperties: + $ref: '#/components/schemas/ScrollLockStatus' + +paths: + # Scroll Endpoints + /api/v1/scroll: + get: + operationId: getScroll + summary: Get current scroll + description: Returns the currently loaded scroll configuration + tags: [scroll, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Current scroll file + content: + application/json: + schema: + $ref: '#/components/schemas/ScrollFile' + /api/v1/scroll/commands/{command}: + put: + operationId: addCommand + summary: Add command to current scroll + description: Adds a tempoary command to current scroll, useful to add temporary functionality (e.g. used for developer mode at druid.gg) + tags: [scroll, daemon] + security: + - bearerAuth: [] + parameters: + - name: command + in: path + required: true + schema: + type: string + description: Command Name + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CommandInstructionSet' + responses: + '201': + description: Command created + /api/v1/command: + post: + operationId: runCommand + summary: Run a command + description: Execute a command from the scroll configuration + tags: [scroll, daemon] + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StartCommandRequest' + responses: + '200': + description: Command completed synchronously + '201': + description: Command started asynchronously + '400': + description: Bad request + '500': + description: Internal server error + + /api/v1/procedure: + post: + operationId: runProcedure + summary: Run a procedure + description: Execute a standalone procedure + tags: [scroll, daemon] + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StartProcedureRequest' + responses: + '200': + description: Procedure completed synchronously + content: + application/json: + schema: + type: object + '201': + description: Procedure started asynchronously + '400': + description: Bad request + + /api/v1/procedures: + get: + operationId: getProcedures + summary: Get procedure statuses + description: Get the status of all running procedures + tags: [scroll, process, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Map of procedure statuses + content: + application/json: + schema: + type: object + additionalProperties: + type: object + + # Log Endpoints + /api/v1/logs: + get: + operationId: listAllLogs + summary: List all log streams + description: Get all available log streams with their content + tags: [logs, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Array of log streams + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ScrollLogStream' + + /api/v1/logs/{stream}: + get: + operationId: listStreamLogs + summary: List logs for a specific stream + description: Get logs for a specific stream identifier + tags: [logs, daemon] + security: + - bearerAuth: [] + parameters: + - name: stream + in: path + required: true + schema: + type: string + description: Stream identifier + responses: + '200': + description: Log stream content + content: + application/json: + schema: + $ref: '#/components/schemas/ScrollLogStream' + '404': + description: Stream not found + + # Metrics Endpoints + /api/v1/metrics: + get: + operationId: getMetrics + summary: Get process metrics + description: Get metrics for all monitored processes + tags: [metrics, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Process metrics map + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/ProcessMonitorMetrics' + + /api/v1/pstree: + get: + operationId: getPsTree + summary: Get process tree + description: Get the process tree for all running processes + tags: [metrics, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Process tree map + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/ProcessTreeRoot' + + # Process Endpoints + /api/v1/processes: + get: + operationId: getProcesses + summary: List running processes + description: Get all currently running processes + tags: [process, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Map of running processes + content: + application/json: + schema: + $ref: '#/components/schemas/ProcessesResponse' + + # Queue Endpoint + /api/v1/queue: + get: + operationId: getQueue + summary: Get command queue + description: Get the current command execution queue + tags: [queue, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Queue status map + content: + application/json: + schema: + $ref: '#/components/schemas/QueueResponse' + + # WebSocket Endpoints + /api/v1/token: + get: + operationId: createToken + summary: Create WebSocket token + description: Generate a short-lived token for WebSocket authentication + tags: [websocket, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Generated token + content: + application/json: + schema: + $ref: '#/components/schemas/TokenResponse' + + /api/v1/consoles: + get: + operationId: getConsoles + summary: List all consoles + description: Get all available console connections + tags: [websocket, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Map of available consoles + content: + application/json: + schema: + $ref: '#/components/schemas/ConsolesResponse' + + # Port Endpoint + /api/v1/ports: + get: + operationId: getPorts + summary: Get port information + description: Get information about all configured ports + tags: [port, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Array of port information + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AugmentedPort' + + # Health Endpoint (authenticated) + /api/v1/health: + get: + operationId: getHealthAuth + summary: Get health status + description: Get daemon health status + tags: [health, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Healthy + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + '503': + description: Service unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + + # Coldstarter Endpoint + /api/v1/coldstarter/finish: + post: + operationId: finishColdstarter + summary: Finish cold start + description: Signal that cold start process is complete + tags: [coldstarter, daemon] + security: + - bearerAuth: [] + responses: + '202': + description: Accepted + + # Daemon Control + /api/v1/daemon/stop: + post: + operationId: stopDaemon + summary: Stop daemon + description: Gracefully stop the daemon + tags: [daemon, daemon] + security: + - bearerAuth: [] + responses: + '201': + description: Stop initiated + + # Watch/Dev Mode Endpoints + /api/v1/watch/enable: + post: + operationId: enableWatch + summary: Enable development mode + description: Start file watching for development + tags: [watch, daemon] + security: + - bearerAuth: [] + requestBody: + required: false + content: + application/json: + schema: + $ref: '#/components/schemas/WatchModeRequest' + responses: + '200': + description: Watch mode enabled + content: + application/json: + schema: + $ref: '#/components/schemas/WatchModeResponse' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '412': + description: Already active + content: + application/json: + schema: + $ref: '#/components/schemas/WatchModeResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/v1/watch/disable: + post: + operationId: disableWatch + summary: Disable development mode + description: Stop file watching + tags: [watch, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Watch mode disabled + content: + application/json: + schema: + $ref: '#/components/schemas/WatchModeResponse' + '500': + description: Internal error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/v1/watch/status: + get: + operationId: getWatchStatus + summary: Get watch mode status + description: Check if watch mode is enabled and what paths are being watched + tags: [watch, daemon] + security: + - bearerAuth: [] + responses: + '200': + description: Watch status + content: + application/json: + schema: + $ref: '#/components/schemas/WatchStatusResponse' diff --git a/cmd/serve.go b/cmd/serve.go index 76f0011..48f6da2 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -131,7 +131,6 @@ to interact and monitor the Scroll Application`, snapshotService := snapshotServices.NewSnapshotService() coldStarter := services.NewColdStarter(portService, queueManager, snapshotService, scrollService.GetDir()) - uiService := services.NewUiService(scrollService) uiDevService := services.NewUiDevService( queueManager, scrollService, ) @@ -144,8 +143,7 @@ to interact and monitor the Scroll Application`, portHandler := handler.NewPortHandler(portService) healthHandler := handler.NewHealthHandler(portService, maxStartupHealthCheckTimeout, snapshotService) coldstarterHandler := handler.NewColdstarterHandler(coldStarter) - uiHandler := handler.NewUiHandler(uiService) - uiDevHandler := handler.NewUiDevHandler(uiDevService, scrollService) + uiDevHandler := handler.NewWatchHandler(uiDevService, scrollService) var annotationHandler *handler.AnnotationHandler = nil @@ -158,7 +156,7 @@ to interact and monitor the Scroll Application`, signalHandler := signals.NewSignalHandler(ctx, queueManager, processManager, nil, shutdownWait) daemonHander := handler.NewDaemonHandler(signalHandler) - s := web.NewServer(jwksUrl, scrollHandler, scrollLogHandler, scrollMetricHandler, annotationHandler, processHandler, queueHandler, websocketHandler, portHandler, healthHandler, coldstarterHandler, daemonHander, authorizer, uiHandler, uiDevHandler, cwd, scrollService.GetDir()) + s := web.NewServer(jwksUrl, scrollHandler, scrollLogHandler, scrollMetricHandler, annotationHandler, processHandler, queueHandler, websocketHandler, portHandler, healthHandler, coldstarterHandler, daemonHander, authorizer, uiDevHandler, cwd, scrollService.GetDir()) a := s.Initialize() diff --git a/cmd/server/web/middlewares/auth_middleware.go b/cmd/server/web/middlewares/auth.go similarity index 100% rename from cmd/server/web/middlewares/auth_middleware.go rename to cmd/server/web/middlewares/auth.go diff --git a/cmd/server/web/middlewares/header_middleware.go b/cmd/server/web/middlewares/header.go similarity index 100% rename from cmd/server/web/middlewares/header_middleware.go rename to cmd/server/web/middlewares/header.go diff --git a/cmd/server/web/middlewares/validation.go b/cmd/server/web/middlewares/validation.go new file mode 100644 index 0000000..258cc67 --- /dev/null +++ b/cmd/server/web/middlewares/validation.go @@ -0,0 +1,125 @@ +package middlewares + +import ( + "bytes" + "context" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" +) + +// OpenAPIValidator middleware validates incoming requests against the OpenAPI specification +type OpenAPIValidator struct { + router routers.Router + spec *openapi3.T +} + +// NewOpenAPIValidator creates a new OpenAPI validation middleware +func NewOpenAPIValidator() (*OpenAPIValidator, error) { + swagger, err := api.GetSwagger() + if err != nil { + return nil, err + } + + // Create router for finding routes + router, err := gorillamux.NewRouter(swagger) + if err != nil { + return nil, err + } + + return &OpenAPIValidator{ + router: router, + spec: swagger, + }, nil +} + +// Middleware returns a Fiber middleware handler that validates requests +func (v *OpenAPIValidator) Middleware() fiber.Handler { + return func(c *fiber.Ctx) error { + // Convert Fiber context to http.Request + req, err := fiberToHTTPRequest(c) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "status": "error", + "error": "Failed to process request", + }) + } + + // Find the route in OpenAPI spec + route, pathParams, err := v.router.FindRoute(req) + if err != nil { + // Route not found in OpenAPI spec, skip validation + return c.Next() + } + + // Validate request + requestValidationInput := &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + Options: &openapi3filter.Options{ + AuthenticationFunc: openapi3filter.NoopAuthenticationFunc, + }, + } + + ctx := context.Background() + if err := openapi3filter.ValidateRequest(ctx, requestValidationInput); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "status": "error", + "error": err.Error(), + }) + } + + return c.Next() + } +} + +// fiberToHTTPRequest converts a Fiber context to a standard http.Request +func fiberToHTTPRequest(c *fiber.Ctx) (*http.Request, error) { + // Get the request body + body := c.Body() + bodyReader := bytes.NewReader(body) + + // Create the HTTP request + method := c.Method() + url := c.OriginalURL() + + // Build full URL with scheme and host + scheme := "http" + if c.Protocol() == "https" { + scheme = "https" + } + fullURL := scheme + "://" + c.Hostname() + url + + req, err := http.NewRequest(method, fullURL, bodyReader) + if err != nil { + return nil, err + } + + // Copy headers + c.Request().Header.VisitAll(func(key, value []byte) { + req.Header.Add(string(key), string(value)) + }) + + // Set Content-Type if present + contentType := c.Get("Content-Type") + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + return req, nil +} + +// MustNewOpenAPIValidator creates a new validator or panics on error +func MustNewOpenAPIValidator() *OpenAPIValidator { + validator, err := NewOpenAPIValidator() + if err != nil { + panic(err) + } + return validator +} diff --git a/cmd/server/web/server.go b/cmd/server/web/server.go index d692e5b..12bf6bd 100644 --- a/cmd/server/web/server.go +++ b/cmd/server/web/server.go @@ -14,13 +14,12 @@ import ( "github.com/highcard-dev/daemon/cmd/server/web/middlewares" constants "github.com/highcard-dev/daemon/internal" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/ports" "github.com/highcard-dev/daemon/internal/utils/logger" "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/net/webdav" - - _ "github.com/highcard-dev/daemon/docs" "go.uber.org/zap" + "golang.org/x/net/webdav" ) type Server struct { @@ -40,8 +39,7 @@ type Server struct { healthHandler ports.HealthHandlerInterface coldstarterHandler ports.ColdstarterHandlerInterface daemonHandler ports.SignalHandlerInterface - uiHandler ports.UiHandlerInterface - uiDevHandler ports.UiDevHandlerInterface + watchHandler ports.WatchHandlerInterface webdavPath string scrollPath string } @@ -60,8 +58,7 @@ func NewServer( coldstarterHandler ports.ColdstarterHandlerInterface, daemonHandler ports.SignalHandlerInterface, authorizerService ports.AuthorizerServiceInterface, - uiHandler ports.UiHandlerInterface, - uiDevHandler ports.UiDevHandlerInterface, + watchHandler ports.WatchHandlerInterface, webdavPath string, scrollPath string, ) *Server { @@ -88,8 +85,7 @@ func NewServer( webdavPath: webdavPath, scrollPath: scrollPath, daemonHandler: daemonHandler, - uiHandler: uiHandler, - uiDevHandler: uiDevHandler, + watchHandler: watchHandler, } if jwlsUrl != "" { @@ -138,13 +134,14 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { // Define websocket routes immediately after creating the group wsRoutes.Get("/serve/:console", websocket.New(s.websocketHandler.HandleProcess)).Name("ws.serve") - wsRoutes.Get("/dev/notify", websocket.New(s.uiDevHandler.NotifyChange)).Name("ws.dev.notify") + wsRoutes.Get("/watch/notify", websocket.New(s.watchHandler.NotifyChange)).Name("ws.watch.notify") // Now create other route groups - v1 := app.Group("/api/v1") - apiRoutes := v1.Group("/") + apiRoutes := app.Group("/") webdavRoutes := app.Group("/webdav") + apiRoutes.Use(middlewares.MustNewOpenAPIValidator().Middleware()) + // Create properly isolated UI route groups privateUiRoutes := app.Group("") publicUiRoutes := app.Group("") @@ -153,42 +150,22 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { apiRoutes.Use(s.jwtMiddleware, s.injectUserMiddleware) webdavRoutes.Use(s.jwtMiddleware, s.injectUserMiddleware) privateUiRoutes.Use(s.jwtMiddleware, s.injectUserMiddleware) - } //Scroll Group - apiRoutes.Get("/scroll", s.scrollHandler.GetScroll).Name("scrolls.current") - apiRoutes.Post("/command", s.scrollHandler.RunCommand).Name("command.start") - apiRoutes.Post("/procedure", s.scrollHandler.RunProcedure).Name("procedure.start") - apiRoutes.Get("/procedures", s.scrollHandler.Procedures).Name("procedures.list") - - //Scroll Logs Group - apiRoutes.Get("/logs", s.scrollLogHandler.ListAllLogs).Name("scrolls.logs") - apiRoutes.Get("/logs/:stream", s.scrollLogHandler.ListStreamLogs).Name("scrolls.log") - - //Authentication Group - apiRoutes.Get("/token", s.websocketHandler.CreateToken).Name("token.create") - - //Metrics Group - apiRoutes.Get("/metrics", s.scrollMetricHandler.Metrics).Name("scrolls.metrics") - apiRoutes.Get("/pstree", s.scrollMetricHandler.PsTree).Name("scrolls.pstree") - - //Processes Group - apiRoutes.Get("/processes", s.processHandler.Processes).Name("processes.list") - - apiRoutes.Get("/queue", s.queueHandler.Queue).Name("queue.list") - - //Websocket Group - apiRoutes.Get("/consoles", s.websocketHandler.Consoles).Name("consoles.list") - - apiRoutes.Post("/coldstarter/finish", s.coldstarterHandler.Finish).Name("coldstarter.finish") - - apiRoutes.Get("/health", s.healthHandler.Health).Name("health-authenticated") - - apiRoutes.Post("/daemon/stop", s.daemonHandler.Stop).Name("daemon.stop") + } - //UI Dev Group - apiRoutes.Post("/dev/enable", s.uiDevHandler.Enable).Name("dev.enable") - apiRoutes.Post("/dev/build", s.uiDevHandler.Build).Name("dev.build") - apiRoutes.Post("/dev/disable", s.uiDevHandler.Disable).Name("dev.disable") - apiRoutes.Get("/dev/status", s.uiDevHandler.Status).Name("dev.status") + // Use the generated RegisterHandlersWithOptions to set up all API routes + api.RegisterHandlersWithOptions(apiRoutes, &apiServer{ + ScrollHandlerInterface: s.scrollHandler, + ScrollLogHandlerInterface: s.scrollLogHandler, + ScrollMetricHandlerInterface: s.scrollMetricHandler, + ProcessHandlerInterface: s.processHandler, + QueueHandlerInterface: s.queueHandler, + WebsocketHandlerInterface: s.websocketHandler, + PortHandlerInterface: s.portHandler, + HealthHandlerInterface: s.healthHandler, + ColdstarterHandlerInterface: s.coldstarterHandler, + SignalHandlerInterface: s.daemonHandler, + WatchHandlerInterface: s.watchHandler, + }, api.FiberServerOptions{}) // Create the WebDAV handler webdavHandler := &webdav.Handler{ @@ -201,13 +178,11 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { apiRoutes.Get("/ports", s.portHandler.GetPorts).Name("ports.list") - publicUiRoutes.Get("/public/index", s.uiHandler.PublicIndex).Name("ui.public_index") publicUiRoutes.Use("/public", filesystem.New(filesystem.Config{ Root: http.Dir(s.scrollPath + "/public"), Browse: false, })) - privateUiRoutes.Get("/private/index", s.uiHandler.PrivateIndex).Name("ui.private_index") privateUiRoutes.Use("/private", filesystem.New(filesystem.Config{ Root: http.Dir(s.scrollPath + "/private"), Browse: false, @@ -218,7 +193,7 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { } app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler())).Name("metrics") - app.Get("/health", s.healthHandler.Health).Name("health") + app.Get("/health", s.healthHandler.GetHealthAuth).Name("health") app.Get("/info", func(ctx *fiber.Ctx) error { return ctx.JSON(fiber.Map{ @@ -237,7 +212,7 @@ func (s *Server) SetAPI(app *fiber.App) *fiber.App { } func (s *Server) SetDaemonRoute(app *fiber.App, signalHandler ports.SignalHandlerInterface) { - app.Post("/stop", signalHandler.Stop).Name("daemon.stop") + app.Post("/stop", signalHandler.StopDaemon).Name("daemon.stop") } func (s *Server) Serve(app *fiber.App, port int) error { @@ -248,3 +223,18 @@ func (s *Server) Serve(app *fiber.App, port int) error { } return nil } + +// apiServer embeds all handler interfaces to implement api.ServerInterface directly +type apiServer struct { + ports.ScrollHandlerInterface + ports.ScrollLogHandlerInterface + ports.ScrollMetricHandlerInterface + ports.ProcessHandlerInterface + ports.QueueHandlerInterface + ports.WebsocketHandlerInterface + ports.PortHandlerInterface + ports.HealthHandlerInterface + ports.ColdstarterHandlerInterface + ports.SignalHandlerInterface + ports.WatchHandlerInterface +} diff --git a/docs/docs.go b/docs/docs.go deleted file mode 100644 index b9a1d64..0000000 --- a/docs/docs.go +++ /dev/null @@ -1,1246 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/api/v1/coldstarter/finish": { - "post": { - "consumes": [ - "*/*" - ], - "tags": [ - "coldstarter", - "druid", - "daemon" - ], - "summary": "Finish Coldstarter", - "operationId": "finishColdStarter", - "responses": { - "202": { - "description": "Accepted" - } - } - } - }, - "/api/v1/command": { - "post": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "runCommand", - "parameters": [ - { - "description": "Scroll Body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.StartScrollRequestBody" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/v1/consoles": { - "get": { - "description": "Get List of all consoles", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "druid", - "daemon", - "console" - ], - "summary": "Get All Consoles", - "operationId": "getConsoles", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ConsolesResponse" - } - } - } - } - }, - "/api/v1/daemon/stop": { - "post": { - "consumes": [ - "*/*" - ], - "tags": [ - "druid", - "daemon" - ], - "summary": "Finish Coldstarter", - "operationId": "stopDaemon", - "responses": { - "202": { - "description": "Accepted" - } - } - } - }, - "/api/v1/dev/build": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Build the UI in development mode", - "operationId": "buildDev", - "responses": { - "200": { - "description": "OK" - }, - "412": { - "description": "Precondition Failed" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/v1/dev/disable": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Disable development mode", - "operationId": "disableDev", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevModeResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ErrorResponse" - } - } - } - } - }, - "/api/v1/dev/enable": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Enable development mode", - "operationId": "enableDev", - "parameters": [ - { - "description": "Optional commands to run on file changes", - "name": "body", - "in": "body", - "schema": { - "$ref": "#/definitions/handler.DevModeBody" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevModeResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ErrorResponse" - } - } - } - } - }, - "/api/v1/dev/status": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Get development mode status", - "operationId": "getDevStatus", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevStatusResponse" - } - } - } - } - }, - "/api/v1/health": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "health", - "druid", - "daemon" - ], - "summary": "Get ports from scroll with additional information", - "operationId": "getHealth", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.HealhResponse" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/handler.HealhResponse" - } - } - } - } - }, - "/api/v1/logs": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "logs", - "druid", - "daemon" - ], - "summary": "List all logs", - "operationId": "listLogs", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/ScrollLogStream" - } - } - } - } - } - }, - "/api/v1/logs/{stream}": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "logs", - "druid", - "daemon" - ], - "summary": "List stream logs", - "operationId": "listLog", - "parameters": [ - { - "type": "string", - "description": "Stream name", - "name": "stream", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ScrollLogStream" - } - } - } - } - }, - "/api/v1/metrics": { - "get": { - "description": "Get the metrics for all processes.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "metrics", - "druid", - "daemon" - ], - "summary": "Get all process metrics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ProcessMonitorMetricsMap" - } - } - } - } - }, - "/api/v1/ports": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "port", - "druid", - "daemon" - ], - "summary": "Get ports from scroll with additional information", - "operationId": "getPorts", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.AugmentedPort" - } - } - } - } - } - }, - "/api/v1/procedure": { - "post": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Run procedure", - "operationId": "runProcedure", - "parameters": [ - { - "description": "Procedure Body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.StartProcedureRequestBody" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object" - } - }, - "201": { - "description": "Created" - } - } - } - }, - "/api/v1/procedures": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "process", - "procedures", - "druid", - "daemon" - ], - "summary": "Get process procedure statuses", - "operationId": "getProcedures", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.ScrollLockStatus" - } - } - } - } - } - }, - "/api/v1/processes": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "process", - "druid", - "daemon" - ], - "summary": "Get running processes", - "operationId": "getRunningProcesses", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.ProcessesBody" - } - } - } - } - }, - "/api/v1/pstree": { - "get": { - "description": "Get pstree of running process", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "metrics", - "druid", - "daemon" - ], - "summary": "Get all process metrics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/PsTreeMap" - } - } - } - } - }, - "/api/v1/queue": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "queue", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "getQueue", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.ScrollLockStatus" - } - } - } - } - } - }, - "/api/v1/scroll": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "getScroll", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ScrollFile" - } - } - } - } - }, - "/api/v1/token": { - "get": { - "description": "Get the metrics for all processes.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "websocket", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "createToken", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/WebsocketToken" - } - } - } - } - }, - "/private/index": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "druid", - "daemon" - ], - "summary": "Serve private UI index", - "operationId": "getPrivateUIIndex", - "responses": { - "200": { - "description": "List of files in private UI directory", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "Private UI directory not found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error with details", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/public/index": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "druid", - "daemon" - ], - "summary": "Serve public UI index", - "operationId": "getPublicUIIndex", - "responses": { - "200": { - "description": "List of files in public UI directory", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "Public UI directory not found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error with details", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - } - }, - "definitions": { - "CommandInstructionSet": { - "type": "object", - "properties": { - "dependencies": { - "type": "array", - "items": { - "type": "string" - } - }, - "needs": { - "type": "array", - "items": { - "type": "string" - } - }, - "procedures": { - "type": "array", - "items": { - "$ref": "#/definitions/Procedure" - } - }, - "run": { - "$ref": "#/definitions/domain.RunMode" - } - } - }, - "Console": { - "type": "object", - "required": [ - "inputMode", - "type" - ], - "properties": { - "exit": { - "type": "integer" - }, - "inputMode": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/domain.ConsoleType" - } - } - }, - "ConsolesResponse": { - "type": "object", - "required": [ - "consoles" - ], - "properties": { - "consoles": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Console" - } - } - } - }, - "DevModeResponse": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "status": { - "type": "string" - } - } - }, - "DevStatusResponse": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "watchedPaths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "Procedure": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "ignore_failure": { - "type": "boolean" - }, - "mode": { - "type": "string" - }, - "wait": {} - } - }, - "ProcessMonitorMetrics": { - "type": "object", - "properties": { - "connections": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu": { - "type": "number" - }, - "memory": { - "type": "integer" - }, - "pid": { - "type": "integer" - } - } - }, - "ProcessMonitorMetricsMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProcessMonitorMetrics" - } - }, - "ProcessTreeNode": { - "type": "object", - "properties": { - "children": { - "type": "array", - "items": { - "$ref": "#/definitions/ProcessTreeNode" - } - }, - "cmdline": { - "type": "string" - }, - "cpu_percent": { - "type": "number" - }, - "gids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "io_counters": { - "type": "string" - }, - "memory": { - "type": "string" - }, - "memory_ex": { - "type": "string" - }, - "name": { - "type": "string" - }, - "process": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "ProcessTreeRoot": { - "type": "object", - "properties": { - "root": { - "$ref": "#/definitions/ProcessTreeNode" - }, - "total_cpu_percent": { - "type": "number" - }, - "total_io_counters_read": { - "type": "integer" - }, - "total_io_counters_write": { - "type": "integer" - }, - "total_memory_rss": { - "type": "integer" - }, - "total_memory_swap": { - "type": "integer" - }, - "total_memory_vms": { - "type": "integer" - }, - "total_process_count": { - "type": "integer" - } - } - }, - "PsTreeMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProcessTreeRoot" - } - }, - "ScrollFile": { - "type": "object", - "properties": { - "app_version": { - "description": "don't make this a semver, it's not allways", - "type": "string" - }, - "commands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - }, - "cronjobs": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Cronjob" - } - }, - "desc": { - "type": "string" - }, - "init": { - "type": "string" - }, - "keepAlivePPM": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Port" - } - }, - "version": { - "type": "string" - } - } - }, - "ScrollLogStream": { - "type": "object", - "required": [ - "key", - "log" - ], - "properties": { - "key": { - "type": "string" - }, - "log": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "WebsocketToken": { - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - }, - "domain.AugmentedPort": { - "type": "object", - "properties": { - "check_activity": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "finish_after_command": { - "type": "string" - }, - "inactive_since": { - "type": "string" - }, - "inactive_since_sec": { - "type": "integer" - }, - "mandatory": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "open": { - "type": "boolean" - }, - "port": { - "type": "integer" - }, - "protocol": { - "type": "string" - }, - "sleep_handler": { - "type": "string" - }, - "start_delay": { - "type": "integer" - }, - "vars": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ColdStarterVars" - } - } - } - }, - "domain.ColdStarterVars": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "domain.ConsoleType": { - "type": "string", - "enum": [ - "tty", - "process", - "plugin" - ], - "x-enum-varnames": [ - "ConsoleTypeTTY", - "ConsoleTypeProcess", - "ConsoleTypePlugin" - ] - }, - "domain.Cronjob": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "name": { - "type": "string" - }, - "schedule": { - "type": "string" - } - } - }, - "domain.Port": { - "type": "object", - "properties": { - "check_activity": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "finish_after_command": { - "type": "string" - }, - "mandatory": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "protocol": { - "type": "string" - }, - "sleep_handler": { - "type": "string" - }, - "start_delay": { - "type": "integer" - }, - "vars": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ColdStarterVars" - } - } - } - }, - "domain.Process": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "domain.RunMode": { - "type": "string", - "enum": [ - "always", - "once", - "restart", - "persistent" - ], - "x-enum-comments": { - "RunModeAlways": "default", - "RunModeOnce": "runs only once", - "RunModePersistent": "restarts on failure and on program restart", - "RunModeRestart": "restarts on failure" - }, - "x-enum-varnames": [ - "RunModeAlways", - "RunModeOnce", - "RunModeRestart", - "RunModePersistent" - ] - }, - "domain.ScrollLockStatus": { - "type": "string", - "enum": [ - "running", - "done", - "error", - "waiting" - ], - "x-enum-varnames": [ - "ScrollLockStatusRunning", - "ScrollLockStatusDone", - "ScrollLockStatusError", - "ScrollLockStatusWaiting" - ] - }, - "handler.DevModeBody": { - "type": "object", - "properties": { - "buildCommands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - }, - "hotReloadCommands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - } - } - }, - "handler.HealhResponse": { - "type": "object", - "properties": { - "mode": { - "type": "string" - }, - "progress": { - "type": "number" - }, - "start_date": { - "type": "string" - } - } - }, - "handler.ProcessesBody": { - "type": "object", - "properties": { - "processes": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.Process" - } - } - } - }, - "handler.StartProcedureRequestBody": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "dependencies": { - "type": "array", - "items": { - "type": "string" - } - }, - "mode": { - "type": "string" - }, - "process": { - "type": "string" - }, - "sync": { - "type": "boolean" - } - } - }, - "handler.StartScrollRequestBody": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "sync": { - "type": "boolean" - } - } - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "0.1.0", - Host: "", - BasePath: "", - Schemes: []string{}, - Title: "Druid CLI", - Description: "Druid CLI is a process runner to launches and manages various sorts of applications, like gameservers, databases or webservers.", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/docs/swagger.json b/docs/swagger.json deleted file mode 100644 index 301f77c..0000000 --- a/docs/swagger.json +++ /dev/null @@ -1,1220 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "Druid CLI is a process runner to launches and manages various sorts of applications, like gameservers, databases or webservers.", - "title": "Druid CLI", - "contact": {}, - "version": "0.1.0" - }, - "paths": { - "/api/v1/coldstarter/finish": { - "post": { - "consumes": [ - "*/*" - ], - "tags": [ - "coldstarter", - "druid", - "daemon" - ], - "summary": "Finish Coldstarter", - "operationId": "finishColdStarter", - "responses": { - "202": { - "description": "Accepted" - } - } - } - }, - "/api/v1/command": { - "post": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "runCommand", - "parameters": [ - { - "description": "Scroll Body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.StartScrollRequestBody" - } - } - ], - "responses": { - "200": { - "description": "OK" - }, - "201": { - "description": "Created" - }, - "400": { - "description": "Bad Request" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/v1/consoles": { - "get": { - "description": "Get List of all consoles", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "druid", - "daemon", - "console" - ], - "summary": "Get All Consoles", - "operationId": "getConsoles", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ConsolesResponse" - } - } - } - } - }, - "/api/v1/daemon/stop": { - "post": { - "consumes": [ - "*/*" - ], - "tags": [ - "druid", - "daemon" - ], - "summary": "Finish Coldstarter", - "operationId": "stopDaemon", - "responses": { - "202": { - "description": "Accepted" - } - } - } - }, - "/api/v1/dev/build": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Build the UI in development mode", - "operationId": "buildDev", - "responses": { - "200": { - "description": "OK" - }, - "412": { - "description": "Precondition Failed" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/v1/dev/disable": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Disable development mode", - "operationId": "disableDev", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevModeResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ErrorResponse" - } - } - } - } - }, - "/api/v1/dev/enable": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Enable development mode", - "operationId": "enableDev", - "parameters": [ - { - "description": "Optional commands to run on file changes", - "name": "body", - "in": "body", - "schema": { - "$ref": "#/definitions/handler.DevModeBody" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevModeResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/ErrorResponse" - } - } - } - } - }, - "/api/v1/dev/status": { - "get": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "dev", - "druid", - "daemon" - ], - "summary": "Get development mode status", - "operationId": "getDevStatus", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/DevStatusResponse" - } - } - } - } - }, - "/api/v1/health": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "health", - "druid", - "daemon" - ], - "summary": "Get ports from scroll with additional information", - "operationId": "getHealth", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.HealhResponse" - } - }, - "503": { - "description": "Service Unavailable", - "schema": { - "$ref": "#/definitions/handler.HealhResponse" - } - } - } - } - }, - "/api/v1/logs": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "logs", - "druid", - "daemon" - ], - "summary": "List all logs", - "operationId": "listLogs", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/ScrollLogStream" - } - } - } - } - } - }, - "/api/v1/logs/{stream}": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "logs", - "druid", - "daemon" - ], - "summary": "List stream logs", - "operationId": "listLog", - "parameters": [ - { - "type": "string", - "description": "Stream name", - "name": "stream", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ScrollLogStream" - } - } - } - } - }, - "/api/v1/metrics": { - "get": { - "description": "Get the metrics for all processes.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "metrics", - "druid", - "daemon" - ], - "summary": "Get all process metrics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ProcessMonitorMetricsMap" - } - } - } - } - }, - "/api/v1/ports": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "port", - "druid", - "daemon" - ], - "summary": "Get ports from scroll with additional information", - "operationId": "getPorts", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.AugmentedPort" - } - } - } - } - } - }, - "/api/v1/procedure": { - "post": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Run procedure", - "operationId": "runProcedure", - "parameters": [ - { - "description": "Procedure Body", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.StartProcedureRequestBody" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object" - } - }, - "201": { - "description": "Created" - } - } - } - }, - "/api/v1/procedures": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "process", - "procedures", - "druid", - "daemon" - ], - "summary": "Get process procedure statuses", - "operationId": "getProcedures", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.ScrollLockStatus" - } - } - } - } - } - }, - "/api/v1/processes": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "process", - "druid", - "daemon" - ], - "summary": "Get running processes", - "operationId": "getRunningProcesses", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.ProcessesBody" - } - } - } - } - }, - "/api/v1/pstree": { - "get": { - "description": "Get pstree of running process", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "metrics", - "druid", - "daemon" - ], - "summary": "Get all process metrics", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/PsTreeMap" - } - } - } - } - }, - "/api/v1/queue": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "queue", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "getQueue", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.ScrollLockStatus" - } - } - } - } - } - }, - "/api/v1/scroll": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scroll", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "getScroll", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ScrollFile" - } - } - } - } - }, - "/api/v1/token": { - "get": { - "description": "Get the metrics for all processes.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "websocket", - "druid", - "daemon" - ], - "summary": "Get current scroll", - "operationId": "createToken", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/WebsocketToken" - } - } - } - } - }, - "/private/index": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "druid", - "daemon" - ], - "summary": "Serve private UI index", - "operationId": "getPrivateUIIndex", - "responses": { - "200": { - "description": "List of files in private UI directory", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "Private UI directory not found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error with details", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/public/index": { - "get": { - "consumes": [ - "*/*" - ], - "produces": [ - "application/json" - ], - "tags": [ - "ui", - "druid", - "daemon" - ], - "summary": "Serve public UI index", - "operationId": "getPublicUIIndex", - "responses": { - "200": { - "description": "List of files in public UI directory", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "Public UI directory not found", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "500": { - "description": "Internal server error with details", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - } - }, - "definitions": { - "CommandInstructionSet": { - "type": "object", - "properties": { - "dependencies": { - "type": "array", - "items": { - "type": "string" - } - }, - "needs": { - "type": "array", - "items": { - "type": "string" - } - }, - "procedures": { - "type": "array", - "items": { - "$ref": "#/definitions/Procedure" - } - }, - "run": { - "$ref": "#/definitions/domain.RunMode" - } - } - }, - "Console": { - "type": "object", - "required": [ - "inputMode", - "type" - ], - "properties": { - "exit": { - "type": "integer" - }, - "inputMode": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/domain.ConsoleType" - } - } - }, - "ConsolesResponse": { - "type": "object", - "required": [ - "consoles" - ], - "properties": { - "consoles": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Console" - } - } - } - }, - "DevModeResponse": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "status": { - "type": "string" - } - } - }, - "DevStatusResponse": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - }, - "watchedPaths": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "ErrorResponse": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "Procedure": { - "type": "object", - "properties": { - "data": {}, - "id": { - "type": "string" - }, - "ignore_failure": { - "type": "boolean" - }, - "mode": { - "type": "string" - }, - "wait": {} - } - }, - "ProcessMonitorMetrics": { - "type": "object", - "properties": { - "connections": { - "type": "array", - "items": { - "type": "string" - } - }, - "cpu": { - "type": "number" - }, - "memory": { - "type": "integer" - }, - "pid": { - "type": "integer" - } - } - }, - "ProcessMonitorMetricsMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProcessMonitorMetrics" - } - }, - "ProcessTreeNode": { - "type": "object", - "properties": { - "children": { - "type": "array", - "items": { - "$ref": "#/definitions/ProcessTreeNode" - } - }, - "cmdline": { - "type": "string" - }, - "cpu_percent": { - "type": "number" - }, - "gids": { - "type": "array", - "items": { - "type": "integer" - } - }, - "io_counters": { - "type": "string" - }, - "memory": { - "type": "string" - }, - "memory_ex": { - "type": "string" - }, - "name": { - "type": "string" - }, - "process": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "ProcessTreeRoot": { - "type": "object", - "properties": { - "root": { - "$ref": "#/definitions/ProcessTreeNode" - }, - "total_cpu_percent": { - "type": "number" - }, - "total_io_counters_read": { - "type": "integer" - }, - "total_io_counters_write": { - "type": "integer" - }, - "total_memory_rss": { - "type": "integer" - }, - "total_memory_swap": { - "type": "integer" - }, - "total_memory_vms": { - "type": "integer" - }, - "total_process_count": { - "type": "integer" - } - } - }, - "PsTreeMap": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProcessTreeRoot" - } - }, - "ScrollFile": { - "type": "object", - "properties": { - "app_version": { - "description": "don't make this a semver, it's not allways", - "type": "string" - }, - "commands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - }, - "cronjobs": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Cronjob" - } - }, - "desc": { - "type": "string" - }, - "init": { - "type": "string" - }, - "keepAlivePPM": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "plugins": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Port" - } - }, - "version": { - "type": "string" - } - } - }, - "ScrollLogStream": { - "type": "object", - "required": [ - "key", - "log" - ], - "properties": { - "key": { - "type": "string" - }, - "log": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "WebsocketToken": { - "type": "object", - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - }, - "domain.AugmentedPort": { - "type": "object", - "properties": { - "check_activity": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "finish_after_command": { - "type": "string" - }, - "inactive_since": { - "type": "string" - }, - "inactive_since_sec": { - "type": "integer" - }, - "mandatory": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "open": { - "type": "boolean" - }, - "port": { - "type": "integer" - }, - "protocol": { - "type": "string" - }, - "sleep_handler": { - "type": "string" - }, - "start_delay": { - "type": "integer" - }, - "vars": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ColdStarterVars" - } - } - } - }, - "domain.ColdStarterVars": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "domain.ConsoleType": { - "type": "string", - "enum": [ - "tty", - "process", - "plugin" - ], - "x-enum-varnames": [ - "ConsoleTypeTTY", - "ConsoleTypeProcess", - "ConsoleTypePlugin" - ] - }, - "domain.Cronjob": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "name": { - "type": "string" - }, - "schedule": { - "type": "string" - } - } - }, - "domain.Port": { - "type": "object", - "properties": { - "check_activity": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "finish_after_command": { - "type": "string" - }, - "mandatory": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "protocol": { - "type": "string" - }, - "sleep_handler": { - "type": "string" - }, - "start_delay": { - "type": "integer" - }, - "vars": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.ColdStarterVars" - } - } - } - }, - "domain.Process": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "domain.RunMode": { - "type": "string", - "enum": [ - "always", - "once", - "restart", - "persistent" - ], - "x-enum-comments": { - "RunModeAlways": "default", - "RunModeOnce": "runs only once", - "RunModePersistent": "restarts on failure and on program restart", - "RunModeRestart": "restarts on failure" - }, - "x-enum-varnames": [ - "RunModeAlways", - "RunModeOnce", - "RunModeRestart", - "RunModePersistent" - ] - }, - "domain.ScrollLockStatus": { - "type": "string", - "enum": [ - "running", - "done", - "error", - "waiting" - ], - "x-enum-varnames": [ - "ScrollLockStatusRunning", - "ScrollLockStatusDone", - "ScrollLockStatusError", - "ScrollLockStatusWaiting" - ] - }, - "handler.DevModeBody": { - "type": "object", - "properties": { - "buildCommands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - }, - "hotReloadCommands": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/CommandInstructionSet" - } - } - } - }, - "handler.HealhResponse": { - "type": "object", - "properties": { - "mode": { - "type": "string" - }, - "progress": { - "type": "number" - }, - "start_date": { - "type": "string" - } - } - }, - "handler.ProcessesBody": { - "type": "object", - "properties": { - "processes": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/domain.Process" - } - } - } - }, - "handler.StartProcedureRequestBody": { - "type": "object", - "properties": { - "data": { - "type": "string" - }, - "dependencies": { - "type": "array", - "items": { - "type": "string" - } - }, - "mode": { - "type": "string" - }, - "process": { - "type": "string" - }, - "sync": { - "type": "boolean" - } - } - }, - "handler.StartScrollRequestBody": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "sync": { - "type": "boolean" - } - } - } - } -} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index d852400..0000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,822 +0,0 @@ -definitions: - CommandInstructionSet: - properties: - dependencies: - items: - type: string - type: array - needs: - items: - type: string - type: array - procedures: - items: - $ref: '#/definitions/Procedure' - type: array - run: - $ref: '#/definitions/domain.RunMode' - type: object - Console: - properties: - exit: - type: integer - inputMode: - type: string - type: - $ref: '#/definitions/domain.ConsoleType' - required: - - inputMode - - type - type: object - ConsolesResponse: - properties: - consoles: - additionalProperties: - $ref: '#/definitions/Console' - type: object - required: - - consoles - type: object - DevModeResponse: - properties: - enabled: - type: boolean - status: - type: string - type: object - DevStatusResponse: - properties: - enabled: - type: boolean - watchedPaths: - items: - type: string - type: array - type: object - ErrorResponse: - properties: - error: - type: string - status: - type: string - type: object - Procedure: - properties: - data: {} - id: - type: string - ignore_failure: - type: boolean - mode: - type: string - wait: {} - type: object - ProcessMonitorMetrics: - properties: - connections: - items: - type: string - type: array - cpu: - type: number - memory: - type: integer - pid: - type: integer - type: object - ProcessMonitorMetricsMap: - additionalProperties: - $ref: '#/definitions/ProcessMonitorMetrics' - type: object - ProcessTreeNode: - properties: - children: - items: - $ref: '#/definitions/ProcessTreeNode' - type: array - cmdline: - type: string - cpu_percent: - type: number - gids: - items: - type: integer - type: array - io_counters: - type: string - memory: - type: string - memory_ex: - type: string - name: - type: string - process: - type: string - username: - type: string - type: object - ProcessTreeRoot: - properties: - root: - $ref: '#/definitions/ProcessTreeNode' - total_cpu_percent: - type: number - total_io_counters_read: - type: integer - total_io_counters_write: - type: integer - total_memory_rss: - type: integer - total_memory_swap: - type: integer - total_memory_vms: - type: integer - total_process_count: - type: integer - type: object - PsTreeMap: - additionalProperties: - $ref: '#/definitions/ProcessTreeRoot' - type: object - ScrollFile: - properties: - app_version: - description: don't make this a semver, it's not allways - type: string - commands: - additionalProperties: - $ref: '#/definitions/CommandInstructionSet' - type: object - cronjobs: - items: - $ref: '#/definitions/domain.Cronjob' - type: array - desc: - type: string - init: - type: string - keepAlivePPM: - type: integer - name: - type: string - plugins: - additionalProperties: - additionalProperties: - type: string - type: object - type: object - ports: - items: - $ref: '#/definitions/domain.Port' - type: array - version: - type: string - type: object - ScrollLogStream: - properties: - key: - type: string - log: - items: - type: string - type: array - required: - - key - - log - type: object - WebsocketToken: - properties: - token: - type: string - required: - - token - type: object - domain.AugmentedPort: - properties: - check_activity: - type: boolean - description: - type: string - finish_after_command: - type: string - inactive_since: - type: string - inactive_since_sec: - type: integer - mandatory: - type: boolean - name: - type: string - open: - type: boolean - port: - type: integer - protocol: - type: string - sleep_handler: - type: string - start_delay: - type: integer - vars: - items: - $ref: '#/definitions/domain.ColdStarterVars' - type: array - type: object - domain.ColdStarterVars: - properties: - name: - type: string - value: - type: string - type: object - domain.ConsoleType: - enum: - - tty - - process - - plugin - type: string - x-enum-varnames: - - ConsoleTypeTTY - - ConsoleTypeProcess - - ConsoleTypePlugin - domain.Cronjob: - properties: - command: - type: string - name: - type: string - schedule: - type: string - type: object - domain.Port: - properties: - check_activity: - type: boolean - description: - type: string - finish_after_command: - type: string - mandatory: - type: boolean - name: - type: string - port: - type: integer - protocol: - type: string - sleep_handler: - type: string - start_delay: - type: integer - vars: - items: - $ref: '#/definitions/domain.ColdStarterVars' - type: array - type: object - domain.Process: - properties: - name: - type: string - type: - type: string - type: object - domain.RunMode: - enum: - - always - - once - - restart - - persistent - type: string - x-enum-comments: - RunModeAlways: default - RunModeOnce: runs only once - RunModePersistent: restarts on failure and on program restart - RunModeRestart: restarts on failure - x-enum-varnames: - - RunModeAlways - - RunModeOnce - - RunModeRestart - - RunModePersistent - domain.ScrollLockStatus: - enum: - - running - - done - - error - - waiting - type: string - x-enum-varnames: - - ScrollLockStatusRunning - - ScrollLockStatusDone - - ScrollLockStatusError - - ScrollLockStatusWaiting - handler.DevModeBody: - properties: - buildCommands: - additionalProperties: - $ref: '#/definitions/CommandInstructionSet' - type: object - hotReloadCommands: - additionalProperties: - $ref: '#/definitions/CommandInstructionSet' - type: object - type: object - handler.HealhResponse: - properties: - mode: - type: string - progress: - type: number - start_date: - type: string - type: object - handler.ProcessesBody: - properties: - processes: - additionalProperties: - $ref: '#/definitions/domain.Process' - type: object - type: object - handler.StartProcedureRequestBody: - properties: - data: - type: string - dependencies: - items: - type: string - type: array - mode: - type: string - process: - type: string - sync: - type: boolean - type: object - handler.StartScrollRequestBody: - properties: - command: - type: string - sync: - type: boolean - type: object -info: - contact: {} - description: Druid CLI is a process runner to launches and manages various sorts - of applications, like gameservers, databases or webservers. - title: Druid CLI - version: 0.1.0 -paths: - /api/v1/coldstarter/finish: - post: - consumes: - - '*/*' - operationId: finishColdStarter - responses: - "202": - description: Accepted - summary: Finish Coldstarter - tags: - - coldstarter - - druid - - daemon - /api/v1/command: - post: - consumes: - - '*/*' - operationId: runCommand - parameters: - - description: Scroll Body - in: body - name: body - required: true - schema: - $ref: '#/definitions/handler.StartScrollRequestBody' - produces: - - application/json - responses: - "200": - description: OK - "201": - description: Created - "400": - description: Bad Request - "500": - description: Internal Server Error - summary: Get current scroll - tags: - - scroll - - druid - - daemon - /api/v1/consoles: - get: - consumes: - - application/json - description: Get List of all consoles - operationId: getConsoles - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/ConsolesResponse' - summary: Get All Consoles - tags: - - druid - - daemon - - console - /api/v1/daemon/stop: - post: - consumes: - - '*/*' - operationId: stopDaemon - responses: - "202": - description: Accepted - summary: Finish Coldstarter - tags: - - druid - - daemon - /api/v1/dev/build: - post: - consumes: - - application/json - operationId: buildDev - produces: - - application/json - responses: - "200": - description: OK - "412": - description: Precondition Failed - "500": - description: Internal Server Error - summary: Build the UI in development mode - tags: - - ui - - dev - - druid - - daemon - /api/v1/dev/disable: - post: - consumes: - - application/json - operationId: disableDev - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/DevModeResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ErrorResponse' - summary: Disable development mode - tags: - - ui - - dev - - druid - - daemon - /api/v1/dev/enable: - post: - consumes: - - application/json - operationId: enableDev - parameters: - - description: Optional commands to run on file changes - in: body - name: body - schema: - $ref: '#/definitions/handler.DevModeBody' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/DevModeResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/ErrorResponse' - summary: Enable development mode - tags: - - ui - - dev - - druid - - daemon - /api/v1/dev/status: - get: - consumes: - - application/json - operationId: getDevStatus - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/DevStatusResponse' - summary: Get development mode status - tags: - - ui - - dev - - druid - - daemon - /api/v1/health: - get: - consumes: - - '*/*' - operationId: getHealth - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.HealhResponse' - "503": - description: Service Unavailable - schema: - $ref: '#/definitions/handler.HealhResponse' - summary: Get ports from scroll with additional information - tags: - - health - - druid - - daemon - /api/v1/logs: - get: - consumes: - - '*/*' - operationId: listLogs - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/ScrollLogStream' - type: array - summary: List all logs - tags: - - logs - - druid - - daemon - /api/v1/logs/{stream}: - get: - consumes: - - '*/*' - operationId: listLog - parameters: - - description: Stream name - in: path - name: stream - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/ScrollLogStream' - summary: List stream logs - tags: - - logs - - druid - - daemon - /api/v1/metrics: - get: - consumes: - - application/json - description: Get the metrics for all processes. - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/ProcessMonitorMetricsMap' - summary: Get all process metrics - tags: - - metrics - - druid - - daemon - /api/v1/ports: - get: - consumes: - - '*/*' - operationId: getPorts - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/domain.AugmentedPort' - type: array - summary: Get ports from scroll with additional information - tags: - - port - - druid - - daemon - /api/v1/procedure: - post: - consumes: - - '*/*' - operationId: runProcedure - parameters: - - description: Procedure Body - in: body - name: body - required: true - schema: - $ref: '#/definitions/handler.StartProcedureRequestBody' - produces: - - application/json - responses: - "200": - description: OK - schema: - type: object - "201": - description: Created - summary: Run procedure - tags: - - scroll - - druid - - daemon - /api/v1/procedures: - get: - consumes: - - '*/*' - operationId: getProcedures - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: - $ref: '#/definitions/domain.ScrollLockStatus' - type: object - summary: Get process procedure statuses - tags: - - process - - procedures - - druid - - daemon - /api/v1/processes: - get: - consumes: - - '*/*' - operationId: getRunningProcesses - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.ProcessesBody' - summary: Get running processes - tags: - - process - - druid - - daemon - /api/v1/pstree: - get: - consumes: - - application/json - description: Get pstree of running process - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/PsTreeMap' - summary: Get all process metrics - tags: - - metrics - - druid - - daemon - /api/v1/queue: - get: - consumes: - - '*/*' - operationId: getQueue - produces: - - application/json - responses: - "200": - description: OK - schema: - additionalProperties: - $ref: '#/definitions/domain.ScrollLockStatus' - type: object - summary: Get current scroll - tags: - - queue - - druid - - daemon - /api/v1/scroll: - get: - consumes: - - '*/*' - operationId: getScroll - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/ScrollFile' - summary: Get current scroll - tags: - - scroll - - druid - - daemon - /api/v1/token: - get: - consumes: - - application/json - description: Get the metrics for all processes. - operationId: createToken - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/WebsocketToken' - summary: Get current scroll - tags: - - websocket - - druid - - daemon - /private/index: - get: - consumes: - - '*/*' - operationId: getPrivateUIIndex - produces: - - application/json - responses: - "200": - description: List of files in private UI directory - schema: - items: - type: string - type: array - "404": - description: Private UI directory not found - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal server error with details - schema: - additionalProperties: - type: string - type: object - summary: Serve private UI index - tags: - - ui - - druid - - daemon - /public/index: - get: - consumes: - - '*/*' - operationId: getPublicUIIndex - produces: - - application/json - responses: - "200": - description: List of files in public UI directory - schema: - items: - type: string - type: array - "404": - description: Public UI directory not found - schema: - additionalProperties: - type: string - type: object - "500": - description: Internal server error with details - schema: - additionalProperties: - type: string - type: object - summary: Serve public UI index - tags: - - ui - - druid - - daemon -swagger: "2.0" diff --git a/go.mod b/go.mod index 03ec003..ca7c31b 100644 --- a/go.mod +++ b/go.mod @@ -15,14 +15,12 @@ require ( github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/swaggo/swag v1.16.3 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 // indirect oras.land/oras-go/v2 v2.5.0 ) require ( - github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect @@ -32,10 +30,8 @@ require ( github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/spec v0.20.9 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 @@ -78,7 +74,6 @@ require ( golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 @@ -86,21 +81,28 @@ require ( ) require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/goccy/go-json v0.10.4 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect go.uber.org/atomic v1.9.0 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect ) @@ -109,6 +111,7 @@ require ( al.essio.dev/pkg/shellescape v1.6.0 github.com/MicahParks/keyfunc v1.9.0 github.com/creack/pty v1.1.21 + github.com/getkin/kin-openapi v0.133.0 github.com/go-co-op/gocron v1.37.0 github.com/gofiber/jwt/v3 v3.3.10 github.com/golang-jwt/jwt/v4 v4.5.0 @@ -116,6 +119,7 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/highcard-dev/gorcon v1.3.10 github.com/minio/minio-go/v7 v7.0.84 + github.com/oapi-codegen/runtime v1.1.2 github.com/otiai10/copy v1.14.0 github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 github.com/yuin/gopher-lua v1.1.1 diff --git a/go.sum b/go.sum index 7d6772f..5c4f262 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -12,11 +10,15 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -42,26 +44,20 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= -github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= @@ -86,6 +82,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A= github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -110,6 +108,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -127,9 +126,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -158,7 +154,14 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -173,6 +176,8 @@ github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 h1:p4VuaitqUAqSZ github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5/go.mod h1:zIAoVKeWP0mz4zXY50UYQt6NLg2uwKRswMDcGEqOms4= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -194,8 +199,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -225,38 +230,39 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= -github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -288,8 +294,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -347,8 +351,6 @@ golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -360,17 +362,14 @@ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6h google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= diff --git a/internal/api/generated.go b/internal/api/generated.go new file mode 100644 index 0000000..eb35261 --- /dev/null +++ b/internal/api/generated.go @@ -0,0 +1,926 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. +package api + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "strings" + "time" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/gofiber/fiber/v2" + "github.com/oapi-codegen/runtime" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Defines values for CommandInstructionSetRun. +const ( + Always CommandInstructionSetRun = "always" + Once CommandInstructionSetRun = "once" + Persistent CommandInstructionSetRun = "persistent" + Restart CommandInstructionSetRun = "restart" +) + +// Defines values for ConsoleType. +const ( + ConsoleTypePlugin ConsoleType = "plugin" + ConsoleTypeProcess ConsoleType = "process" + ConsoleTypeTty ConsoleType = "tty" +) + +// Defines values for ScrollLockStatus. +const ( + Done ScrollLockStatus = "done" + Error ScrollLockStatus = "error" + Running ScrollLockStatus = "running" + Waiting ScrollLockStatus = "waiting" +) + +// AugmentedPort defines model for AugmentedPort. +type AugmentedPort struct { + CheckActivity *bool `json:"check_activity,omitempty"` + Description *string `json:"description,omitempty"` + FinishAfterCommand *string `json:"finish_after_command,omitempty"` + + // InactiveSince When the port became inactive + InactiveSince time.Time `json:"inactive_since"` + + // InactiveSinceSec Seconds since port became inactive + InactiveSinceSec int `json:"inactive_since_sec"` + Mandatory *bool `json:"mandatory,omitempty"` + + // Name Port name/identifier + Name string `json:"name"` + + // Open Whether the port is currently open + Open bool `json:"open"` + + // Port Port number + Port int `json:"port"` + + // Protocol Network protocol + Protocol string `json:"protocol"` + SleepHandler *string `json:"sleep_handler"` + StartDelay *int `json:"start_delay,omitempty"` + Vars *[]ColdStarterVars `json:"vars,omitempty"` +} + +// ColdStarterVars defines model for ColdStarterVars. +type ColdStarterVars struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// CommandInstructionSet defines model for CommandInstructionSet. +type CommandInstructionSet struct { + Dependencies *[]string `json:"dependencies,omitempty"` + Needs *[]string `json:"needs,omitempty"` + Procedures []Procedure `json:"procedures"` + + // Run Run mode for the command + Run *CommandInstructionSetRun `json:"run,omitempty"` +} + +// CommandInstructionSetRun Run mode for the command +type CommandInstructionSetRun string + +// Console defines model for Console. +type Console struct { + // Exit Exit code if console has exited + Exit *int `json:"exit"` + + // InputMode Input mode for the console + InputMode string `json:"inputMode"` + + // Type Console type + Type ConsoleType `json:"type"` +} + +// ConsoleType Console type +type ConsoleType string + +// ConsolesResponse defines model for ConsolesResponse. +type ConsolesResponse struct { + Consoles map[string]Console `json:"consoles"` +} + +// Cronjob defines model for Cronjob. +type Cronjob struct { + Command string `json:"command"` + Name string `json:"name"` + + // Schedule Cron schedule expression + Schedule string `json:"schedule"` +} + +// ErrorResponse defines model for ErrorResponse. +type ErrorResponse struct { + // Error Error message + Error string `json:"error"` + Status string `json:"status"` +} + +// HealthResponse defines model for HealthResponse. +type HealthResponse struct { + // Mode Current health status mode + Mode string `json:"mode"` + + // Progress Progress percentage for loading operations + Progress *float32 `json:"progress,omitempty"` + + // StartDate When the daemon started + StartDate *time.Time `json:"start_date"` +} + +// Port defines model for Port. +type Port struct { + // CheckActivity Whether to monitor port activity + CheckActivity *bool `json:"check_activity,omitempty"` + + // Description Port description + Description *string `json:"description,omitempty"` + + // FinishAfterCommand Command to run after port is available + FinishAfterCommand *string `json:"finish_after_command,omitempty"` + + // Mandatory Whether this port must be open for health check + Mandatory *bool `json:"mandatory,omitempty"` + + // Name Port name/identifier + Name string `json:"name"` + + // Port Port number + Port int `json:"port"` + + // Protocol Network protocol + Protocol string `json:"protocol"` + + // SleepHandler Handler to call when port becomes inactive + SleepHandler *string `json:"sleep_handler"` + + // StartDelay Delay in seconds before starting port check + StartDelay *int `json:"start_delay,omitempty"` + Vars *[]ColdStarterVars `json:"vars,omitempty"` +} + +// Procedure defines model for Procedure. +type Procedure struct { + // Data Procedure data payload + Data interface{} `json:"data,omitempty"` + + // Id Unique procedure identifier + Id *string `json:"id"` + + // IgnoreFailure Whether to continue on failure + IgnoreFailure *bool `json:"ignore_failure,omitempty"` + + // Mode Procedure execution mode + Mode string `json:"mode"` + + // Wait Wait condition + Wait *Procedure_Wait `json:"wait,omitempty"` +} + +// ProcedureWait0 defines model for . +type ProcedureWait0 = string + +// ProcedureWait1 defines model for . +type ProcedureWait1 = int + +// ProcedureWait2 defines model for . +type ProcedureWait2 = bool + +// Procedure_Wait Wait condition +type Procedure_Wait struct { + union json.RawMessage +} + +// Process defines model for Process. +type Process struct { + // Name Process name/identifier + Name string `json:"name"` + + // Type Process type + Type string `json:"type"` +} + +// ProcessMonitorMetrics defines model for ProcessMonitorMetrics. +type ProcessMonitorMetrics struct { + // Connections Active network connections + Connections []string `json:"connections"` + + // Cpu CPU usage percentage + Cpu float64 `json:"cpu"` + + // Memory Memory usage in bytes + Memory int `json:"memory"` + + // Pid Process ID + Pid int `json:"pid"` +} + +// ProcessTreeNode defines model for ProcessTreeNode. +type ProcessTreeNode struct { + Children *[]ProcessTreeNode `json:"children,omitempty"` + Cmdline *string `json:"cmdline,omitempty"` + CpuPercent *float64 `json:"cpu_percent,omitempty"` + Gids *[]int `json:"gids,omitempty"` + + // IoCounters I/O counters + IoCounters *string `json:"io_counters,omitempty"` + + // Memory Memory statistics + Memory *string `json:"memory,omitempty"` + + // MemoryEx Extended memory statistics + MemoryEx *string `json:"memory_ex,omitempty"` + Name *string `json:"name,omitempty"` + + // Process Process information (simplified from gopsutil) + Process *string `json:"process,omitempty"` + Username *string `json:"username,omitempty"` +} + +// ProcessTreeRoot defines model for ProcessTreeRoot. +type ProcessTreeRoot struct { + Root ProcessTreeNode `json:"root"` + TotalCpuPercent float64 `json:"total_cpu_percent"` + TotalIoCountersRead int64 `json:"total_io_counters_read"` + TotalIoCountersWrite int64 `json:"total_io_counters_write"` + TotalMemoryRss int64 `json:"total_memory_rss"` + TotalMemorySwap int64 `json:"total_memory_swap"` + TotalMemoryVms int64 `json:"total_memory_vms"` + TotalProcessCount int `json:"total_process_count"` +} + +// ProcessesResponse defines model for ProcessesResponse. +type ProcessesResponse struct { + Processes map[string]Process `json:"processes"` +} + +// QueueResponse Map of command IDs to their execution status +type QueueResponse map[string]ScrollLockStatus + +// ScrollFile Scroll configuration file structure +type ScrollFile struct { + // AppVersion Application version (not necessarily semver) + AppVersion *string `json:"app_version,omitempty"` + Commands *map[string]CommandInstructionSet `json:"commands,omitempty"` + Cronjobs *[]Cronjob `json:"cronjobs,omitempty"` + + // Desc Scroll description + Desc *string `json:"desc,omitempty"` + + // Init Initialization command + Init *string `json:"init,omitempty"` + + // KeepAlivePPM Keep alive packets per minute + KeepAlivePPM *int `json:"keepAlivePPM,omitempty"` + + // Name Scroll name + Name *string `json:"name,omitempty"` + Plugins *map[string]map[string]string `json:"plugins,omitempty"` + Ports *[]Port `json:"ports,omitempty"` + + // Version Scroll version (semver) + Version *string `json:"version,omitempty"` +} + +// ScrollLockStatus Status of a command in the queue +type ScrollLockStatus string + +// ScrollLogStream defines model for ScrollLogStream. +type ScrollLogStream struct { + // Key The log stream identifier + Key string `json:"key"` + + // Log Array of log lines + Log []string `json:"log"` +} + +// StartCommandRequest defines model for StartCommandRequest. +type StartCommandRequest struct { + // Command The command ID to execute + Command string `json:"command"` + + // Sync Whether to run synchronously (wait for completion) + Sync *bool `json:"sync,omitempty"` +} + +// StartProcedureRequest defines model for StartProcedureRequest. +type StartProcedureRequest struct { + // Data The data payload for the procedure + Data string `json:"data"` + + // Dependencies List of dependency IDs this procedure depends on + Dependencies *[]string `json:"dependencies,omitempty"` + + // Mode The procedure mode (e.g., "stdin", or plugin mode) + Mode string `json:"mode"` + + // Process The process name to run the procedure against + Process string `json:"process"` + + // Sync Whether to run synchronously + Sync *bool `json:"sync,omitempty"` +} + +// TokenResponse defines model for TokenResponse. +type TokenResponse struct { + // Token The generated authentication token + Token string `json:"token"` +} + +// WatchModeRequest defines model for WatchModeRequest. +type WatchModeRequest struct { + // BuildCommands Build commands to run before hot reload + BuildCommands *[]string `json:"buildCommands,omitempty"` + + // HotReloadCommands Commands to run when files change + HotReloadCommands *[]string `json:"hotReloadCommands,omitempty"` +} + +// WatchModeResponse defines model for WatchModeResponse. +type WatchModeResponse struct { + // Enabled Current watch mode state + Enabled bool `json:"enabled"` + + // Status Result status of the operation + Status string `json:"status"` +} + +// WatchStatusResponse defines model for WatchStatusResponse. +type WatchStatusResponse struct { + // Enabled Whether watch mode is currently enabled + Enabled bool `json:"enabled"` + + // WatchedPaths List of currently watched file paths + WatchedPaths []string `json:"watchedPaths"` +} + +// RunCommandJSONRequestBody defines body for RunCommand for application/json ContentType. +type RunCommandJSONRequestBody = StartCommandRequest + +// RunProcedureJSONRequestBody defines body for RunProcedure for application/json ContentType. +type RunProcedureJSONRequestBody = StartProcedureRequest + +// AddCommandJSONRequestBody defines body for AddCommand for application/json ContentType. +type AddCommandJSONRequestBody = CommandInstructionSet + +// EnableWatchJSONRequestBody defines body for EnableWatch for application/json ContentType. +type EnableWatchJSONRequestBody = WatchModeRequest + +// AsProcedureWait0 returns the union data inside the Procedure_Wait as a ProcedureWait0 +func (t Procedure_Wait) AsProcedureWait0() (ProcedureWait0, error) { + var body ProcedureWait0 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromProcedureWait0 overwrites any union data inside the Procedure_Wait as the provided ProcedureWait0 +func (t *Procedure_Wait) FromProcedureWait0(v ProcedureWait0) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeProcedureWait0 performs a merge with any union data inside the Procedure_Wait, using the provided ProcedureWait0 +func (t *Procedure_Wait) MergeProcedureWait0(v ProcedureWait0) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsProcedureWait1 returns the union data inside the Procedure_Wait as a ProcedureWait1 +func (t Procedure_Wait) AsProcedureWait1() (ProcedureWait1, error) { + var body ProcedureWait1 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromProcedureWait1 overwrites any union data inside the Procedure_Wait as the provided ProcedureWait1 +func (t *Procedure_Wait) FromProcedureWait1(v ProcedureWait1) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeProcedureWait1 performs a merge with any union data inside the Procedure_Wait, using the provided ProcedureWait1 +func (t *Procedure_Wait) MergeProcedureWait1(v ProcedureWait1) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsProcedureWait2 returns the union data inside the Procedure_Wait as a ProcedureWait2 +func (t Procedure_Wait) AsProcedureWait2() (ProcedureWait2, error) { + var body ProcedureWait2 + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromProcedureWait2 overwrites any union data inside the Procedure_Wait as the provided ProcedureWait2 +func (t *Procedure_Wait) FromProcedureWait2(v ProcedureWait2) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeProcedureWait2 performs a merge with any union data inside the Procedure_Wait, using the provided ProcedureWait2 +func (t *Procedure_Wait) MergeProcedureWait2(v ProcedureWait2) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +func (t Procedure_Wait) MarshalJSON() ([]byte, error) { + b, err := t.union.MarshalJSON() + return b, err +} + +func (t *Procedure_Wait) UnmarshalJSON(b []byte) error { + err := t.union.UnmarshalJSON(b) + return err +} + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // Finish cold start + // (POST /api/v1/coldstarter/finish) + FinishColdstarter(c *fiber.Ctx) error + // Run a command + // (POST /api/v1/command) + RunCommand(c *fiber.Ctx) error + // List all consoles + // (GET /api/v1/consoles) + GetConsoles(c *fiber.Ctx) error + // Stop daemon + // (POST /api/v1/daemon/stop) + StopDaemon(c *fiber.Ctx) error + // Get health status + // (GET /api/v1/health) + GetHealthAuth(c *fiber.Ctx) error + // List all log streams + // (GET /api/v1/logs) + ListAllLogs(c *fiber.Ctx) error + // List logs for a specific stream + // (GET /api/v1/logs/{stream}) + ListStreamLogs(c *fiber.Ctx, stream string) error + // Get process metrics + // (GET /api/v1/metrics) + GetMetrics(c *fiber.Ctx) error + // Get port information + // (GET /api/v1/ports) + GetPorts(c *fiber.Ctx) error + // Run a procedure + // (POST /api/v1/procedure) + RunProcedure(c *fiber.Ctx) error + // Get procedure statuses + // (GET /api/v1/procedures) + GetProcedures(c *fiber.Ctx) error + // List running processes + // (GET /api/v1/processes) + GetProcesses(c *fiber.Ctx) error + // Get process tree + // (GET /api/v1/pstree) + GetPsTree(c *fiber.Ctx) error + // Get command queue + // (GET /api/v1/queue) + GetQueue(c *fiber.Ctx) error + // Get current scroll + // (GET /api/v1/scroll) + GetScroll(c *fiber.Ctx) error + // Add command to current scroll + // (PUT /api/v1/scroll/commands/{command}) + AddCommand(c *fiber.Ctx, command string) error + // Create WebSocket token + // (GET /api/v1/token) + CreateToken(c *fiber.Ctx) error + // Disable development mode + // (POST /api/v1/watch/disable) + DisableWatch(c *fiber.Ctx) error + // Enable development mode + // (POST /api/v1/watch/enable) + EnableWatch(c *fiber.Ctx) error + // Get watch mode status + // (GET /api/v1/watch/status) + GetWatchStatus(c *fiber.Ctx) error +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface +} + +type MiddlewareFunc fiber.Handler + +// FinishColdstarter operation middleware +func (siw *ServerInterfaceWrapper) FinishColdstarter(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.FinishColdstarter(c) +} + +// RunCommand operation middleware +func (siw *ServerInterfaceWrapper) RunCommand(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.RunCommand(c) +} + +// GetConsoles operation middleware +func (siw *ServerInterfaceWrapper) GetConsoles(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetConsoles(c) +} + +// StopDaemon operation middleware +func (siw *ServerInterfaceWrapper) StopDaemon(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.StopDaemon(c) +} + +// GetHealthAuth operation middleware +func (siw *ServerInterfaceWrapper) GetHealthAuth(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetHealthAuth(c) +} + +// ListAllLogs operation middleware +func (siw *ServerInterfaceWrapper) ListAllLogs(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.ListAllLogs(c) +} + +// ListStreamLogs operation middleware +func (siw *ServerInterfaceWrapper) ListStreamLogs(c *fiber.Ctx) error { + + var err error + + // ------------- Path parameter "stream" ------------- + var stream string + + err = runtime.BindStyledParameterWithOptions("simple", "stream", c.Params("stream"), &stream, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter stream: %w", err).Error()) + } + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.ListStreamLogs(c, stream) +} + +// GetMetrics operation middleware +func (siw *ServerInterfaceWrapper) GetMetrics(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetMetrics(c) +} + +// GetPorts operation middleware +func (siw *ServerInterfaceWrapper) GetPorts(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetPorts(c) +} + +// RunProcedure operation middleware +func (siw *ServerInterfaceWrapper) RunProcedure(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.RunProcedure(c) +} + +// GetProcedures operation middleware +func (siw *ServerInterfaceWrapper) GetProcedures(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetProcedures(c) +} + +// GetProcesses operation middleware +func (siw *ServerInterfaceWrapper) GetProcesses(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetProcesses(c) +} + +// GetPsTree operation middleware +func (siw *ServerInterfaceWrapper) GetPsTree(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetPsTree(c) +} + +// GetQueue operation middleware +func (siw *ServerInterfaceWrapper) GetQueue(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetQueue(c) +} + +// GetScroll operation middleware +func (siw *ServerInterfaceWrapper) GetScroll(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetScroll(c) +} + +// AddCommand operation middleware +func (siw *ServerInterfaceWrapper) AddCommand(c *fiber.Ctx) error { + + var err error + + // ------------- Path parameter "command" ------------- + var command string + + err = runtime.BindStyledParameterWithOptions("simple", "command", c.Params("command"), &command, runtime.BindStyledParameterOptions{Explode: false, Required: true}) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Errorf("Invalid format for parameter command: %w", err).Error()) + } + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.AddCommand(c, command) +} + +// CreateToken operation middleware +func (siw *ServerInterfaceWrapper) CreateToken(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.CreateToken(c) +} + +// DisableWatch operation middleware +func (siw *ServerInterfaceWrapper) DisableWatch(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.DisableWatch(c) +} + +// EnableWatch operation middleware +func (siw *ServerInterfaceWrapper) EnableWatch(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.EnableWatch(c) +} + +// GetWatchStatus operation middleware +func (siw *ServerInterfaceWrapper) GetWatchStatus(c *fiber.Ctx) error { + + c.Context().SetUserValue(BearerAuthScopes, []string{}) + + return siw.Handler.GetWatchStatus(c) +} + +// FiberServerOptions provides options for the Fiber server. +type FiberServerOptions struct { + BaseURL string + Middlewares []MiddlewareFunc +} + +// RegisterHandlers creates http.Handler with routing matching OpenAPI spec. +func RegisterHandlers(router fiber.Router, si ServerInterface) { + RegisterHandlersWithOptions(router, si, FiberServerOptions{}) +} + +// RegisterHandlersWithOptions creates http.Handler with additional options +func RegisterHandlersWithOptions(router fiber.Router, si ServerInterface, options FiberServerOptions) { + wrapper := ServerInterfaceWrapper{ + Handler: si, + } + + for _, m := range options.Middlewares { + router.Use(fiber.Handler(m)) + } + + router.Post(options.BaseURL+"/api/v1/coldstarter/finish", wrapper.FinishColdstarter) + + router.Post(options.BaseURL+"/api/v1/command", wrapper.RunCommand) + + router.Get(options.BaseURL+"/api/v1/consoles", wrapper.GetConsoles) + + router.Post(options.BaseURL+"/api/v1/daemon/stop", wrapper.StopDaemon) + + router.Get(options.BaseURL+"/api/v1/health", wrapper.GetHealthAuth) + + router.Get(options.BaseURL+"/api/v1/logs", wrapper.ListAllLogs) + + router.Get(options.BaseURL+"/api/v1/logs/:stream", wrapper.ListStreamLogs) + + router.Get(options.BaseURL+"/api/v1/metrics", wrapper.GetMetrics) + + router.Get(options.BaseURL+"/api/v1/ports", wrapper.GetPorts) + + router.Post(options.BaseURL+"/api/v1/procedure", wrapper.RunProcedure) + + router.Get(options.BaseURL+"/api/v1/procedures", wrapper.GetProcedures) + + router.Get(options.BaseURL+"/api/v1/processes", wrapper.GetProcesses) + + router.Get(options.BaseURL+"/api/v1/pstree", wrapper.GetPsTree) + + router.Get(options.BaseURL+"/api/v1/queue", wrapper.GetQueue) + + router.Get(options.BaseURL+"/api/v1/scroll", wrapper.GetScroll) + + router.Put(options.BaseURL+"/api/v1/scroll/commands/:command", wrapper.AddCommand) + + router.Get(options.BaseURL+"/api/v1/token", wrapper.CreateToken) + + router.Post(options.BaseURL+"/api/v1/watch/disable", wrapper.DisableWatch) + + router.Post(options.BaseURL+"/api/v1/watch/enable", wrapper.EnableWatch) + + router.Get(options.BaseURL+"/api/v1/watch/status", wrapper.GetWatchStatus) + +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/9RcWVNct5f/Kqo782BP9R+w45lK8UbwEhI7IUDGDzZFiavT3Qq60rUWcI+L7z6l5e5H", + "vYBJZiov5mo7Opt+Z+l8K0pV1UqCtKY4/FaYcgkVDf88cosKpAV2qrT1H2qtatCWQxgul1DeXNHS8ltu", + "V/6LXdVQHBbXSgmgsrifFQxMqXltuZK9CcZqLhd+fM4lN8srOregr0pVVVQydCKX4SC4MlyW4KcMti4+", + "LkESuwRSK23JNZS0AtIsKmbFXOmK2uKwYNTCvyyv/McNp1wZKKcnnUOpJDMkTMkdl3bm0sICtN/a34xa", + "pTOMkrRCbuUZT/zQPmcgLZ9z0BjdqgaJ8sQuQXds4YaUTmuQVqxIWDNDSKmTtDFSXHXdp6B3v1orq0ol", + "pit/A3un9A1pZyAXMAKgvlpSyQRov4V0QtBrAcWh1Q6wFZZqe8VA0D5HewTdUh3UlFuowj/+XcO8OCz+", + "bb9T+P2k7fvHSrBzvyPo//br7tsTqdZ0VdzfzwoNXxzXwIrDT5FHvUsnAU70FFWpJK/L9gx1/ReU1h86", + "pmNidI2eTPhxS4XDRkaEJzLjbJyCYIQn0ljtSi/Cc0CMn0ENkoEs098tlyeEDfk4KyQA23FJrVUJzGnY", + "XqCnzRJsP+0QazlzklSKAZmraDKNO5oVIF3lmUfFHV0ZL78oWw1BC70igDbcWJC2x9SMCHq3wQUgjRIw", + "ZTl85YhdvvnKLSk93XxOyriWLKkhfjp46jOm1DMVLmtnPyiGeKATPzTmSyRwlhPceI90IxJGO25auyqS", + "bI1nai3cgsvN/EvbdESv4aI5A1MraRB2pmuEf1PGuCeWitPBnPUuI7LhfnL6iN72IJROreRf6hojL/8Y", + "Zp2Ap405gclAK0maYQJfaw3G+KFZAV9pVfs1xQH5j/hfMdvOjbTnzVp6sUu+0VrpvCTADyOa7T+TCoyh", + "C1TbjKXWJdNorhD32kR+WjlL0zGafwYq7DJPdIWay3F8XskyrCbxnGA+A0arG+w+tVYLLxbk8U0jpAZd", + "grR0Ec1RKMq4XPi3XFM/1/Shzlwo6p1TRb/yytvci4ODWVFxGf86aElIz3r3qlK7DmIxCpXXpvBOsRy4", + "2vCCjyRS5Qx5W+yZgT6KVEpyq3REQO2K2WawiuCf/qfZ9mh27A7DgKdNO0nC7Baf0VvKI9uQ/QcYMof1", + "uImbVc54ZBpwXlCWpJOBdej9H4dBt4GNrQX8ePDjwaNRZGdPtqy3QpXDLX+OA14QJRWC3HkFbwC9qsD0", + "Ef2ugHR41Gv/mXBJTIoermGuNEQb8hYcjh2J5h8Gs6g1trBqigippajniguIHyc1XXmfFUAHYhp/Sv7F", + "AWkREhmo3EYR8IVUGq7mlItEYtYrlEpaLh0QJUkzH7MJ3M13t4KvUDr/tXHyE6LuKAbbPtIA22TEHQFS", + "wu/z4vDT9FVHgptpBHm5vUNNgCsbWiBXNWYbD4Cjv2aDBNu2whVh0hriP0Sf/gGs5qVBsZ2EMj6IE4KO", + "gkkTmRxKf+5sh6CkrB3i20//JM7jld5LPXgglRv49u7lraBCHfuH8D1tyiW5XlkweASOWVTD/ZPXyJox", + "UK1d0RIyK4aM8buvkciFBvgt2cr4qeaC6Zif2D506+2I8b5igkscA5e1u0rM9+NbsH7B8YC0x9sxAVxd", + "lcpJCxpRsJP930k7ij3j60XtQSM31it2dvEVfMVCQetjckaqbTbKBhF15yBwXeIyMtV7vWeGV7XwLoGR", + "uVYVWajaOMvFc+xMZ0Bnzr1fr1tnSiEwUKevu6qTslRc7a4ocV1P9lcaKBss5tL+1yvUPKeL7zSPOHvr", + "1Un4Okpn12XmjtYPWXdb7XZcUqB4UcyeRn4nSBG5IkIGdqOsXPI8x1QAp36Ny1uXXKibKQ/NLjSv9Mbs", + "QncSRuofDhz0yXwILeelVkK8V+XNeYybx7FS8YHWRM2bjBk5eW08xrJL4LqHkNqoe0JmPOEtxzIXccw/", + "03O+cDHMJXMuPHjWrrQRuQ3ZT+v66ha0QSO5o7oWvIz7pEnkmVSWSPCcpJqLFTFQ3YJGnVi65SMSR1iG", + "FfN/ZUwO7YD8UzYJebE8D7LM3RDXcolB2BPJLaeC/09kZZcunay/AaiPBL+F09MP031+BagJ9cOkpuUN", + "2JDmIBWXzuK1FBypprv0IeTgWQvpxbVSy33PYcFOUpMPPqbaIVvtIzBEaFkdTndt1bdT1y4ofrF3sHeA", + "Iu6M/fUsfHpgzGSpOaGtmfOYDvrifUwvr6udlP6kWcGUhDbJFiMhP3CJSKehYHFuNdBq6lFvAEFMF0sg", + "Qi28JwBakfXhiVALxBl4Vvtr+W08ptwlCBi5Yk9iPAZzxSEiT7Z/Bl8cGLs26zu9aedevXeNfnWYV2zK", + "EdPkxEom659TJ2xxOKfCwCwfH2sniV+01EoqZ8SKPPPiC4kkr8AC/JrnSMw8yX7n88KBJW0snWUKnlm4", + "WA5zCm19os0eYIwY162Ge77nxnplaGet4lsWkmpdJiOMGhKc5fYBI55KuOhTHAstz2BvsTcjnwtjGZef", + "ixlRmkT3FSY8H8mccZlJJ+M4vj0yhfWNvAfMI3RBuTRPpE2b9SalU4Lsu7tgWnShbkDmoZj1wzgTFiBB", + "UwuMUGeX3nckWBDXbCxEhVkYSR+pLZcfFMvr9LXjgh33kMSQup/8cGPwpmFhShoulSUaQh5tF/1bKnsW", + "VuWPPR4dGDKiHmsZUi6pDLmMHZzjOr5ki0GSXgtg+dLKnd8jGooHlHjazmSesTMwTtimLqPmQefb6snQ", + "rlyZCpPb1pIS5VmFiG/oA67eWFLv6oNWjmYhxomwBtgptcs1Dq/bK82PCLsOqx78IHZ0Dai4xGIaA6XT", + "3K7OPSRKNgJUgz5ydtn99bYJQn/5eFGMPc4vHy+i6cZURIQDduWd2i1nARUExBVYFLbrWLa0to4h7A3I", + "5swRBloqbf/lgSrzoEevmsOUJh/h+lx58DrOJ/qFYXKTVj8sRt6F1vxX8MwLSHuuUgLT0tL7jUms9Vo7", + "zsjx+5NQLmoduUddof5DLRHUyXIJhni0UFFJF2DILdVcOUOMR6ZEzT9L2gVCZkYEvwGyoBUY0B5XzsLr", + "ek0NGP/+3MF1Gtj7HIjnNphJS07Rg6vFwd6LvYOmM4nWvDgsfgifZkXdaOI+rfn+7Yv9UgkWa4l6P1bQ", + "gmkog8Qc53whqYi39OtiAaVlgreKCE4gdtlEuz5hxWHxNmx93B0WGziCLQZ6Xh68xJLFJdQW2EBHQ5q+", + "r52fLu8vZ4VxVUX1qj2rR6FnGF2YCIj6FMRaahHS9x1DWhCIc+FNhH49KB703fsyg0TLE1acOXncRms6", + "PlE/KbZqNC9lxHr6sf+XiWFIjFg2ZgwQrHs/9A5WO7ifSOAgXy5tBMtGSOJ+Vrw8eJFfl8rUhE6WvcKO", + "+4kyohuSZ8V/YnNOpAXt9TBaBIkBzk4acuZkJ76edkT5ZRWja1lZAKIY78ASKkRXRW47goZeaagO78A2", + "7TIFLpHvohSTlpygEWgiaUK/2Y274Vmj0Q6aezUc9o4seOoMk+O3fWNVnbfAd5qWMHdCrIif2GuLmHD3", + "3Kr6dTM0Yu4LLNBWNeEhtbKz2wlrWzKaC7cfsNvGpoC1CpXaPQYtLZgOxY6ZQNkTatGoLwfRoThjFa33", + "h7/x4HPQt7wE4mTXxbGT/Dy3x2xupJgEhUtRqMUuTqFLmRhyx+0yZWobNo1l663pKCRnHu0ftsqGjXNB", + "U5g5Yf0gjZOu9kCX0d+hY7+Il88xf/9bXHK/Vgp+ZkCLlJgaSj7nJZq6mrI/ciJJoKaaVhBrj5+m/mO6", + "XwChHnd1GDQeW4xf5FlPVuOw5/IJrXoi8qmI33d5vubI8Ia/wnxomCaVJXPlJHuAJuRFtZ1SVF1rQlYd", + "0px4jBBNpxow0hV1ED/bdD08UhyPqEqN2i+QGtVEek3tuLlzRevdfWM93KQnie4LJow2GZ8VRb+kTa+V", + "a9FDQNFeImELRBqnaeDp3eLw5zi7OMXYXNhd8QGcH+/QsT51j6F8H3SLbQhljKWSUaHkMIM7iVxOe4NP", + "FrtMktLbRy9bE7GdzYRc7K6xT7fyMdHP7vHMIPW+TUQz/GVH1jxDZNtVoIQgqchEehtgxtkffRJvuakS", + "mA1x6r6IrDO7hjitPxzugfC9+4VFVgJNp8Ja/NjlCAfMzz1Tp73BJ8MN01aMPMenVO8OC7Cbt45wPZ89", + "eoCNWt48cX5yCwy243docfoHUUHbprULHgj3fBQYsPHW2yGBWKveJISk6W12rWthaWrdE+7/kQaeTNOH", + "nTwIR8OE9hcnD+Foc93mkg1Lm78xhiYvk+PoGVinpelzVazCz1b8W7ZNrvId2PPGkz1x+BH6jxDONpWn", + "RO+cPySuLwebbPs+xsEmI2z2v6V/hTizdgjHjxgzhBILVa2oXrVCtWpEwow4A3Mn/AhlLK7QfsncyTL6", + "AG5XsRrt58YKO4NbECo06CgGhFrCtONsb7F4PpHdEWNdnnlt1Noka39rfkU7CVjLQcJ6h4j1+yPETAPX", + "NghxTZ661LB7xu+IsbyEt1WytkKecYqxSO4heq8AhpW+hlX0iTochxtepOrXkxnzsB8Ased3bdk/3nwn", + "jsdL9C7dVvO2zC+HUug+4yb+lCVb6bKqjkXYsCA2VA35+TruEWrLT8nQadUeYerHrjad7sZ69ZPvQsfw", + "Z6QIDW1d5gEFmcTMxr/5SLv9SU8j2cTovFRjwXudUKm2Q6n2nWqFZV/fyKGQv78/m3Sr3Cdf9n9En5o2", + "gi5o/XvUaVQKfPXi5d/LgiOhgbIVSb9B/P9kTVFpH2dMXQ8P+i4dL6G8IXw+6opJyhJaH+6W1MYeFkI1", + "kGvw9pZaUTCg2evSeXJ/OmoGylpA4sLOeHPUJjWoJU15H3YP3R0IOOs6To5OT4pZ4bQoDov9wh+atsz0", + "J8fekyD+VMIfxlO9ekQEKPezb9m0vxeeX619hAe3VHSrQyJ+unacdA7tMDF3Hd/TtLxqE9nZHdqLdKvq", + "9icaOUgXAid0bQyppivRNiIS0d0NhNp92qGDGQjVo2wtSX0QUQsa8mMW+RtaPY0/RTbowlSJxK7dNuQM", + "/ocELYLvGl+mi2OJnAg+h3JVCpxtSWGnq98OnlRPNeJ6Gs4F7Z/ucSSlsr2fl9Cm/y+to+24Ke4v7/83", + "AAD//wp7W/k5SwAA", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/core/domain/scroll.go b/internal/core/domain/scroll.go index 58b44ad..a0815f6 100644 --- a/internal/core/domain/scroll.go +++ b/internal/core/domain/scroll.go @@ -63,11 +63,11 @@ type File struct { Commands map[string]*CommandInstructionSet `yaml:"commands" json:"commands"` Plugins map[string]map[string]string `yaml:"plugins" json:"plugins"` Cronjobs []*Cronjob `yaml:"cronjobs" json:"cronjobs"` -} // @name ScrollFile +} type Scroll struct { File -} // @name Scroll +} type Procedure struct { Mode string `yaml:"mode" json:"mode"` @@ -75,14 +75,14 @@ type Procedure struct { Wait interface{} `yaml:"wait" json:"wait"` Data interface{} `yaml:"data" json:"data"` IgnoreFailure bool `yaml:"ignore_failure" json:"ignore_failure"` -} // @name Procedure +} type CommandInstructionSet struct { Dependencies []string `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` Procedures []*Procedure `yaml:"procedures" json:"procedures"` Needs []string `yaml:"needs,omitempty" json:"needs,omitempty"` Run RunMode `yaml:"run,omitempty" json:"run,omitempty"` -} // @name CommandInstructionSet +} var ErrScrollDoesNotExist = fmt.Errorf("scroll does not exist") diff --git a/internal/core/ports/handler_ports.go b/internal/core/ports/handler_ports.go index 523e40e..e9accde 100644 --- a/internal/core/ports/handler_ports.go +++ b/internal/core/ports/handler_ports.go @@ -9,17 +9,18 @@ type ScrollHandlerInterface interface { GetScroll(c *fiber.Ctx) error RunCommand(c *fiber.Ctx) error RunProcedure(c *fiber.Ctx) error - Procedures(c *fiber.Ctx) error + GetProcedures(c *fiber.Ctx) error + AddCommand(c *fiber.Ctx, command string) error } type ScrollLogHandlerInterface interface { ListAllLogs(c *fiber.Ctx) error - ListStreamLogs(c *fiber.Ctx) error + ListStreamLogs(c *fiber.Ctx, stream string) error } type ScrollMetricHandlerInterface interface { - Metrics(c *fiber.Ctx) error - PsTree(c *fiber.Ctx) error + GetMetrics(c *fiber.Ctx) error + GetPsTree(c *fiber.Ctx) error } type AnnotationHandlerInterface interface { @@ -29,41 +30,35 @@ type AnnotationHandlerInterface interface { type WebsocketHandlerInterface interface { CreateToken(c *fiber.Ctx) error HandleProcess(c *websocket.Conn) - Consoles(c *fiber.Ctx) error + GetConsoles(c *fiber.Ctx) error } type ProcessHandlerInterface interface { - Processes(c *fiber.Ctx) error + GetProcesses(c *fiber.Ctx) error } type QueueHandlerInterface interface { - Queue(c *fiber.Ctx) error + GetQueue(c *fiber.Ctx) error } type PortHandlerInterface interface { GetPorts(c *fiber.Ctx) error } type HealthHandlerInterface interface { - Health(ctx *fiber.Ctx) error + GetHealthAuth(c *fiber.Ctx) error } type ColdstarterHandlerInterface interface { - Finish(ctx *fiber.Ctx) error + FinishColdstarter(c *fiber.Ctx) error } type SignalHandlerInterface interface { - Stop(ctx *fiber.Ctx) error + StopDaemon(c *fiber.Ctx) error } -type UiHandlerInterface interface { - PublicIndex(ctx *fiber.Ctx) error - PrivateIndex(ctx *fiber.Ctx) error -} - -type UiDevHandlerInterface interface { - Enable(ctx *fiber.Ctx) error - Build(ctx *fiber.Ctx) error - Disable(ctx *fiber.Ctx) error - Status(ctx *fiber.Ctx) error +type WatchHandlerInterface interface { + EnableWatch(c *fiber.Ctx) error + DisableWatch(c *fiber.Ctx) error + GetWatchStatus(c *fiber.Ctx) error NotifyChange(c *websocket.Conn) } diff --git a/internal/core/ports/services_ports.go b/internal/core/ports/services_ports.go index 6afef50..3f3eef6 100644 --- a/internal/core/ports/services_ports.go +++ b/internal/core/ports/services_ports.go @@ -175,16 +175,14 @@ type UiServiceInterface interface { GetIndex(filePath string) ([]string, error) } -type UiDevServiceInterface interface { +type WatchServiceInterface interface { StartWatching(basePath string, paths ...string) error StopWatching() error Subscribe() chan *[]byte Unsubscribe(client chan *[]byte) GetWatchedPaths() []string IsWatching() bool - SetHotReloadCommands(procs map[string]*domain.CommandInstructionSet) - SetBuildCommands(procs map[string]*domain.CommandInstructionSet) - Build() error + SetHotReloadCommands(procs []string) error } type NixDependencyServiceInterface interface { diff --git a/internal/core/services/ui_service.go b/internal/core/services/ui_service.go deleted file mode 100644 index ce42edc..0000000 --- a/internal/core/services/ui_service.go +++ /dev/null @@ -1,63 +0,0 @@ -package services - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/highcard-dev/daemon/internal/core/ports" -) - -// UiService handles serving UI-related files and assets -type UiService struct { - scrollService ports.ScrollServiceInterface -} - -// NewUiService creates a new instance of UiService -func NewUiService(scrollService ports.ScrollServiceInterface) ports.UiServiceInterface { - return &UiService{ - scrollService: scrollService, - } -} - -// GetIndex serves an index file from the specified file path -// This method validates the file exists and is accessible before serving -func (us *UiService) GetIndex(filePath string) ([]string, error) { - scrollDir := us.scrollService.GetDir() - uiDir := scrollDir + "/" + filePath - - // Check if directory exists - info, err := os.Stat(uiDir) - if err != nil { - if os.IsNotExist(err) { - return nil, os.ErrNotExist - } - return nil, err // This will be handled in the handler for a 500 error, but let's make it more informative - } - if !info.IsDir() { - return nil, os.ErrInvalid - } - - fileList := []string{} - walkErr := filepath.Walk(uiDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !info.IsDir() { - if relPath, err := filepath.Rel(uiDir, path); err == nil { - fileList = append(fileList, relPath) - } else { - fileList = append(fileList, path) - } - } - return nil - }) - if walkErr != nil { - // Return a wrapped error for more informative 500 error - return nil, fmt.Errorf("failed to walk directory '%s': %w", uiDir, walkErr) - } - return fileList, nil -} - -// Ensure UiService implements UiServiceInterface at compile time -var _ ports.UiServiceInterface = (*UiService)(nil) diff --git a/internal/core/services/ui_service_test.go b/internal/core/services/ui_service_test.go deleted file mode 100644 index 2408d79..0000000 --- a/internal/core/services/ui_service_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package services_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/highcard-dev/daemon/internal/core/services" - mock_ports "github.com/highcard-dev/daemon/test/mock" - "go.uber.org/mock/gomock" -) - -func TestUiService_GetIndex(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockScrollService := mock_ports.NewMockScrollServiceInterface(ctrl) - - // Create a temporary directory structure for testing - tempDir := t.TempDir() - publicDir := filepath.Join(tempDir, "public") - err := os.MkdirAll(publicDir, 0755) - if err != nil { - t.Fatalf("Failed to create temp directory: %v", err) - } - - // Create test files - testFiles := []string{"index.html", "style.css", "script.js"} - for _, file := range testFiles { - filePath := filepath.Join(publicDir, file) - err := os.WriteFile(filePath, []byte("test content"), 0644) - if err != nil { - t.Fatalf("Failed to create test file %s: %v", file, err) - } - } - - // Create a subdirectory with a file - subDir := filepath.Join(publicDir, "assets") - err = os.MkdirAll(subDir, 0755) - if err != nil { - t.Fatalf("Failed to create subdirectory: %v", err) - } - err = os.WriteFile(filepath.Join(subDir, "image.png"), []byte("image data"), 0644) - if err != nil { - t.Fatalf("Failed to create subdirectory file: %v", err) - } - - mockScrollService.EXPECT().GetDir().Return(tempDir).AnyTimes() - - uiService := services.NewUiService(mockScrollService) - - t.Run("GetIndex_Success", func(t *testing.T) { - files, err := uiService.GetIndex("public") - if err != nil { - t.Errorf("GetIndex returned error: %v", err) - } - - if len(files) != 4 { // 3 files + 1 in subdirectory - t.Errorf("Expected 4 files, got %d", len(files)) - } - - // Verify all expected files are present (now expecting relative paths) - expectedFiles := []string{ - "index.html", - "style.css", - "script.js", - "assets/image.png", - } - - for _, expected := range expectedFiles { - found := false - for _, actual := range files { - if actual == expected { - found = true - break - } - } - if !found { - t.Errorf("Expected file %s not found in result", expected) - } - } - }) - - t.Run("GetIndex_NonExistentDirectory", func(t *testing.T) { - files, err := uiService.GetIndex("nonexistent") - if err == nil { - t.Error("Expected error for non-existent directory") - } - if len(files) != 0 { - t.Error("Expected empty files list for non-existent directory") - } - }) -} diff --git a/internal/core/services/ui_dev_service.go b/internal/core/services/watch_service.go similarity index 79% rename from internal/core/services/ui_dev_service.go rename to internal/core/services/watch_service.go index c3bf8fc..281fabb 100644 --- a/internal/core/services/ui_dev_service.go +++ b/internal/core/services/watch_service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" "path/filepath" "sync" @@ -28,8 +29,8 @@ type CommandDoneEvent struct { Timestamp time.Time `json:"timestamp"` } -// UiDevService handles file watching and change notifications for UI development -type UiDevService struct { +// WatchService handles file watching and change notifications for UI development +type WatchService struct { watcher *fsnotify.Watcher broadcastChannel *domain.BroadcastChannel watchPaths []string @@ -38,8 +39,7 @@ type UiDevService struct { ctx context.Context cancel context.CancelFunc isWatching bool - hotReloadCommands map[string]*domain.CommandInstructionSet - buildCommands map[string]*domain.CommandInstructionSet + hotReloadCommands []string queueManager ports.QueueManagerInterface scrollService ports.ScrollServiceInterface buildActive bool @@ -49,8 +49,8 @@ type UiDevService struct { // NewUiDevService creates a new instance of UiDevService func NewUiDevService( queueManager ports.QueueManagerInterface, scrollService ports.ScrollServiceInterface, -) ports.UiDevServiceInterface { - return &UiDevService{ +) ports.WatchServiceInterface { + return &WatchService{ watchPaths: make([]string, 0), isWatching: false, queueManager: queueManager, @@ -58,38 +58,25 @@ func NewUiDevService( } } -func (uds *UiDevService) SetHotReloadCommands(commands map[string]*domain.CommandInstructionSet) { - uds.mu.Lock() - defer uds.mu.Unlock() - uds.hotReloadCommands = commands - for key, cmd := range commands { - uds.scrollService.AddTemporaryCommand(key, cmd) +func (uds *WatchService) SetHotReloadCommands(commands []string) error { + + // Validate commands + for _, cmd := range commands { + _, err := uds.scrollService.GetCommand(cmd) + if err != nil { + return fmt.Errorf("invalid command '%s': %w", cmd, err) + } } -} -func (uds *UiDevService) SetBuildCommands(commands map[string]*domain.CommandInstructionSet) { + uds.mu.Lock() defer uds.mu.Unlock() - uds.buildCommands = commands - for key, cmd := range commands { - uds.scrollService.AddTemporaryCommand(key, cmd) - } -} - -func (uds *UiDevService) Build() error { - uds.mu.RLock() - isWatching := uds.isWatching - uds.mu.RUnlock() - - if !isWatching { - return errors.New("cannot build: dev mode is not enabled") - } + uds.hotReloadCommands = commands - uds.handleBuildCommands() return nil } // StartWatching initializes the file watcher and starts monitoring specified paths -func (uds *UiDevService) StartWatching(basePath string, paths ...string) error { +func (uds *WatchService) StartWatching(basePath string, paths ...string) error { uds.mu.Lock() defer uds.mu.Unlock() @@ -137,12 +124,15 @@ func (uds *UiDevService) StartWatching(basePath string, paths ...string) error { // Start the broadcast hub go uds.broadcastChannel.Run() + // run hot reload commands initially + go uds.runHotReloadCommand() + logger.Log().Info("UI dev file watcher started") return nil } // StopWatching stops the file watcher and cleans up resources -func (uds *UiDevService) StopWatching() error { +func (uds *WatchService) StopWatching() error { uds.mu.Lock() defer uds.mu.Unlock() @@ -181,7 +171,7 @@ func (uds *UiDevService) StopWatching() error { } // Subscribe returns a channel for receiving file change notifications -func (uds *UiDevService) Subscribe() chan *[]byte { +func (uds *WatchService) Subscribe() chan *[]byte { uds.mu.RLock() defer uds.mu.RUnlock() @@ -194,7 +184,7 @@ func (uds *UiDevService) Subscribe() chan *[]byte { } // Unsubscribe removes a client from receiving file change notifications -func (uds *UiDevService) Unsubscribe(client chan *[]byte) { +func (uds *WatchService) Unsubscribe(client chan *[]byte) { uds.mu.RLock() defer uds.mu.RUnlock() @@ -204,7 +194,7 @@ func (uds *UiDevService) Unsubscribe(client chan *[]byte) { } // GetWatchedPaths returns the list of currently watched paths (relative to base path) -func (uds *UiDevService) GetWatchedPaths() []string { +func (uds *WatchService) GetWatchedPaths() []string { uds.mu.RLock() defer uds.mu.RUnlock() @@ -221,14 +211,14 @@ func (uds *UiDevService) GetWatchedPaths() []string { } // IsWatching returns whether the file watcher is currently active -func (uds *UiDevService) IsWatching() bool { +func (uds *WatchService) IsWatching() bool { uds.mu.RLock() defer uds.mu.RUnlock() return uds.isWatching } // addWatchPath adds a path to the file watcher, including subdirectories -func (uds *UiDevService) addWatchPath(path string) error { +func (uds *WatchService) addWatchPath(path string) error { return filepath.Walk(path, func(walkPath string, info os.FileInfo, err error) error { if err != nil { return nil // Skip paths that can't be accessed @@ -246,7 +236,7 @@ func (uds *UiDevService) addWatchPath(path string) error { } // processEvents handles file system events and broadcasts them to subscribers -func (uds *UiDevService) processEvents() { +func (uds *WatchService) processEvents() { defer func() { if r := recover(); r != nil { logger.Log().Error("File watcher panic recovered", zap.Any("error", r)) @@ -282,7 +272,7 @@ func (uds *UiDevService) processEvents() { } // handleFileEvent processes a single file system event and broadcasts it -func (uds *UiDevService) handleFileEvent(event fsnotify.Event) { +func (uds *WatchService) handleFileEvent(event fsnotify.Event) { // Convert absolute path to relative path uds.mu.RLock() basePath := uds.basePath @@ -335,21 +325,13 @@ func (uds *UiDevService) handleFileEvent(event fsnotify.Event) { } // Handle hot reload commands in a separate goroutine to avoid blocking the event loop - go uds.handleHotReloadCommands() + go uds.runHotReloadCommand() } -// handleHotReloadCommands processes hot reload commands triggered by file changes -func (uds *UiDevService) handleHotReloadCommands() { - uds.executeCommands(uds.hotReloadCommands, "hot-reload") -} - -// handleBuildCommands processes build commands with proper synchronization -func (uds *UiDevService) handleBuildCommands() { - uds.executeCommands(uds.buildCommands, "build") -} +// runHotReloadCommand is a unified method for executing both build and hot reload commands +func (uds *WatchService) runHotReloadCommand() { + commands := uds.hotReloadCommands -// executeCommands is a unified method for executing both build and hot reload commands -func (uds *UiDevService) executeCommands(commandsMap map[string]*domain.CommandInstructionSet, commandType string) { uds.mu.Lock() // Prevent overlapping builds - if build is active, mark that a change occurred @@ -360,17 +342,14 @@ func (uds *UiDevService) executeCommands(commandsMap map[string]*domain.CommandI } // Check if there are commands to execute - if commandsMap == nil || len(commandsMap) == 0 { + if len(commands) == 0 { uds.mu.Unlock() return } // Mark build as active and get snapshot of commands uds.buildActive = true - commands := make(map[string]*domain.CommandInstructionSet, len(commandsMap)) - for key, cmd := range commandsMap { - commands[key] = cmd - } + broadcastChannel := uds.broadcastChannel uds.mu.Unlock() @@ -390,7 +369,7 @@ func (uds *UiDevService) executeCommands(commandsMap map[string]*domain.CommandI broadcastChannel.Broadcast(eventCmdData) } - for key := range commands { + for _, key := range commands { broadcastEvent("build-started") uds.queueManager.AddTempItemWithWait(key) broadcastEvent("build-ended") diff --git a/internal/core/services/ui_dev_service_test.go b/internal/core/services/watch_service_test.go similarity index 95% rename from internal/core/services/ui_dev_service_test.go rename to internal/core/services/watch_service_test.go index 233f05c..14a8d75 100644 --- a/internal/core/services/ui_dev_service_test.go +++ b/internal/core/services/watch_service_test.go @@ -9,7 +9,7 @@ import ( "go.uber.org/mock/gomock" ) -func TestUiDevService_BasicFunctionality(t *testing.T) { +func TestWatchService_BasicFunctionality(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -67,7 +67,7 @@ func TestUiDevService_BasicFunctionality(t *testing.T) { } } -func TestUiDevService_MultipleSubscribers(t *testing.T) { +func TestWatchService_MultipleSubscribers(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -101,7 +101,7 @@ func TestUiDevService_MultipleSubscribers(t *testing.T) { uiDevService.StopWatching() } -func TestUiDevService_ContinuousStartStop(t *testing.T) { +func TestWatchService_ContinuousStartStop(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -173,7 +173,7 @@ func TestUiDevService_ContinuousStartStop(t *testing.T) { } } -func TestUiDevService_SubscribeBeforeStart(t *testing.T) { +func TestWatchService_SubscribeBeforeStart(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/internal/handler/annotation_handler_test.go b/internal/handler/annotation_handler_test.go new file mode 100644 index 0000000..0f3621a --- /dev/null +++ b/internal/handler/annotation_handler_test.go @@ -0,0 +1,159 @@ +package handler + +import ( + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/gofiber/fiber/v2" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// AnnotationTestContext holds all mocked services for annotation handler testing +type AnnotationTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + ScrollService *mock_ports.MockScrollServiceInterface + Handler *AnnotationHandler +} + +// setupAnnotationTestApp creates a Fiber app with mocked dependencies for testing +func setupAnnotationTestApp(t *testing.T) *AnnotationTestContext { + ctrl := gomock.NewController(t) + + scrollService := mock_ports.NewMockScrollServiceInterface(ctrl) + handler := NewAnnotationHandler(scrollService) + + app := fiber.New() + app.Get("/annotations", handler.Annotations) + + return &AnnotationTestContext{ + App: app, + Ctrl: ctrl, + ScrollService: scrollService, + Handler: handler, + } +} + +func TestAnnotationHandler_Annotations_Success(t *testing.T) { + tc := setupAnnotationTestApp(t) + defer tc.Ctrl.Finish() + + // Create a temporary directory and file for testing + tempDir, err := os.MkdirTemp("", "annotation-test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create annotations.json file + annotationsFile := filepath.Join(tempDir, "annotations.json") + annotationsContent := `{"key": "value"}` + if err := os.WriteFile(annotationsFile, []byte(annotationsContent), 0644); err != nil { + t.Fatalf("Failed to create annotations file: %v", err) + } + + tc.ScrollService.EXPECT().GetDir().Return(tempDir) + + req := httptest.NewRequest("GET", "/annotations", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestAnnotationHandler_Annotations_FileNotFound(t *testing.T) { + tc := setupAnnotationTestApp(t) + defer tc.Ctrl.Finish() + + // Return a directory that doesn't exist + tc.ScrollService.EXPECT().GetDir().Return("/non/existent/path") + + req := httptest.NewRequest("GET", "/annotations", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Fiber returns 404 when file is not found + if resp.StatusCode != 404 { + t.Errorf("Expected status 404, got %d", resp.StatusCode) + } +} + +func TestAnnotationHandler_Annotations_EmptyDir(t *testing.T) { + tc := setupAnnotationTestApp(t) + defer tc.Ctrl.Finish() + + // Create temp dir without annotations file + tempDir, err := os.MkdirTemp("", "annotation-test-empty") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + tc.ScrollService.EXPECT().GetDir().Return(tempDir) + + req := httptest.NewRequest("GET", "/annotations", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Should return 404 when file doesn't exist + if resp.StatusCode != 404 { + t.Errorf("Expected status 404, got %d", resp.StatusCode) + } +} + +func TestAnnotationHandler_Annotations_ValidJSON(t *testing.T) { + tc := setupAnnotationTestApp(t) + defer tc.Ctrl.Finish() + + tempDir, err := os.MkdirTemp("", "annotation-test-json") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create a valid JSON annotations file + annotationsFile := filepath.Join(tempDir, "annotations.json") + annotationsContent := `{ + "annotations": [ + {"name": "cpu", "value": "50%"}, + {"name": "memory", "value": "1GB"} + ] + }` + if err := os.WriteFile(annotationsFile, []byte(annotationsContent), 0644); err != nil { + t.Fatalf("Failed to create annotations file: %v", err) + } + + tc.ScrollService.EXPECT().GetDir().Return(tempDir) + + req := httptest.NewRequest("GET", "/annotations", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Check content type + contentType := resp.Header.Get("Content-Type") + if contentType != "application/json" { + // Note: Fiber sets content type based on file extension + t.Logf("Content-Type: %s", contentType) + } +} diff --git a/internal/handler/coldstarter_handler.go b/internal/handler/coldstarter_handler.go index fcbbff2..51d2b38 100644 --- a/internal/handler/coldstarter_handler.go +++ b/internal/handler/coldstarter_handler.go @@ -15,13 +15,7 @@ func NewColdstarterHandler(coldstarter ports.ColdStarterInterface) *ColdstarterH } } -// @Summary Finish Coldstarter -// @ID finishColdStarter -// @Tags coldstarter, druid, daemon -// @Accept */* -// @Success 202 -// @Router /api/v1/coldstarter/finish [POST] -func (ah ColdstarterHandler) Finish(c *fiber.Ctx) error { +func (ah ColdstarterHandler) FinishColdstarter(c *fiber.Ctx) error { ah.coldstarter.Finish(nil) c.Status(202) return nil diff --git a/internal/handler/coldstarter_handler_test.go b/internal/handler/coldstarter_handler_test.go new file mode 100644 index 0000000..fd078ed --- /dev/null +++ b/internal/handler/coldstarter_handler_test.go @@ -0,0 +1,94 @@ +package handler + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// ColdstarterTestContext holds all mocked services for coldstarter handler testing +type ColdstarterTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + Coldstarter *mock_ports.MockColdStarterInterface + Handler *ColdstarterHandler +} + +// setupColdstarterTestApp creates a Fiber app with mocked dependencies for testing +func setupColdstarterTestApp(t *testing.T) *ColdstarterTestContext { + ctrl := gomock.NewController(t) + + coldstarter := mock_ports.NewMockColdStarterInterface(ctrl) + handler := NewColdstarterHandler(coldstarter) + + app := fiber.New() + app.Post("/api/v1/coldstarter/finish", handler.FinishColdstarter) + + return &ColdstarterTestContext{ + App: app, + Ctrl: ctrl, + Coldstarter: coldstarter, + Handler: handler, + } +} + +func TestColdstarterHandler_Finish_Success(t *testing.T) { + tc := setupColdstarterTestApp(t) + defer tc.Ctrl.Finish() + + // Finish is called with nil argument + tc.Coldstarter.EXPECT().Finish(nil) + + req := httptest.NewRequest("POST", "/api/v1/coldstarter/finish", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 202 { + t.Errorf("Expected status 202, got %d", resp.StatusCode) + } +} + +func TestColdstarterHandler_Finish_CalledOnce(t *testing.T) { + tc := setupColdstarterTestApp(t) + defer tc.Ctrl.Finish() + + // Verify Finish is called exactly once + tc.Coldstarter.EXPECT().Finish(nil).Times(1) + + req := httptest.NewRequest("POST", "/api/v1/coldstarter/finish", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 202 { + t.Errorf("Expected status 202, got %d", resp.StatusCode) + } +} + +func TestColdstarterHandler_Finish_WithBody(t *testing.T) { + tc := setupColdstarterTestApp(t) + defer tc.Ctrl.Finish() + + // Handler ignores request body, still calls Finish with nil + tc.Coldstarter.EXPECT().Finish(nil) + + req := httptest.NewRequest("POST", "/api/v1/coldstarter/finish", nil) + req.Header.Set("Content-Type", "application/json") + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 202 { + t.Errorf("Expected status 202, got %d", resp.StatusCode) + } +} diff --git a/internal/handler/daemon_handler.go b/internal/handler/daemon_handler.go index ff5f1df..c8e406b 100644 --- a/internal/handler/daemon_handler.go +++ b/internal/handler/daemon_handler.go @@ -15,13 +15,7 @@ func NewDaemonHandler(shutdown *signals.SignalHandler) *DaemonHandler { } } -// @Summary Finish Coldstarter -// @ID stopDaemon -// @Tags druid, daemon -// @Accept */* -// @Success 202 -// @Router /api/v1/daemon/stop [POST] -func (ah DaemonHandler) Stop(c *fiber.Ctx) error { +func (ah DaemonHandler) StopDaemon(c *fiber.Ctx) error { ah.shutdown.Stop() c.Status(201) return nil diff --git a/internal/handler/health_handler.go b/internal/handler/health_handler.go index 075e12a..43191a3 100644 --- a/internal/handler/health_handler.go +++ b/internal/handler/health_handler.go @@ -4,15 +4,10 @@ import ( "time" "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/ports" ) -type HealhResponse struct { - Mode string `json:"mode"` - Progress float64 `json:"progress"` - StartDate *time.Time `json:"start_date,omitempty"` -} - type HealthHandler struct { portService ports.PortServiceInterface timeoutDone bool @@ -42,27 +37,19 @@ func NewHealthHandler( return h } -// @Summary Get ports from scroll with additional information -// @ID getHealth -// @Tags health, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} HealhResponse -// @Success 503 {object} HealhResponse -// @Router /api/v1/health [get] -func (p *HealthHandler) Health(c *fiber.Ctx) error { +func (p *HealthHandler) GetHealthAuth(c *fiber.Ctx) error { portsOpen := p.portService.MandatoryPortsOpen() if !p.timeoutDone && !portsOpen { c.SendStatus(503) - return c.JSON(HealhResponse{ + return c.JSON(api.HealthResponse{ Mode: "manditory_ports", }) } if p.Started == nil { - return c.JSON(HealhResponse{ + return c.JSON(api.HealthResponse{ Mode: "idle", }) } @@ -73,14 +60,14 @@ func (p *HealthHandler) Health(c *fiber.Ctx) error { if pt != nil { perc = (*pt).GetPercent() } - - return c.JSON(HealhResponse{ + percFloat32 := float32(perc) + return c.JSON(api.HealthResponse{ Mode: string(p.snapshotService.GetCurrentMode()), - Progress: perc, + Progress: &percFloat32, }) } - return c.JSON(HealhResponse{ + return c.JSON(api.HealthResponse{ Mode: "ok", StartDate: p.Started, }) diff --git a/internal/handler/health_handler_test.go b/internal/handler/health_handler_test.go new file mode 100644 index 0000000..0497089 --- /dev/null +++ b/internal/handler/health_handler_test.go @@ -0,0 +1,276 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + "github.com/highcard-dev/daemon/internal/core/ports" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// HealthTestContext holds all mocked services for health handler testing +type HealthTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + PortService *mock_ports.MockPortServiceInterface + SnapshotService *mock_ports.MockSnapshotService + Handler *HealthHandler +} + +// setupHealthTestApp creates a Fiber app with mocked dependencies for testing +func setupHealthTestApp(t *testing.T, timeoutSec uint) *HealthTestContext { + ctrl := gomock.NewController(t) + + portService := mock_ports.NewMockPortServiceInterface(ctrl) + snapshotService := mock_ports.NewMockSnapshotService(ctrl) + + handler := NewHealthHandler(portService, timeoutSec, snapshotService) + + app := fiber.New() + app.Get("/api/v1/health", handler.GetHealthAuth) + + return &HealthTestContext{ + App: app, + Ctrl: ctrl, + PortService: portService, + SnapshotService: snapshotService, + Handler: handler, + } +} + +func TestHealthHandler_Health_MandatoryPortsNotOpen(t *testing.T) { + tc := setupHealthTestApp(t, 0) // No timeout + defer tc.Ctrl.Finish() + + // Ports not open and timeout not done + tc.PortService.EXPECT().MandatoryPortsOpen().Return(false) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 503 { + t.Errorf("Expected status 503, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "manditory_ports" { + t.Errorf("Expected mode 'manditory_ports', got '%s'", result.Mode) + } +} + +func TestHealthHandler_Health_Idle(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + // Ports open, but Started is nil + tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "idle" { + t.Errorf("Expected mode 'idle', got '%s'", result.Mode) + } +} + +func TestHealthHandler_Health_Snapshot(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + // Set Started time + now := time.Now() + tc.Handler.Started = &now + + tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) + // GetCurrentMode is called twice: once in the condition, once for the response + tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeSnapshot).Times(2) + + // Create a mock progress tracker + var pt ports.ProgressTracker = &mockProgressTracker{percent: 50.0} + tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(&pt) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "snapshot" { + t.Errorf("Expected mode 'snapshot', got '%s'", result.Mode) + } + if *result.Progress != 50.0 { + t.Errorf("Expected progress 50.0, got %f", *result.Progress) + } +} + +func TestHealthHandler_Health_Restore(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + now := time.Now() + tc.Handler.Started = &now + + tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) + // GetCurrentMode is called twice: once in the condition, once for the response + tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeRestore).Times(2) + + var pt ports.ProgressTracker = &mockProgressTracker{percent: 75.0} + tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(&pt) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "restore" { + t.Errorf("Expected mode 'restore', got '%s'", result.Mode) + } +} + +func TestHealthHandler_Health_Ok(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + now := time.Now() + tc.Handler.Started = &now + + tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) + tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeNoop) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "ok" { + t.Errorf("Expected mode 'ok', got '%s'", result.Mode) + } + if result.StartDate == nil { + t.Error("Expected StartDate to be set") + } +} + +func TestHealthHandler_Health_TimeoutDone_PortsClosed(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + // Manually set timeoutDone to true + tc.Handler.timeoutDone = true + + // Even with ports closed, if timeout is done, we proceed + tc.PortService.EXPECT().MandatoryPortsOpen().Return(false) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Should return idle since Started is nil + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if result.Mode != "idle" { + t.Errorf("Expected mode 'idle', got '%s'", result.Mode) + } +} + +func TestHealthHandler_Health_SnapshotNilProgressTracker(t *testing.T) { + tc := setupHealthTestApp(t, 0) + defer tc.Ctrl.Finish() + + now := time.Now() + tc.Handler.Started = &now + + tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) + // GetCurrentMode is called twice: once in the condition, once for the response + tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeSnapshot).Times(2) + tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/health", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.HealthResponse + json.Unmarshal(body, &result) + + if *result.Progress != 0 { + t.Errorf("Expected progress 0, got %f", *result.Progress) + } +} + +// mockProgressTracker implements ports.ProgressTracker for testing +type mockProgressTracker struct { + percent float64 +} + +func (m *mockProgressTracker) LogTrackProgress(current int64) {} +func (m *mockProgressTracker) GetPercent() float64 { return m.percent } diff --git a/internal/handler/port_handler.go b/internal/handler/port_handler.go index e63df6c..29a49b2 100644 --- a/internal/handler/port_handler.go +++ b/internal/handler/port_handler.go @@ -17,13 +17,6 @@ func NewPortHandler( } } -// @Summary Get ports from scroll with additional information -// @ID getPorts -// @Tags port, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} []domain.AugmentedPort -// @Router /api/v1/ports [get] func (p PortHandler) GetPorts(c *fiber.Ctx) error { augmentedPorts := p.portService.GetPorts() diff --git a/internal/handler/port_handler_test.go b/internal/handler/port_handler_test.go new file mode 100644 index 0000000..ee7a562 --- /dev/null +++ b/internal/handler/port_handler_test.go @@ -0,0 +1,180 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// PortTestContext holds all mocked services for port handler testing +type PortTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + PortService *mock_ports.MockPortServiceInterface + Handler *PortHandler +} + +// setupPortTestApp creates a Fiber app with mocked dependencies for testing +func setupPortTestApp(t *testing.T) *PortTestContext { + ctrl := gomock.NewController(t) + + portService := mock_ports.NewMockPortServiceInterface(ctrl) + handler := NewPortHandler(portService) + + app := fiber.New() + app.Get("/api/v1/ports", handler.GetPorts) + + return &PortTestContext{ + App: app, + Ctrl: ctrl, + PortService: portService, + Handler: handler, + } +} + +func TestPortHandler_GetPorts_Success(t *testing.T) { + tc := setupPortTestApp(t) + defer tc.Ctrl.Finish() + + expectedPorts := []*domain.AugmentedPort{ + { + Port: domain.Port{ + Port: 8080, + Protocol: "tcp", + Name: "http", + }, + Open: true, + InactiveSince: time.Now(), + InactiveSinceSec: 0, + }, + { + Port: domain.Port{ + Port: 443, + Protocol: "tcp", + Name: "https", + }, + Open: true, + InactiveSinceSec: 10, + }, + } + tc.PortService.EXPECT().GetPorts().Return(expectedPorts) + + req := httptest.NewRequest("GET", "/api/v1/ports", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result []*domain.AugmentedPort + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 2 { + t.Errorf("Expected 2 ports, got %d", len(result)) + } + if result[0].Port.Port != 8080 { + t.Errorf("Expected port 8080, got %d", result[0].Port.Port) + } + if result[1].Port.Port != 443 { + t.Errorf("Expected port 443, got %d", result[1].Port.Port) + } +} + +func TestPortHandler_GetPorts_Empty(t *testing.T) { + tc := setupPortTestApp(t) + defer tc.Ctrl.Finish() + + tc.PortService.EXPECT().GetPorts().Return([]*domain.AugmentedPort{}) + + req := httptest.NewRequest("GET", "/api/v1/ports", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result []*domain.AugmentedPort + json.Unmarshal(body, &result) + + if len(result) != 0 { + t.Errorf("Expected 0 ports, got %d", len(result)) + } +} + +func TestPortHandler_GetPorts_Nil(t *testing.T) { + tc := setupPortTestApp(t) + defer tc.Ctrl.Finish() + + tc.PortService.EXPECT().GetPorts().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/ports", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestPortHandler_GetPorts_WithMandatoryPort(t *testing.T) { + tc := setupPortTestApp(t) + defer tc.Ctrl.Finish() + + expectedPorts := []*domain.AugmentedPort{ + { + Port: domain.Port{ + Port: 25565, + Protocol: "tcp", + Name: "minecraft", + Mandatory: true, + }, + Open: false, + InactiveSinceSec: 120, + }, + } + tc.PortService.EXPECT().GetPorts().Return(expectedPorts) + + req := httptest.NewRequest("GET", "/api/v1/ports", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result []*domain.AugmentedPort + json.Unmarshal(body, &result) + + if len(result) != 1 { + t.Errorf("Expected 1 port, got %d", len(result)) + } + if !result[0].Mandatory { + t.Error("Expected port to be mandatory") + } +} diff --git a/internal/handler/process_handler.go b/internal/handler/process_handler.go index e9c2b33..cd32216 100644 --- a/internal/handler/process_handler.go +++ b/internal/handler/process_handler.go @@ -2,6 +2,7 @@ package handler import ( "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" ) @@ -10,23 +11,25 @@ type ProcessHandler struct { ProcessManager ports.ProcessManagerInterface } -type ProcessesBody struct { - Processes map[string]*domain.Process `json:"processes"` +func domainProcessToAPI(dp *domain.Process) api.Process { + return api.Process{ + Name: dp.Name, + Type: dp.Type, + } } func NewProcessHandler(processManager ports.ProcessManagerInterface) *ProcessHandler { return &ProcessHandler{ProcessManager: processManager} } -// @Summary Get running processes -// @ID getRunningProcesses -// @Tags process, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} ProcessesBody -// @Router /api/v1/processes [get] -func (ph ProcessHandler) Processes(c *fiber.Ctx) error { +func (ph ProcessHandler) GetProcesses(c *fiber.Ctx) error { processes := ph.ProcessManager.GetRunningProcesses() - return c.JSON(ProcessesBody{Processes: processes}) + // Convert domain processes to API processes + apiProcesses := make(map[string]api.Process, len(processes)) + for k, v := range processes { + apiProcesses[k] = domainProcessToAPI(v) + } + + return c.JSON(api.ProcessesResponse{Processes: apiProcesses}) } diff --git a/internal/handler/process_handler_test.go b/internal/handler/process_handler_test.go new file mode 100644 index 0000000..bbec768 --- /dev/null +++ b/internal/handler/process_handler_test.go @@ -0,0 +1,163 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// ProcessTestContext holds all mocked services for process handler testing +type ProcessTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + ProcessManager *mock_ports.MockProcessManagerInterface + Handler *ProcessHandler +} + +// setupProcessTestApp creates a Fiber app with mocked dependencies for testing +func setupProcessTestApp(t *testing.T) *ProcessTestContext { + ctrl := gomock.NewController(t) + + processManager := mock_ports.NewMockProcessManagerInterface(ctrl) + handler := NewProcessHandler(processManager) + + app := fiber.New() + app.Get("/api/v1/processes", handler.GetProcesses) + + return &ProcessTestContext{ + App: app, + Ctrl: ctrl, + ProcessManager: processManager, + Handler: handler, + } +} + +func TestProcessHandler_Processes_Success(t *testing.T) { + tc := setupProcessTestApp(t) + defer tc.Ctrl.Finish() + + expectedProcesses := map[string]*domain.Process{ + "start": { + Name: "start", + Type: "tty", + }, + "install": { + Name: "install", + Type: "exec", + }, + } + tc.ProcessManager.EXPECT().GetRunningProcesses().Return(expectedProcesses) + + req := httptest.NewRequest("GET", "/api/v1/processes", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ProcessesResponse + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result.Processes) != 2 { + t.Errorf("Expected 2 processes, got %d", len(result.Processes)) + } + if _, ok := result.Processes["start"]; !ok { + t.Error("Expected 'start' process to be present") + } + if _, ok := result.Processes["install"]; !ok { + t.Error("Expected 'install' process to be present") + } +} + +func TestProcessHandler_Processes_Empty(t *testing.T) { + tc := setupProcessTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessManager.EXPECT().GetRunningProcesses().Return(map[string]*domain.Process{}) + + req := httptest.NewRequest("GET", "/api/v1/processes", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ProcessesResponse + json.Unmarshal(body, &result) + + if len(result.Processes) != 0 { + t.Errorf("Expected 0 processes, got %d", len(result.Processes)) + } +} + +func TestProcessHandler_Processes_Nil(t *testing.T) { + tc := setupProcessTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessManager.EXPECT().GetRunningProcesses().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/processes", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestProcessHandler_Processes_SingleProcess(t *testing.T) { + tc := setupProcessTestApp(t) + defer tc.Ctrl.Finish() + + expectedProcesses := map[string]*domain.Process{ + "main": { + Name: "main", + Type: "tty", + }, + } + tc.ProcessManager.EXPECT().GetRunningProcesses().Return(expectedProcesses) + + req := httptest.NewRequest("GET", "/api/v1/processes", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ProcessesResponse + json.Unmarshal(body, &result) + + if len(result.Processes) != 1 { + t.Errorf("Expected 1 process, got %d", len(result.Processes)) + } + if result.Processes["main"].Name != "main" { + t.Errorf("Expected name 'main', got '%s'", result.Processes["main"].Name) + } +} diff --git a/internal/handler/queue_hander.go b/internal/handler/queue_hander.go index a41ed99..9b197ef 100644 --- a/internal/handler/queue_hander.go +++ b/internal/handler/queue_hander.go @@ -13,13 +13,6 @@ func NewQueueHandler(queueManager ports.QueueManagerInterface) *ScrollHandler { return &ScrollHandler{QueueManager: queueManager} } -// @Summary Get current scroll -// @ID getQueue -// @Tags queue, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} map[string]domain.ScrollLockStatus -// @Router /api/v1/queue [get] -func (sl ScrollHandler) Queue(c *fiber.Ctx) error { +func (sl ScrollHandler) GetQueue(c *fiber.Ctx) error { return c.JSON(sl.QueueManager.GetQueue()) } diff --git a/internal/handler/queue_handler_test.go b/internal/handler/queue_handler_test.go new file mode 100644 index 0000000..da0c15e --- /dev/null +++ b/internal/handler/queue_handler_test.go @@ -0,0 +1,152 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// QueueTestContext holds all mocked services for queue handler testing +type QueueTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + QueueManager *mock_ports.MockQueueManagerInterface + Handler *ScrollHandler // Note: Queue uses ScrollHandler struct +} + +// setupQueueTestApp creates a Fiber app with mocked dependencies for testing +func setupQueueTestApp(t *testing.T) *QueueTestContext { + ctrl := gomock.NewController(t) + + queueManager := mock_ports.NewMockQueueManagerInterface(ctrl) + // NewQueueHandler returns *ScrollHandler + handler := NewQueueHandler(queueManager) + + app := fiber.New() + app.Get("/api/v1/queue", handler.GetQueue) + + return &QueueTestContext{ + App: app, + Ctrl: ctrl, + QueueManager: queueManager, + Handler: handler, + } +} + +func TestQueueHandler_Queue_Success(t *testing.T) { + tc := setupQueueTestApp(t) + defer tc.Ctrl.Finish() + + expectedQueue := map[string]domain.ScrollLockStatus{ + "install": "done", + "start": "running", + "backup": "waiting", + } + tc.QueueManager.EXPECT().GetQueue().Return(expectedQueue) + + req := httptest.NewRequest("GET", "/api/v1/queue", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]domain.ScrollLockStatus + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 3 { + t.Errorf("Expected 3 queue items, got %d", len(result)) + } + if result["install"] != "done" { + t.Errorf("Expected install status 'done', got '%s'", result["install"]) + } + if result["start"] != "running" { + t.Errorf("Expected start status 'running', got '%s'", result["start"]) + } +} + +func TestQueueHandler_Queue_Empty(t *testing.T) { + tc := setupQueueTestApp(t) + defer tc.Ctrl.Finish() + + tc.QueueManager.EXPECT().GetQueue().Return(map[string]domain.ScrollLockStatus{}) + + req := httptest.NewRequest("GET", "/api/v1/queue", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]domain.ScrollLockStatus + json.Unmarshal(body, &result) + + if len(result) != 0 { + t.Errorf("Expected 0 queue items, got %d", len(result)) + } +} + +func TestQueueHandler_Queue_Nil(t *testing.T) { + tc := setupQueueTestApp(t) + defer tc.Ctrl.Finish() + + tc.QueueManager.EXPECT().GetQueue().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/queue", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestQueueHandler_Queue_SingleItem(t *testing.T) { + tc := setupQueueTestApp(t) + defer tc.Ctrl.Finish() + + expectedQueue := map[string]domain.ScrollLockStatus{ + "init": "done", + } + tc.QueueManager.EXPECT().GetQueue().Return(expectedQueue) + + req := httptest.NewRequest("GET", "/api/v1/queue", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]domain.ScrollLockStatus + json.Unmarshal(body, &result) + + if len(result) != 1 { + t.Errorf("Expected 1 queue item, got %d", len(result)) + } +} diff --git a/internal/handler/scroll_handler.go b/internal/handler/scroll_handler.go index 1f1353f..c977b05 100644 --- a/internal/handler/scroll_handler.go +++ b/internal/handler/scroll_handler.go @@ -2,6 +2,7 @@ package handler import ( "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" "github.com/highcard-dev/daemon/internal/utils/logger" @@ -16,19 +17,6 @@ type ScrollHandler struct { ProcessManager ports.ProcessManagerInterface } -type StartScrollRequestBody struct { - CommandId string `json:"command"` - Sync bool `json:"sync"` -} - -type StartProcedureRequestBody struct { - Mode string `json:"mode"` - Data string `json:"data"` - Process string `json:"process"` - Dependencies []string `json:"dependencies"` - Sync bool `json:"sync"` -} - func NewScrollHandler( scrollService ports.ScrollServiceInterface, pluginManager ports.PluginManagerInterface, @@ -39,39 +27,26 @@ func NewScrollHandler( return &ScrollHandler{ScrollService: scrollService, PluginManager: pluginManager, ProcessLauncher: processLauncher, QueueManager: queueManager, ProcessManager: processManager} } -// @Summary Get current scroll -// @ID getScroll -// @Tags scroll, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} domain.File -// @Router /api/v1/scroll [get] func (sl ScrollHandler) GetScroll(c *fiber.Ctx) error { return c.JSON(sl.ScrollService.GetFile()) } -// @Summary Get current scroll -// @ID runCommand -// @Tags scroll, druid, daemon -// @Accept */* -// @Param body body StartScrollRequestBody true "Scroll Body" -// @Produce json -// @Success 200 -// @Success 201 -// @Failure 400 -// @Failure 500 -// @Router /api/v1/command [post] func (sl ScrollHandler) RunCommand(c *fiber.Ctx) error { - - var requestBody StartScrollRequestBody + var requestBody api.StartCommandRequest err := c.BodyParser(&requestBody) if err != nil { return c.SendStatus(400) } - if requestBody.Sync { - err = sl.QueueManager.AddTempItem(requestBody.CommandId) + // Handle optional Sync field + sync := false + if requestBody.Sync != nil { + sync = *requestBody.Sync + } + + if sync { + err = sl.QueueManager.AddTempItem(requestBody.Command) if err != nil { logger.Log().Error("Error running command (sync)", zap.Error(err)) return c.SendStatus(500) @@ -79,7 +54,7 @@ func (sl ScrollHandler) RunCommand(c *fiber.Ctx) error { return c.SendStatus(200) } else { go func() { - err = sl.QueueManager.AddTempItem(requestBody.CommandId) + err = sl.QueueManager.AddTempItem(requestBody.Command) if err != nil { logger.Log().Error("Error running command (async)", zap.Error(err)) } @@ -87,20 +62,10 @@ func (sl ScrollHandler) RunCommand(c *fiber.Ctx) error { c.SendStatus(201) return nil } - } -// @Summary Run procedure -// @ID runProcedure -// @Tags scroll, druid, daemon -// @Accept */* -// @Param body body StartProcedureRequestBody true "Procedure Body" -// @Produce json -// @Success 201 -// @Success 200 {object} any -// @Router /api/v1/procedure [post] func (sl ScrollHandler) RunProcedure(c *fiber.Ctx) error { - var requestBody StartProcedureRequestBody + var requestBody api.StartProcedureRequest err := c.BodyParser(&requestBody) if err != nil { @@ -133,15 +98,26 @@ func (sl ScrollHandler) RunProcedure(c *fiber.Ctx) error { } command := requestBody.Process - deps := requestBody.Dependencies + + // Handle optional Dependencies field + deps := []string{} + if requestBody.Dependencies != nil { + deps = *requestBody.Dependencies + } + process := sl.ProcessManager.GetRunningProcess(command) if process == nil { c.SendString("Running process not found") return c.SendStatus(400) } - if !requestBody.Sync { + // Handle optional Sync field + sync := false + if requestBody.Sync != nil { + sync = *requestBody.Sync + } + if !sync { go sl.ProcessLauncher.RunProcedure(&procedure, command, deps) return c.SendStatus(201) } else { @@ -154,14 +130,19 @@ func (sl ScrollHandler) RunProcedure(c *fiber.Ctx) error { } } -// @Summary Get process procedure statuses -// @ID getProcedures -// @Tags process, procedures, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} map[string]domain.ScrollLockStatus -// @Router /api/v1/procedures [get] -func (sh ScrollHandler) Procedures(c *fiber.Ctx) error { +func (sh ScrollHandler) GetProcedures(c *fiber.Ctx) error { process := sh.ProcessLauncher.GetProcedureStatuses() return c.JSON(process) } + +func (sh ScrollHandler) AddCommand(c *fiber.Ctx, command string) error { + + var commands *domain.CommandInstructionSet + err := c.BodyParser(&commands) + if err != nil { + return c.SendStatus(400) + } + sh.ScrollService.AddTemporaryCommand(command, commands) + + return c.SendStatus(201) +} diff --git a/internal/handler/scroll_handler_test.go b/internal/handler/scroll_handler_test.go new file mode 100644 index 0000000..2dac9e4 --- /dev/null +++ b/internal/handler/scroll_handler_test.go @@ -0,0 +1,677 @@ +package handler + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// Helper functions for creating pointer values in test structs +func boolPtr(b bool) *bool { + return &b +} + +func stringSlicePtr(s []string) *[]string { + return &s +} + +// TestContext holds all mocked services for testing +type TestContext struct { + App *fiber.App + Ctrl *gomock.Controller + ScrollService *mock_ports.MockScrollServiceInterface + PluginManager *mock_ports.MockPluginManagerInterface + ProcedureLauncher *mock_ports.MockProcedureLauchnerInterface + QueueManager *mock_ports.MockQueueManagerInterface + ProcessManager *mock_ports.MockProcessManagerInterface + Handler *ScrollHandler +} + +// setupTestApp creates a Fiber app with mocked dependencies for testing +func setupTestApp(t *testing.T) *TestContext { + ctrl := gomock.NewController(t) + + // Create mocked services + scrollService := mock_ports.NewMockScrollServiceInterface(ctrl) + pluginManager := mock_ports.NewMockPluginManagerInterface(ctrl) + procedureLauncher := mock_ports.NewMockProcedureLauchnerInterface(ctrl) + queueManager := mock_ports.NewMockQueueManagerInterface(ctrl) + processManager := mock_ports.NewMockProcessManagerInterface(ctrl) + + // Create handler with mocks + handler := NewScrollHandler(scrollService, pluginManager, procedureLauncher, queueManager, processManager) + + // Create minimal Fiber app for testing + app := fiber.New() + app.Get("/api/v1/scroll", handler.GetScroll) + app.Post("/api/v1/command", handler.RunCommand) + app.Post("/api/v1/procedure", handler.RunProcedure) + app.Get("/api/v1/procedures", handler.GetProcedures) + + return &TestContext{ + App: app, + Ctrl: ctrl, + ScrollService: scrollService, + PluginManager: pluginManager, + ProcedureLauncher: procedureLauncher, + QueueManager: queueManager, + ProcessManager: processManager, + Handler: handler, + } +} + +// ============================================================================ +// GET /api/v1/scroll Tests +// ============================================================================ + +func TestScrollHandler_GetScroll_Success(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations + expectedFile := &domain.File{ + Name: "test-scroll", + Desc: "Test scroll description", + AppVersion: "1.0.0", + } + tc.ScrollService.EXPECT().GetFile().Return(expectedFile) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/scroll", nil) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Verify response body + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + var result domain.File + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result.Name != expectedFile.Name { + t.Errorf("Expected name %s, got %s", expectedFile.Name, result.Name) + } + if result.Desc != expectedFile.Desc { + t.Errorf("Expected desc %s, got %s", expectedFile.Desc, result.Desc) + } +} + +func TestScrollHandler_GetScroll_NilFile(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock to return nil + tc.ScrollService.EXPECT().GetFile().Return(nil) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/scroll", nil) + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Should still return 200 with null body + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +// ============================================================================ +// POST /api/v1/command Tests +// ============================================================================ + +func TestScrollHandler_RunCommand_SyncSuccess(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations for sync command + tc.QueueManager.EXPECT().AddTempItem("test-command").Return(nil) + + // Create request body + requestBody := api.StartCommandRequest{ + Command: "test-command", + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/command", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (200 for sync) + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunCommand_AsyncSuccess(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations for async command + tc.QueueManager.EXPECT().AddTempItem("test-command").Return(nil) + + // Create request body + requestBody := api.StartCommandRequest{ + Command: "test-command", + Sync: boolPtr(false), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/command", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (201 for async) + if resp.StatusCode != 201 { + t.Errorf("Expected status 201, got %d", resp.StatusCode) + } + + // Give async goroutine time to complete + time.Sleep(100 * time.Millisecond) +} + +func TestScrollHandler_RunCommand_InvalidBody(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Create request with invalid JSON + req := httptest.NewRequest("POST", "/api/v1/command", bytes.NewReader([]byte("invalid json"))) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for invalid body) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunCommand_SyncError(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock to return error + tc.QueueManager.EXPECT().AddTempItem("test-command").Return(fiber.NewError(500, "internal error")) + + // Create request body + requestBody := api.StartCommandRequest{ + Command: "test-command", + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/command", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (500 for error) + if resp.StatusCode != 500 { + t.Errorf("Expected status 500, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunCommand_EmptyBody(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Create request with empty body + req := httptest.NewRequest("POST", "/api/v1/command", nil) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for empty body) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +// ============================================================================ +// POST /api/v1/procedure Tests +// ============================================================================ + +func TestScrollHandler_RunProcedure_SyncSuccess(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("rcon").Return(true) + tc.ProcessManager.EXPECT().GetRunningProcess("test-process").Return(&domain.Process{}) + tc.ProcedureLauncher.EXPECT().RunProcedure(gomock.Any(), "test-process", []string{"dep1"}).Return("result", nil, nil) + + // Create request body + requestBody := api.StartProcedureRequest{ + Mode: "rcon", + Data: "test-data", + Process: "test-process", + Dependencies: stringSlicePtr([]string{"dep1"}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (200 for sync success) + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) + } +} + +func TestScrollHandler_RunProcedure_AsyncSuccess(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("rcon").Return(true) + tc.ProcessManager.EXPECT().GetRunningProcess("test-process").Return(&domain.Process{}) + tc.ProcedureLauncher.EXPECT().RunProcedure(gomock.Any(), "test-process", []string{}).Return("", nil, nil) + + // Create request body + requestBody := api.StartProcedureRequest{ + Mode: "rcon", + Data: "test-data", + Process: "test-process", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(false), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (201 for async) + if resp.StatusCode != 201 { + t.Errorf("Expected status 201, got %d", resp.StatusCode) + } + + // Give async goroutine time to complete + time.Sleep(100 * time.Millisecond) +} + +func TestScrollHandler_RunProcedure_StdinMode(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations - stdin mode is always allowed + // Note: CanRunStandaloneProcedure is still called due to evaluation order, but the condition short-circuits + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("stdin").Return(false) + tc.ProcessManager.EXPECT().GetRunningProcess("test-process").Return(&domain.Process{}) + tc.ProcedureLauncher.EXPECT().RunProcedure(gomock.Any(), "test-process", []string{}).Return("result", nil, nil) + + // Create request body with stdin mode + requestBody := api.StartProcedureRequest{ + Mode: "stdin", + Data: "test-data", + Process: "test-process", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) + } +} + +func TestScrollHandler_RunProcedure_InvalidMode(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock - mode not allowed + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("invalid-mode").Return(false) + + // Create request body + requestBody := api.StartProcedureRequest{ + Mode: "invalid-mode", + Data: "test-data", + Process: "test-process", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for invalid mode) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunProcedure_EmptyData(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("rcon").Return(true) + + // Create request body with empty data + requestBody := api.StartProcedureRequest{ + Mode: "rcon", + Data: "", + Process: "test-process", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for empty data) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunProcedure_ProcessNotFound(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock - process not found + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("rcon").Return(true) + tc.ProcessManager.EXPECT().GetRunningProcess("non-existent").Return(nil) + + // Create request body + requestBody := api.StartProcedureRequest{ + Mode: "rcon", + Data: "test-data", + Process: "non-existent", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for process not found) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunProcedure_InvalidBody(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Create request with invalid JSON + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader([]byte("invalid json"))) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for invalid body) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +func TestScrollHandler_RunProcedure_SyncError(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations - procedure returns error + tc.PluginManager.EXPECT().CanRunStandaloneProcedure("rcon").Return(true) + tc.ProcessManager.EXPECT().GetRunningProcess("test-process").Return(&domain.Process{}) + tc.ProcedureLauncher.EXPECT().RunProcedure(gomock.Any(), "test-process", []string{}).Return("", nil, fiber.NewError(500, "procedure failed")) + + // Create request body + requestBody := api.StartProcedureRequest{ + Mode: "rcon", + Data: "test-data", + Process: "test-process", + Dependencies: stringSlicePtr([]string{}), + Sync: boolPtr(true), + } + bodyBytes, _ := json.Marshal(requestBody) + + // Create request + req := httptest.NewRequest("POST", "/api/v1/procedure", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status (400 for procedure error) + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } +} + +// ============================================================================ +// GET /api/v1/procedures Tests +// ============================================================================ + +func TestScrollHandler_Procedures_Success(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock expectations + expectedStatuses := map[string]domain.ScrollLockStatus{ + "install": "done", + "start": "running", + } + tc.ProcedureLauncher.EXPECT().GetProcedureStatuses().Return(expectedStatuses) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/procedures", nil) + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Verify response body + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + var result map[string]domain.ScrollLockStatus + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != len(expectedStatuses) { + t.Errorf("Expected %d statuses, got %d", len(expectedStatuses), len(result)) + } + + for key, expectedValue := range expectedStatuses { + if result[key] != expectedValue { + t.Errorf("Expected status %s for %s, got %s", expectedValue, key, result[key]) + } + } +} + +func TestScrollHandler_Procedures_Empty(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock to return empty map + tc.ProcedureLauncher.EXPECT().GetProcedureStatuses().Return(map[string]domain.ScrollLockStatus{}) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/procedures", nil) + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Verify response status + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Verify empty response + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + var result map[string]domain.ScrollLockStatus + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 0 { + t.Errorf("Expected 0 statuses, got %d", len(result)) + } +} + +func TestScrollHandler_Procedures_NilMap(t *testing.T) { + tc := setupTestApp(t) + defer tc.Ctrl.Finish() + + // Setup mock to return nil + tc.ProcedureLauncher.EXPECT().GetProcedureStatuses().Return(nil) + + // Create request + req := httptest.NewRequest("GET", "/api/v1/procedures", nil) + + // Execute request + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + // Should still return 200 + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} diff --git a/internal/handler/scroll_log_handler.go b/internal/handler/scroll_log_handler.go index 59a0c27..81bef92 100644 --- a/internal/handler/scroll_log_handler.go +++ b/internal/handler/scroll_log_handler.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" ) @@ -15,27 +16,15 @@ type ScrollLogHandler struct { processManager ports.ProcessManagerInterface } -type ScrollLogStream struct { - Key string `json:"key" validate:"required"` - Log []string `json:"log" validate:"required"` -} // @name ScrollLogStream - func NewScrollLogHandler(scrollService ports.ScrollServiceInterface, logManager ports.LogManagerInterface, processManager ports.ProcessManagerInterface) *ScrollLogHandler { return &ScrollLogHandler{scrollService: scrollService, logManager: logManager, processManager: processManager} } -// @Summary List all logs -// @ID listLogs -// @Tags logs, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {object} []ScrollLogStream -// @Router /api/v1/logs [get] func (sl ScrollLogHandler) ListAllLogs(c *fiber.Ctx) error { streams := sl.logManager.GetStreams() - responseData := make([]ScrollLogStream, 0, len(streams)) + responseData := make([]api.ScrollLogStream, 0, len(streams)) mutex := sync.Mutex{} wg := sync.WaitGroup{} @@ -46,7 +35,7 @@ func (sl ScrollLogHandler) ListAllLogs(c *fiber.Ctx) error { go func(streamName string, res <-chan []byte, log *domain.Log) { defer wg.Done() - logResponse := ScrollLogStream{ + logResponse := api.ScrollLogStream{ Key: streamName, Log: make([]string, 0, log.Capacity), } @@ -66,16 +55,7 @@ func (sl ScrollLogHandler) ListAllLogs(c *fiber.Ctx) error { return c.JSON(responseData) } -// @Summary List stream logs -// @ID listLog -// @Tags logs, druid, daemon -// @Accept */* -// @Produce json -// @Param stream path string true "Stream name" -// @Success 200 {object} ScrollLogStream -// @Router /api/v1/logs/{stream} [get] -// ListStreamLogs lists logs for a specific stream. -func (sl ScrollLogHandler) ListStreamLogs(c *fiber.Ctx) error { +func (sl ScrollLogHandler) ListStreamLogs(c *fiber.Ctx, stream string) error { steam, ok := sl.logManager.GetStreams()[c.Params("stream")] if !ok { @@ -83,7 +63,7 @@ func (sl ScrollLogHandler) ListStreamLogs(c *fiber.Ctx) error { return nil } - responseData := ScrollLogStream{ + responseData := api.ScrollLogStream{ Key: c.Params("stream"), Log: make([]string, 0, steam.Capacity), } diff --git a/internal/handler/scroll_metric_handler.go b/internal/handler/scroll_metric_handler.go index e5630ab..3c063c8 100644 --- a/internal/handler/scroll_metric_handler.go +++ b/internal/handler/scroll_metric_handler.go @@ -15,32 +15,14 @@ func NewScrollMetricHandler(scrollService ports.ScrollServiceInterface, processM return &ScrollMetricHandler{ScrollService: scrollService, ProcessMonitor: processMonitor} } -type PsTress = map[string]*domain.ProcessTreeRoot // @name PsTreeMap +// Keep original type aliases (use pointers to match service return types) +type PsTress = map[string]*domain.ProcessTreeRoot +type Metrics = map[string]*domain.ProcessMonitorMetrics -type Metrics = map[string]*domain.ProcessMonitorMetrics // @name ProcessMonitorMetricsMap - -// Metrics returns the metrics for all processes. -// -// @Summary Get all process metrics -// @Description Get the metrics for all processes. -// @Tags metrics, druid, daemon -// @Accept json -// @Produce json -// @Success 200 {object} Metrics -// @Router /api/v1/metrics [get] -func (sl ScrollMetricHandler) Metrics(c *fiber.Ctx) error { +func (sl ScrollMetricHandler) GetMetrics(c *fiber.Ctx) error { return c.JSON(sl.ProcessMonitor.GetAllProcessesMetrics()) } -// Returns whole PSTree of process -// -// @Summary Get all process metrics -// @Description Get pstree of running process -// @Tags metrics, druid, daemon -// @Accept json -// @Produce json -// @Success 200 {object} PsTress -// @Router /api/v1/pstree [get] -func (sl ScrollMetricHandler) PsTree(c *fiber.Ctx) error { +func (sl ScrollMetricHandler) GetPsTree(c *fiber.Ctx) error { return c.JSON(sl.ProcessMonitor.GetPsTrees()) } diff --git a/internal/handler/scroll_metric_handler_test.go b/internal/handler/scroll_metric_handler_test.go new file mode 100644 index 0000000..a4089d8 --- /dev/null +++ b/internal/handler/scroll_metric_handler_test.go @@ -0,0 +1,231 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// ScrollMetricTestContext holds all mocked services for scroll metric handler testing +type ScrollMetricTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + ScrollService *mock_ports.MockScrollServiceInterface + ProcessMonitor *mock_ports.MockProcessMonitorInterface + Handler *ScrollMetricHandler +} + +// setupScrollMetricTestApp creates a Fiber app with mocked dependencies for testing +func setupScrollMetricTestApp(t *testing.T) *ScrollMetricTestContext { + ctrl := gomock.NewController(t) + + scrollService := mock_ports.NewMockScrollServiceInterface(ctrl) + processMonitor := mock_ports.NewMockProcessMonitorInterface(ctrl) + handler := NewScrollMetricHandler(scrollService, processMonitor) + + app := fiber.New() + app.Get("/api/v1/metrics", handler.GetMetrics) + app.Get("/api/v1/pstree", handler.GetPsTree) + + return &ScrollMetricTestContext{ + App: app, + Ctrl: ctrl, + ScrollService: scrollService, + ProcessMonitor: processMonitor, + Handler: handler, + } +} + +func TestScrollMetricHandler_Metrics_Success(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + expectedMetrics := map[string]*domain.ProcessMonitorMetrics{ + "start": { + Cpu: 25.5, + Memory: 1024000, + Pid: 1234, + }, + "worker": { + Cpu: 10.0, + Memory: 512000, + Pid: 5678, + }, + } + tc.ProcessMonitor.EXPECT().GetAllProcessesMetrics().Return(expectedMetrics) + + req := httptest.NewRequest("GET", "/api/v1/metrics", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]*domain.ProcessMonitorMetrics + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 2 { + t.Errorf("Expected 2 metrics, got %d", len(result)) + } + if result["start"].Cpu != 25.5 { + t.Errorf("Expected CPU 25.5, got %f", result["start"].Cpu) + } +} + +func TestScrollMetricHandler_Metrics_Empty(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessMonitor.EXPECT().GetAllProcessesMetrics().Return(map[string]*domain.ProcessMonitorMetrics{}) + + req := httptest.NewRequest("GET", "/api/v1/metrics", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestScrollMetricHandler_Metrics_Nil(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessMonitor.EXPECT().GetAllProcessesMetrics().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/metrics", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestScrollMetricHandler_PsTree_Success(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + expectedPsTree := map[string]*domain.ProcessTreeRoot{ + "start": { + TotalProcessCount: 5, + TotalCpuPercent: 25.0, + }, + } + tc.ProcessMonitor.EXPECT().GetPsTrees().Return(expectedPsTree) + + req := httptest.NewRequest("GET", "/api/v1/pstree", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]*domain.ProcessTreeRoot + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result) != 1 { + t.Errorf("Expected 1 ps tree entry, got %d", len(result)) + } + if result["start"].TotalProcessCount != 5 { + t.Errorf("Expected TotalProcessCount 5, got %d", result["start"].TotalProcessCount) + } +} + +func TestScrollMetricHandler_PsTree_Empty(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessMonitor.EXPECT().GetPsTrees().Return(map[string]*domain.ProcessTreeRoot{}) + + req := httptest.NewRequest("GET", "/api/v1/pstree", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestScrollMetricHandler_PsTree_Nil(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + tc.ProcessMonitor.EXPECT().GetPsTrees().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/pstree", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestScrollMetricHandler_PsTree_MultipleProcesses(t *testing.T) { + tc := setupScrollMetricTestApp(t) + defer tc.Ctrl.Finish() + + expectedPsTree := map[string]*domain.ProcessTreeRoot{ + "start": { + TotalProcessCount: 3, + TotalCpuPercent: 15.0, + }, + "worker": { + TotalProcessCount: 2, + TotalCpuPercent: 10.0, + }, + } + tc.ProcessMonitor.EXPECT().GetPsTrees().Return(expectedPsTree) + + req := httptest.NewRequest("GET", "/api/v1/pstree", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result map[string]*domain.ProcessTreeRoot + json.Unmarshal(body, &result) + + if len(result) != 2 { + t.Errorf("Expected 2 ps tree entries, got %d", len(result)) + } +} diff --git a/internal/handler/ui_dev_handler.go b/internal/handler/ui_dev_handler.go deleted file mode 100644 index e5ffa33..0000000 --- a/internal/handler/ui_dev_handler.go +++ /dev/null @@ -1,296 +0,0 @@ -package handler - -import ( - "encoding/json" - "path/filepath" - "time" - - "github.com/gofiber/contrib/websocket" - "github.com/gofiber/fiber/v2" - "github.com/highcard-dev/daemon/internal/core/domain" - "github.com/highcard-dev/daemon/internal/core/ports" - "github.com/highcard-dev/daemon/internal/utils/logger" - "go.uber.org/zap" -) - -// DevModeResponse represents the response for enable/disable dev mode operations -type DevModeResponse struct { - Status string `json:"status"` - Enabled bool `json:"enabled"` -} // @name DevModeResponse - -// DevStatusResponse represents the response for dev mode status -type DevStatusResponse struct { - Enabled bool `json:"enabled"` - WatchedPaths []string `json:"watchedPaths"` -} // @name DevStatusResponse - -// ErrorResponse represents an error response -type ErrorResponse struct { - Status string `json:"status"` - Error string `json:"error"` -} // @name ErrorResponse - -type UiDevHandler struct { - uiDevService ports.UiDevServiceInterface - scrollService ports.ScrollServiceInterface -} - -type DevModeBody struct { - HotReloadCommands map[string]*domain.CommandInstructionSet `json:"hotReloadCommands,omitempty"` - BuildCommands map[string]*domain.CommandInstructionSet `json:"buildCommands,omitempty"` -} - -func NewUiDevHandler(uiDevService ports.UiDevServiceInterface, scrollService ports.ScrollServiceInterface) *UiDevHandler { - return &UiDevHandler{ - uiDevService: uiDevService, - scrollService: scrollService, - } -} - -// @Summary Build the UI in development mode -// @ID buildDev -// @Tags ui, dev, druid, daemon -// @Accept json -// @Produce json -// @Success 200 -// @Success 412 -// @Failure 500 -// @Router /api/v1/dev/build [post] -func (udh *UiDevHandler) Build(ctx *fiber.Ctx) error { - if !udh.uiDevService.IsWatching() { - ctx.Status(fiber.StatusPreconditionFailed) - return nil - } - - err := udh.uiDevService.Build() - if err != nil { - logger.Log().Error("Failed to run build commands", zap.Error(err)) - errorResponse := ErrorResponse{ - Status: "error", - Error: err.Error(), - } - return ctx.Status(500).JSON(errorResponse) - } - - return nil -} - -// @Summary Enable development mode -// @ID enableDev -// @Tags ui, dev, druid, daemon -// @Accept json -// @Produce json -// @Param body body DevModeBody false "Optional commands to run on file changes" -// @Success 200 {object} DevModeResponse -// @Failure 500 {object} ErrorResponse -// @Router /api/v1/dev/enable [post] -func (udh *UiDevHandler) Enable(ctx *fiber.Ctx) error { - if udh.uiDevService.IsWatching() { - response := DevModeResponse{ - Status: "already-active", - Enabled: true, - } - ctx.Status(fiber.StatusPreconditionFailed) - return ctx.JSON(response) - } - - var watchPaths []string - // Get current scroll to determine watch paths - scrollDir := udh.scrollService.GetDir() - if scrollDir == "" { - logger.Log().Error("Cannot enable development mode: No scroll loaded") - errorResponse := ErrorResponse{ - Status: "error", - Error: "No scroll loaded. Please load a scroll before enabling development mode.", - } - return ctx.Status(400).JSON(errorResponse) - } - - var requestBody DevModeBody - - err := ctx.BodyParser(&requestBody) - if err == nil { - udh.uiDevService.SetHotReloadCommands(requestBody.HotReloadCommands) - udh.uiDevService.SetBuildCommands(requestBody.BuildCommands) - } - - watchPaths = append(watchPaths, filepath.Join(scrollDir, "public/src"), filepath.Join(scrollDir, "private/src")) - - // Start file watching with scroll directory as base path - err = udh.uiDevService.StartWatching(scrollDir, watchPaths...) - if err != nil { - logger.Log().Error("Failed to start file watcher", zap.Error(err)) - errorResponse := ErrorResponse{ - Status: "error", - Error: err.Error(), - } - return ctx.Status(500).JSON(errorResponse) - } - - logger.Log().Info("UI development mode enabled") - - response := DevModeResponse{ - Status: "success", - Enabled: udh.uiDevService.IsWatching(), - } - return ctx.JSON(response) -} - -// @Summary Disable development mode -// @ID disableDev -// @Tags ui, dev, druid, daemon -// @Accept json -// @Produce json -// @Success 200 {object} DevModeResponse -// @Failure 500 {object} ErrorResponse -// @Router /api/v1/dev/disable [post] -func (udh *UiDevHandler) Disable(ctx *fiber.Ctx) error { - if !udh.uiDevService.IsWatching() { - response := DevModeResponse{ - Status: "success", - Enabled: false, - } - return ctx.JSON(response) - } - - // Stop file watching - err := udh.uiDevService.StopWatching() - if err != nil { - logger.Log().Error("Failed to stop file watcher", zap.Error(err)) - errorResponse := ErrorResponse{ - Status: "error", - Error: err.Error(), - } - return ctx.Status(500).JSON(errorResponse) - } - - logger.Log().Info("UI development mode disabled") - - response := DevModeResponse{ - Status: "success", - Enabled: udh.uiDevService.IsWatching(), - } - return ctx.JSON(response) -} - -// @Summary Get development mode status -// @ID getDevStatus -// @Tags ui, dev, druid, daemon -// @Accept json -// @Produce json -// @Success 200 {object} DevStatusResponse -// @Router /api/v1/dev/status [get] -func (udh *UiDevHandler) Status(ctx *fiber.Ctx) error { - isWatching := udh.uiDevService.IsWatching() - response := DevStatusResponse{ - Enabled: isWatching, - WatchedPaths: udh.uiDevService.GetWatchedPaths(), - } - return ctx.JSON(response) -} - -// NotifyChange handles WebSocket connections for real-time file change notifications -func (udh *UiDevHandler) NotifyChange(c *websocket.Conn) { - defer c.Close() - - // Check if development mode is enabled - if !udh.uiDevService.IsWatching() { - logger.Log().Warn("WebSocket connection attempted but development mode is not enabled") - c.WriteJSON(map[string]interface{}{ - "type": "error", - "message": "Development mode is not enabled", - }) - return - } - - // Subscribe to file change notifications - changesChan := udh.uiDevService.Subscribe() - if changesChan == nil { - logger.Log().Error("Failed to subscribe to file changes") - c.WriteJSON(map[string]interface{}{ - "type": "error", - "message": "Failed to subscribe to file changes", - }) - return - } - defer udh.uiDevService.Unsubscribe(changesChan) - - // Set up ping/pong - c.SetReadDeadline(time.Now().Add(60 * time.Second)) - c.SetPongHandler(func(string) error { - c.SetReadDeadline(time.Now().Add(60 * time.Second)) - return nil - }) - - // Send initial connection message - c.SetWriteDeadline(time.Now().Add(10 * time.Second)) - if err := c.WriteJSON(map[string]interface{}{ - "type": "connected", - "message": "Connected to file watcher", - "watchedPaths": udh.uiDevService.GetWatchedPaths(), - "timestamp": time.Now(), - }); err != nil { - logger.Log().Debug("Failed to send initial message, client disconnected", zap.Error(err)) - return - } - - logger.Log().Info("WebSocket client connected for file change notifications") - - // Create ping ticker - pingTicker := time.NewTicker(54 * time.Second) - defer pingTicker.Stop() - - // Start reader goroutine to detect disconnects - done := make(chan struct{}) - go func() { - defer close(done) - for { - _, _, err := c.ReadMessage() - if err != nil { - logger.Log().Debug("WebSocket client disconnected", zap.Error(err)) - return - } - } - }() - - // Main event loop - for { - select { - case <-done: - return - - case data := <-changesChan: - if data == nil { - return - } - - // Parse and send file change event - var fileEvent map[string]interface{} - if err := json.Unmarshal(*data, &fileEvent); err != nil { - logger.Log().Error("Failed to parse file change event", zap.Error(err)) - continue - } - - c.SetWriteDeadline(time.Now().Add(10 * time.Second)) - if err := c.WriteJSON(map[string]interface{}{ - "type": "file_change", - "data": fileEvent, - "timestamp": time.Now(), - }); err != nil { - logger.Log().Debug("Failed to send file change, client disconnected", zap.Error(err)) - return - } - - case <-pingTicker.C: - c.SetWriteDeadline(time.Now().Add(10 * time.Second)) - if err := c.WriteMessage(websocket.PingMessage, nil); err != nil { - logger.Log().Debug("Failed to send ping, client disconnected", zap.Error(err)) - return - } - } - } -} - -// Ensure UiDevHandler implements UiDevHandlerInterface at compile time -var _ ports.UiDevHandlerInterface = (*UiDevHandler)(nil) diff --git a/internal/handler/ui_handler.go b/internal/handler/ui_handler.go deleted file mode 100644 index 9fe4433..0000000 --- a/internal/handler/ui_handler.go +++ /dev/null @@ -1,66 +0,0 @@ -package handler - -import ( - "errors" - "os" - - "github.com/gofiber/fiber/v2" - "github.com/highcard-dev/daemon/internal/core/ports" -) - -type UiHandler struct { - uiService ports.UiServiceInterface -} - -func NewUiHandler(uiService ports.UiServiceInterface) *UiHandler { - return &UiHandler{ - uiService: uiService, - } -} - -// @Summary Serve public UI index -// @ID getPublicUIIndex -// @Tags ui, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {array} string "List of files in public UI directory" -// @Failure 404 {object} map[string]string "Public UI directory not found" -// @Failure 500 {object} map[string]string "Internal server error with details" -// @Router /public/index [get] -func (uh *UiHandler) PublicIndex(ctx *fiber.Ctx) error { - files, err := uh.uiService.GetIndex("public") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return ctx.Status(fiber.StatusNotFound).SendString("Public UI directory not found") - } - return ctx.Status(500).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return ctx.JSON(files) -} - -// @Summary Serve private UI index -// @ID getPrivateUIIndex -// @Tags ui, druid, daemon -// @Accept */* -// @Produce json -// @Success 200 {array} string "List of files in private UI directory" -// @Failure 404 {object} map[string]string "Private UI directory not found" -// @Failure 500 {object} map[string]string "Internal server error with details" -// @Router /private/index [get] -func (uh *UiHandler) PrivateIndex(ctx *fiber.Ctx) error { - files, err := uh.uiService.GetIndex("private") - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return ctx.Status(fiber.StatusNotFound).SendString("Private UI directory not found") - } - return ctx.Status(500).JSON(fiber.Map{ - "error": err.Error(), - }) - } - return ctx.JSON(files) -} - -// Ensure UiHandler implements UiHandlerInterface at compile time -var _ ports.UiHandlerInterface = (*UiHandler)(nil) diff --git a/internal/handler/ui_handler_test.go b/internal/handler/ui_handler_test.go deleted file mode 100644 index c777932..0000000 --- a/internal/handler/ui_handler_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package handler_test - -import ( - "errors" - "io" - "net/http/httptest" - "os" - "testing" - - "github.com/gofiber/fiber/v2" - "github.com/highcard-dev/daemon/internal/handler" - mock_ports "github.com/highcard-dev/daemon/test/mock" - "go.uber.org/mock/gomock" -) - -func TestUiHandler_PublicIndex(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockUiService := mock_ports.NewMockUiServiceInterface(ctrl) - uiHandler := handler.NewUiHandler(mockUiService) - - app := fiber.New() - app.Get("/public/index", uiHandler.PublicIndex) - - t.Run("PublicIndex_Success", func(t *testing.T) { - expectedFiles := []string{"/scroll/public/index.html", "/scroll/public/style.css"} - mockUiService.EXPECT(). - GetIndex("public"). - Return(expectedFiles, nil) - - req := httptest.NewRequest("GET", "/public/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 200 { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedJSON := `["/scroll/public/index.html","/scroll/public/style.css"]` - if string(body) != expectedJSON { - t.Errorf("Expected body %s, got %s", expectedJSON, string(body)) - } - }) - - t.Run("PublicIndex_404", func(t *testing.T) { - mockUiService.EXPECT(). - GetIndex("public"). - Return(nil, os.ErrNotExist) - - req := httptest.NewRequest("GET", "/public/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 404 { - t.Errorf("Expected status 404, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedBody := "Public UI directory not found" - if string(body) != expectedBody { - t.Errorf("Expected body %s, got %s", expectedBody, string(body)) - } - }) - - t.Run("PublicIndex_500", func(t *testing.T) { - errMsg := "some internal error" - mockUiService.EXPECT(). - GetIndex("public"). - Return(nil, errors.New(errMsg)) - - req := httptest.NewRequest("GET", "/public/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 500 { - t.Errorf("Expected status 500, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedBody := `{"error":"` + errMsg + `"}` - if string(body) != expectedBody { - t.Errorf("Expected body %s, got %s", expectedBody, string(body)) - } - }) -} - -func TestUiHandler_PrivateIndex(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockUiService := mock_ports.NewMockUiServiceInterface(ctrl) - uiHandler := handler.NewUiHandler(mockUiService) - - app := fiber.New() - app.Get("/private/index", uiHandler.PrivateIndex) - - t.Run("PrivateIndex_Success", func(t *testing.T) { - expectedFiles := []string{"/scroll/private/admin.html", "/scroll/private/config.js"} - mockUiService.EXPECT(). - GetIndex("private"). - Return(expectedFiles, nil) - - req := httptest.NewRequest("GET", "/private/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 200 { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedJSON := `["/scroll/private/admin.html","/scroll/private/config.js"]` - if string(body) != expectedJSON { - t.Errorf("Expected body %s, got %s", expectedJSON, string(body)) - } - }) - - t.Run("PrivateIndex_404", func(t *testing.T) { - mockUiService.EXPECT(). - GetIndex("private"). - Return(nil, os.ErrNotExist) - - req := httptest.NewRequest("GET", "/private/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 404 { - t.Errorf("Expected status 404, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedBody := "Private UI directory not found" - if string(body) != expectedBody { - t.Errorf("Expected body %s, got %s", expectedBody, string(body)) - } - }) - - t.Run("PrivateIndex_500", func(t *testing.T) { - errMsg := "some internal error" - mockUiService.EXPECT(). - GetIndex("private"). - Return(nil, errors.New(errMsg)) - - req := httptest.NewRequest("GET", "/private/index", nil) - resp, err := app.Test(req) - if err != nil { - t.Fatalf("Failed to make request: %v", err) - } - if resp.StatusCode != 500 { - t.Errorf("Expected status 500, got %d", resp.StatusCode) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Failed to read response body: %v", err) - } - expectedBody := `{"error":"` + errMsg + `"}` - if string(body) != expectedBody { - t.Errorf("Expected body %s, got %s", expectedBody, string(body)) - } - }) -} diff --git a/internal/handler/watch_handler.go b/internal/handler/watch_handler.go new file mode 100644 index 0000000..a73df77 --- /dev/null +++ b/internal/handler/watch_handler.go @@ -0,0 +1,228 @@ +package handler + +import ( + "encoding/json" + "path/filepath" + "time" + + "github.com/gofiber/contrib/websocket" + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + "github.com/highcard-dev/daemon/internal/core/ports" + "github.com/highcard-dev/daemon/internal/utils/logger" + "go.uber.org/zap" +) + +type WatchHandler struct { + uiWatchService ports.WatchServiceInterface + scrollService ports.ScrollServiceInterface +} + +func NewWatchHandler(uiWatchService ports.WatchServiceInterface, scrollService ports.ScrollServiceInterface) *WatchHandler { + return &WatchHandler{ + uiWatchService: uiWatchService, + scrollService: scrollService, + } +} + +func (udh *WatchHandler) EnableWatch(c *fiber.Ctx) error { + if udh.uiWatchService.IsWatching() { + response := api.WatchModeResponse{ + Status: "already-active", + Enabled: true, + } + c.Status(fiber.StatusPreconditionFailed) + return c.JSON(response) + } + + var watchPaths []string + // Get current scroll to determine watch paths + scrollDir := udh.scrollService.GetDir() + if scrollDir == "" { + logger.Log().Error("Cannot enable development mode: No scroll loaded") + errorResponse := api.ErrorResponse{ + Status: "error", + Error: "No scroll loaded. Please load a scroll before enabling development mode.", + } + return c.Status(400).JSON(errorResponse) + } + + var requestBody api.WatchModeRequest + + err := c.BodyParser(&requestBody) + if err == nil && requestBody.HotReloadCommands != nil { + err = udh.uiWatchService.SetHotReloadCommands(*requestBody.HotReloadCommands) + if err != nil { + logger.Log().Error("Invalid hot reload commands", zap.Error(err)) + errorResponse := api.ErrorResponse{ + Status: "error", + Error: err.Error(), + } + return c.Status(400).JSON(errorResponse) + } + } + + watchPaths = append(watchPaths, filepath.Join(scrollDir, "public/src"), filepath.Join(scrollDir, "private/src")) + + // Start file watching with scroll directory as base path + err = udh.uiWatchService.StartWatching(scrollDir, watchPaths...) + if err != nil { + logger.Log().Error("Failed to start file watcher", zap.Error(err)) + errorResponse := api.ErrorResponse{ + Status: "error", + Error: err.Error(), + } + return c.Status(500).JSON(errorResponse) + } + + logger.Log().Info("UI development mode enabled") + + response := api.WatchModeResponse{ + Status: "success", + Enabled: udh.uiWatchService.IsWatching(), + } + return c.JSON(response) +} + +func (udh *WatchHandler) DisableWatch(c *fiber.Ctx) error { + if !udh.uiWatchService.IsWatching() { + response := api.WatchModeResponse{ + Status: "success", + Enabled: false, + } + return c.JSON(response) + } + + // Stop file watching + err := udh.uiWatchService.StopWatching() + if err != nil { + logger.Log().Error("Failed to stop file watcher", zap.Error(err)) + errorResponse := api.ErrorResponse{ + Status: "error", + Error: err.Error(), + } + return c.Status(500).JSON(errorResponse) + } + + logger.Log().Info("UI development mode disabled") + + response := api.WatchModeResponse{ + Status: "success", + Enabled: udh.uiWatchService.IsWatching(), + } + return c.JSON(response) +} + +func (udh *WatchHandler) GetWatchStatus(c *fiber.Ctx) error { + isWatching := udh.uiWatchService.IsWatching() + response := api.WatchStatusResponse{ + Enabled: isWatching, + WatchedPaths: udh.uiWatchService.GetWatchedPaths(), + } + return c.JSON(response) +} + +// NotifyChange handles WebSocket connections for real-time file change notifications +func (udh *WatchHandler) NotifyChange(c *websocket.Conn) { + defer c.Close() + + // Check if development mode is enabled + if !udh.uiWatchService.IsWatching() { + logger.Log().Warn("WebSocket connection attempted but development mode is not enabled") + c.WriteJSON(map[string]interface{}{ + "type": "error", + "message": "Watchelopment mode is not enabled", + }) + return + } + + // Subscribe to file change notifications + changesChan := udh.uiWatchService.Subscribe() + if changesChan == nil { + logger.Log().Error("Failed to subscribe to file changes") + c.WriteJSON(map[string]interface{}{ + "type": "error", + "message": "Failed to subscribe to file changes", + }) + return + } + defer udh.uiWatchService.Unsubscribe(changesChan) + + // Set up ping/pong + c.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.SetPongHandler(func(string) error { + c.SetReadDeadline(time.Now().Add(60 * time.Second)) + return nil + }) + + // Send initial connection message + c.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.WriteJSON(map[string]interface{}{ + "type": "connected", + "message": "Connected to file watcher", + "watchedPaths": udh.uiWatchService.GetWatchedPaths(), + "timestamp": time.Now(), + }); err != nil { + logger.Log().Debug("Failed to send initial message, client disconnected", zap.Error(err)) + return + } + + logger.Log().Info("WebSocket client connected for file change notifications") + + // Create ping ticker + pingTicker := time.NewTicker(54 * time.Second) + defer pingTicker.Stop() + + // Start reader goroutine to detect disconnects + done := make(chan struct{}) + go func() { + defer close(done) + for { + _, _, err := c.ReadMessage() + if err != nil { + logger.Log().Debug("WebSocket client disconnected", zap.Error(err)) + return + } + } + }() + + // Main event loop + for { + select { + case <-done: + return + + case data := <-changesChan: + if data == nil { + return + } + + // Parse and send file change event + var fileEvent map[string]interface{} + if err := json.Unmarshal(*data, &fileEvent); err != nil { + logger.Log().Error("Failed to parse file change event", zap.Error(err)) + continue + } + + c.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.WriteJSON(map[string]interface{}{ + "type": "file_change", + "data": fileEvent, + "timestamp": time.Now(), + }); err != nil { + logger.Log().Debug("Failed to send file change, client disconnected", zap.Error(err)) + return + } + + case <-pingTicker.C: + c.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.WriteMessage(websocket.PingMessage, nil); err != nil { + logger.Log().Debug("Failed to send ping, client disconnected", zap.Error(err)) + return + } + } + } +} + +// Ensure WatchHandler implements WatchHandlerInterface at compile time +var _ ports.WatchHandlerInterface = (*WatchHandler)(nil) diff --git a/internal/handler/watch_handler_test.go b/internal/handler/watch_handler_test.go new file mode 100644 index 0000000..d709494 --- /dev/null +++ b/internal/handler/watch_handler_test.go @@ -0,0 +1,352 @@ +package handler + +import ( + "bytes" + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// WatchTestContext holds all mocked services for watch handler testing +type WatchTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + WatchService *mock_ports.MockWatchServiceInterface + ScrollService *mock_ports.MockScrollServiceInterface + Handler *WatchHandler +} + +// setupWatchTestApp creates a Fiber app with mocked dependencies for testing +func setupWatchTestApp(t *testing.T) *WatchTestContext { + ctrl := gomock.NewController(t) + + watchService := mock_ports.NewMockWatchServiceInterface(ctrl) + scrollService := mock_ports.NewMockScrollServiceInterface(ctrl) + + handler := NewWatchHandler(watchService, scrollService) + + app := fiber.New() + app.Post("/api/v1/watch/enable", handler.EnableWatch) + app.Post("/api/v1/watch/disable", handler.DisableWatch) + app.Get("/api/v1/watch/status", handler.GetWatchStatus) + + return &WatchTestContext{ + App: app, + Ctrl: ctrl, + WatchService: watchService, + ScrollService: scrollService, + Handler: handler, + } +} + +// ============================================================================ +// POST /api/v1/watch/enable Tests +// ============================================================================ + +func TestWatchHandler_Enable_Success(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + tc.ScrollService.EXPECT().GetDir().Return("/path/to/scroll") + // When request has no body or empty body, BodyParser fails, so SetHotReloadCommands is NOT called + tc.WatchService.EXPECT().StartWatching("/path/to/scroll", gomock.Any(), gomock.Any()).Return(nil) + tc.WatchService.EXPECT().IsWatching().Return(true) + + req := httptest.NewRequest("POST", "/api/v1/watch/enable", nil) + req.Header.Set("Content-Type", "application/json") + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + t.Errorf("Expected status 200, got %d, body: %s", resp.StatusCode, string(body)) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchModeResponse + json.Unmarshal(body, &result) + + if result.Status != "success" { + t.Errorf("Expected status 'success', got '%s'", result.Status) + } + if !result.Enabled { + t.Error("Expected enabled to be true") + } +} + +func TestWatchHandler_Enable_AlreadyActive(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(true) + + req := httptest.NewRequest("POST", "/api/v1/watch/enable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 412 { + t.Errorf("Expected status 412, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchModeResponse + json.Unmarshal(body, &result) + + if result.Status != "already-active" { + t.Errorf("Expected status 'already-active', got '%s'", result.Status) + } +} + +func TestWatchHandler_Enable_NoScrollLoaded(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + tc.ScrollService.EXPECT().GetDir().Return("") + + req := httptest.NewRequest("POST", "/api/v1/watch/enable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 400 { + t.Errorf("Expected status 400, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ErrorResponse + json.Unmarshal(body, &result) + + if result.Status != "error" { + t.Errorf("Expected status 'error', got '%s'", result.Status) + } +} + +func TestWatchHandler_Enable_StartWatchingError(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + tc.ScrollService.EXPECT().GetDir().Return("/path/to/scroll") + // When request has no body, BodyParser fails, so SetHotReloadCommands is NOT called + tc.WatchService.EXPECT().StartWatching("/path/to/scroll", gomock.Any(), gomock.Any()).Return(fiber.NewError(500, "watcher error")) + + req := httptest.NewRequest("POST", "/api/v1/watch/enable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 500 { + t.Errorf("Expected status 500, got %d", resp.StatusCode) + } +} + +func TestWatchHandler_Enable_WithCommands(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + tc.ScrollService.EXPECT().GetDir().Return("/path/to/scroll") + tc.WatchService.EXPECT().SetHotReloadCommands([]string{"npm run dev"}) + tc.WatchService.EXPECT().StartWatching("/path/to/scroll", gomock.Any(), gomock.Any()).Return(nil) + tc.WatchService.EXPECT().IsWatching().Return(true) + + hotReloadCmds := []string{"npm run dev"} + requestBody := api.WatchModeRequest{ + HotReloadCommands: &hotReloadCmds, + } + bodyBytes, _ := json.Marshal(requestBody) + + req := httptest.NewRequest("POST", "/api/v1/watch/enable", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +// ============================================================================ +// POST /api/v1/watch/disable Tests +// ============================================================================ + +func TestWatchHandler_Disable_Success(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(true) + tc.WatchService.EXPECT().StopWatching().Return(nil) + tc.WatchService.EXPECT().IsWatching().Return(false) + + req := httptest.NewRequest("POST", "/api/v1/watch/disable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchModeResponse + json.Unmarshal(body, &result) + + if result.Status != "success" { + t.Errorf("Expected status 'success', got '%s'", result.Status) + } + if result.Enabled { + t.Error("Expected enabled to be false") + } +} + +func TestWatchHandler_Disable_NotWatching(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + + req := httptest.NewRequest("POST", "/api/v1/watch/disable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchModeResponse + json.Unmarshal(body, &result) + + if result.Status != "success" { + t.Errorf("Expected status 'success', got '%s'", result.Status) + } +} + +func TestWatchHandler_Disable_StopWatchingError(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(true) + tc.WatchService.EXPECT().StopWatching().Return(fiber.NewError(500, "stop error")) + + req := httptest.NewRequest("POST", "/api/v1/watch/disable", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 500 { + t.Errorf("Expected status 500, got %d", resp.StatusCode) + } +} + +// ============================================================================ +// GET /api/v1/watch/status Tests +// ============================================================================ + +func TestWatchHandler_Status_Enabled(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + watchedPaths := []string{"/path/to/public/src", "/path/to/private/src"} + tc.WatchService.EXPECT().IsWatching().Return(true) + tc.WatchService.EXPECT().GetWatchedPaths().Return(watchedPaths) + + req := httptest.NewRequest("GET", "/api/v1/watch/status", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchStatusResponse + json.Unmarshal(body, &result) + + if !result.Enabled { + t.Error("Expected enabled to be true") + } + if len(result.WatchedPaths) != 2 { + t.Errorf("Expected 2 watched paths, got %d", len(result.WatchedPaths)) + } +} + +func TestWatchHandler_Status_Disabled(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(false) + tc.WatchService.EXPECT().GetWatchedPaths().Return([]string{}) + + req := httptest.NewRequest("GET", "/api/v1/watch/status", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.WatchStatusResponse + json.Unmarshal(body, &result) + + if result.Enabled { + t.Error("Expected enabled to be false") + } + if len(result.WatchedPaths) != 0 { + t.Errorf("Expected 0 watched paths, got %d", len(result.WatchedPaths)) + } +} + +func TestWatchHandler_Status_NilPaths(t *testing.T) { + tc := setupWatchTestApp(t) + defer tc.Ctrl.Finish() + + tc.WatchService.EXPECT().IsWatching().Return(true) + tc.WatchService.EXPECT().GetWatchedPaths().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/watch/status", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} diff --git a/internal/handler/websocket_handler.go b/internal/handler/websocket_handler.go index 668324b..f462e86 100644 --- a/internal/handler/websocket_handler.go +++ b/internal/handler/websocket_handler.go @@ -5,6 +5,7 @@ import ( "github.com/gofiber/contrib/websocket" "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" "github.com/highcard-dev/daemon/internal/utils/logger" @@ -17,13 +18,13 @@ type WebsocketHandler struct { consoleService ports.ConsoleManagerInterface } -type TokenHttpResponse struct { - Token string `json:"token" validate:"required"` -} // @name WebsocketToken - -type ConsolesHttpResponse struct { - Consoles map[string]*domain.Console `json:"consoles" validate:"required"` -} // @name ConsolesResponse +func domainConsoleToAPI(dc *domain.Console) api.Console { + return api.Console{ + Type: api.ConsoleType(dc.Type), + InputMode: dc.InputMode, + Exit: dc.Exit, + } +} const ( // Time allowed to write a message to the peer. @@ -51,33 +52,23 @@ func NewWebsocketHandler( } } -// @Summary Get current scroll -// @Description Get the metrics for all processes. -// @ID createToken -// @Tags websocket, druid, daemon -// @Accept json -// @Produce json -// @Success 200 {object} TokenHttpResponse -// @Router /api/v1/token [get] func (ah WebsocketHandler) CreateToken(c *fiber.Ctx) error { token := ah.authorizerService.GenerateQueryToken() - c.JSON(TokenHttpResponse{Token: token}) + c.JSON(api.TokenResponse{Token: token}) return nil } -// @Summary Get All Consoles -// @Description Get List of all consoles -// @ID getConsoles -// @Tags druid, daemon, console -// @Accept json -// @Produce json -// @Success 200 {object} ConsolesHttpResponse -// @Router /api/v1/consoles [get] -func (ah WebsocketHandler) Consoles(c *fiber.Ctx) error { +func (ah WebsocketHandler) GetConsoles(c *fiber.Ctx) error { consoles := ah.consoleService.GetConsoles() - c.JSON(ConsolesHttpResponse{Consoles: consoles}) + // Convert domain consoles to API consoles + apiConsoles := make(map[string]api.Console, len(consoles)) + for k, v := range consoles { + apiConsoles[k] = domainConsoleToAPI(v) + } + + c.JSON(api.ConsolesResponse{Consoles: apiConsoles}) return nil } diff --git a/internal/handler/websocket_handler_test.go b/internal/handler/websocket_handler_test.go new file mode 100644 index 0000000..724c99a --- /dev/null +++ b/internal/handler/websocket_handler_test.go @@ -0,0 +1,220 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/highcard-dev/daemon/internal/api" + "github.com/highcard-dev/daemon/internal/core/domain" + mock_ports "github.com/highcard-dev/daemon/test/mock" + "go.uber.org/mock/gomock" +) + +// WebsocketTestContext holds all mocked services for websocket handler testing +type WebsocketTestContext struct { + App *fiber.App + Ctrl *gomock.Controller + AuthorizerService *mock_ports.MockAuthorizerServiceInterface + ScrollService *mock_ports.MockScrollServiceInterface + ConsoleService *mock_ports.MockConsoleManagerInterface + Handler *WebsocketHandler +} + +// setupWebsocketTestApp creates a Fiber app with mocked dependencies for testing +func setupWebsocketTestApp(t *testing.T) *WebsocketTestContext { + ctrl := gomock.NewController(t) + + authorizerService := mock_ports.NewMockAuthorizerServiceInterface(ctrl) + scrollService := mock_ports.NewMockScrollServiceInterface(ctrl) + consoleService := mock_ports.NewMockConsoleManagerInterface(ctrl) + + handler := NewWebsocketHandler(authorizerService, scrollService, consoleService) + + app := fiber.New() + app.Get("/api/v1/token", handler.CreateToken) + app.Get("/api/v1/consoles", handler.GetConsoles) + + return &WebsocketTestContext{ + App: app, + Ctrl: ctrl, + AuthorizerService: authorizerService, + ScrollService: scrollService, + ConsoleService: consoleService, + Handler: handler, + } +} + +func TestWebsocketHandler_CreateToken_Success(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + expectedToken := "test-token-12345" + tc.AuthorizerService.EXPECT().GenerateQueryToken().Return(expectedToken) + + req := httptest.NewRequest("GET", "/api/v1/token", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.TokenResponse + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if result.Token != expectedToken { + t.Errorf("Expected token '%s', got '%s'", expectedToken, result.Token) + } +} + +func TestWebsocketHandler_CreateToken_EmptyToken(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + tc.AuthorizerService.EXPECT().GenerateQueryToken().Return("") + + req := httptest.NewRequest("GET", "/api/v1/token", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.TokenResponse + json.Unmarshal(body, &result) + + if result.Token != "" { + t.Errorf("Expected empty token, got '%s'", result.Token) + } +} + +func TestWebsocketHandler_Consoles_Success(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + expectedConsoles := map[string]*domain.Console{ + "start.0": { + InputMode: "stdin", + }, + "worker.0": { + InputMode: "rcon", + }, + } + tc.ConsoleService.EXPECT().GetConsoles().Return(expectedConsoles) + + req := httptest.NewRequest("GET", "/api/v1/consoles", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ConsolesResponse + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + if len(result.Consoles) != 2 { + t.Errorf("Expected 2 consoles, got %d", len(result.Consoles)) + } + if _, ok := result.Consoles["start.0"]; !ok { + t.Error("Expected 'start.0' console to be present") + } +} + +func TestWebsocketHandler_Consoles_Empty(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + tc.ConsoleService.EXPECT().GetConsoles().Return(map[string]*domain.Console{}) + + req := httptest.NewRequest("GET", "/api/v1/consoles", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ConsolesResponse + json.Unmarshal(body, &result) + + if len(result.Consoles) != 0 { + t.Errorf("Expected 0 consoles, got %d", len(result.Consoles)) + } +} + +func TestWebsocketHandler_Consoles_Nil(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + tc.ConsoleService.EXPECT().GetConsoles().Return(nil) + + req := httptest.NewRequest("GET", "/api/v1/consoles", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } +} + +func TestWebsocketHandler_Consoles_SingleConsole(t *testing.T) { + tc := setupWebsocketTestApp(t) + defer tc.Ctrl.Finish() + + expectedConsoles := map[string]*domain.Console{ + "main.0": { + InputMode: "stdin", + }, + } + tc.ConsoleService.EXPECT().GetConsoles().Return(expectedConsoles) + + req := httptest.NewRequest("GET", "/api/v1/consoles", nil) + resp, err := tc.App.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + var result api.ConsolesResponse + json.Unmarshal(body, &result) + + if len(result.Consoles) != 1 { + t.Errorf("Expected 1 console, got %d", len(result.Consoles)) + } + if result.Consoles["main.0"].InputMode != "stdin" { + t.Errorf("Expected input mode 'stdin', got '%s'", result.Consoles["main.0"].InputMode) + } +} diff --git a/main.go b/main.go index 8f5148d..31a4a83 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,5 @@ package main -// @title Druid CLI -// @version 0.1.0 -// @description Druid CLI is a process runner to launches and manages various sorts of applications, like gameservers, databases or webservers. - import ( "os" diff --git a/override.swag b/override.swag deleted file mode 100644 index e1efddd..0000000 --- a/override.swag +++ /dev/null @@ -1,5 +0,0 @@ -replace semver.Version string -replace processutil.MemoryInfoStat string -replace processutil.MemoryInfoExStat string -replace processutil.Process string -replace processutil.IOCountersStat string \ No newline at end of file diff --git a/test/mock/services.go b/test/mock/services.go index 4e4ab90..28cd2b8 100644 --- a/test/mock/services.go +++ b/test/mock/services.go @@ -1594,32 +1594,32 @@ func (mr *MockUiServiceInterfaceMockRecorder) GetIndex(filePath any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndex", reflect.TypeOf((*MockUiServiceInterface)(nil).GetIndex), filePath) } -// MockUiDevServiceInterface is a mock of UiDevServiceInterface interface. -type MockUiDevServiceInterface struct { +// MockWatchServiceInterface is a mock of WatchServiceInterface interface. +type MockWatchServiceInterface struct { ctrl *gomock.Controller - recorder *MockUiDevServiceInterfaceMockRecorder + recorder *MockWatchServiceInterfaceMockRecorder isgomock struct{} } -// MockUiDevServiceInterfaceMockRecorder is the mock recorder for MockUiDevServiceInterface. -type MockUiDevServiceInterfaceMockRecorder struct { - mock *MockUiDevServiceInterface +// MockWatchServiceInterfaceMockRecorder is the mock recorder for MockWatchServiceInterface. +type MockWatchServiceInterfaceMockRecorder struct { + mock *MockWatchServiceInterface } -// NewMockUiDevServiceInterface creates a new mock instance. -func NewMockUiDevServiceInterface(ctrl *gomock.Controller) *MockUiDevServiceInterface { - mock := &MockUiDevServiceInterface{ctrl: ctrl} - mock.recorder = &MockUiDevServiceInterfaceMockRecorder{mock} +// NewMockWatchServiceInterface creates a new mock instance. +func NewMockWatchServiceInterface(ctrl *gomock.Controller) *MockWatchServiceInterface { + mock := &MockWatchServiceInterface{ctrl: ctrl} + mock.recorder = &MockWatchServiceInterfaceMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockUiDevServiceInterface) EXPECT() *MockUiDevServiceInterfaceMockRecorder { +func (m *MockWatchServiceInterface) EXPECT() *MockWatchServiceInterfaceMockRecorder { return m.recorder } // GetWatchedPaths mocks base method. -func (m *MockUiDevServiceInterface) GetWatchedPaths() []string { +func (m *MockWatchServiceInterface) GetWatchedPaths() []string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetWatchedPaths") ret0, _ := ret[0].([]string) @@ -1627,13 +1627,13 @@ func (m *MockUiDevServiceInterface) GetWatchedPaths() []string { } // GetWatchedPaths indicates an expected call of GetWatchedPaths. -func (mr *MockUiDevServiceInterfaceMockRecorder) GetWatchedPaths() *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) GetWatchedPaths() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWatchedPaths", reflect.TypeOf((*MockUiDevServiceInterface)(nil).GetWatchedPaths)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWatchedPaths", reflect.TypeOf((*MockWatchServiceInterface)(nil).GetWatchedPaths)) } // IsWatching mocks base method. -func (m *MockUiDevServiceInterface) IsWatching() bool { +func (m *MockWatchServiceInterface) IsWatching() bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsWatching") ret0, _ := ret[0].(bool) @@ -1641,25 +1641,27 @@ func (m *MockUiDevServiceInterface) IsWatching() bool { } // IsWatching indicates an expected call of IsWatching. -func (mr *MockUiDevServiceInterfaceMockRecorder) IsWatching() *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) IsWatching() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWatching", reflect.TypeOf((*MockUiDevServiceInterface)(nil).IsWatching)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWatching", reflect.TypeOf((*MockWatchServiceInterface)(nil).IsWatching)) } -// SetCommands mocks base method. -func (m *MockUiDevServiceInterface) SetCommands(procs map[string]*domain.CommandInstructionSet) { +// SetHotReloadCommands mocks base method. +func (m *MockWatchServiceInterface) SetHotReloadCommands(procs []string) error { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetCommands", procs) + ret := m.ctrl.Call(m, "SetHotReloadCommands", procs) + ret0, _ := ret[0].(error) + return ret0 } -// SetCommands indicates an expected call of SetCommands. -func (mr *MockUiDevServiceInterfaceMockRecorder) SetCommands(procs any) *gomock.Call { +// SetHotReloadCommands indicates an expected call of SetHotReloadCommands. +func (mr *MockWatchServiceInterfaceMockRecorder) SetHotReloadCommands(procs any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCommands", reflect.TypeOf((*MockUiDevServiceInterface)(nil).SetCommands), procs) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHotReloadCommands", reflect.TypeOf((*MockWatchServiceInterface)(nil).SetHotReloadCommands), procs) } // StartWatching mocks base method. -func (m *MockUiDevServiceInterface) StartWatching(basePath string, paths ...string) error { +func (m *MockWatchServiceInterface) StartWatching(basePath string, paths ...string) error { m.ctrl.T.Helper() varargs := []any{basePath} for _, a := range paths { @@ -1671,14 +1673,14 @@ func (m *MockUiDevServiceInterface) StartWatching(basePath string, paths ...stri } // StartWatching indicates an expected call of StartWatching. -func (mr *MockUiDevServiceInterfaceMockRecorder) StartWatching(basePath any, paths ...any) *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) StartWatching(basePath any, paths ...any) *gomock.Call { mr.mock.ctrl.T.Helper() varargs := append([]any{basePath}, paths...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWatching", reflect.TypeOf((*MockUiDevServiceInterface)(nil).StartWatching), varargs...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartWatching", reflect.TypeOf((*MockWatchServiceInterface)(nil).StartWatching), varargs...) } // StopWatching mocks base method. -func (m *MockUiDevServiceInterface) StopWatching() error { +func (m *MockWatchServiceInterface) StopWatching() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "StopWatching") ret0, _ := ret[0].(error) @@ -1686,13 +1688,13 @@ func (m *MockUiDevServiceInterface) StopWatching() error { } // StopWatching indicates an expected call of StopWatching. -func (mr *MockUiDevServiceInterfaceMockRecorder) StopWatching() *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) StopWatching() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopWatching", reflect.TypeOf((*MockUiDevServiceInterface)(nil).StopWatching)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopWatching", reflect.TypeOf((*MockWatchServiceInterface)(nil).StopWatching)) } // Subscribe mocks base method. -func (m *MockUiDevServiceInterface) Subscribe() chan *[]byte { +func (m *MockWatchServiceInterface) Subscribe() chan *[]byte { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Subscribe") ret0, _ := ret[0].(chan *[]byte) @@ -1700,21 +1702,21 @@ func (m *MockUiDevServiceInterface) Subscribe() chan *[]byte { } // Subscribe indicates an expected call of Subscribe. -func (mr *MockUiDevServiceInterfaceMockRecorder) Subscribe() *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) Subscribe() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockUiDevServiceInterface)(nil).Subscribe)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockWatchServiceInterface)(nil).Subscribe)) } // Unsubscribe mocks base method. -func (m *MockUiDevServiceInterface) Unsubscribe(client chan *[]byte) { +func (m *MockWatchServiceInterface) Unsubscribe(client chan *[]byte) { m.ctrl.T.Helper() m.ctrl.Call(m, "Unsubscribe", client) } // Unsubscribe indicates an expected call of Unsubscribe. -func (mr *MockUiDevServiceInterfaceMockRecorder) Unsubscribe(client any) *gomock.Call { +func (mr *MockWatchServiceInterfaceMockRecorder) Unsubscribe(client any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockUiDevServiceInterface)(nil).Unsubscribe), client) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockWatchServiceInterface)(nil).Unsubscribe), client) } // MockNixDependencyServiceInterface is a mock of NixDependencyServiceInterface interface. diff --git a/test/utils/daemon_http_api.go b/test/utils/daemon_http_api.go index 9de794d..f5cf6e9 100644 --- a/test/utils/daemon_http_api.go +++ b/test/utils/daemon_http_api.go @@ -6,8 +6,8 @@ import ( "log" "time" + "github.com/highcard-dev/daemon/internal/api" "github.com/highcard-dev/daemon/internal/core/domain" - "github.com/highcard-dev/daemon/internal/handler" ) func WaitForConsoleRunning(console string, duration time.Duration) error { @@ -25,7 +25,7 @@ func WaitForConsoleRunning(console string, duration time.Duration) error { continue } - var resp handler.ConsolesHttpResponse + var resp api.ConsolesResponse json.Unmarshal(body, &resp)