Client SDK specification
Client SDKs are libraries developers use to integrate their code with Kessel over a network. These are valuable. They help developers integrate easily by abstracting the complexity of remote procedure calls. They simplify the jobs of operators, who can expect more consistent and resilient client behavior.
However, creating them comes at a steep cost because they have to be maintained for as many languages as we support. To reduce this cost, client SDKs should be developed with utmost consistency in mind, from language to language. This consistency in turn also benefits the developer experience: each client SDK, well designed, is easier to understand across multiple languages.
This document outlines the standard which we expect all client SDKs to follow. If you are contributing to a client SDK, please follow this standard. If you wish to introduce a new API to the clients, please first update this standard with multiple languages in mind. Then, implement the new API in the language(s) you need.
Specification
Section titled “Specification”- Libraries SHOULD be scoped to Kessel, not to an individual service. Consider libraries “Kessel SDKs” for a given language, more than a client of a specific API.
- This decouples developers, to some extent, to the architecture of Kessel, allowing the possibility to evolve without impacting their dependencies or client API.
- It allows libraries to coordinate among Kessel components where relevant (e.g. between RBAC & Inventory).
Protocols
Section titled “Protocols”- Our primary protocol is gRPC, but it is not required
- For gRPC services, HTTP REST should be supported, but not the default, for client libraries
- HTTP may not support all of the features of the corresponding gRPC API, e.g. streaming endpoints.
Client generation
Section titled “Client generation”- Client “stubs” SHOULD be generated from API specifications. Use buf tooling for gRPC APIs. Use openapi-generator tooling for HTTP APIs.
- If relevant (e.g. Python, Typescript), interface definitions should be generated
- Generation MUST be repeatable across different builds and developer machines. For example, plugins should be pulled in remotely, with pinned versions, so we do not rely on local environment setup which may differ from machine to machine.
Dependencies
Section titled “Dependencies”- Clients SHOULD minimize dependencies while balancing minimum usability
- Required protocol dependencies SHOULD be transitively included. This includes anything for the client to work in the most basic sense, without considering any middleware like authentication etc. For example, this should generally only be gRPC or protobuf specific dependencies.
- Dependencies not required for basic communication SHOULD NOT be transitively included.
- Specifically “vendored” packages are an exception. E.g. using ‘kessel-client-spring’ or ‘kessel-client-quarkus’ for a library that builds on top of the base client with Spring or Quarkus integrations is perfectly acceptable (and encouraged, for vendor specific integrations).
- If a package is not dedicated to a specific vendor or integration, the dependency SHOULD be optional. For example, if OAuth requires a third party library, then don’t cause the third party library to be pulled in transitively (unless, again, the package is an alternative Kessel library explicitly named for including this dependency). The developer can include it if they need OAuth for their application.
- Some exceptions can be commonly used libraries designed for transitive import, such as the google-auth-* libraries for various languages in support of gRPC, or cryptography libraries, etc. These SHOULD be used as a base (e.g. python, java) for authentication middleware and SHOULD be included transitively where relevant.
- Any non-optional dependencies included transitively MUST be designed to be a library dependency. That means:
- It MUST follow a well documented versioning scheme which distinguishes between compatible and breaking changes (e.g. SemVer)
- It MUST be explicitly developed with the expectation of being depended on as a library.
- We MUST stay up to date to avoid CVE exposure and compatibility issues.
- Clients MUST NOT depend on the server, and servers MUST NOT depend on client libraries, to prevent cyclic dependencies.
Client API
Section titled “Client API”- No additional abstractions are required beyond what is generated by gRPC
- Utility methods SHOULD be included to instantiate channels with best practices or convenient settings
- e.g. to include the auth method, keep alive settings
- Configuration should use common names, and common file formats (if applicable)
- Reusable middleware or replication abstractions SHOULD be included
- Utilities for creating common RBAC resource types SHOULD be included
- Class, method names, and import hierarchy SHOULD be consistent from language to language. Before adding to or changing the API, it is strongly recommended to document the specification of this API, or make a reference implementation.
Imports
Section titled “Imports”Most languages have some concept of a module or package hierarchy for imports, usually derived from the directory structure, file path, repository, etc. For the purpose of this spec we will use the term “package.”
The prefix
of these imports may be language specific but MUST at least include “kessel” somewhere. It can simply be “kessel” unless that would violate language convention (e.g. Java or Go). Prefix SHOULD NOT include a specific service given the scope of libraries is not service specific. The prefix MUST be the same for all imports.
The suffixes of these imports SHOULD be consistent from language to language (aside from language-specific delimiters). These packages are documented in detail following the links to API reference:
{prefix}.{service}.{major_version}
: Package for code specific to service and API version, where{service}
is the separately versioned service (e.g. “inventory” or “rbac”) and{major_version}
is the major revision of the API (e.g. v1beta2, v1, v2), such as generated client code.{prefix}.grpc
: Package for utility methods or middleware specific to gRPC and are therefore only coupled to gRPC versions, not to a service API version (e.g. generic gRPC authentication middleware code goes here).{prefix}.http
: Package for utility methods or middleware specific to HTTP (whatever the library used) and therefore coupled to that library (or language runtime), not to a specific API version.{prefix}.middleware
: Package for reusable middleware or utilities not specific to transport (e.g. gRPC) or API version.
Occasionally, a piece of code is dependent on multiple boundaries (such as RBAC and a particular API version, or gRPC and a particular integration). In these cases, use some language specific means to further delineate (such as sub-packages, files, classes, etc).
Defaults (across all languages)
Section titled “Defaults (across all languages)”- Deadlines, retries, load balancing, and other network level behavior MUST NOT be specified by the client defaults, and instead defined by the server and discovered by the client through service config. This keeps each client consistent, and allows this to be managed centrally.
- TODO: We may want to revisit this since we cannot easily add TXT records in all contexts
- One possible exception is HTTP/2 Keepalive. This requires explicitly configuring the channel or stub on the client and is not available through service config.
Exceptions
Section titled “Exceptions”- If relevant for the language, Clients SHOULD each define their own top-level exception or error type which wraps errors which are NOT thrown by gRPC
- gRPC exceptions SHOULD NOT be wrapped
Object lifecycle
Section titled “Object lifecycle”- The lifecycle of stubs and channels SHOULD be managed by the application (e.g. through the application’s DI container)
- Library and website documentation SHOULD instruct developers to reuse Channels and Stubs, per gRPC performance best practices. Library abstractions SHOULD guide users to best practices like these.
- We MAY provide convenience facilities for this, but these are not required. Dependency rules apply. Possible approaches include:
- Basic, zero-dependency globals accessible via the
ClientBuilder
, if acacheKey
is provided. This should either create a channel or stub with the provided configuration, or retrieve a cached one using the same key value. It is assumed that the same key is always used with the same configuration. - Integrations with popular DI containers (e.g. Spring, CDI), either in separate branded libraries or with optional dependencies.
- Basic, zero-dependency globals accessible via the
Releases and versioning
Section titled “Releases and versioning”- Each client library MUST be versioned independently, following SemVer and the conventions of that language’s packaging ecosystem. This is because there is no one server or API version to correlate them too, and languages may have their own reasons for changes (e.g. CVE, API change, etc).
- Each client library’s release MUST be published to the central registry for that language
Updating this specification
Section titled “Updating this specification”This is a living specification, expected to change over time. To ensure updates are implementable and maintainable, it is necessary to FIRST prototype the desired updates in at least one (ideally two) SDKs to get an understanding of what contract will actually work in practice. Once satisified, THEN propose an update to the spec, which may undergo revision both in the specification and prototypes. The final implementation then follows the refined spec, and may or may not be based off of the prototypes.