This guide describes how developers can write new extensions for Jikkou.
More information:
This is the multi-page printable view of this section. Click here to print.
This guide describes how developers can write new extensions for Jikkou.
More information:
You can extend Jikkou’s capabilities by developing custom extensions and resources.
An extension must be developed in Java and packaged as a tarball or ZIP archive. The archive must contain a single top-level directory containing the extension JAR files, as well as any resource files or third party libraries required by your extensions. An alternative approach is to create an uber-JAR that contains all the extension’s JAR files and other resource files needed.
An extension package is more commonly described as an Extension Provider.
Jikkou’s sources are available on Maven Central
To start developing custom extension for Jikkou, simply add the Core library to your project’s dependencies.
For Maven:
<dependency>
<groupId>io.streamthoughts</groupId>
<artifactId>jikkou-core</artifactId>
<version>${jikkou.version}</version>
</dependency>
For Gradle:
implementation group: 'io.streamthoughts', name: 'jikkou-core', version: ${jikkou.version}
Jikkou uses the standard Java ServiceLoader
mechanism to discover and registers custom extensions and resources. For this, you will need to the implement
the Service Provider Interface: io.streamthoughts.jikkou.spi.ExtensionProvider
/**
* <pre>
* Service interface for registering extensions and resources to Jikkou at runtime.
* The implementations are discovered using the standard Java {@link java.util.ServiceLoader} mechanism.
*
* Hence, the fully qualified name of the extension classes that implement the {@link ExtensionProvider}
* interface must be added to a {@code META-INF/services/io.streamthoughts.jikkou.spi.ExtensionProvider} file.
* </pre>
*/
public interface ExtensionProvider extends HasName, Configurable {
/**
* Registers the extensions for this provider.
*
* @param registry The ExtensionRegistry.
*/
void registerExtensions(@NotNull ExtensionRegistry registry);
/**
* Registers the resources for this provider.
*
* @param registry The ResourceRegistry.
*/
void registerResources(@NotNull ResourceRegistry registry);
}
If you are using Maven as project management tool, we recommended to use the Apache Maven Assembly Plugin to package your extensions as a tarball or ZIP archive.
Simply create an assembly descriptor in your project as follows:
src/main/assembly/package.xml
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>package</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>${organization.name}-${project.artifactId}/doc</outputDirectory>
<includes>
<include>README*</include>
<include>LICENSE*</include>
<include>NOTICE*</include>
</includes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>${organization.name}-${project.artifactId}/lib</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<useTransitiveFiltering>true</useTransitiveFiltering>
<unpack>false</unpack>
<excludes>
<exclude>io.streamthoughts:jikkou-core</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
Then, configure the maven-assembly-plugin
in the pom.xml
file of your project:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<finalName>${organization.name}-${project.artifactId}-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/assembly/package.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
<execution>
<id>test-make-assembly</id>
<phase>pre-integration-test</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Finally, use the mvn clean package
to build your project and create the archive.
To install an Extension Provider, all you need to do is to unpacks the archive into a desired location (
e.g., /usr/share/jikkou-extensions
).
Also, you should ensure that the archive’s top-level directory name is unique, to prevent overwriting existing files or
extensions.
Custom extensions can be supplied to the Jikkou’s API Server and Jikkou CLI (when running the Java Binary Distribution,
i.e., not the native version). For this, you simply need to configure the jikkou.extension.paths
property. The
property accepts a list of paths from which to load extension providers.
Example for the Jikkou API Server:
# application.yaml
jikkou:
extension.paths:
- /usr/share/jikkou-extensions
Once your extensions are configured you should be able to list your extensions using either :
jikkou api-extensions list
command, orGET /apis/core.jikkou.io/v1/extensions -H "Accept: application/json"
This section covers the core classes to develop validation extensions.
To create a custom validation
, you will need to implement the Java
interface: io.streamthoughts.jikkou.core.validation.Validation
.
This interface defines two methods, with a default implementation for each, to give you the option of validating either all resources accepted by validation at once, or each resource one by one.
public interface Validation<T extends HasMetadata> extends Interceptor {
/**
* Validates the specified resource list.
*
* @param resources The list of resources to be validated.
* @return The ValidationResult.
*/
default ValidationResult validate(@NotNull final List<T> resources) {
// code omitted for clarity
}
/**
* Validates the specified resource.
*
* @param resource The resource to be validated.
* @return The ValidationResult.
*/
default ValidationResult validate(@NotNull final T resource) {
// code omitted for clarity
}
}
The validation class below shows how to validate that any resource has a specific non-empty label.
@Title("HasNonEmptyLabelValidation allows validating that resources have a non empty label.")
@Description("This validation can be used to ensure that all resources are associated to a specific label. The labe key is passed through the configuration of the extension.")
@Example(
title = "Validate that resources have a non-empty label with key 'owner'.",
full = true,
code = {"""
validations:
- name: "resourceMustHaveNonEmptyLabelOwner"
type: "com.example.jikkou.validation.HasNonEmptyLabelValidation"
priority: 100
config:
key: owner
"""
}
)
@SupportedResources(value = {}) // an empty list implies that the extension supports any resource-type
public final class HasNonEmptyLabelValidation implements Validation {
// The required config property.
static final ConfigProperty<String> LABEL_KEY_CONFIG = ConfigProperty.ofString("key");
private String key;
/**
* Empty constructor - required.
*/
public HasNonEmptyLabelValidation() {
}
/**
* {@inheritDoc}
*/
@Override
public void configure(@NotNull final Configuration config) {
// Get the key from the configuration.
this.key = LABEL_KEY_CONFIG
.getOptional(config)
.orElseThrow(() -> new ConfigException(
String.format("The '%s' configuration property is required for %s",
LABEL_KEY_CONFIG.key(),
TopicNamePrefixValidation.class.getSimpleName()
)
));
}
/**
* {@inheritDoc}
*/
@Override
public ValidationResult validate(final @NotNull HasMetadata resource) {
Optional<String> label = resource.getMetadata()
.findLabelByKey(this.key)
.map(NamedValue::getValue)
.map(Value::asString)
.filter(String::isEmpty);
// Failure
if (label.isEmpty()) {
String error = String.format(
"Resource for name '%s' have no defined or empty label for key: '%s'",
resource.getMetadata().getName(),
this.key
);
return ValidationResult.failure(new ValidationError(getName(), resource, error));
}
// Success
return ValidationResult.success();
}
}
This section covers the core classes to develop action extensions.
To create a custom action
, you will need to implement the Java
interface: io.streamthoughts.jikkou.core.action.Action
.
/**
* Interface for executing a one-shot action on a specific type of resources.
*
* @param <T> The type of the resource.
*/
@Category(ExtensionCategory.ACTION)
public interface Action<T extends HasMetadata> extends HasMetadataAcceptable, Extension {
/**
* Executes the action.
*
* @param configuration The configuration
* @return The ExecutionResultSet
*/
@NotNull ExecutionResultSet<T> execute(@NotNull Configuration configuration);
}
The Action
class below shows how to implement a custom action accepting options`.
@Named(EchoAction.NAME)
@Title("Print the input.")
@Description("The EchoAction allows printing the text provided in input.")
@ExtensionSpec(
options = {
@ExtensionOptionSpec(
name = INPUT_CONFIG_NAME,
description = "The input text to print.",
type = String.class,
required = true
)
}
)
public final class EchoAction extends ContextualExtension implements Action<HasMetadata> {
public static final String NAME = "EchoAction";
public static final String INPUT_CONFIG_NAME = "input";
@Override
public @NotNull ExecutionResultSet<HasMetadata> execute(@NotNull Configuration configuration) {
String input = extensionContext().<String>configProperty(INPUT_CONFIG_NAME).get(configuration);
return ExecutionResultSet
.newBuilder()
.result(ExecutionResult
.newBuilder()
.status(ExecutionStatus.SUCCEEDED)
.data(new EchoOut(input))
.build())
.build();
}
@Kind("EchoOutput")
@ApiVersion("core.jikkou.io/v1")
@Reflectable
record EchoOut(@JsonProperty("out") String out) implements HasMetadata {
@Override
public ObjectMeta getMetadata() {
return new ObjectMeta();
}
@Override
public HasMetadata withMetadata(ObjectMeta objectMeta) {
throw new UnsupportedOperationException();
}
}
}
This section covers the core classes to develop transformation extensions.
To create a custom transformation
, you will need to implement the Java
interface: io.streamthoughts.jikkou.core.transformation.Transformation
.
/**
* This interface is used to transform or filter resources.
*
* @param <T> The resource type supported by the transformation.
*/
public interface Transformation<T extends HasMetadata> extends Interceptor {
/**
* Executes the transformation on the specified {@link HasMetadata} object.
*
* @param resource The {@link HasMetadata} to be transformed.
* @param resources The {@link ResourceListObject} involved in the current operation.
* @param context The {@link ReconciliationContext}.
* @return The list of resources resulting from the transformation.
*/
@NotNull Optional<T> transform(@NotNull T resource,
@NotNull HasItems resources,
@NotNull ReconciliationContext context);
}
The transformation class below shows how to filter resource having an annotation exclude: true
.
import java.util.Optional;
@Named("ExcludeIgnoreResource")
@Title("ExcludeIgnoreResource allows filtering resources whose 'metadata.annotations.ignore' property is equal to 'true'")
@Description("The ExcludeIgnoreResource transformation is used to exclude from the"
+ " reconciliation process any resource whose 'metadata.annotations.ignore'"
+ " property is equal to 'true'. This transformation is automatically enabled."
)
@Enabled
@Priority(HasPriority.HIGHEST_PRECEDENCE)
public final class ExcludeIgnoreResourceTransformation implements Transformation<HasMetadata> {
/** {@inheritDoc}**/
@Override
public @NotNull Optional<HasMetadata> transform(@NotNull HasMetadata resource,
@NotNull HasItems resources,
@NotNull ReconciliationContext context) {
return Optional.of(resource)
.filter(r -> HasMetadata.getMetadataAnnotation(resource, "ignore")
.map(NamedValue::getValue)
.map(Value::asBoolean)
.orElse(false)
);
}
}