import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs'; import { BackendSrvRequest, config, getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getAPINamespace } from '../../api/utils'; import { ListOptions, ListOptionsFieldSelector, ListOptionsLabelSelector, MetaStatus, Resource, ResourceForCreate, ResourceList, ResourceClient, ObjectMeta, WatchOptions, K8sAPIGroupList, AnnoKeySavedFromUI, ResourceEvent, } from './types'; export interface GroupVersionResource { group: string; version: string; resource: string; } export class ScopedResourceClient implements ResourceClient { readonly url: string; constructor(gvr: GroupVersionResource, namespaced = true) { const ns = namespaced ? `namespaces/${getAPINamespace()}/` : ''; this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`; } public async get(name: string): Promise> { return getBackendSrv().get>(`${this.url}/${name}`); } public watch( params?: WatchOptions, config?: Pick ): Observable> { const decoder = new TextDecoder(); const { name, ...rest } = params ?? {}; // name needs to be added to fieldSelector const requestParams = { ...rest, watch: true, labelSelector: this.parseListOptionsSelector(params?.labelSelector), fieldSelector: this.parseListOptionsSelector(params?.fieldSelector), }; if (name) { requestParams.fieldSelector = `metadata.name=${name}`; } return getBackendSrv() .chunked({ url: this.url, params: requestParams, ...config, }) .pipe( filter((response) => response.ok && response.data instanceof Uint8Array), map((response) => { const text = decoder.decode(response.data); return text.split('\n'); }), mergeMap((text) => from(text)), filter((line) => line.length > 0), map((line) => { try { return JSON.parse(line); } catch (e) { console.warn('Invalid JSON in watch stream:', e); return null; } }), filter((event): event is ResourceEvent => event !== null), retry({ count: 3, delay: 1000 }), catchError((error) => { console.error('Watch stream error:', error); throw error; }) ); } public async subresource(name: string, path: string): Promise { return getBackendSrv().get(`${this.url}/${name}/${path}`); } public async list(opts?: ListOptions | undefined): Promise> { const finalOpts = opts || {}; finalOpts.labelSelector = this.parseListOptionsSelector(finalOpts?.labelSelector); finalOpts.fieldSelector = this.parseListOptionsSelector(finalOpts?.fieldSelector); return getBackendSrv().get>(this.url, opts); } public async create(obj: ResourceForCreate): Promise> { if (!obj.metadata.name && !obj.metadata.generateName) { const login = contextSrv.user.login; // GenerateName lets the apiserver create a new uid for the name // THe passed in value is the suggested prefix obj.metadata.generateName = login ? login.slice(0, 2) : 'g'; } setSavedFromUIAnnotation(obj.metadata); return getBackendSrv().post(this.url, obj); } public async update(obj: Resource): Promise> { setSavedFromUIAnnotation(obj.metadata); return getBackendSrv().put>(`${this.url}/${obj.metadata.name}`, obj); } public async delete(name: string, showSuccessAlert: boolean): Promise { return getBackendSrv().delete(`${this.url}/${name}`, undefined, { showSuccessAlert, }); } private parseListOptionsSelector = parseListOptionsSelector; } // add the origin annotations so we know what was set from the UI function setSavedFromUIAnnotation(meta: Partial) { if (!meta.annotations) { meta.annotations = {}; } meta.annotations[AnnoKeySavedFromUI] = config.buildInfo.versionString; } export class DatasourceAPIVersions { private apiVersions?: { [pluginID: string]: string }; async get(pluginID: string): Promise { if (this.apiVersions) { return this.apiVersions[pluginID]; } const apis = await getBackendSrv().get('/apis'); const apiVersions: { [pluginID: string]: string } = {}; apis.groups.forEach((group) => { if (group.name.includes('datasource.grafana.app')) { const id = group.name.split('.')[0]; apiVersions[id] = group.preferredVersion.version; // workaround for plugins that don't append '-datasource' for the group name // e.g. org-plugin-datasource uses org-plugin.datasource.grafana.app if (!id.endsWith('-datasource')) { if (!id.includes('-')) { // workaroud for Grafana plugins that don't include the org either // e.g. testdata uses testdata.datasource.grafana.app apiVersions[`grafana-${id}-datasource`] = group.preferredVersion.version; } else { apiVersions[`${id}-datasource`] = group.preferredVersion.version; } } } }); this.apiVersions = apiVersions; return apiVersions[pluginID]; } } export const parseListOptionsSelector = (selector: ListOptionsLabelSelector | ListOptionsFieldSelector | undefined) => { if (!Array.isArray(selector)) { return selector; } return selector .map((label) => { const key = String(label.key); const operator = label.operator; switch (operator) { case '=': case '!=': return `${key}${operator}${label.value}`; case 'in': case 'notin': return `${key} ${operator} (${label.value.join(',')})`; case '': case '!': return `${operator}${key}`; default: return null; } }) .filter(Boolean) .join(','); };