trying to make it work

This commit is contained in:
Serge Zaitsev
2025-12-23 15:55:39 +01:00
parent b3589300b3
commit 3071482bf4
+177 -2
View File
@@ -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<K8sAnnotationSpec>;
constructor() {
this.client = new ScopedResourceClient<K8sAnnotationSpec>(K8S_ANNOTATION_API_CONFIG);
}
async query(params: Record<string, unknown>, requestId: string): Promise<DataFrame> {
// 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<K8sAnnotationSpec>) => ({
id: item.metadata.name,
...item.spec,
panelId: item.spec.panelID, // Map panelID back to panelId
}));
return toDataFrame(annotations);
}
async forAlert(alertUID: string): Promise<StateHistoryItem[]> {
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<K8sAnnotationSpec>) => ({
id: item.metadata.name,
...item.spec,
panelId: item.spec.panelID,
})) as any;
}
async save(annotation: AnnotationEvent): Promise<AnnotationEvent> {
const resource: ResourceForCreate<K8sAnnotationSpec> = {
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<unknown> {
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<K8sAnnotationSpec> = {
...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<unknown> {
if (!annotation.id) {
throw new Error('Annotation ID is required for delete');
}
return this.client.delete(String(annotation.id), false);
}
async tags(): Promise<Array<{ term: string; count: number }>> {
// 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<K8sAnnotationTagsResponse>(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;
}