From 0af099e7743950a76cdb488e6d27477505a5b961 Mon Sep 17 00:00:00 2001 From: Denis Tu Date: Sat, 8 Nov 2025 07:05:24 -0800 Subject: [PATCH] minimal api implement --- cmd/main.go | 47 ++- docs/swagger/docs.go | 540 ++++++++++++++++++++++++++++++ docs/swagger/swagger.json | 520 ++++++++++++++++++++++++++++ docs/swagger/swagger.yaml | 369 ++++++++++++++++++++ go.mod | 42 ++- go.sum | 109 ++++-- internal/apiserver/events.go | 227 +++++++++++++ internal/apiserver/health.go | 161 +++++++++ internal/apiserver/hooks.go | 76 +++++ internal/apiserver/json.go | 13 + internal/apiserver/server.go | 227 +++++++++++++ internal/apiserver/stats.go | 178 ++++++++++ internal/apiserver/swagger.go | 29 ++ internal/config/config.go | 7 + internal/interfaces/controller.go | 2 + internal/workflow/coordinator.go | 11 + 16 files changed, 2510 insertions(+), 48 deletions(-) create mode 100644 docs/swagger/docs.go create mode 100644 docs/swagger/swagger.json create mode 100644 docs/swagger/swagger.yaml create mode 100644 internal/apiserver/events.go create mode 100644 internal/apiserver/health.go create mode 100644 internal/apiserver/hooks.go create mode 100644 internal/apiserver/json.go create mode 100644 internal/apiserver/server.go create mode 100644 internal/apiserver/stats.go create mode 100644 internal/apiserver/swagger.go diff --git a/cmd/main.go b/cmd/main.go index 309d579..b823d15 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,6 +17,10 @@ import ( kagentv1alpha2 "github.com/kagent-dev/khook/api/v1alpha2" kclient "github.com/kagent-dev/khook/internal/client" "github.com/kagent-dev/khook/internal/config" + "github.com/kagent-dev/khook/internal/deduplication" + "github.com/kagent-dev/khook/internal/status" + apiserver "github.com/kagent-dev/khook/internal/apiserver" + "github.com/kagent-dev/khook/internal/interfaces" "github.com/kagent-dev/khook/internal/workflow" ) @@ -49,7 +53,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // Load configuration - _, err := config.Load(configFile) + cfg, err := config.Load(configFile) if err != nil { setupLog.Error(err, "unable to load configuration") os.Exit(1) @@ -76,14 +80,35 @@ func main() { os.Exit(1) } + // Initialize shared services for workflow and API server + dedupManager := deduplication.NewManager() + statusManager := status.NewManager(mgr.GetClient(), mgr.GetEventRecorderFor("khook")) + // Add workflow coordinator to manage hooks and event processing - if err := mgr.Add(newWorkflowCoordinator(mgr)); err != nil { + if err := mgr.Add(newWorkflowCoordinator(mgr, dedupManager, statusManager)); err != nil { setupLog.Error(err, "unable to add workflow coordinator") os.Exit(1) } + // Create and start API server + apiServer := apiserver.NewServer(apiserver.Config{ + Port: cfg.Controller.APIServerPort, + DedupManager: dedupManager, + K8sClient: mgr.GetClient(), + StatusManager: statusManager, + }) + + ctx := ctrl.SetupSignalHandler() + + // Start API server in a goroutine + go func() { + if err := apiServer.Start(ctx); err != nil { + setupLog.Error(err, "problem running API server") + } + }() + setupLog.Info("starting manager") - if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + if err := mgr.Start(ctx); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } @@ -91,11 +116,17 @@ func main() { // workflowCoordinator manages the complete workflow lifecycle using proper services type workflowCoordinator struct { - mgr ctrl.Manager + mgr ctrl.Manager + dedupManager interfaces.DeduplicationManager + statusManager interfaces.StatusManager } -func newWorkflowCoordinator(mgr ctrl.Manager) *workflowCoordinator { - return &workflowCoordinator{mgr: mgr} +func newWorkflowCoordinator(mgr ctrl.Manager, dedupManager interfaces.DeduplicationManager, statusManager interfaces.StatusManager) *workflowCoordinator { + return &workflowCoordinator{ + mgr: mgr, + dedupManager: dedupManager, + statusManager: statusManager, + } } func (w *workflowCoordinator) NeedLeaderElection() bool { return true } @@ -119,9 +150,9 @@ func (w *workflowCoordinator) Start(ctx context.Context) error { return err } - // Create workflow coordinator + // Create workflow coordinator with shared managers eventRecorder := w.mgr.GetEventRecorderFor("khook") - coordinator := workflow.NewCoordinator(k8s, w.mgr.GetClient(), kagentCli, eventRecorder) + coordinator := workflow.NewCoordinatorWithManagers(k8s, w.mgr.GetClient(), kagentCli, eventRecorder, w.dedupManager, w.statusManager) // Start the coordinator return coordinator.Start(ctx) diff --git a/docs/swagger/docs.go b/docs/swagger/docs.go new file mode 100644 index 0000000..ab8a570 --- /dev/null +++ b/docs/swagger/docs.go @@ -0,0 +1,540 @@ +// Package swagger Code generated by swaggo/swag. DO NOT EDIT +package swagger + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "https://kagent.dev", + "email": "support@kagent.dev" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/diagnostics": { + "get": { + "description": "Returns detailed diagnostic information including memory usage, connection info, and event statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Get diagnostics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.DiagnosticsResponse" + } + } + } + } + }, + "/events": { + "get": { + "description": "Returns a list of all active events with optional filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "events" + ], + "summary": "List events", + "parameters": [ + { + "type": "string", + "description": "Filter by namespace", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Filter by event type (pod-restart, oom-kill, pod-pending, probe-failed, node-not-ready)", + "name": "eventType", + "in": "query" + }, + { + "type": "string", + "description": "Filter by resource name", + "name": "resourceName", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status (firing, resolved)", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventListResponse" + } + } + } + } + }, + "/events/stream": { + "get": { + "description": "Returns a Server-Sent Events stream for real-time event updates", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "events" + ], + "summary": "Stream events", + "responses": { + "200": { + "description": "Event stream", + "schema": { + "type": "string" + } + } + } + } + }, + "/health": { + "get": { + "description": "Returns the health status of the API server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "Healthy status", + "schema": { + "$ref": "#/definitions/internal_apiserver.HealthResponse" + } + } + } + } + }, + "/hooks": { + "get": { + "description": "Returns a list of all Hook configurations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "hooks" + ], + "summary": "List hooks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.HookListResponse" + } + } + } + } + }, + "/metrics": { + "get": { + "description": "Returns Prometheus-style metrics in text format", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "health" + ], + "summary": "Get Prometheus metrics", + "responses": { + "200": { + "description": "Prometheus metrics", + "schema": { + "type": "string" + } + } + } + } + }, + "/stats/events/by-type": { + "get": { + "description": "Returns events grouped by type with counts and percentages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "statistics" + ], + "summary": "Get events by type", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventsByTypeResponse" + } + } + } + } + }, + "/stats/events/summary": { + "get": { + "description": "Returns event summary statistics including counts by severity and type", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "statistics" + ], + "summary": "Get event summary", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventSummaryResponse" + } + } + } + } + } + }, + "definitions": { + "github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus": { + "type": "object", + "properties": { + "eventType": { + "description": "EventType is the type of the active event\n+kubebuilder:validation:Required", + "type": "string" + }, + "firstSeen": { + "description": "FirstSeen is when the event was first observed\n+kubebuilder:validation:Required", + "type": "string" + }, + "lastSeen": { + "description": "LastSeen is when the event was last observed\n+kubebuilder:validation:Required", + "type": "string" + }, + "resourceName": { + "description": "ResourceName is the name of the Kubernetes resource involved\n+kubebuilder:validation:Required", + "type": "string" + }, + "status": { + "description": "Status indicates whether the event is firing or resolved\n+kubebuilder:validation:Enum=firing;resolved\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration": { + "type": "object", + "properties": { + "agentRef": { + "description": "AgentRef specifies the Kagent agent to call when this event occurs\n+kubebuilder:validation:Required", + "allOf": [ + { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ObjectReference" + } + ] + }, + "eventType": { + "description": "EventType specifies the type of Kubernetes event to monitor\n+kubebuilder:validation:Enum=pod-restart;pod-pending;oom-kill;probe-failed;node-not-ready\n+kubebuilder:validation:Required", + "type": "string" + }, + "prompt": { + "description": "Prompt specifies the prompt template to send to the agent\n+kubebuilder:validation:Required\n+kubebuilder:validation:MinLength=1", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.HookStatus": { + "type": "object", + "properties": { + "activeEvents": { + "description": "ActiveEvents contains the list of currently active events", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus" + } + }, + "lastUpdated": { + "description": "LastUpdated indicates when the status was last updated", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.ObjectReference": { + "type": "object", + "properties": { + "name": { + "description": "Name of the referent.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names\n+kubebuilder:validation:Required\n+kubebuilder:validation:MinLength=1", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent.\nIf unspecified, the namespace of the Hook will be used.\n+kubebuilder:validation:Optional", + "type": "string" + } + } + }, + "internal_apiserver.ConnectionInfo": { + "type": "object", + "properties": { + "activeEventStreams": { + "type": "integer" + } + } + }, + "internal_apiserver.DiagnosticsResponse": { + "type": "object", + "properties": { + "apiStatus": { + "type": "string" + }, + "connectionInfo": { + "$ref": "#/definitions/internal_apiserver.ConnectionInfo" + }, + "eventStats": { + "$ref": "#/definitions/internal_apiserver.EventStats" + }, + "memoryUsage": { + "$ref": "#/definitions/internal_apiserver.MemoryStats" + } + } + }, + "internal_apiserver.EventListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_apiserver.EventResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.EventResponse": { + "type": "object", + "properties": { + "eventType": { + "type": "string" + }, + "firstSeen": { + "type": "string" + }, + "lastSeen": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "namespace": { + "type": "string" + }, + "resourceName": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "internal_apiserver.EventStats": { + "type": "object", + "properties": { + "totalEvents": { + "type": "integer" + }, + "totalHooks": { + "type": "integer" + } + } + }, + "internal_apiserver.EventSummaryResponse": { + "type": "object", + "properties": { + "byEventType": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "bySeverity": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "firing": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.EventsByTypeResponse": { + "type": "object", + "properties": { + "byType": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/internal_apiserver.TypeStats" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "internal_apiserver.HookListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_apiserver.HookResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.HookResponse": { + "type": "object", + "properties": { + "eventConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.HookStatus" + } + } + }, + "internal_apiserver.MemoryStats": { + "type": "object", + "properties": { + "alloc": { + "type": "integer" + }, + "numGC": { + "type": "integer" + }, + "sys": { + "type": "integer" + }, + "totalAlloc": { + "type": "integer" + } + } + }, + "internal_apiserver.TypeStats": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "percentage": { + "type": "number" + } + } + } + }, + "tags": [ + { + "description": "Health and diagnostics endpoints", + "name": "health" + }, + { + "description": "Event query and streaming endpoints", + "name": "events" + }, + { + "description": "Hook configuration endpoints", + "name": "hooks" + }, + { + "description": "Event statistics endpoints", + "name": "statistics" + } + ] +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8082", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "Khook API Server", + Description: "HTTP API server for querying events, hooks, statistics, and health information", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json new file mode 100644 index 0000000..e1c35d6 --- /dev/null +++ b/docs/swagger/swagger.json @@ -0,0 +1,520 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "HTTP API server for querying events, hooks, statistics, and health information", + "title": "Khook API Server", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "https://kagent.dev", + "email": "support@kagent.dev" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8082", + "basePath": "/api/v1", + "paths": { + "/diagnostics": { + "get": { + "description": "Returns detailed diagnostic information including memory usage, connection info, and event statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Get diagnostics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.DiagnosticsResponse" + } + } + } + } + }, + "/events": { + "get": { + "description": "Returns a list of all active events with optional filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "events" + ], + "summary": "List events", + "parameters": [ + { + "type": "string", + "description": "Filter by namespace", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Filter by event type (pod-restart, oom-kill, pod-pending, probe-failed, node-not-ready)", + "name": "eventType", + "in": "query" + }, + { + "type": "string", + "description": "Filter by resource name", + "name": "resourceName", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status (firing, resolved)", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventListResponse" + } + } + } + } + }, + "/events/stream": { + "get": { + "description": "Returns a Server-Sent Events stream for real-time event updates", + "consumes": [ + "application/json" + ], + "produces": [ + "text/event-stream" + ], + "tags": [ + "events" + ], + "summary": "Stream events", + "responses": { + "200": { + "description": "Event stream", + "schema": { + "type": "string" + } + } + } + } + }, + "/health": { + "get": { + "description": "Returns the health status of the API server", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Health check", + "responses": { + "200": { + "description": "Healthy status", + "schema": { + "$ref": "#/definitions/internal_apiserver.HealthResponse" + } + } + } + } + }, + "/hooks": { + "get": { + "description": "Returns a list of all Hook configurations", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "hooks" + ], + "summary": "List hooks", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.HookListResponse" + } + } + } + } + }, + "/metrics": { + "get": { + "description": "Returns Prometheus-style metrics in text format", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "health" + ], + "summary": "Get Prometheus metrics", + "responses": { + "200": { + "description": "Prometheus metrics", + "schema": { + "type": "string" + } + } + } + } + }, + "/stats/events/by-type": { + "get": { + "description": "Returns events grouped by type with counts and percentages", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "statistics" + ], + "summary": "Get events by type", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventsByTypeResponse" + } + } + } + } + }, + "/stats/events/summary": { + "get": { + "description": "Returns event summary statistics including counts by severity and type", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "statistics" + ], + "summary": "Get event summary", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/internal_apiserver.EventSummaryResponse" + } + } + } + } + } + }, + "definitions": { + "github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus": { + "type": "object", + "properties": { + "eventType": { + "description": "EventType is the type of the active event\n+kubebuilder:validation:Required", + "type": "string" + }, + "firstSeen": { + "description": "FirstSeen is when the event was first observed\n+kubebuilder:validation:Required", + "type": "string" + }, + "lastSeen": { + "description": "LastSeen is when the event was last observed\n+kubebuilder:validation:Required", + "type": "string" + }, + "resourceName": { + "description": "ResourceName is the name of the Kubernetes resource involved\n+kubebuilder:validation:Required", + "type": "string" + }, + "status": { + "description": "Status indicates whether the event is firing or resolved\n+kubebuilder:validation:Enum=firing;resolved\n+kubebuilder:validation:Required", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration": { + "type": "object", + "properties": { + "agentRef": { + "description": "AgentRef specifies the Kagent agent to call when this event occurs\n+kubebuilder:validation:Required", + "allOf": [ + { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ObjectReference" + } + ] + }, + "eventType": { + "description": "EventType specifies the type of Kubernetes event to monitor\n+kubebuilder:validation:Enum=pod-restart;pod-pending;oom-kill;probe-failed;node-not-ready\n+kubebuilder:validation:Required", + "type": "string" + }, + "prompt": { + "description": "Prompt specifies the prompt template to send to the agent\n+kubebuilder:validation:Required\n+kubebuilder:validation:MinLength=1", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.HookStatus": { + "type": "object", + "properties": { + "activeEvents": { + "description": "ActiveEvents contains the list of currently active events", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus" + } + }, + "lastUpdated": { + "description": "LastUpdated indicates when the status was last updated", + "type": "string" + } + } + }, + "github_com_kagent-dev_khook_api_v1alpha2.ObjectReference": { + "type": "object", + "properties": { + "name": { + "description": "Name of the referent.\nMore info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names\n+kubebuilder:validation:Required\n+kubebuilder:validation:MinLength=1", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent.\nIf unspecified, the namespace of the Hook will be used.\n+kubebuilder:validation:Optional", + "type": "string" + } + } + }, + "internal_apiserver.ConnectionInfo": { + "type": "object", + "properties": { + "activeEventStreams": { + "type": "integer" + } + } + }, + "internal_apiserver.DiagnosticsResponse": { + "type": "object", + "properties": { + "apiStatus": { + "type": "string" + }, + "connectionInfo": { + "$ref": "#/definitions/internal_apiserver.ConnectionInfo" + }, + "eventStats": { + "$ref": "#/definitions/internal_apiserver.EventStats" + }, + "memoryUsage": { + "$ref": "#/definitions/internal_apiserver.MemoryStats" + } + } + }, + "internal_apiserver.EventListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_apiserver.EventResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.EventResponse": { + "type": "object", + "properties": { + "eventType": { + "type": "string" + }, + "firstSeen": { + "type": "string" + }, + "lastSeen": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "namespace": { + "type": "string" + }, + "resourceName": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "internal_apiserver.EventStats": { + "type": "object", + "properties": { + "totalEvents": { + "type": "integer" + }, + "totalHooks": { + "type": "integer" + } + } + }, + "internal_apiserver.EventSummaryResponse": { + "type": "object", + "properties": { + "byEventType": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "bySeverity": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "firing": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.EventsByTypeResponse": { + "type": "object", + "properties": { + "byType": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/internal_apiserver.TypeStats" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "internal_apiserver.HookListResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_apiserver.HookResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "internal_apiserver.HookResponse": { + "type": "object", + "properties": { + "eventConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/github_com_kagent-dev_khook_api_v1alpha2.HookStatus" + } + } + }, + "internal_apiserver.MemoryStats": { + "type": "object", + "properties": { + "alloc": { + "type": "integer" + }, + "numGC": { + "type": "integer" + }, + "sys": { + "type": "integer" + }, + "totalAlloc": { + "type": "integer" + } + } + }, + "internal_apiserver.TypeStats": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "percentage": { + "type": "number" + } + } + } + }, + "tags": [ + { + "description": "Health and diagnostics endpoints", + "name": "health" + }, + { + "description": "Event query and streaming endpoints", + "name": "events" + }, + { + "description": "Hook configuration endpoints", + "name": "hooks" + }, + { + "description": "Event statistics endpoints", + "name": "statistics" + } + ] +} \ No newline at end of file diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml new file mode 100644 index 0000000..8d38bd7 --- /dev/null +++ b/docs/swagger/swagger.yaml @@ -0,0 +1,369 @@ +basePath: /api/v1 +definitions: + github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus: + properties: + eventType: + description: |- + EventType is the type of the active event + +kubebuilder:validation:Required + type: string + firstSeen: + description: |- + FirstSeen is when the event was first observed + +kubebuilder:validation:Required + type: string + lastSeen: + description: |- + LastSeen is when the event was last observed + +kubebuilder:validation:Required + type: string + resourceName: + description: |- + ResourceName is the name of the Kubernetes resource involved + +kubebuilder:validation:Required + type: string + status: + description: |- + Status indicates whether the event is firing or resolved + +kubebuilder:validation:Enum=firing;resolved + +kubebuilder:validation:Required + type: string + type: object + github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration: + properties: + agentRef: + allOf: + - $ref: '#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ObjectReference' + description: |- + AgentRef specifies the Kagent agent to call when this event occurs + +kubebuilder:validation:Required + eventType: + description: |- + EventType specifies the type of Kubernetes event to monitor + +kubebuilder:validation:Enum=pod-restart;pod-pending;oom-kill;probe-failed;node-not-ready + +kubebuilder:validation:Required + type: string + prompt: + description: |- + Prompt specifies the prompt template to send to the agent + +kubebuilder:validation:Required + +kubebuilder:validation:MinLength=1 + type: string + type: object + github_com_kagent-dev_khook_api_v1alpha2.HookStatus: + properties: + activeEvents: + description: ActiveEvents contains the list of currently active events + items: + $ref: '#/definitions/github_com_kagent-dev_khook_api_v1alpha2.ActiveEventStatus' + type: array + lastUpdated: + description: LastUpdated indicates when the status was last updated + type: string + type: object + github_com_kagent-dev_khook_api_v1alpha2.ObjectReference: + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + +kubebuilder:validation:Required + +kubebuilder:validation:MinLength=1 + type: string + namespace: + description: |- + Namespace of the referent. + If unspecified, the namespace of the Hook will be used. + +kubebuilder:validation:Optional + type: string + type: object + internal_apiserver.ConnectionInfo: + properties: + activeEventStreams: + type: integer + type: object + internal_apiserver.DiagnosticsResponse: + properties: + apiStatus: + type: string + connectionInfo: + $ref: '#/definitions/internal_apiserver.ConnectionInfo' + eventStats: + $ref: '#/definitions/internal_apiserver.EventStats' + memoryUsage: + $ref: '#/definitions/internal_apiserver.MemoryStats' + type: object + internal_apiserver.EventListResponse: + properties: + data: + items: + $ref: '#/definitions/internal_apiserver.EventResponse' + type: array + total: + type: integer + type: object + internal_apiserver.EventResponse: + properties: + eventType: + type: string + firstSeen: + type: string + lastSeen: + type: string + metadata: + additionalProperties: + type: string + type: object + namespace: + type: string + resourceName: + type: string + status: + type: string + type: object + internal_apiserver.EventStats: + properties: + totalEvents: + type: integer + totalHooks: + type: integer + type: object + internal_apiserver.EventSummaryResponse: + properties: + byEventType: + additionalProperties: + type: integer + type: object + bySeverity: + additionalProperties: + type: integer + type: object + firing: + type: integer + resolved: + type: integer + total: + type: integer + type: object + internal_apiserver.EventsByTypeResponse: + properties: + byType: + additionalProperties: + $ref: '#/definitions/internal_apiserver.TypeStats' + type: object + total: + type: integer + type: object + internal_apiserver.HealthResponse: + properties: + status: + type: string + type: object + internal_apiserver.HookListResponse: + properties: + items: + items: + $ref: '#/definitions/internal_apiserver.HookResponse' + type: array + total: + type: integer + type: object + internal_apiserver.HookResponse: + properties: + eventConfigurations: + items: + $ref: '#/definitions/github_com_kagent-dev_khook_api_v1alpha2.EventConfiguration' + type: array + name: + type: string + namespace: + type: string + status: + $ref: '#/definitions/github_com_kagent-dev_khook_api_v1alpha2.HookStatus' + type: object + internal_apiserver.MemoryStats: + properties: + alloc: + type: integer + numGC: + type: integer + sys: + type: integer + totalAlloc: + type: integer + type: object + internal_apiserver.TypeStats: + properties: + count: + type: integer + percentage: + type: number + type: object +host: localhost:8082 +info: + contact: + email: support@kagent.dev + name: API Support + url: https://kagent.dev + description: HTTP API server for querying events, hooks, statistics, and health + information + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + termsOfService: http://swagger.io/terms/ + title: Khook API Server + version: "1.0" +paths: + /diagnostics: + get: + consumes: + - application/json + description: Returns detailed diagnostic information including memory usage, + connection info, and event statistics + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_apiserver.DiagnosticsResponse' + summary: Get diagnostics + tags: + - health + /events: + get: + consumes: + - application/json + description: Returns a list of all active events with optional filtering + parameters: + - description: Filter by namespace + in: query + name: namespace + type: string + - description: Filter by event type (pod-restart, oom-kill, pod-pending, probe-failed, + node-not-ready) + in: query + name: eventType + type: string + - description: Filter by resource name + in: query + name: resourceName + type: string + - description: Filter by status (firing, resolved) + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_apiserver.EventListResponse' + summary: List events + tags: + - events + /events/stream: + get: + consumes: + - application/json + description: Returns a Server-Sent Events stream for real-time event updates + produces: + - text/event-stream + responses: + "200": + description: Event stream + schema: + type: string + summary: Stream events + tags: + - events + /health: + get: + consumes: + - application/json + description: Returns the health status of the API server + produces: + - application/json + responses: + "200": + description: Healthy status + schema: + $ref: '#/definitions/internal_apiserver.HealthResponse' + summary: Health check + tags: + - health + /hooks: + get: + consumes: + - application/json + description: Returns a list of all Hook configurations + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_apiserver.HookListResponse' + summary: List hooks + tags: + - hooks + /metrics: + get: + consumes: + - application/json + description: Returns Prometheus-style metrics in text format + produces: + - text/plain + responses: + "200": + description: Prometheus metrics + schema: + type: string + summary: Get Prometheus metrics + tags: + - health + /stats/events/by-type: + get: + consumes: + - application/json + description: Returns events grouped by type with counts and percentages + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_apiserver.EventsByTypeResponse' + summary: Get events by type + tags: + - statistics + /stats/events/summary: + get: + consumes: + - application/json + description: Returns event summary statistics including counts by severity and + type + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/internal_apiserver.EventSummaryResponse' + summary: Get event summary + tags: + - statistics +schemes: +- http +- https +swagger: "2.0" +tags: +- description: Health and diagnostics endpoints + name: health +- description: Event query and streaming endpoints + name: events +- description: Hook configuration endpoints + name: hooks +- description: Event statistics endpoints + name: statistics diff --git a/go.mod b/go.mod index 595ce6b..df8939a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.24.6 require ( github.com/go-logr/logr v1.4.3 github.com/kagent-dev/kagent/go v0.0.0-20250827151700-a9cc8a1f7d57 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.34.0 k8s.io/apimachinery v0.34.0 @@ -15,6 +17,7 @@ require ( ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -28,9 +31,21 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.11.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.25.1 // indirect + github.com/go-openapi/swag/cmdutils v0.25.1 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/netutils v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect @@ -44,7 +59,6 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lestrrat-go/blackmagic v1.0.3 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -52,7 +66,6 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect github.com/lestrrat-go/option v1.0.1 // indirect - github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect @@ -67,19 +80,22 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.38.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index 3cfd1c1..a2caf38 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,12 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -30,12 +33,43 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/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.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= +github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= +github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= +github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= +github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -69,7 +103,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -79,8 +112,11 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -97,8 +133,9 @@ github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVf github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -109,6 +146,7 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= @@ -142,8 +180,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV 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.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/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -156,50 +200,57 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -209,15 +260,19 @@ gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuB google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +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= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= diff --git a/internal/apiserver/events.go b/internal/apiserver/events.go new file mode 100644 index 0000000..072352f --- /dev/null +++ b/internal/apiserver/events.go @@ -0,0 +1,227 @@ +package apiserver + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "k8s.io/apimachinery/pkg/types" +) + +// EventHandlers handles event-related API endpoints +type EventHandlers struct { + server *Server +} + +// NewEventHandlers creates a new event handlers instance +func NewEventHandlers(server *Server) *EventHandlers { + return &EventHandlers{server: server} +} + +// EventResponse represents an event in the API response +type EventResponse struct { + EventType string `json:"eventType"` + ResourceName string `json:"resourceName"` + Namespace string `json:"namespace"` + FirstSeen time.Time `json:"firstSeen"` + LastSeen time.Time `json:"lastSeen"` + Status string `json:"status"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// EventListResponse represents a paginated list of events +type EventListResponse struct { + Data []EventResponse `json:"data"` + Total int `json:"total"` +} + +// ListEvents handles GET /api/v1/events +// @Summary List events +// @Description Returns a list of all active events with optional filtering +// @Tags events +// @Accept json +// @Produce json +// @Param namespace query string false "Filter by namespace" +// @Param eventType query string false "Filter by event type (pod-restart, oom-kill, pod-pending, probe-failed, node-not-ready)" +// @Param resourceName query string false "Filter by resource name" +// @Param status query string false "Filter by status (firing, resolved)" +// @Success 200 {object} EventListResponse +// @Router /events [get] +func (h *EventHandlers) ListEvents(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + // Parse query parameters + namespace := r.URL.Query().Get("namespace") + eventType := r.URL.Query().Get("eventType") + resourceName := r.URL.Query().Get("resourceName") + status := r.URL.Query().Get("status") + + // Get all hooks that have active events + hookNames := h.server.dedupManager.GetAllHookNames() + + var allEvents []EventResponse + + // Collect events from all hooks + for _, hookNameStr := range hookNames { + // Parse hook reference (format: "namespace/name") + parts := strings.Split(hookNameStr, "/") + if len(parts) != 2 { + h.server.logger.V(1).Info("Invalid hook name format", "hookName", hookNameStr) + continue + } + + hookRef := types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + } + + // Get active events for this hook + activeEvents := h.server.dedupManager.GetActiveEventsWithStatus(hookRef) + + // Convert to API response format + for _, event := range activeEvents { + // Apply filters + if namespace != "" && parts[0] != namespace { + continue + } + if eventType != "" && event.EventType != eventType { + continue + } + if resourceName != "" && event.ResourceName != resourceName { + continue + } + if status != "" && event.Status != status { + continue + } + + allEvents = append(allEvents, EventResponse{ + EventType: event.EventType, + ResourceName: event.ResourceName, + Namespace: parts[0], + FirstSeen: event.FirstSeen, + LastSeen: event.LastSeen, + Status: event.Status, + }) + } + } + + response := EventListResponse{ + Data: allEvents, + Total: len(allEvents), + } + + h.server.writeJSON(w, http.StatusOK, response) +} + +// StreamEvents handles GET /api/v1/events/stream (Server-Sent Events) +// @Summary Stream events +// @Description Returns a Server-Sent Events stream for real-time event updates +// @Tags events +// @Accept json +// @Produce text/event-stream +// @Success 200 {string} string "Event stream" +// @Router /events/stream [get] +func (h *EventHandlers) StreamEvents(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + // Set headers for SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Access-Control-Allow-Origin", "*") + + // Create a channel for events + eventCh := make(chan EventResponse, 10) + + // Start a goroutine to periodically send events + go h.streamEventLoop(r.Context(), eventCh) + + // Send initial connection message + fmt.Fprintf(w, "data: %s\n\n", `{"type":"connected"}`) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + // Send heartbeat every 30 seconds + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-r.Context().Done(): + h.server.logger.Info("Client disconnected from event stream") + return + + case event := <-eventCh: + eventJSON, err := json.Marshal(event) + if err != nil { + h.server.logger.Error(err, "Failed to marshal event") + continue + } + fmt.Fprintf(w, "data: %s\n\n", eventJSON) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + case <-ticker.C: + // Send heartbeat + fmt.Fprintf(w, ": heartbeat\n\n") + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } + } +} + +// streamEventLoop periodically collects and sends events +func (h *EventHandlers) streamEventLoop(ctx context.Context, eventCh chan<- EventResponse) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + // Get all active events + hookNames := h.server.dedupManager.GetAllHookNames() + for _, hookNameStr := range hookNames { + parts := strings.Split(hookNameStr, "/") + if len(parts) != 2 { + continue + } + + hookRef := types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + } + + activeEvents := h.server.dedupManager.GetActiveEventsWithStatus(hookRef) + for _, event := range activeEvents { + select { + case eventCh <- EventResponse{ + EventType: event.EventType, + ResourceName: event.ResourceName, + Namespace: parts[0], + FirstSeen: event.FirstSeen, + LastSeen: event.LastSeen, + Status: event.Status, + }: + case <-ctx.Done(): + return + } + } + } + } + } +} diff --git a/internal/apiserver/health.go b/internal/apiserver/health.go new file mode 100644 index 0000000..3582e39 --- /dev/null +++ b/internal/apiserver/health.go @@ -0,0 +1,161 @@ +package apiserver + +import ( + "fmt" + "net/http" + "runtime" +) + +// HealthHandlers handles health and diagnostics endpoints +type HealthHandlers struct { + server *Server +} + +// NewHealthHandlers creates a new health handlers instance +func NewHealthHandlers(server *Server) *HealthHandlers { + return &HealthHandlers{server: server} +} + +// HealthResponse represents the health status +type HealthResponse struct { + Status string `json:"status"` +} + +// DiagnosticsResponse represents diagnostic information +type DiagnosticsResponse struct { + APIStatus string `json:"apiStatus"` + MemoryUsage MemoryStats `json:"memoryUsage"` + ConnectionInfo ConnectionInfo `json:"connectionInfo"` + EventStats EventStats `json:"eventStats"` +} + +// MemoryStats represents memory usage statistics +type MemoryStats struct { + Alloc uint64 `json:"alloc"` + TotalAlloc uint64 `json:"totalAlloc"` + Sys uint64 `json:"sys"` + NumGC uint32 `json:"numGC"` +} + +// ConnectionInfo represents connection information +type ConnectionInfo struct { + ActiveEventStreams int `json:"activeEventStreams"` +} + +// EventStats represents event statistics +type EventStats struct { + TotalEvents int `json:"totalEvents"` + TotalHooks int `json:"totalHooks"` +} + +// Health handles GET /api/v1/health +// @Summary Health check +// @Description Returns the health status of the API server +// @Tags health +// @Accept json +// @Produce json +// @Success 200 {object} HealthResponse "Healthy status" +// @Router /health [get] +func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + response := HealthResponse{ + Status: "healthy", + } + + h.server.writeJSON(w, http.StatusOK, response) +} + +// Diagnostics handles GET /api/v1/diagnostics +// @Summary Get diagnostics +// @Description Returns detailed diagnostic information including memory usage, connection info, and event statistics +// @Tags health +// @Accept json +// @Produce json +// @Success 200 {object} DiagnosticsResponse +// @Router /diagnostics [get] +func (h *HealthHandlers) Diagnostics(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + var m runtime.MemStats + runtime.ReadMemStats(&m) + + // Get event statistics + hookNames := h.server.dedupManager.GetAllHookNames() + totalEvents := h.server.dedupManager.GetEventCount() + + response := DiagnosticsResponse{ + APIStatus: "running", + MemoryUsage: MemoryStats{ + Alloc: m.Alloc, + TotalAlloc: m.TotalAlloc, + Sys: m.Sys, + NumGC: m.NumGC, + }, + ConnectionInfo: ConnectionInfo{ + ActiveEventStreams: 0, + }, + EventStats: EventStats{ + TotalEvents: totalEvents, + TotalHooks: len(hookNames), + }, + } + + h.server.writeJSON(w, http.StatusOK, response) +} + +// Metrics handles GET /api/v1/metrics (Prometheus-style metrics) +// @Summary Get Prometheus metrics +// @Description Returns Prometheus-style metrics in text format +// @Tags health +// @Accept json +// @Produce text/plain +// @Success 200 {string} string "Prometheus metrics" +// @Router /metrics [get] +func (h *HealthHandlers) Metrics(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + var m runtime.MemStats + runtime.ReadMemStats(&m) + + hookNames := h.server.dedupManager.GetAllHookNames() + totalEvents := h.server.dedupManager.GetEventCount() + + // Generate Prometheus-style metrics + metrics := []string{ + "# HELP khook_events_total Total number of active events", + "# TYPE khook_events_total gauge", + fmt.Sprintf("khook_events_total %d", totalEvents), + "", + "# HELP khook_hooks_total Total number of hooks with active events", + "# TYPE khook_hooks_total gauge", + fmt.Sprintf("khook_hooks_total %d", len(hookNames)), + "", + "# HELP khook_memory_alloc_bytes Memory allocated in bytes", + "# TYPE khook_memory_alloc_bytes gauge", + fmt.Sprintf("khook_memory_alloc_bytes %d", m.Alloc), + "", + "# HELP khook_memory_sys_bytes System memory in bytes", + "# TYPE khook_memory_sys_bytes gauge", + fmt.Sprintf("khook_memory_sys_bytes %d", m.Sys), + "", + "# HELP khook_gc_runs_total Total number of GC runs", + "# TYPE khook_gc_runs_total counter", + fmt.Sprintf("khook_gc_runs_total %d", m.NumGC), + } + + w.Header().Set("Content-Type", "text/plain; version=0.0.4") + w.WriteHeader(http.StatusOK) + for _, metric := range metrics { + w.Write([]byte(metric + "\n")) + } +} diff --git a/internal/apiserver/hooks.go b/internal/apiserver/hooks.go new file mode 100644 index 0000000..4570c8f --- /dev/null +++ b/internal/apiserver/hooks.go @@ -0,0 +1,76 @@ +package apiserver + +import ( + "net/http" + + "sigs.k8s.io/controller-runtime/pkg/client" + + kagentv1alpha2 "github.com/kagent-dev/khook/api/v1alpha2" +) + +// HookHandlers handles hook-related API endpoints +type HookHandlers struct { + server *Server +} + +// NewHookHandlers creates a new hook handlers instance +func NewHookHandlers(server *Server) *HookHandlers { + return &HookHandlers{server: server} +} + +// HookResponse represents a hook in the API response +type HookResponse struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + EventConfigurations []kagentv1alpha2.EventConfiguration `json:"eventConfigurations"` + Status kagentv1alpha2.HookStatus `json:"status"` +} + +// HookListResponse represents a list of hooks +type HookListResponse struct { + Items []HookResponse `json:"items"` + Total int `json:"total"` +} + +// ListHooks handles GET /api/v1/hooks +// @Summary List hooks +// @Description Returns a list of all Hook configurations +// @Tags hooks +// @Accept json +// @Produce json +// @Success 200 {object} HookListResponse +// @Router /hooks [get] +func (h *HookHandlers) ListHooks(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + ctx := r.Context() + + // List all hooks + var hookList kagentv1alpha2.HookList + if err := h.server.k8sClient.List(ctx, &hookList, &client.ListOptions{}); err != nil { + h.server.logger.Error(err, "Failed to list hooks") + h.server.writeError(w, http.StatusInternalServerError, "Failed to list hooks") + return + } + + // Convert to API response format + items := make([]HookResponse, 0, len(hookList.Items)) + for _, hook := range hookList.Items { + items = append(items, HookResponse{ + Name: hook.Name, + Namespace: hook.Namespace, + EventConfigurations: hook.Spec.EventConfigurations, + Status: hook.Status, + }) + } + + response := HookListResponse{ + Items: items, + Total: len(items), + } + + h.server.writeJSON(w, http.StatusOK, response) +} diff --git a/internal/apiserver/json.go b/internal/apiserver/json.go new file mode 100644 index 0000000..89148ab --- /dev/null +++ b/internal/apiserver/json.go @@ -0,0 +1,13 @@ +package apiserver + +import ( + "encoding/json" + "net/http" +) + +// jsonEncode encodes data to JSON and writes it to the response writer +func jsonEncode(w http.ResponseWriter, data interface{}) error { + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} diff --git a/internal/apiserver/server.go b/internal/apiserver/server.go new file mode 100644 index 0000000..1d6e7d5 --- /dev/null +++ b/internal/apiserver/server.go @@ -0,0 +1,227 @@ +package apiserver + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "github.com/go-logr/logr" + httpSwagger "github.com/swaggo/http-swagger" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kagent-dev/khook/internal/interfaces" +) + +// Server represents the HTTP API server +type Server struct { + port string + dedupManager interfaces.DeduplicationManager + k8sClient client.Client + statusManager interfaces.StatusManager + logger logr.Logger + httpServer *http.Server + eventHandlers *EventHandlers + hookHandlers *HookHandlers + healthHandlers *HealthHandlers + statsHandlers *StatsHandlers +} + +// Config holds configuration for the API server +type Config struct { + Port string + DedupManager interfaces.DeduplicationManager + K8sClient client.Client + StatusManager interfaces.StatusManager +} + +// NewServer creates a new API server instance +func NewServer(config Config) *Server { + logger := log.Log.WithName("apiserver") + + // Set default port if not specified + port := config.Port + if port == "" { + port = "8082" + } + + server := &Server{ + port: port, + dedupManager: config.DedupManager, + k8sClient: config.K8sClient, + statusManager: config.StatusManager, + logger: logger, + } + + // Initialize handlers + server.eventHandlers = NewEventHandlers(server) + server.hookHandlers = NewHookHandlers(server) + server.healthHandlers = NewHealthHandlers(server) + server.statsHandlers = NewStatsHandlers(server) + + return server +} + +// Start starts the HTTP API server +func (s *Server) Start(ctx context.Context) error { + mux := http.NewServeMux() + + // Register routes + s.registerRoutes(mux) + + // Create HTTP server with timeouts + s.httpServer = &http.Server{ + Addr: ":" + s.port, + Handler: s.middleware(mux), + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + s.logger.Info("Starting API server", "port", s.port) + + // Start server in a goroutine + errCh := make(chan error, 1) + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errCh <- fmt.Errorf("failed to start API server: %w", err) + } + }() + + // Wait for context cancellation or server error + select { + case <-ctx.Done(): + s.logger.Info("Shutting down API server") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := s.httpServer.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("error during server shutdown: %w", err) + } + return ctx.Err() + case err := <-errCh: + return err + } +} + +// GetMux returns a new ServeMux with all routes registered +func (s *Server) GetMux() *http.ServeMux { + mux := http.NewServeMux() + s.registerRoutes(mux) + return mux +} + +// registerRoutes registers all API routes +func (s *Server) registerRoutes(mux *http.ServeMux) { + // API v1 routes + apiV1 := "/api/v1" + + // Event endpoints + mux.HandleFunc(apiV1+"/events", s.eventHandlers.ListEvents) + mux.HandleFunc(apiV1+"/events/stream", s.eventHandlers.StreamEvents) + + // Hook endpoints + mux.HandleFunc(apiV1+"/hooks", s.hookHandlers.ListHooks) + + // Statistics endpoints + mux.HandleFunc(apiV1+"/stats/events/summary", s.statsHandlers.EventSummary) + mux.HandleFunc(apiV1+"/stats/events/by-type", s.statsHandlers.EventsByType) + + // Health and diagnostics endpoints + mux.HandleFunc(apiV1+"/health", s.healthHandlers.Health) + mux.HandleFunc(apiV1+"/diagnostics", s.healthHandlers.Diagnostics) + mux.HandleFunc(apiV1+"/metrics", s.healthHandlers.Metrics) + + // Swagger documentation endpoints + s.registerSwaggerRoutes(mux) +} + +// Middleware applies common middleware to all requests +func (s *Server) Middleware(next http.Handler) http.Handler { + return s.middleware(next) +} + +// middleware applies common middleware to all requests +func (s *Server) middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight requests + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + // Request logging + s.logger.V(1).Info("API request", + "method", r.Method, + "path", r.URL.Path, + "remoteAddr", r.RemoteAddr) + + // Call next handler + next.ServeHTTP(w, r) + + // Log response + s.logger.V(2).Info("API response", + "method", r.Method, + "path", r.URL.Path, + "duration", time.Since(start)) + }) +} + +// writeJSON writes a JSON response +func (s *Server) writeJSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := jsonEncode(w, data); err != nil { + s.logger.Error(err, "Failed to encode JSON response") + } +} + +// writeError writes an error response +func (s *Server) writeError(w http.ResponseWriter, statusCode int, message string) { + s.writeJSON(w, statusCode, map[string]string{ + "error": message, + }) +} + +// registerSwaggerRoutes registers Swagger UI routes +func (s *Server) registerSwaggerRoutes(mux *http.ServeMux) { + swaggerJSONPath := "./docs/swagger/swagger.json" + if _, err := os.Stat(swaggerJSONPath); err == nil { + // Serve swagger.json directly + mux.HandleFunc("/swagger/doc.json", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, swaggerJSONPath) + }) + + // Use http-swagger to serve Swagger UI + mux.HandleFunc("/swagger/", func(w http.ResponseWriter, r *http.Request) { + // Build URL dynamically based on request + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + host := r.Host + if host == "" { + host = "localhost:" + s.port + } + docURL := scheme + "://" + host + "/swagger/doc.json" + + handler := httpSwagger.Handler( + httpSwagger.URL(docURL), + httpSwagger.DeepLinking(true), + httpSwagger.DocExpansion("none"), + httpSwagger.DomID("swagger-ui"), + ) + handler.ServeHTTP(w, r) + }) + } else { + s.logger.Info("Swagger JSON not found, skipping Swagger UI", "path", swaggerJSONPath) + } +} diff --git a/internal/apiserver/stats.go b/internal/apiserver/stats.go new file mode 100644 index 0000000..7d64bab --- /dev/null +++ b/internal/apiserver/stats.go @@ -0,0 +1,178 @@ +package apiserver + +import ( + "net/http" + "strings" + + "k8s.io/apimachinery/pkg/types" +) + +// StatsHandlers handles statistics-related API endpoints +type StatsHandlers struct { + server *Server +} + +// NewStatsHandlers creates a new stats handlers instance +func NewStatsHandlers(server *Server) *StatsHandlers { + return &StatsHandlers{server: server} +} + +// EventSummaryResponse represents event summary statistics +type EventSummaryResponse struct { + Total int `json:"total"` + Firing int `json:"firing"` + Resolved int `json:"resolved"` + BySeverity map[string]int `json:"bySeverity"` + ByEventType map[string]int `json:"byEventType"` +} + +// EventsByTypeResponse represents events grouped by type +type EventsByTypeResponse struct { + ByType map[string]TypeStats `json:"byType"` + Total int `json:"total"` +} + +// TypeStats represents statistics for a specific event type +type TypeStats struct { + Count int `json:"count"` + Percentage float64 `json:"percentage"` +} + +// EventSummary handles GET /api/v1/stats/events/summary +// @Summary Get event summary +// @Description Returns event summary statistics including counts by severity and type +// @Tags statistics +// @Accept json +// @Produce json +// @Success 200 {object} EventSummaryResponse +// @Router /stats/events/summary [get] +func (h *StatsHandlers) EventSummary(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + hookNames := h.server.dedupManager.GetAllHookNames() + + total := 0 + firing := 0 + resolved := 0 + byEventType := make(map[string]int) + bySeverity := make(map[string]int) + + // Collect statistics from all hooks + for _, hookNameStr := range hookNames { + parts := strings.Split(hookNameStr, "/") + if len(parts) != 2 { + continue + } + + hookRef := types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + } + + activeEvents := h.server.dedupManager.GetActiveEventsWithStatus(hookRef) + for _, event := range activeEvents { + total++ + byEventType[event.EventType]++ + + if event.Status == "firing" { + firing++ + } else if event.Status == "resolved" { + resolved++ + } + + // Determine severity based on event type (default mapping) + severity := determineSeverity(event.EventType) + bySeverity[severity]++ + } + } + + response := EventSummaryResponse{ + Total: total, + Firing: firing, + Resolved: resolved, + BySeverity: bySeverity, + ByEventType: byEventType, + } + + h.server.writeJSON(w, http.StatusOK, response) +} + +// EventsByType handles GET /api/v1/stats/events/by-type +// @Summary Get events by type +// @Description Returns events grouped by type with counts and percentages +// @Tags statistics +// @Accept json +// @Produce json +// @Success 200 {object} EventsByTypeResponse +// @Router /stats/events/by-type [get] +func (h *StatsHandlers) EventsByType(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + h.server.writeError(w, http.StatusMethodNotAllowed, "Method not allowed") + return + } + + hookNames := h.server.dedupManager.GetAllHookNames() + + byType := make(map[string]int) + total := 0 + + // Count events by type + for _, hookNameStr := range hookNames { + parts := strings.Split(hookNameStr, "/") + if len(parts) != 2 { + continue + } + + hookRef := types.NamespacedName{ + Namespace: parts[0], + Name: parts[1], + } + + activeEvents := h.server.dedupManager.GetActiveEventsWithStatus(hookRef) + for _, event := range activeEvents { + byType[event.EventType]++ + total++ + } + } + + // Calculate percentages + byTypeStats := make(map[string]TypeStats) + for eventType, count := range byType { + percentage := 0.0 + if total > 0 { + percentage = float64(count) / float64(total) * 100.0 + } + byTypeStats[eventType] = TypeStats{ + Count: count, + Percentage: percentage, + } + } + + response := EventsByTypeResponse{ + ByType: byTypeStats, + Total: total, + } + + h.server.writeJSON(w, http.StatusOK, response) +} + +// determineSeverity determines severity based on event type using a default mapping +func determineSeverity(eventType string) string { + switch eventType { + case "oom-kill": + return "critical" + case "probe-failed": + return "warning" + case "pod-restart": + return "warning" + case "pod-pending": + return "info" + case "node-not-ready": + return "critical" + default: + return "info" + } +} diff --git a/internal/apiserver/swagger.go b/internal/apiserver/swagger.go new file mode 100644 index 0000000..679060f --- /dev/null +++ b/internal/apiserver/swagger.go @@ -0,0 +1,29 @@ +package apiserver + +// @title Khook API Server +// @version 1.0 +// @description HTTP API server for querying events, hooks, statistics, and health information +// @termsOfService http://swagger.io/terms/ + +// @contact.name API Support +// @contact.url https://kagent.dev +// @contact.email support@kagent.dev + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8082 +// @BasePath /api/v1 +// @schemes http https + +// @tag.name health +// @tag.description Health and diagnostics endpoints + +// @tag.name events +// @tag.description Event query and streaming endpoints + +// @tag.name hooks +// @tag.description Hook configuration endpoints + +// @tag.name statistics +// @tag.description Event statistics endpoints diff --git a/internal/config/config.go b/internal/config/config.go index 6e9fc70..c2829b3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -47,6 +47,9 @@ type ControllerConfig struct { // MaxConcurrentReconciles is the maximum number of concurrent reconciles MaxConcurrentReconciles int `yaml:"maxConcurrentReconciles"` + + // APIServerPort is the port for the HTTP API server (default: 8082) + APIServerPort string `yaml:"apiServerPort"` } // LoggingConfig holds logging configuration @@ -70,6 +73,7 @@ func DefaultConfig() *Config { EventDeduplicationTimeout: 10 * time.Minute, EventCleanupInterval: 5 * time.Minute, MaxConcurrentReconciles: 1, + APIServerPort: "8082", }, Logging: LoggingConfig{ Level: "info", @@ -126,6 +130,9 @@ func Load(configFile string) (*Config, error) { if apiKey := os.Getenv("KAGENT_API_KEY"); apiKey != "" { config.Kagent.APIKey = apiKey } + if apiPort := os.Getenv("API_SERVER_PORT"); apiPort != "" { + config.Controller.APIServerPort = apiPort + } // Load from file if specified if configFile != "" { diff --git a/internal/interfaces/controller.go b/internal/interfaces/controller.go index 8e5f950..cd3adab 100644 --- a/internal/interfaces/controller.go +++ b/internal/interfaces/controller.go @@ -85,6 +85,8 @@ type DeduplicationManager interface { GetActiveEvents(hookRef types.NamespacedName) []ActiveEvent GetActiveEventsWithStatus(hookRef types.NamespacedName) []ActiveEvent MarkNotified(hookRef types.NamespacedName, event Event) + GetAllHookNames() []string + GetEventCount() int } // EventRecorder handles Kubernetes event recording diff --git a/internal/workflow/coordinator.go b/internal/workflow/coordinator.go index f70c444..d54004e 100644 --- a/internal/workflow/coordinator.go +++ b/internal/workflow/coordinator.go @@ -34,7 +34,18 @@ func NewCoordinator( ) *Coordinator { dedupManager := deduplication.NewManager() statusManager := status.NewManager(ctrlClient, eventRecorder) + return NewCoordinatorWithManagers(k8sClient, ctrlClient, kagentClient, eventRecorder, dedupManager, statusManager) +} +// NewCoordinatorWithManagers creates a new workflow coordinator with shared managers +func NewCoordinatorWithManagers( + k8sClient kubernetes.Interface, + ctrlClient client.Client, + kagentClient interfaces.KagentClient, + eventRecorder interfaces.EventRecorder, + dedupManager interfaces.DeduplicationManager, + statusManager interfaces.StatusManager, +) *Coordinator { hookDiscovery := NewHookDiscoveryService(ctrlClient) workflowManager := NewWorkflowManager( k8sClient,