K8s/Typescript: Support generic status for ScopedResourceClient (#98509)

This commit is contained in:
Ryan McKinley 2025-01-07 09:26:00 +03:00 committed by GitHub
parent addc1c95a5
commit 322c7d9548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 49 deletions

View File

@ -21,7 +21,7 @@ export interface GroupVersionResource {
resource: string; resource: string;
} }
export class ScopedResourceClient<T = object, K = string> implements ResourceClient<T, K> { export class ScopedResourceClient<T = object, S = object, K = string> implements ResourceClient<T, S, K> {
readonly url: string; readonly url: string;
constructor(gvr: GroupVersionResource, namespaced = true) { constructor(gvr: GroupVersionResource, namespaced = true) {
@ -30,23 +30,23 @@ export class ScopedResourceClient<T = object, K = string> implements ResourceCli
this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`; this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`;
} }
public async get(name: string): Promise<Resource<T, K>> { public async get(name: string): Promise<Resource<T, S, K>> {
return getBackendSrv().get<Resource<T, K>>(`${this.url}/${name}`); return getBackendSrv().get<Resource<T, S, K>>(`${this.url}/${name}`);
} }
public async subresource<S>(name: string, path: string): Promise<S> { public async subresource<S>(name: string, path: string): Promise<S> {
return getBackendSrv().get<S>(`${this.url}/${name}/${path}`); return getBackendSrv().get<S>(`${this.url}/${name}/${path}`);
} }
public async list(opts?: ListOptions | undefined): Promise<ResourceList<T, K>> { public async list(opts?: ListOptions | undefined): Promise<ResourceList<T, S, K>> {
const finalOpts = opts || {}; const finalOpts = opts || {};
finalOpts.labelSelector = this.parseListOptionsSelector(finalOpts?.labelSelector); finalOpts.labelSelector = this.parseListOptionsSelector(finalOpts?.labelSelector);
finalOpts.fieldSelector = this.parseListOptionsSelector(finalOpts?.fieldSelector); finalOpts.fieldSelector = this.parseListOptionsSelector(finalOpts?.fieldSelector);
return getBackendSrv().get<ResourceList<T, K>>(this.url, opts); return getBackendSrv().get<ResourceList<T, S, K>>(this.url, opts);
} }
public async create(obj: ResourceForCreate<T, K>): Promise<Resource<T, K>> { public async create(obj: ResourceForCreate<T, K>): Promise<Resource<T, S, K>> {
if (!obj.metadata.name && !obj.metadata.generateName) { if (!obj.metadata.name && !obj.metadata.generateName) {
const login = contextSrv.user.login; const login = contextSrv.user.login;
// GenerateName lets the apiserver create a new uid for the name // GenerateName lets the apiserver create a new uid for the name
@ -57,47 +57,16 @@ export class ScopedResourceClient<T = object, K = string> implements ResourceCli
return getBackendSrv().post(this.url, obj); return getBackendSrv().post(this.url, obj);
} }
public async update(obj: Resource<T, K>): Promise<Resource<T, K>> { public async update(obj: Resource<T, S, K>): Promise<Resource<T, S, K>> {
setSavedFromUIAnnotation(obj.metadata); setSavedFromUIAnnotation(obj.metadata);
return getBackendSrv().put<Resource<T, K>>(`${this.url}/${obj.metadata.name}`, obj); return getBackendSrv().put<Resource<T, S, K>>(`${this.url}/${obj.metadata.name}`, obj);
} }
public async delete(name: string): Promise<MetaStatus> { public async delete(name: string): Promise<MetaStatus> {
return getBackendSrv().delete<MetaStatus>(`${this.url}/${name}`); return getBackendSrv().delete<MetaStatus>(`${this.url}/${name}`);
} }
private parseListOptionsSelector( private parseListOptionsSelector = parseListOptionsSelector;
selector: ListOptionsLabelSelector | ListOptionsFieldSelector | undefined
): string | 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(',');
}
} }
// add the origin annotations so we know what was set from the UI // add the origin annotations so we know what was set from the UI
@ -138,3 +107,34 @@ export class DatasourceAPIVersions {
return apiVersions[pluginID]; 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(',');
};

View File

@ -84,9 +84,10 @@ type GrafanaClientAnnotations = {
[AnnoKeyDashboardGnetId]?: string; [AnnoKeyDashboardGnetId]?: string;
}; };
export interface Resource<T = object, K = string> extends TypeMeta<K> { export interface Resource<T = object, S = object, K = string> extends TypeMeta<K> {
metadata: ObjectMeta; metadata: ObjectMeta;
spec: T; spec: T;
status?: S;
} }
export interface ResourceForCreate<T = object, K = string> extends Partial<TypeMeta<K>> { export interface ResourceForCreate<T = object, K = string> extends Partial<TypeMeta<K>> {
@ -103,9 +104,9 @@ export interface ListMeta {
remainingItemCount?: number; remainingItemCount?: number;
} }
export interface ResourceList<T, K = string> extends TypeMeta { export interface ResourceList<T, S = object, K = string> extends TypeMeta {
metadata: ListMeta; metadata: ListMeta;
items: Array<Resource<T, K>>; items: Array<Resource<T, S, K>>;
} }
export type ListOptionsLabelSelector = export type ListOptionsLabelSelector =
@ -168,12 +169,12 @@ export interface MetaStatus {
details?: object; details?: object;
} }
export interface ResourceClient<T = object, K = string> { export interface ResourceClient<T = object, S = object, K = string> {
create(obj: ResourceForCreate<T, K>): Promise<Resource<T, K>>; create(obj: ResourceForCreate<T, K>): Promise<Resource<T, S, K>>;
get(name: string): Promise<Resource<T, K>>; get(name: string): Promise<Resource<T, S, K>>;
subresource<S>(name: string, path: string): Promise<S>; subresource<S>(name: string, path: string): Promise<S>;
list(opts?: ListOptions): Promise<ResourceList<T, K>>; list(opts?: ListOptions): Promise<ResourceList<T, S, K>>;
update(obj: ResourceForCreate<T, K>): Promise<Resource<T, K>>; update(obj: ResourceForCreate<T, K>): Promise<Resource<T, S, K>>;
delete(name: string): Promise<MetaStatus>; delete(name: string): Promise<MetaStatus>;
} }

View File

@ -12,7 +12,7 @@ const namespace = config.namespace ?? 'default';
const nodesEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_node_children`; const nodesEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_node_children`;
const dashboardsEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_dashboard_bindings`; const dashboardsEndpoint = `/apis/${group}/${version}/namespaces/${namespace}/find/scope_dashboard_bindings`;
const scopesClient = new ScopedResourceClient<ScopeSpec, 'Scope'>({ const scopesClient = new ScopedResourceClient<ScopeSpec, unknown, 'Scope'>({
group, group,
version, version,
resource: 'scopes', resource: 'scopes',