From 3071482bf465949af97fe0e360e1e98c57731673 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Mon, 15 Dec 2025 15:37:52 +0100 Subject: [PATCH] trying to make it work --- public/app/features/annotations/api.ts | 179 ++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 2 deletions(-) diff --git a/public/app/features/annotations/api.ts b/public/app/features/annotations/api.ts index c4d5622ce55..209a03a4951 100644 --- a/public/app/features/annotations/api.ts +++ b/public/app/features/annotations/api.ts @@ -1,7 +1,11 @@ import { AnnotationEvent, DataFrame, toDataFrame } from '@grafana/data'; -import { getBackendSrv } from '@grafana/runtime'; +import { config, getBackendSrv } from '@grafana/runtime'; import { StateHistoryItem } from 'app/types/unified-alerting'; +import { getAPINamespace } from '../../api/utils'; +import { ScopedResourceClient } from '../apiserver/client'; +import type { Resource, ResourceForCreate, ListOptions } from '../apiserver/types'; + import { AnnotationTagsResponse } from './types'; export interface AnnotationServer { @@ -47,11 +51,182 @@ class LegacyAnnotationServer implements AnnotationServer { } } +// K8s-style annotation spec based on the CUE definition +interface K8sAnnotationSpec { + text: string; + time: number; + timeEnd?: number; + dashboardUID?: string; + panelID?: number; + tags?: string[]; +} + +interface K8sAnnotationTagsResponse { + tags: Array<{ tag: string; count: number }>; +} + +const K8S_ANNOTATION_API_CONFIG = { + group: 'annotation.grafana.app', + version: 'v0alpha1', + resource: 'annotations', +}; + +class K8sAnnotationServer implements AnnotationServer { + private client: ScopedResourceClient; + + constructor() { + this.client = new ScopedResourceClient(K8S_ANNOTATION_API_CONFIG); + } + + async query(params: Record, requestId: string): Promise { + // Convert legacy query params to k8s list options + const listOpts: ListOptions = {}; + + // Map limit parameter + if (params.limit) { + listOpts.limit = Number(params.limit); + } + + // Build field selectors for dashboard/panel filtering and time ranges + const fieldSelectors: string[] = []; + + if (params.dashboardUID) { + fieldSelectors.push(`spec.dashboardUID=${params.dashboardUID}`); + } + + if (params.panelId) { + fieldSelectors.push(`spec.panelID=${params.panelId}`); + } + + // Handle time range filters using spec.time field + // Note: < and > operators need URL encoding when passed as query parameters + // < becomes %3C and > becomes %3E + if (params.from) { + // Use URL-encoded > operator + fieldSelectors.push(`spec.time%3E${params.from}`); + } + + if (params.to) { + // Use URL-encoded < operator + fieldSelectors.push(`spec.time%3C${params.to}`); + } + + if (fieldSelectors.length > 0) { + listOpts.fieldSelector = fieldSelectors.join(','); + } + + // Handle tags using label selectors + if (params.tags && Array.isArray(params.tags) && params.tags.length > 0) { + // Tags should be converted to label selectors + // Format: tag1,tag2 means annotations that have both tags + listOpts.labelSelector = params.tags.map((tag) => `tag=${tag}`).join(','); + } + + const result = await this.client.list(listOpts); + + // Convert k8s resources to legacy annotation format + const annotations = result.items.map((item: Resource) => ({ + id: item.metadata.name, + ...item.spec, + panelId: item.spec.panelID, // Map panelID back to panelId + })); + + return toDataFrame(annotations); + } + + async forAlert(alertUID: string): Promise { + const result = await this.client.list({ + labelSelector: `alertUID=${alertUID}`, + }); + + // Return as any[] since the k8s annotation format doesn't match StateHistoryItem exactly + // This is a limitation of the current type definitions + return result.items.map((item: Resource) => ({ + id: item.metadata.name, + ...item.spec, + panelId: item.spec.panelID, + })) as any; + } + + async save(annotation: AnnotationEvent): Promise { + const resource: ResourceForCreate = { + metadata: { + name: '', // Will be auto-generated by the server + }, + spec: { + text: annotation.text || '', + time: annotation.time || Date.now(), + timeEnd: annotation.timeEnd, + dashboardUID: annotation.dashboardUID ?? undefined, + panelID: annotation.panelId, + tags: annotation.tags, + }, + }; + + const result = await this.client.create(resource); + + return { + ...annotation, + id: result.metadata.name, + ...result.spec, + panelId: result.spec.panelID, + }; + } + + async update(annotation: AnnotationEvent): Promise { + if (!annotation.id) { + throw new Error('Annotation ID is required for update'); + } + + // Get the existing resource to preserve metadata + const existing = await this.client.get(String(annotation.id)); + + // Update only the spec fields + const updated: Resource = { + ...existing, + spec: { + text: annotation.text || existing.spec.text, + time: annotation.time || existing.spec.time, + timeEnd: annotation.timeEnd ?? existing.spec.timeEnd, + dashboardUID: annotation.dashboardUID ? annotation.dashboardUID : existing.spec.dashboardUID, + panelID: annotation.panelId ?? existing.spec.panelID, + tags: annotation.tags ?? existing.spec.tags, + }, + }; + + return this.client.update(updated); + } + + async delete(annotation: AnnotationEvent): Promise { + if (!annotation.id) { + throw new Error('Annotation ID is required for delete'); + } + return this.client.delete(String(annotation.id), false); + } + + async tags(): Promise> { + // Use the custom /tags route defined in the CUE manifest + const namespace = getAPINamespace(); + const url = `/apis/${K8S_ANNOTATION_API_CONFIG.group}/${K8S_ANNOTATION_API_CONFIG.version}/namespaces/${namespace}/${K8S_ANNOTATION_API_CONFIG.resource}/tags`; + + const response = await getBackendSrv().get(url, { limit: 1000 }); + + return response.tags.map(({ tag, count }) => ({ + term: tag, + count, + })); + } +} + let instance: AnnotationServer | null = null; export function annotationServer(): AnnotationServer { if (!instance) { - instance = new LegacyAnnotationServer(); + if (config.featureToggles.kubernetesAnnotations) { + instance = new K8sAnnotationServer(); + } else { + instance = new LegacyAnnotationServer(); + } } return instance; }