Skip to content

Commit

Permalink
migrate to polling watcher (remove chokidar) (#2202)
Browse files Browse the repository at this point in the history
  • Loading branch information
holgerkoser authored Nov 28, 2024
1 parent 8524f86 commit 77f60e2
Show file tree
Hide file tree
Showing 20 changed files with 216 additions and 492 deletions.
168 changes: 28 additions & 140 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/kube-config/__tests__/client-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('client-config', () => {
const initClientConfig = signal => {
const config = new Config(kubeconfig)
const reactive = !!signal
clientConfig = new ClientConfig(config, { reactive, signal })
clientConfig = new ClientConfig(config, { reactive, interval: 500, signal })
return reactive
? onceEvent(clientConfig.watcher, 'ready')
: Promise.resolve()
Expand Down
9 changes: 0 additions & 9 deletions packages/kube-config/__tests__/kube-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const fs = require('fs')
const path = require('path')
const os = require('os')
const yaml = require('js-yaml')
const EventEmitter = require('events')
const chokidar = require('chokidar')
const { mockGetToken } = require('gtoken')
const { cloneDeep } = require('lodash')
const Config = require('../lib/Config')
Expand All @@ -33,17 +31,10 @@ describe('kube-config', () => {
const accessToken = 'access-token'

beforeEach(() => {
jest.spyOn(chokidar, 'watch').mockImplementationOnce(() => {
const emitter = new EventEmitter()
emitter.close = jest.fn().mockImplementation(() => Promise.resolve())
process.nextTick(() => emitter.emit('ready'))
return emitter
})
jest.spyOn(fs, 'readFileSync')
})

afterEach(() => {
chokidar.watch.mockRestore()
fs.readFileSync.mockRestore()
})

Expand Down
103 changes: 103 additions & 0 deletions packages/kube-config/__tests__/polling-watcher.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

'use strict'

const fs = require('fs/promises')
const { globalLogger: logger } = require('@gardener-dashboard/logger')
const PollingWatcher = require('../lib/PollingWatcher')

const actualNextTick = jest.requireActual('process').nextTick
const flushPromises = () => new Promise(actualNextTick)

describe('polling-watcher', () => {
beforeAll(() => {
jest.useFakeTimers()
})

afterAll(() => {
jest.useRealTimers()
})

describe('#run', () => {
const filename = '/path/to/file'
const content = 'content'
const interval = 10
let ac
let fn

const createWatcher = (signal = ac.signal) => {
return new PollingWatcher([filename], {
interval,
signal,
})
}

const advanceTimersByOneInterval = async () => {
jest.advanceTimersByTime(interval + 1)
await flushPromises()
}

beforeEach(() => {
ac = new AbortController()
fn = jest.fn()
jest.spyOn(fs, 'readFile').mockResolvedValue(content)
logger.info.mockClear()
logger.error.mockClear()
})

afterEach(() => {
fs.readFile.mockRestore()
ac.abort()
})

it('should read the file content and call the callback function', async () => {
createWatcher().run(fn)
expect(fn).toHaveBeenCalledTimes(0)
await advanceTimersByOneInterval()
expect(fn).toHaveBeenCalledTimes(1)
expect(fn.mock.calls[0]).toEqual([filename, content])
expect(logger.info).toHaveBeenCalledTimes(1)
expect(logger.info.mock.calls[0]).toEqual(['[kube-config] updated content of file `%s`', filename])
})

it('should fail to read file content and not call the callback function', async () => {
const error = new Error('no such file')
fs.readFile.mockRejectedValueOnce(error)
createWatcher().run(fn)
expect(fn).toHaveBeenCalledTimes(0)
await advanceTimersByOneInterval()
expect(fn).toHaveBeenCalledTimes(0)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error.mock.calls[0]).toEqual(['[kube-config] failed to stat file `%s`: %s', filename, error.message])
})

it('should do nothing if abortet before run', async () => {
const watcher = createWatcher()
ac.abort()
watcher.run(fn)
await advanceTimersByOneInterval()
expect(fn).toHaveBeenCalledTimes(0)
})

it('should do nothing if already abortet', async () => {
ac.abort()
const watcher = createWatcher()
watcher.run(fn)
await advanceTimersByOneInterval()
expect(fn).toHaveBeenCalledTimes(0)
})

it('should run a watcher without abort signal', async () => {
const watcher = createWatcher(null)
ac.abort()
watcher.run(fn)
await advanceTimersByOneInterval()
expect(fn).toHaveBeenCalledTimes(1)
watcher.abort()
})
})
})
137 changes: 0 additions & 137 deletions packages/kube-config/__tests__/watcher.test.js

This file was deleted.

14 changes: 9 additions & 5 deletions packages/kube-config/lib/ClientConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

const assert = require('assert').strict
const fs = require('fs')
const Watcher = require('./Watcher')
const Watcher = require('./PollingWatcher')

function getCluster ({ currentCluster }, files) {
const cluster = {}
Expand All @@ -25,7 +25,8 @@ function getCluster ({ currentCluster }, files) {
cluster.certificateAuthority = base64Decode(caData)
} else if (caFile) {
files.set(caFile, 'certificateAuthority')
cluster.certificateAuthority = fs.readFileSync(caFile, 'utf8') // eslint-disable-line security/detect-non-literal-fs-filename
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caFile is not user input
cluster.certificateAuthority = fs.readFileSync(caFile, 'utf8')
}
if (typeof insecureSkipTlsVerify === 'boolean') {
cluster.insecureSkipTlsVerify = insecureSkipTlsVerify
Expand Down Expand Up @@ -55,14 +56,17 @@ function getUser ({ currentUser }, files) {
user.clientKey = base64Decode(keyData)
} else if (certFile && keyFile) {
files.set(certFile, 'clientCert')
user.clientCert = fs.readFileSync(certFile, 'utf8') // eslint-disable-line security/detect-non-literal-fs-filename
// eslint-disable-next-line security/detect-non-literal-fs-filename -- certFile is not user input
user.clientCert = fs.readFileSync(certFile, 'utf8')
files.set(keyFile, 'clientKey')
user.clientKey = fs.readFileSync(keyFile, 'utf8') // eslint-disable-line security/detect-non-literal-fs-filename
// eslint-disable-next-line security/detect-non-literal-fs-filename -- keyFile is not user input
user.clientKey = fs.readFileSync(keyFile, 'utf8')
} else if (token) {
user.token = token
} else if (tokenFile) {
files.set(tokenFile, 'token')
user.token = fs.readFileSync(tokenFile, 'utf8') // eslint-disable-line security/detect-non-literal-fs-filename
// eslint-disable-next-line security/detect-non-literal-fs-filename -- tokenFile is not user input
user.token = fs.readFileSync(tokenFile, 'utf8')
} else if (username && password) {
user.username = username
user.password = password
Expand Down
65 changes: 65 additions & 0 deletions packages/kube-config/lib/PollingWatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
//
// SPDX-License-Identifier: Apache-2.0
//

'use strict'

const fs = require('fs/promises')
const EventEmitter = require('events')
const { globalLogger: logger } = require('@gardener-dashboard/logger')

class PollingWatcher extends EventEmitter {
#paths
#interval
#intervalId
#state = 'initial'

constructor (paths, options) {
super()
this.#paths = paths
this.#interval = options?.interval ?? 300_000
process.nextTick(() => {
this.#state = 'ready'
logger.debug('[kube-config] watcher ready')
this.emit('ready')
})
const signal = options?.signal
if (signal instanceof AbortSignal) {
if (!signal.aborted) {
signal.addEventListener('abort', () => this.abort(), { once: true })
} else {
this.#state = 'aborted'
}
}
}

run (fn) {
if (['initial', 'ready'].includes(this.#state)) {
const tryReadFile = async path => {
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path is not user input
const value = await fs.readFile(path, 'utf8')
logger.info('[kube-config] updated content of file `%s`', path)
fn(path, value)
} catch (err) {
logger.error('[kube-config] failed to stat file `%s`: %s', path, err.message)
}
}
this.#state = 'running'
this.#intervalId = setInterval(() => {
for (const path of this.#paths) {
tryReadFile(path)
}
}, this.#interval)
}
}

abort () {
this.#state = 'aborted'
clearInterval(this.#intervalId)
}
}

module.exports = PollingWatcher
Loading

0 comments on commit 77f60e2

Please sign in to comment.