mirror of https://github.com/grafana/grafana
K8s/Frontend: Add generic resource server and use it for playlists (#83339)
Co-authored-by: Bogdan Matei <bogdan.matei@grafana.com>pull/85257/head
parent
85a646b4dc
commit
45d1766524
@ -0,0 +1,81 @@ |
||||
import { config, getBackendSrv } from '@grafana/runtime'; |
||||
|
||||
import { |
||||
ListOptions, |
||||
ListOptionsLabelSelector, |
||||
MetaStatus, |
||||
Resource, |
||||
ResourceForCreate, |
||||
ResourceList, |
||||
ResourceServer, |
||||
} from './types'; |
||||
|
||||
export interface GroupVersionResource { |
||||
group: string; |
||||
version: string; |
||||
resource: string; |
||||
} |
||||
|
||||
export class ScopedResourceServer<T = object, K = string> implements ResourceServer<T, K> { |
||||
readonly url: string; |
||||
|
||||
constructor(gvr: GroupVersionResource, namespaced = true) { |
||||
const ns = namespaced ? `namespaces/${config.namespace}/` : ''; |
||||
|
||||
this.url = `/apis/${gvr.group}/${gvr.version}/${ns}${gvr.resource}`; |
||||
} |
||||
|
||||
public async create(obj: ResourceForCreate<T, K>): Promise<void> { |
||||
return getBackendSrv().post(this.url, obj); |
||||
} |
||||
|
||||
public async get(name: string): Promise<Resource<T, K>> { |
||||
return getBackendSrv().get<Resource<T, K>>(`${this.url}/${name}`); |
||||
} |
||||
|
||||
public async list(opts?: ListOptions<T> | undefined): Promise<ResourceList<T, K>> { |
||||
const finalOpts = opts || {}; |
||||
finalOpts.labelSelector = this.parseLabelSelector(finalOpts?.labelSelector); |
||||
|
||||
return getBackendSrv().get<ResourceList<T, K>>(this.url, opts); |
||||
} |
||||
|
||||
public async update(obj: Resource<T, K>): Promise<Resource<T, K>> { |
||||
return getBackendSrv().put<Resource<T, K>>(`${this.url}/${obj.metadata.name}`, obj); |
||||
} |
||||
|
||||
public async delete(name: string): Promise<MetaStatus> { |
||||
return getBackendSrv().delete<MetaStatus>(`${this.url}/${name}`); |
||||
} |
||||
|
||||
private parseLabelSelector<T>(labelSelector: ListOptionsLabelSelector<T> | undefined): string | undefined { |
||||
if (!Array.isArray(labelSelector)) { |
||||
return labelSelector; |
||||
} |
||||
|
||||
return labelSelector |
||||
.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(','); |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
import { AnnoKeyCreatedBy, Resource } from './types'; |
||||
|
||||
interface MyObjSpec { |
||||
value: string; |
||||
count: number; |
||||
} |
||||
|
||||
describe('simple typescript tests', () => { |
||||
const val: Resource<MyObjSpec, 'MyObject'> = { |
||||
apiVersion: 'xxx', |
||||
kind: 'MyObject', |
||||
metadata: { |
||||
name: 'A', |
||||
resourceVersion: '1', |
||||
creationTimestamp: '123', |
||||
}, |
||||
spec: { |
||||
value: 'a', |
||||
count: 2, |
||||
}, |
||||
}; |
||||
|
||||
describe('typescript helper', () => { |
||||
it('read and write annotations', () => { |
||||
expect(val.metadata.annotations?.[AnnoKeyCreatedBy]).toBeUndefined(); |
||||
val.metadata.annotations = { 'grafana.app/createdBy': 'me' }; |
||||
expect(val.metadata.annotations?.[AnnoKeyCreatedBy]).toBe('me'); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,135 @@ |
||||
/** |
||||
* This file holds generic kubernetes compatible types. |
||||
* |
||||
* This is very much a work in progress aiming to simplify common access patterns for k8s resource |
||||
* Please update and improve types/utilities while we find a good pattern here! |
||||
* |
||||
* Once this is more stable and represents a more general pattern, it should be moved to @grafana/data |
||||
* |
||||
*/ |
||||
|
||||
/** The object type and version */ |
||||
export interface TypeMeta<K = string> { |
||||
apiVersion: string; |
||||
kind: K; |
||||
} |
||||
|
||||
export interface ObjectMeta { |
||||
// Name is the unique identifier in k8s -- it maps to the "uid" value in most existing grafana objects
|
||||
name: string; |
||||
// Namespace maps the owner group -- it is typically the org or stackId for most grafana resources
|
||||
namespace?: string; |
||||
// Resource version will increase (not sequentially!) with any change to the saved value
|
||||
resourceVersion: string; |
||||
// The first time this was saved
|
||||
creationTimestamp: string; |
||||
// General resource annotations -- including the common grafana.app values
|
||||
annotations?: GrafanaAnnotations; |
||||
// General application level key+value pairs
|
||||
labels?: Record<string, string>; |
||||
} |
||||
|
||||
export const AnnoKeyCreatedBy = 'grafana.app/createdBy'; |
||||
export const AnnoKeyUpdatedTimestamp = 'grafana.app/updatedTimestamp'; |
||||
export const AnnoKeyUpdatedBy = 'grafana.app/updatedBy'; |
||||
export const AnnoKeyFolder = 'grafana.app/folder'; |
||||
export const AnnoKeySlug = 'grafana.app/slug'; |
||||
|
||||
// Identify where values came from
|
||||
const AnnoKeyOriginName = 'grafana.app/originName'; |
||||
const AnnoKeyOriginPath = 'grafana.app/originPath'; |
||||
const AnnoKeyOriginKey = 'grafana.app/originKey'; |
||||
const AnnoKeyOriginTimestamp = 'grafana.app/originTimestamp'; |
||||
|
||||
type GrafanaAnnotations = { |
||||
[AnnoKeyCreatedBy]?: string; |
||||
[AnnoKeyUpdatedTimestamp]?: string; |
||||
[AnnoKeyUpdatedBy]?: string; |
||||
[AnnoKeyFolder]?: string; |
||||
[AnnoKeySlug]?: string; |
||||
|
||||
[AnnoKeyOriginName]?: string; |
||||
[AnnoKeyOriginPath]?: string; |
||||
[AnnoKeyOriginKey]?: string; |
||||
[AnnoKeyOriginTimestamp]?: string; |
||||
|
||||
// Any key value
|
||||
[key: string]: string | undefined; |
||||
}; |
||||
|
||||
export interface Resource<T = object, K = string> extends TypeMeta<K> { |
||||
metadata: ObjectMeta; |
||||
spec: T; |
||||
} |
||||
|
||||
export interface ResourceForCreate<T = object, K = string> extends Partial<TypeMeta<K>> { |
||||
metadata: Partial<ObjectMeta>; |
||||
spec: T; |
||||
} |
||||
|
||||
export interface ListMeta { |
||||
resourceVersion: string; |
||||
continue?: string; |
||||
remainingItemCount?: number; |
||||
} |
||||
|
||||
export interface ResourceList<T, K = string> extends TypeMeta { |
||||
metadata: ListMeta; |
||||
items: Array<Resource<T, K>>; |
||||
} |
||||
|
||||
export type ListOptionsLabelSelector<T = {}> = |
||||
| string |
||||
| Array< |
||||
| { |
||||
key: keyof T; |
||||
operator: '=' | '!='; |
||||
value: string; |
||||
} |
||||
| { |
||||
key: keyof T; |
||||
operator: 'in' | 'notin'; |
||||
value: string[]; |
||||
} |
||||
| { |
||||
key: keyof T; |
||||
operator: '' | '!'; |
||||
} |
||||
>; |
||||
|
||||
export interface ListOptions<T = {}> { |
||||
// continue the list at a given batch
|
||||
continue?: string; |
||||
|
||||
// Query by labels
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
|
||||
labelSelector?: ListOptionsLabelSelector<T>; |
||||
|
||||
// Limit the response count
|
||||
limit?: number; |
||||
} |
||||
|
||||
export interface MetaStatus { |
||||
// Status of the operation. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
|
||||
status: 'Success' | 'Failure'; |
||||
|
||||
// A human-readable description of the status of this operation.
|
||||
message: string; |
||||
|
||||
// Suggested HTTP return code for this status, 0 if not set.
|
||||
code: number; |
||||
|
||||
// A machine-readable description of why this operation is in the "Failure" status.
|
||||
reason?: string; |
||||
|
||||
// Extended data associated with the reason
|
||||
details?: object; |
||||
} |
||||
|
||||
export interface ResourceServer<T = object, K = string> { |
||||
create(obj: ResourceForCreate<T, K>): Promise<void>; |
||||
get(name: string): Promise<Resource<T, K>>; |
||||
list(opts?: ListOptions<T>): Promise<ResourceList<T, K>>; |
||||
update(obj: ResourceForCreate<T, K>): Promise<Resource<T, K>>; |
||||
delete(name: string): Promise<MetaStatus>; |
||||
} |
@ -0,0 +1,32 @@ |
||||
import { Scope, ScopeDashboard } from '@grafana/data'; |
||||
|
||||
import { ScopedResourceServer } from '../apiserver/server'; |
||||
import { ResourceServer } from '../apiserver/types'; |
||||
|
||||
// config.bootData.settings.listDashboardScopesEndpoint || '/apis/scope.grafana.app/v0alpha1/scopedashboards';
|
||||
// config.bootData.settings.listScopesEndpoint || '/apis/scope.grafana.app/v0alpha1/scopes';
|
||||
|
||||
interface ScopeServers { |
||||
scopes: ResourceServer<Scope>; |
||||
dashboards: ResourceServer<ScopeDashboard>; |
||||
} |
||||
|
||||
let instance: ScopeServers | undefined = undefined; |
||||
|
||||
export function getScopeServers() { |
||||
if (!instance) { |
||||
instance = { |
||||
scopes: new ScopedResourceServer<Scope>({ |
||||
group: 'scope.grafana.app', |
||||
version: 'v0alpha1', |
||||
resource: 'scopes', |
||||
}), |
||||
dashboards: new ScopedResourceServer<ScopeDashboard>({ |
||||
group: 'scope.grafana.app', |
||||
version: 'v0alpha1', |
||||
resource: 'scopedashboards', |
||||
}), |
||||
}; |
||||
} |
||||
return instance; |
||||
} |
Loading…
Reference in new issue