mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 16:52:54 +08:00
213 lines
7.1 KiB
TypeScript
213 lines
7.1 KiB
TypeScript
import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs';
|
|
|
|
import { isLiveChannelMessageEvent, LiveChannelScope } from '@grafana/data';
|
|
import { config, getBackendSrv, getGrafanaLiveSrv } 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,
|
|
ResourceClientWriteParams,
|
|
GroupVersionResource,
|
|
} from './types';
|
|
|
|
export class ScopedResourceClient<T = object, S = object, K = string> implements ResourceClient<T, S, K> {
|
|
readonly url: string;
|
|
readonly gvr: GroupVersionResource;
|
|
|
|
constructor(gvr: GroupVersionResource, namespaced = true) {
|
|
const ns = namespaced ? `namespaces/${getAPINamespace()}/` : '';
|
|
this.gvr = gvr;
|
|
this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`;
|
|
}
|
|
|
|
public async get(name: string): Promise<Resource<T, S, K>> {
|
|
return getBackendSrv().get<Resource<T, S, K>>(`${this.url}/${name}`);
|
|
}
|
|
|
|
public watch(params?: WatchOptions): Observable<ResourceEvent<T, S, K>> {
|
|
const requestParams = {
|
|
watch: true,
|
|
labelSelector: this.parseListOptionsSelector(params?.labelSelector),
|
|
fieldSelector: this.parseListOptionsSelector(params?.fieldSelector),
|
|
};
|
|
if (params?.name) {
|
|
requestParams.fieldSelector = `metadata.name=${params.name}`;
|
|
}
|
|
|
|
// For now, watch over live only supports provisioning
|
|
if (this.gvr.group === 'provisioning.grafana.app') {
|
|
let query = '';
|
|
if (requestParams.fieldSelector?.startsWith('metadata.name=')) {
|
|
query = requestParams.fieldSelector.substring('metadata.name'.length);
|
|
}
|
|
return getGrafanaLiveSrv()
|
|
.getStream<ResourceEvent<T, S, K>>({
|
|
scope: LiveChannelScope.Watch,
|
|
namespace: this.gvr.group,
|
|
path: `${this.gvr.version}/${this.gvr.resource}${query}/${config.bootData.user.uid}`,
|
|
})
|
|
.pipe(
|
|
filter((event) => isLiveChannelMessageEvent(event)),
|
|
map((event) => event.message)
|
|
);
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
return getBackendSrv()
|
|
.chunked({
|
|
url: this.url,
|
|
params: requestParams,
|
|
method: 'GET',
|
|
})
|
|
.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, line);
|
|
return null;
|
|
}
|
|
}),
|
|
filter((event): event is ResourceEvent<T, S, K> => event !== null),
|
|
retry({ count: 3, delay: 1000 }),
|
|
catchError((error) => {
|
|
console.error('Watch stream error:', error);
|
|
throw error;
|
|
})
|
|
);
|
|
}
|
|
|
|
public async subresource<S>(name: string, path: string, params?: Record<string, unknown>): Promise<S> {
|
|
return getBackendSrv().get<S>(`${this.url}/${name}/${path}`, params);
|
|
}
|
|
|
|
public async list(opts?: ListOptions | undefined): Promise<ResourceList<T, S, K>> {
|
|
const finalOpts = opts || {};
|
|
finalOpts.labelSelector = this.parseListOptionsSelector(finalOpts?.labelSelector);
|
|
finalOpts.fieldSelector = this.parseListOptionsSelector(finalOpts?.fieldSelector);
|
|
|
|
return getBackendSrv().get<ResourceList<T, S, K>>(this.url, opts);
|
|
}
|
|
|
|
public async create(obj: ResourceForCreate<T, K>, params?: ResourceClientWriteParams): Promise<Resource<T, S, K>> {
|
|
if (!obj.metadata.name && !obj.metadata.generateName) {
|
|
const login = contextSrv.user.login;
|
|
// GenerateName lets the apiserver create a unique name by appending random characters to the prefix.
|
|
// This strips out special characters, numbers, and symbols to ensure a valid prefix.
|
|
const alphabeticChars = login ? login.replace(/[^a-zA-Z]/g, '').slice(0, 2) : '';
|
|
obj.metadata.generateName = alphabeticChars || 'g';
|
|
}
|
|
setSavedFromUIAnnotation(obj.metadata);
|
|
return getBackendSrv().post(this.url, obj, {
|
|
params,
|
|
});
|
|
}
|
|
|
|
public async update(obj: Resource<T, S, K>, params?: ResourceClientWriteParams): Promise<Resource<T, S, K>> {
|
|
setSavedFromUIAnnotation(obj.metadata);
|
|
const url = `${this.url}/${obj.metadata.name}`;
|
|
return getBackendSrv().put<Resource<T, S, K>>(url, obj, {
|
|
params,
|
|
});
|
|
}
|
|
|
|
public async delete(name: string, showSuccessAlert: boolean): Promise<MetaStatus> {
|
|
return getBackendSrv().delete<MetaStatus>(`${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<ObjectMeta>) {
|
|
if (!meta.annotations) {
|
|
meta.annotations = {};
|
|
}
|
|
meta.annotations[AnnoKeySavedFromUI] = config.buildInfo.versionString;
|
|
}
|
|
|
|
export class DatasourceAPIVersions {
|
|
private apiVersions?: { [pluginID: string]: string };
|
|
|
|
async get(pluginID: string): Promise<string | undefined> {
|
|
if (this.apiVersions) {
|
|
return this.apiVersions[pluginID];
|
|
}
|
|
const apis = await getBackendSrv().get<K8sAPIGroupList>('/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(',');
|
|
};
|