fabric8

The Fabric8 Kubernetes client has been simplifying Java developers' use of Kubernetes for several years. The 6.0.0 release represents a major body of work spanning about five months of effort in both the core of the project and in related utilities.

This article takes a look at new features and other important changes in the Fabric8 Kubernetes client, including:

  • New HTTP clients
  • Java generator
  • Resource API
  • API refinements
  • Kubernetes testing improvements

How to get the new Fabric8 Java client

You can find the most current Fabric8 Java client release on Maven Central. To start using the new client, add it as a dependency in your Maven pom.xml or Gradle build file. For Kubernetes, the dependency is:

<dependency>
  <groupId>io.fabric8</groupId>
  <artifactId>kubernetes-client</artifactId>
  <version>6.0.0</version>
</dependency>

Or for Red Hat OpenShift:

<dependency>
  <groupId>io.fabric8</groupId>
  <artifactId>openshift-client</artifactId>
  <version>6.0.0</version>
</dependency>

The sections below on the new HTTP clients API refinements will help you understand the new choices you have in your Fabric8 client dependencies.

And for your future Fabric8 Kubernetes client needs, please use the snapshot releases being published to Maven Central.

New features

The feature work of the Fabric8 client has been driven by a diverse set of needs with an eye towards compatibility and continuity for existing users. So while the changelog and migration guide may be lengthy, we expect that your upgrade process will be mostly seamless.

New HTTP clients

With some refinements to the HTTP client API introduced in 5.x, we were able to create a JDK and an Eclipse Jetty client implementation. A Vert.x client is still in the works.

This means you can choose the client that is best suited to your needs—which could be one that is already being used in your project. You won't be locked into additional OkHttp dependencies unless you want to be.

Since the abstraction layer and alternative client implementations are new, please let us know if you encounter any issues with them. You should also check the repo for known issues with the respective implementation—the JDK readme, for example.

The FAQ covers how to configure your project for a given HTTP client, along with several other topics. In short, you should exclude the kubernetes-httpclient-okhttp dependency and add a runtime dependency to the desired HTTP client: kubernetes-httpclient-jetty or kubernetes-httpclient-jdk.

If you need to customize the configuration of the client, you should add a compile time reference to the respective HTTP client—kubernetes-httpclient-xxx—and extend its HttpClient.Factory: JettyHttpClientFactory, JdkHttpClientFactory, and OkHttpClientFactory.

Java generator

Kubernetes is still a world dominated by Go tools and operators. When using the kubernetes-client, you often have to deal with externally defined (and generated) Custom Resource Definitions (CRDs). So far, the extensions available for Fabric8 have partially closed the gap by offering typed Java representations for some common use cases. In the 6.x release, we are introducing a full blown Java generator capable of automatically extracting fully typed Java representations from arbitrary YAML/JSON CRDs. You can check some examples of the generated code here.

The tool can generate Java classes annotated with lombok and sundrio annotations in order to provide a seamless API and user experience when using those.

Whether you want to easily and quickly interact with Java code with an external CRD, or you want to go down the path of full contract first development in your Operator, the Java generator is the instrument you are looking for.

Eager to start? You can find detailed instructions on how to use it in the official documentation.

Resource API

Most operations are performed on a resource. The Fabric8 5.x APIs could make dealing with existing KubernetesResource objects a little cumbersome when it came to obtaining the resource. It was quite common to do something like this:

// obtain an existing configMap somehow
ConfigMap configMap = ...
...
Resource<ConfigMap> resource = client.configMaps().inNamespace(configMap.getMetadata().getNamespace()).withName(configMap.getMetadata().getName());
resource.delete(); // or some other operation

In 6.0, several issues have been addressed, including #3407 and #3973, that make it possible to obtain the Resource directly:

Resource<ConfigMap> resource = client.resource(configMap);
// or used fluently
client.resource(configMap).delete();

There are other places to directly get resources. For instance, all of the following return a Stream<Resource>:

client.configMaps().resources(); 
client.configMaps().withLabel("x").resources();
client.resourceList(...).resources(); 

Using any of these, you can implement composite operations easily with lambdas:

client.secrets().resources().forEach(Resource::delete);

Related to these changes are the cleanup and deprecation of many common operations. For example, imagine a scenario where you used both withName() and a resource to perform an operation:

client.configMaps().withName("x").replace(configMap);

This was potentially confusing in that there was an extra check to make sure that the name specified in withName() matched the name of the KubernetesResource passed into replace(). With 6.0, you should instead perform the operations on the resource:

// if you need a specific resource type
client.configMaps().resource(configMap).replace();

// or if it can be a general Resource
client.resource(configMap).replace();

API refinements

The 6.0 update has gone went beyond simply adding a few new things into the domain-specific language (DSL). The DSL has now been split from the implementation and has been cleaned out of anything that seemed unnecessary.

The 5.x client and prior versions had the primary API and implementation as a single kubernetes-client module/jar. While convenient, it did not offer a clear separation of the API from internal classes, and exposed the consuming applications compile time classpath to additional dependencies (such as OkHttp). A refactoring was undertaken to split the client into a separate API and implementation module. You may still continue to use just a reference to the kubernetes-client if you wish, but you now also have the option of having a compile dependency on kubernetes-client-api with a runtime dependency on kubernetes-client.

This split also required us to make direct use of the default clients; DefaultKubernetesClient and DefaultOpenShiftClient have been deprecated. You should transition to using the KubernetesClientBuilder instead, which you will be required to use if you have just the API module as a compile time dependency.

This refactoring allowed us to clean up some of the details of how the client extensions are implemented. They now only have a compile dependency on the kubernetes-client-api, so you may notice a few changes if you use or further extend an extension such as Camel K, Istio, cert-manager, etc.

Finally, we took this opportunity to remove or collapse a lot of the interfaces that were used to make up the DSL. Most users, who are using fluent style calls, won't notice these changes as the operations are still there. However, if you created an intermediate variable at some point in the method chain, that particular class may no longer exist; for example, EditReplacePatchDeleteble is no more. In that case, you'll have to update the type or use var instead.

Kubernetes testing improvements

The integration testing utilities used by the Fabric8 project were refined and exposed for general use as a JUnit 5 extension in a new kubernetes-junit-jupiter artifact. The new module offers a highly declarative set of annotations that you can use to simplify your project's test setup and execution against an actual Kuberentes cluster.

As described in issued #4054, this module allows you to configure your tests like so:

@KubernetesTest
@LoadKubernetesManifests("/test-data.yml")
@RequireK8sSupport(io.fabric8.knative.serving.v1.Service.class) // Optionally require support for a resource  (test env should be reliable)
@RequireK8sVersionAtLeast(majorVersion = 1, minorVersion = 16) // Optionally require a specific K8s version
class MyTest {
  KubernetesClient client;
  // …
}

The extension takes care of creating a temporary namespace and injecting a KubernetesClient instance preconfigured to use that namespace. In addition, the new extension provides an annotation to load and delete Kubernetes manifests before and after the test suite execution, along with two annotations to restrict the test execution to specific cluster versions, or support for specific Kubernetes resources.

Unit testing using the Kubernetes Mock server was also improved:

  • You no longer need extension-specific modules such as tekton-tests. Instead, you may directly reference the client type you need as a member variable. The appropriate instance will be initialized:
@EnableKubernetesMockClient
Class MyTest {
  TektonClient client;
  // …
}
  • Issue #3966: The KubernetesMockServer has new methods— reset() and unsupported()— to reset its state and control what APIs are unsupported. The reset() method is especially useful in crud mode to quickly clean out the mock server resource state in between tests.
  • Issue #3758: VersionInfo in KubernetesMockServer can be overridden.

Other improvements

  • Issue #3334: Added basic support for server-side apply: patch(PatchContext.of(PatchType.SERVER_SIDE_APPLY), service).

  • Issue #3625: Added default maps in generated models, mostly to prevent the need for null checks on things like annotations and labels.

  • Informer improvements:

    • Issue #3968: SharedIndexInformer.initialState can be used to set the store state before the informer starts.
    • SharedIndexInformer allows for the addition and removal of indexes even after starting, and you can remove the default namespace index if you wish.
    • Store.getKey() can be used rather than directly referencing static cache functions.
    • Issues #3472 and #3587: Customization has been opened up for the Informer store/cache key function and the way in which state is stored. See BasicItemStore, ReducedStateItemStore, and the SharedIndexInformer.itemStore() function.
  • Issue #3922: Added Client.supports() and Client.hasApiGroup() methods for API server metadata introspection.

Deprecations and other important changes

Since this is a major release there was quite a bit of legacy removed or deprecated, and there are various breaking changes. This section will cover some of the highlights, but please see the migration guide for a full list.

  • To match the behavior of kubectl, the client will now consider any call to inNamespace as the namespace to use regardless of what is on a passed in item. Only if the client is left at the default namespace or a call has been made to inAnyNamespace will the item namespace be used. This applies to all calls to inNamespace at the client, operation, or resource level, and for all operations (load, create, delete, withItem, etc.).

  • The backwards compatibility interceptor is now disabled by default.

  • The apiVersion on a resource being deserialized is now required.

  • Deleting a collection is now implemented using a single delete call, rather than for each item. When the collection is namespaced and inAnyNamespace is used, a call will be made to first determine the affected namespaces, and then a collection delete issued against each namespace. The result of the delete calls will be a list of StatusDetails rather than a boolean value. A best effort is made to process the response from the server to populate the items that are deleted. This information is generally useful if you wish to implement some kind of blocking delete behavior—that is, if you want to ensure the returned resources (based upon a matching UID) have been deleted.

  • delete(List<T>) and delete(T[]) returning boolean have been deprecated. They will always return TRUE, and 404s on individual items will simply be ignored.

  • Client.isAdaptable() and Client.adapt() will check first if the existing client is an instance of the desired type. Client.adapt() will no longer perform the isAdaptable() check—that is, you may freely adapt from one client to another as long as the extension exists. If you need to make a specific check of support, please use the Client.supports() method.

  • Evictable.evict() will throw an exception rather than returning FALSE if the pod is not found. This ensures that FALSE strictly means that the eviction failed.

  • The usage of piped streams is no longer supported—they make assumptions about reading and writing threads, which the client no longer honors. They should not be passed into the methods accepting InputStreams and OutputStreams.
    ContainerResource.writingInput(PipedOutputStream in) and readingXXX(PipedInputStream out) have been removed—use the redirecting methods instead.

  • TtyExecErrorChannelable methods have been deprecated in favor of ExecWatch.exitCode() and ExecListener.onExit().
    ContainerResource.readingInput(InputStream in) has been deprecated—use redirectingInput() instead.

Learn more about Fabric8

Fabric8's development team consists mostly of Java developers, so a Java developer's perspective heavily influences this client. If you want to work with us, please don't hesitate to join our community. There are a few ways to get involved with the development of the Fabric8 Kubernetes Java client:

Comments