This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Developer Guide

Learn how to use the Jikkou Core API

Here, you will find the necessary information to develop with the Jikkou API.

More information:

1 - Extension Developer Guide

Learn how to write custom extensions for Jikkou

This guide describes how developers can write new extensions for Jikkou.

More information:

1.1 - Package Extensions

Learn how to package and install custom extensions for Jikkou.

Packaging Extensions

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.

Dependencies

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}

Extension Discovery

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);
}

Recommendations

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.

Installing Extension Providers

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.

Configuring Extension Providers

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 :

  • The Jikkou CLI: jikkou api-extensions list command, or
  • The Jikkou API Server: GET /apis/core.jikkou.io/v1/extensions -H "Accept: application/json"

1.2 - Develop Custom Validations

Learn how to develop custom resource validations.

This section covers the core classes to develop validation extensions.

Interface

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
    }
}

Examples

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();
    }
}

1.3 - Develop Custom Action

Learn how to develop custom actions.

This section covers the core classes to develop action extensions.

Interface

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);
}

Examples

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();
        }
    }
}

1.4 - Develop Custom Transformations

Learn how to develop custom resource transformations.

This section covers the core classes to develop transformation extensions.

Interface

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);
}

Examples

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)
                 );
    }
}