Skip to content

Conversation

@Rezrazi
Copy link

@Rezrazi Rezrazi commented Jan 8, 2026

This pull request introduces a new WatchApi class for watching Kubernetes resources using async iterators, along with comprehensive tests and supporting changes. The new implementation is designed to integrate seamlessly with the existing API client infrastructure, supporting both streaming and text-based HTTP responses, and is fully type-safe. Current implementation was limited as it relied on a direct call to node-fetch, closing the opportunity to use wrapHttpLibrary.
The most important changes are:

New Watch API Implementation:

  • Added a new WatchApi class in src/watch_api.ts that provides an async iterator interface for consuming Kubernetes watch events. It supports both streaming and text fallback modes, allows passing arbitrary query parameters, and is compatible with custom HTTP libraries. This class also introduces the WatchEvent and WatchEventType types for strong typing of events.

  • Updated the ResponseBody interface in src/gen/http/http.ts to optionally include a stream() method, enabling proper support for streaming watch responses.

Exports and API Surface:

  • Exported the new WatchApi from src/index.ts to make it available as part of the public API.
  • Original watch implementation remains untouched and available to existing consumers.

Testing:

  • Added a comprehensive test suite in src/watch_api_test.ts covering construction, event iteration (both streaming and text), handling of all event types (ADDED, MODIFIED, DELETED, BOOKMARK, ERROR), error handling, query parameter passing, empty responses, custom HTTP libraries, and type safety.

Examples

Basic example

const kubeConfig = new KubeConfig();

kubeConfig.loadFromDefault();

const watchApi = kubeConfig.makeApiClient(WatchApi);

for await (const event of watchApi.watch<V1Pod>('/api/v1/namespaces/default/pods')) {
  console.log(`${event.type}: ${event.object.metadata?.name}`);
}

/**
 * ➜ bun run.ts
 * ADDED: helm-controller-744dd856d8-gs74t
 * ADDED: kustomize-controller-5b4ffc8554-v2bgz
 * MODIFIED: kustomize-controller-5b4ffc8554-v2bgz
 * DELETED: kustomize-controller-5b4ffc8554-v2bgz
 * MODIFIED: kustomize-controller-5b4ffc8554-tcfhg
 */

Custom httpapi (using ky as an http client)

import {
    createConfiguration,
    KubeConfig,
    ResponseContext,
    ServerConfiguration,
    V1Pod,
    WatchApi,
    wrapHttpLibrary,
} from './src/index.js';
import https from 'node:https';
import ky from 'ky';
import { Readable } from 'node:stream';

const httpApi = wrapHttpLibrary({
    async send(request) {
        const agent = request.getAgent() as https.Agent;

        const response = await ky(request.getUrl(), {
            method: request.getHttpMethod(),
            headers: request.getHeaders(),
            body: request.getBody(),
            hooks: {
                beforeRequest: [
                    (req) => {
                        // custom options to make it compatible with Bun (relevant just for the example)
                        req.tls = {
                            ca: agent?.options.ca,
                            cert: agent.options.cert,
                            key: agent.options.key,
                        };
                    },
                ],
            },
        });

        return new ResponseContext(response.status, Object.fromEntries(response.headers.entries()), {
            text: () => response.text(),
            binary: async () => Buffer.from(await response.arrayBuffer()),
            stream: () => (response.body ? Readable.fromWeb(response.body) : Readable.from([])),
        });
    },
});

const kc = new KubeConfig();
kc.loadFromDefault();

const configuration = createConfiguration({
    baseServer: new ServerConfiguration(kc.getCurrentCluster()!.server, {}),
    authMethods: { default: kc },
    httpApi,
});

const watchApi = new WatchApi(configuration);

for await (const event of watchApi.watch<V1Pod>('/api/v1/namespaces/flux-system/pods')) {
    console.log(`${event.type}: ${event.object.metadata?.name}`);
}

Copilot AI review requested due to automatic review settings January 8, 2026 21:44
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Rezrazi
Once this PR has been reviewed and has the lgtm label, please assign brendandburns for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Jan 8, 2026
@k8s-ci-robot
Copy link
Contributor

Welcome @Rezrazi!

It looks like this is your first PR to kubernetes-client/javascript 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.

You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.

You can also check if kubernetes-client/javascript has its own contribution guidelines.

You may want to refer to our testing guide if you run into trouble with your tests not passing.

If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!

Thank you, and welcome to Kubernetes. 😃

@k8s-ci-robot k8s-ci-robot added the size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. label Jan 8, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces a new WatchApi class that provides an async iterator interface for consuming Kubernetes watch events, designed to integrate with the existing API client infrastructure while supporting both streaming and text-based HTTP responses.

  • Adds WatchApi class with async iterator support for watching Kubernetes resources
  • Extends ResponseBody interface to support optional streaming via stream() method
  • Includes comprehensive test suite covering all watch event types, error conditions, and streaming modes

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/watch_api.ts New WatchApi class implementing async iterator pattern for Kubernetes watch with support for streaming and text fallback modes
src/watch_api_test.ts Comprehensive test suite covering event iteration, error handling, query parameters, custom HTTP libraries, and type safety
src/index.ts Exports WatchApi to make it available as part of the public API
src/gen/http/http.ts Extends ResponseBody interface with optional stream() method to enable streaming support

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +198 to +204
const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);

requestContext.setSignal(AbortSignal.any([controller.signal, timeoutSignal]));

try {
const response = await this.configuration.httpApi.send(requestContext).toPromise();

Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout signal is applied to the entire watch operation including the streaming phase, which will cause the connection to abort after 30 seconds (the default timeout) even if data is actively being received. For long-lived watch connections that remain open for extended periods, this timeout should only apply to the initial connection establishment, not the entire stream duration. Consider using a pattern similar to other streaming implementations where the timeout is only applied until the first response is received, then cleared or replaced with an idle timeout.

Suggested change
const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
requestContext.setSignal(AbortSignal.any([controller.signal, timeoutSignal]));
try {
const response = await this.configuration.httpApi.send(requestContext).toPromise();
// Apply the timeout only to the initial request establishment, not the entire stream.
requestContext.setSignal(controller.signal);
const connectionTimeoutId = setTimeout(() => {
controller.abort();
}, this.requestTimeoutMs);
try {
const response = await this.configuration.httpApi.send(requestContext).toPromise();
clearTimeout(connectionTimeoutId);

Copilot uses AI. Check for mistakes.
await rejects(
async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const event of watchApi.watch('/api/v1/namespaces/default/pods')) {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable event.

Suggested change
for await (const event of watchApi.watch('/api/v1/namespaces/default/pods')) {
for await (const _event of watchApi.watch('/api/v1/namespaces/default/pods')) {

Copilot uses AI. Check for mistakes.
const watchApi = new WatchApi(config);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const event of watchApi.watch('/api/v1/namespaces/default/pods', {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable event.

Copilot uses AI. Check for mistakes.
async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const event of watchApi.watch('/api/v1/namespaces/default/pods')) {
// Should not reach here
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For loop variable event is not used in the loop body.

Suggested change
// Should not reach here
throw new Error('Should not reach here, received event: ' + JSON.stringify(event));

Copilot uses AI. Check for mistakes.
const watchApi = new WatchApi(config);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const event of watchApi.watch('/api/v1/namespaces/default/pods', {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For loop variable event is not used in the loop body.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants