Skip to content

Commit

Permalink
Change filtering to push down into the mgmt and k8 services (hawtio#496)
Browse files Browse the repository at this point in the history
* namespace-client.ts
 * Change name of 2nd callback parameter to fullPodCount to make it clear
   this is the total jolokia pods after filtering but before page slicing
 * Adds filtering and sorting getters/setters to affect the pods returned
   by the pod watchers
 * If any change occurs from filtering or sorting then the callback should
   be called in order to notify the UI

* filter.ts
 * Refactors the TypeFilter to be a single class containing both namespace
   and pod name values

* kubernetes-service.ts
 * Add/remove the pod projects to/from the service depending on if there
   are pods returned after filtering

* management-service.ts
 * Add/remove the mgmt pod projects to/from the service depending on if
   there are pods returned after filtering

* Discover context
 * Use only 1 filter rather than an array
 * Breaks out the empty state into its own component for use in other parts
   of the Discover page

* DiscoverProjectContent.ts
 * Needs refresh Effect to reset the pagination index if it is greater than
   the project count returned

* DiscoverToolbar.ts
 * When the buttons are clicked call the management service functions for
   filtering and sorting so that all pod projects down to the k8 service
   are effected.

* discover-service.ts
 * Only responsible for grouping pods rather as filtering is re-assigned
   to other services
  • Loading branch information
phantomjinx committed Sep 25, 2024
1 parent b48aeca commit 4ec8d23
Show file tree
Hide file tree
Showing 17 changed files with 566 additions and 294 deletions.
165 changes: 145 additions & 20 deletions packages/kubernetes-api/src/client/namespace-client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import jsonpath from 'jsonpath'
import { JOLOKIA_PORT_QUERY, KubeObject, KubePod, Paging } from '../globals'
import { JOLOKIA_PORT_QUERY, KubeObject, KubePod, Paging, log } from '../globals'
import { TypeFilter } from '../filter'
import { WatchTypes } from '../model'
import { isObject } from '../utils'
import { SortOrder } from '../sort'
import { Watched, KOptions, ProcessDataCallback } from './globals'
import { clientFactory } from './client-factory'

export type NamespaceClientCallback = (jolokiaPods: KubePod[], jolokiaTotal: number, error?: Error) => void
export type NamespaceClientCallback = (jolokiaPods: KubePod[], fullPodCount: number, error?: Error) => void

export interface Client<T extends KubeObject> {
watched: Watched<T>
Expand All @@ -23,11 +25,17 @@ interface PodWatchers {

export class NamespaceClient implements Paging {
private _current = 0

private _podList: Set<string> = new Set<string>()
private _filteredList: string[] = []
private _notifyChange: boolean = false

private _podWatchers: PodWatchers = {}
private _nsWatcher?: Client<KubePod>
private _refreshing = 0
private _limit = 3
private _sortOrder?: SortOrder
private _typeFilter?: TypeFilter

constructor(
private _namespace: string,
Expand All @@ -44,6 +52,12 @@ export class NamespaceClient implements Paging {
this._callback(this.getJolokiaPods(), this._podList.size, cbError)
}

private isFiltered(): boolean {
if (!this._typeFilter) return false

return !this._typeFilter.filterNS(this._namespace)
}

private initPodOptions(kind: string, name?: string): KOptions {
const podOptions: KOptions = {
kind: kind,
Expand All @@ -58,24 +72,75 @@ export class NamespaceClient implements Paging {
}

private createPodWatchers() {
if (this._podList.size === 0 || this._current >= this._podList.size) return
log.debug(`[NamespaceClient ${this._namespace}]: creating pod watchers`)

if (this._podList.size === 0 || this._current >= this._podList.size) {
log.debug(`[NamespaceClient ${this._namespace}]: no pods in namespace`)
this._callback(this.getJolokiaPods(), 0)
return
}

const sortFn = (a: string, b: string) => {
const order = this._sortOrder === SortOrder.DESC ? -1 : 1
return a.localeCompare(b) * order
}

/*
* Sort the pods according to the supplied sort order
* then selects the block of pods determined by current and limit
*/
log.debug(`[NamespaceClient ${this._namespace}]: sort order: `, this._sortOrder, ' filter: ', this._typeFilter)

let pagedPods: string[]
if (this.isFiltered()) {
// Filtered out by namespace
this._filteredList = []
pagedPods = []
} else {
/*
* Filter out pods based on pod name and then sort
*/
log.debug(`[NamespaceClient ${this._namespace}]: has pods`, this._podList)

this._filteredList = Array.from(this._podList)
.filter(podName => (!this._typeFilter) ? true : this._typeFilter.filterPod(podName))
.sort(sortFn)

const podNames = Array.from(this._podList)
.sort()
.slice(this._current, this._current + this._limit)
log.debug(`[NamespaceClient ${this._namespace}]: pods after filtering`, this._podList)

// Remove watchers for pods not in the slice of the sorted list
if (this._current >= this._filteredList.length) {
// if current is bigger than filtered podNames list then
// reset it back to 0
this._current = 0
}

// Set the paged pods list base on current and limit
pagedPods = this._filteredList
.slice(this._current, this._current + this._limit)
}

log.debug(`[NamespaceClient ${this._namespace}]: pods to be watched`, pagedPods)

/*
* Remove watchers for pods not in the slice of the filtered/sorted list
*/
Object.entries(this._podWatchers)
.filter(([name, _]) => {
return !podNames.includes(name)
})
.filter(([name, _]) => !pagedPods.includes(name))
.forEach(([name, podWatcher]) => {
log.debug(`[NamespaceClient ${this._namespace}]: deleting pod watcher [${name}]`)

clientFactory.destroy(podWatcher.client.watched, podWatcher.client.watch)
delete this._podWatchers[name]
this._notifyChange = true
})

this._refreshing = podNames.length
podNames.forEach(name => {
log.debug(`[NamespaceClient ${this._namespace}]: pod watchers already initialised`, this._podWatchers)

/*
* Create any new pod watchers
*/
this._refreshing = pagedPods.length
pagedPods.forEach(name => {
// Already watching this pod
if (isObject(this._podWatchers[name])) {
this._refreshing--
Expand All @@ -87,17 +152,23 @@ export class NamespaceClient implements Paging {
const _podWatcher = _podClient.watch(podList => {
if (this._refreshing > 0) this._refreshing--

if (podList.length === 0) return

// podList should only contain 1 pod (due to name)
this._podWatchers[name].jolokiaPod = podList[0]
if (podList.length > 0) {
if (this._podWatchers[name]) {
// podList should only contain 1 pod (due to name)
this._podWatchers[name].jolokiaPod = podList[0]
} else {
log.warn(`[NamespaceClient ${this._namespace}]: pod watcher with name ${name} no longer exists yet still watching`)
}
}

if (this._refreshing === 0) {
if (this._refreshing <= 0) {
// Limit callback to final watch returning
this._callback(this.getJolokiaPods(), this._podList.size)
this._callback(this.getJolokiaPods(), this._filteredList.length)
}
})

log.debug(`[NamespaceClient ${this._namespace}]: connecting new pod client`)

/*
* Pod is part of the current page so connect its pod watcher
*/
Expand All @@ -111,6 +182,16 @@ export class NamespaceClient implements Paging {
jolokiaPod: undefined,
}
})

if (pagedPods.length === 0 || this._notifyChange) {
log.debug(`[NamespaceClient ${this._namespace}]: notifying since page is either empty or watcher configuration has changed`)
this._notifyChange = false
this._callback(this.getJolokiaPods(), this._filteredList.length)
}
}

get namespace(): string {
return this._namespace
}

isConnected(): boolean {
Expand All @@ -123,11 +204,21 @@ export class NamespaceClient implements Paging {
this._limit = limit > 1 ? limit : 1
this._current = 0

if (this.isFiltered()) {
/*
* Do not watch since this namespace has been filtered out
* Return no pods and zero total
*/
this._callback([], 0)
return
}

const _nsClient = clientFactory.create<KubePod>(this.initPodOptions(WatchTypes.PODS))
const _nsWatch = _nsClient.watch(pods => {
/*
* Filter out any non-jolokia pods immediately and add
* the applicable pods to the pod name list
* Filter out any non-jolokia pods or
* any pods that do not conform to any pod filters
* immediately and add the applicable pods to the pod name list
*/
const podNames: string[] = []
pods
Expand Down Expand Up @@ -187,6 +278,40 @@ export class NamespaceClient implements Paging {
delete pr.jolokiaPod
}
this._podWatchers = {}
this._podList.clear()
}

filter(typeFilter: TypeFilter) {
this._typeFilter = typeFilter
this._notifyChange = true

/*
* If already connected then recreate the pod watchers
* according to the new type filter
*/
if (this.isConnected()) {
this.createPodWatchers()
}
else if (! this.isFiltered()) {
this.connect(this._limit)
}

/*
* Not connected and does not conform to the
* namespace part of the type filter
*/
}

sort(sortOrder: SortOrder) {
this._sortOrder = sortOrder
this._notifyChange = true

/*
* If already connected then recreate the pod watchers
* according to the new sort order
*/
if (this.isConnected())
this.createPodWatchers()
}

first() {
Expand Down
75 changes: 75 additions & 0 deletions packages/kubernetes-api/src/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { KubePod } from "./globals"

export enum TypeFilterType {
NAME = 'Name',
NAMESPACE = 'Namespace',
}

export function typeFilterTypeValueOf(str: string): TypeFilterType | undefined {
switch (str) {
case TypeFilterType.NAME:
return TypeFilterType.NAME
case TypeFilterType.NAMESPACE:
return TypeFilterType.NAMESPACE
default:
return undefined
}
}

export class TypeFilter {
private _nsValues: Set<string>
private _nameValues: Set<string>

constructor(nsValues?: string[], nameValues?: string[]) {
this._nsValues = new Set(nsValues ?? [])
this._nameValues = new Set(nameValues ?? [])
}

get nsValues(): string[] {
return Array.from(this._nsValues)
}

addNSValue(ns: string) {
this._nsValues.add(ns)
}

deleteNSValue(ns: string) {
this._nsValues.delete(ns)
}

get nameValues(): string[] {
return Array.from(this._nameValues)
}

addNameValue(name: string) {
this._nameValues.add(name)
}

deleteNameValue(name: string) {
this._nameValues.delete(name)
}

filterNS(ns: string): boolean {
if (this._nsValues.size === 0) return true

let resolved = false
this._nsValues.forEach(v => {
if (ns.includes(v))
resolved = true
})

return resolved
}

filterPod(podName: string): boolean {
if (this._nameValues.size === 0) return true

let resolved = false
this._nameValues.forEach(v => {
if (podName.includes(v))
resolved = true
})

return resolved
}
}
2 changes: 1 addition & 1 deletion packages/kubernetes-api/src/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type KubeProject = KubeObject & {
}

export type KubePodsOrError = {
total: number
fullPodCount: number
pods: KubePod[]
error?: Error
}
Expand Down
2 changes: 2 additions & 0 deletions packages/kubernetes-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export async function isK8ApiRegistered(): Promise<boolean> {
}

export * from './globals'
export * from './filter'
export * from './sort'
export { k8Api, k8Service } from './init'
export * from './utils'
Loading

0 comments on commit 4ec8d23

Please sign in to comment.