Skip to content

Add support for server-side content negotiation (Table and PartialObjectMetadata responses) #7451

@manusa

Description

@manusa

Description

The Kubernetes API supports HTTP requests that return alternative representations of resources via content negotiation using the Accept header. Two key response formats are currently not supported by the Fabric8 Kubernetes Client DSL:

1. Table format (#6823)

Accept: application/json;as=Table;v=v1;g=meta.k8s.io

Returns a meta.k8s.io/v1/Table object with column definitions and rows — the same format kubectl get uses for display. This is useful for tools that need to display resources in a tabular format without downloading the full resource payloads.

2. PartialObjectMetadata (#5716)

Accept: application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1

Returns resources with only the metadata field populated, omitting spec and status. This significantly reduces response sizes when only metadata (labels, annotations, owner references, etc.) is needed.

For list operations:

Accept: application/json;as=PartialObjectMetadataList;g=meta.k8s.io;v=v1

Both features use the same underlying mechanism (Accept header content negotiation) and can share the same infrastructure changes.

Use Cases

  • Memory reduction: A single Pod descriptor can be >50KB. When listing thousands of pods for status monitoring, PartialObjectMetadata drastically reduces memory consumption (Receiving resources as Tables #6823).
  • Tool/UI integration: Table format provides server-defined columns, useful for CLI tools, dashboards, and MCP servers (e.g., kubernetes-mcp-server already uses table format for list output).
  • Efficient informers: PartialObjectMetadata informers could watch large numbers of resources while storing minimal data (complementing the existing ReducedStateItemStore).

Current State and Challenges

The type-safety challenge

The DSL is strongly typed through generics <T, L, R>:

client.pods()                    // → MixedOperation<Pod, PodList, Resource<Pod>>
  .inNamespace("default")        // → NonNamespaceOperation<Pod, PodList, Resource<Pod>>
  .withLabel("app", "web")       // → FilterWatchListDeletable<Pod, PodList, Resource<Pod>>
  .list()                        // → PodList

The list() return type is baked in as L (= PodList). There is no way to change the return type to Table or PartialObjectMetadataList mid-chain without breaking the generic contract.

No Accept header support

Currently, the HTTP requests built by OperationSupport.handleResponse() and BaseOperation.submitList() do not set any Accept header, defaulting to the server's standard JSON response.

Missing model classes

Table, TableColumnDefinition, TableRow, PartialObjectMetadata, and PartialObjectMetadataList do not exist as model classes in the project.

Proposed Approaches

Approach A: Type-switching method on the DSL chain

Add methods that change the return type of subsequent operations:

client.pods().inNamespace("default")
    .asPartialObjectMetadata()   // returns a differently-typed operation
    .list()                      // returns PartialObjectMetadataList

client.pods().inNamespace("default")
    .asTable()
    .list()                      // returns Table

How it works: asPartialObjectMetadata() returns a new Listable<PartialObjectMetadataList> (NOT the full FilterWatchListDeletable). Similarly, asTable() returns Listable<Table>.

Pros: Clean fluent API, type-safe chain.
Cons: Complex generic type changes. asPartialObjectMetadata() on FilterWatchListDeletable<Pod, PodList, Resource<Pod>> must return a different generic type, breaking the chain's type parameter flow. Requires new wrapper classes/interfaces to represent the "type-switched" state.

Approach B: Overloaded list() with response type parameter

client.pods().inNamespace("default")
    .withLabel("app", "web")
    .list(Table.class)               // returns Table

client.pods().inNamespace("default")
    .list(PartialObjectMetadataList.class)  // returns PartialObjectMetadataList

Add a new overload to Listable:

public interface Listable<T> {
    T list();
    <R> R list(Class<R> responseType);  // new
}

Pros: Minimal interface changes, simple to implement.
Cons: Less type-safe. The relationship between the class and the accept header is implicit. Doesn't naturally extend to get(). Users could pass arbitrary classes.

Approach C: New terminal methods on existing interfaces (Recommended)

Add new methods alongside existing list() and resources():

// New interface
public interface MetadataListable {
    Table listAsTable();
    Table listAsTable(ListOptions options);
    PartialObjectMetadataList listAsPartialObjectMetadata();
    PartialObjectMetadataList listAsPartialObjectMetadata(ListOptions options);
}

Mix into existing hierarchy:

public interface FilterWatchListDeletable<T, L, R>
    extends Filterable<...>, Listable<L>, WatchAndWaitable<T>,
    DeletableWithOptions, Informable<T>,
    MetadataListable {  // NEW
}

Usage:

Table table = client.pods().inNamespace("default")
    .withLabel("app", "web")
    .listAsTable();

PartialObjectMetadataList metadata = client.pods().inNamespace("default")
    .listAsPartialObjectMetadata();

For single-resource get (on Resource):

PartialObjectMetadata meta = client.pods().inNamespace("default")
    .withName("mypod")
    .getAsPartialObjectMetadata();

Pros:

  • No generic type changes — return types are fixed (Table, PartialObjectMetadataList)
  • Follows existing patterns — similar to how resources() (Stream<R>) was added alongside list() (L) in FilterWatchListDeletable
  • Incrementally adoptable — ship PartialObjectMetadata first, Table later
  • No breaking changes to existing APIs
  • Clear and explicit method names

Cons:

  • Adds methods to an already broad interface surface
  • Each new response format requires new methods

Detailed Implementation Plan (Approach C)

Phase 1: Infrastructure

1.1 Model classes

Add to kubernetes-model-generator (or kubernetes-client-api as simple POJOs):

Table (meta.k8s.io/v1):

public class Table implements KubernetesResource {
    private String apiVersion;  // meta.k8s.io/v1
    private String kind;        // Table
    private ListMeta metadata;
    private List<TableColumnDefinition> columnDefinitions;
    private List<TableRow> rows;
}

public class TableColumnDefinition {
    private String name;
    private String type;
    private String format;
    private String description;
    private int priority;
}

public class TableRow {
    private List<Object> cells;
    private RawExtension object;  // optional partial object
    // conditions omitted for brevity
}

PartialObjectMetadata (meta.k8s.io/v1):

public class PartialObjectMetadata implements HasMetadata {
    private String apiVersion;
    private String kind;
    private ObjectMeta metadata;
    // No spec, no status - that's the point
}

public class PartialObjectMetadataList extends DefaultKubernetesResourceList<PartialObjectMetadata> {
}

1.2 Accept header constants

public static final String ACCEPT_TABLE_V1 =
    "application/json;as=Table;v=v1;g=meta.k8s.io, " +
    "application/json;as=Table;v=v1beta1;g=meta.k8s.io, " +
    "application/json";

public static final String ACCEPT_PARTIAL_METADATA_V1 =
    "application/json;as=PartialObjectMetadata;v=v1;g=meta.k8s.io";

public static final String ACCEPT_PARTIAL_METADATA_LIST_V1 =
    "application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io";

Phase 2: DSL Interfaces

2.1 New interfaces in kubernetes-client-api

// For list operations
public interface MetadataListable {
    Table listAsTable();
    Table listAsTable(ListOptions options);
    PartialObjectMetadataList listAsPartialObjectMetadata();
    PartialObjectMetadataList listAsPartialObjectMetadata(ListOptions options);
}

// For single-resource get operations
public interface MetadataGettable {
    PartialObjectMetadata getAsPartialObjectMetadata();
}

2.2 Integration points

  • FilterWatchListDeletable extends MetadataListable — available after namespace/filter operations
  • Resource extends MetadataGettable — available for single-resource operations

Phase 3: Implementation in BaseOperation

3.1 New internal method

// In BaseOperation.java
protected <M> CompletableFuture<M> submitListAs(
    ListOptions listOptions, TypeReference<M> typeRef, String acceptHeader) {
    URL fetchListUrl = fetchListUrl(getNamespacedUrl(),
        defaultListOptions(listOptions, null));
    HttpRequest.Builder requestBuilder = withRequestTimeout(
        httpClient.newHttpRequestBuilder()
            .url(fetchListUrl)
            .setHeader("Accept", acceptHeader));
    return handleResponse(httpClient, requestBuilder, typeRef);
}

This mirrors the existing submitList() (line 428) but:

  • Accepts a custom TypeReference instead of using the hardcoded listTypeReference
  • Sets the Accept header on the request builder

3.2 Public implementations

@Override
public Table listAsTable() {
    return listAsTable(new ListOptions());
}

@Override
public Table listAsTable(ListOptions listOptions) {
    try {
        return waitForResult(submitListAs(listOptions,
            new TypeReference<Table>() {},
            ACCEPT_TABLE_V1));
    } catch (IOException e) {
        throw KubernetesClientException.launderThrowable(forOperationType("list"), e);
    }
}

@Override
public PartialObjectMetadataList listAsPartialObjectMetadata() {
    return listAsPartialObjectMetadata(new ListOptions());
}

@Override
public PartialObjectMetadataList listAsPartialObjectMetadata(ListOptions listOptions) {
    try {
        return waitForResult(submitListAs(listOptions,
            new TypeReference<PartialObjectMetadataList>() {},
            ACCEPT_PARTIAL_METADATA_LIST_V1));
    } catch (IOException e) {
        throw KubernetesClientException.launderThrowable(forOperationType("list"), e);
    }
}

Phase 4 (Future): Informer support

The ListerWatcher<T, L> interface currently ties list responses to L. PartialObjectMetadata informers would:

  • Use ACCEPT_PARTIAL_METADATA_LIST_V1 for the initial list
  • Use ACCEPT_PARTIAL_METADATA_V1 for watch events
  • Complement the existing ReducedStateItemStore which already supports reduced-state informers on the storage side

This could be exposed as:

client.pods().informAsPartialObjectMetadata(handler, resyncPeriod);

Key Files Reference

File Role
kubernetes-client-api/.../dsl/Listable.java list() returns L
kubernetes-client-api/.../dsl/Gettable.java get() returns T
kubernetes-client-api/.../dsl/FilterWatchListDeletable.java Combines Filterable + Listable + Watchable; has resources()
kubernetes-client-api/.../dsl/Resource.java Single-resource operations; extends FromServerGettable<T>
kubernetes-client/.../internal/OperationContext.java Carries all operation state
kubernetes-client/.../internal/OperationSupport.java HTTP request/response handling
kubernetes-client/.../internal/BaseOperation.java:428-446 submitList() — builds HTTP request, no Accept header
kubernetes-client/.../internal/BaseOperation.java:791-795 handleGet() — single-resource GET
kubernetes-client/.../internal/BaseOperation.java:1114-1116 resources() — precedent for terminal methods
kubernetes-client/.../informers/cache/ReducedStateItemStore.java Existing reduced-state informer support

Related Issues

Both issues are addressed by this proposal since they share the same underlying mechanism (Accept header content negotiation).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions