Skip to content

feat(quarkus): add support for externalized configuration #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import io.javaoperatorsdk.operator.api.ResourceController;
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.RetryConfiguration;
import io.javaoperatorsdk.quarkus.extension.ConfigurationServiceRecorder;
import io.javaoperatorsdk.quarkus.extension.ExternalConfiguration;
import io.javaoperatorsdk.quarkus.extension.ExternalControllerConfiguration;
import io.javaoperatorsdk.quarkus.extension.OperatorProducer;
import io.javaoperatorsdk.quarkus.extension.QuarkusConfigurationService;
import io.javaoperatorsdk.quarkus.extension.QuarkusControllerConfiguration;
Expand Down Expand Up @@ -49,6 +52,8 @@ class QuarkusExtensionProcessor {
throw new IllegalArgumentException();
};

private ExternalConfiguration externalConfiguration;

@BuildStep
void indexSDKDependencies(
BuildProducer<IndexDependencyBuildItem> indexDependency,
Expand Down Expand Up @@ -110,16 +115,6 @@ private ControllerConfiguration createControllerConfiguration(
.setDefaultScope(APPLICATION_SCOPED)
.build());

// generate configuration
final var controllerAnnotation = info.classAnnotation(CONTROLLER);
if (controllerAnnotation == null) {
throw new IllegalArgumentException(
resourceControllerClassName
+ " is missing the @"
+ Controller.class.getCanonicalName()
+ " annotation");
}

// load CR class
final Class<CustomResource> crClass = (Class<CustomResource>) loadClass(crType);

Expand All @@ -137,38 +132,45 @@ private ControllerConfiguration createControllerConfiguration(
// register CR class for introspection
reflectionClasses.produce(new ReflectiveClassBuildItem(true, true, crClass));

// retrieve the Controller annotation if it exists
final var controllerAnnotation = info.classAnnotation(CONTROLLER);

// retrieve the controller's name
final var defaultControllerName =
ControllerUtils.getDefaultResourceControllerName(resourceControllerClassName);
final var name =
valueOrDefault(
controllerAnnotation,
"name",
AnnotationValue::asString,
() -> ControllerUtils.getDefaultResourceControllerName(resourceControllerClassName));
annotationValueOrDefault(
controllerAnnotation, "name", AnnotationValue::asString, () -> defaultControllerName);

// check if we have externalized configuration to provide values
final var extContConfig = externalConfiguration.controllers.get(name);

final var extractor = new ValueExtractor(controllerAnnotation, extContConfig);

// create the configuration
final var configuration =
new QuarkusControllerConfiguration(
resourceControllerClassName,
name,
crdName,
valueOrDefault(
controllerAnnotation,
extractor.extract(
c -> c.finalizer,
"finalizerName",
AnnotationValue::asString,
() -> ControllerUtils.getDefaultFinalizerName(crdName)),
valueOrDefault(
controllerAnnotation,
extractor.extract(
c -> c.generationAware,
"generationAwareEventProcessing",
AnnotationValue::asBoolean,
() -> true),
QuarkusControllerConfiguration.asSet(
valueOrDefault(
controllerAnnotation,
extractor.extract(
c -> c.namespaces.map(l -> l.toArray(new String[0])),
"namespaces",
AnnotationValue::asStringArray,
() -> new String[] {})),
crType,
null // todo: fix-me
);
retryConfiguration(extContConfig));

log.infov(
"Processed ''{0}'' controller named ''{1}'' for ''{2}'' CR (version ''{3}'')",
Expand All @@ -177,12 +179,72 @@ private ControllerConfiguration createControllerConfiguration(
return configuration;
}

private <T> T valueOrDefault(
private RetryConfiguration retryConfiguration(ExternalControllerConfiguration extConfig) {
return extConfig == null ? null : RetryConfigurationResolver.resolve(extConfig.retry);
}

private static class ValueExtractor {

private final AnnotationInstance controllerAnnotation;
private final ExternalControllerConfiguration extContConfig;

ValueExtractor(
AnnotationInstance controllerAnnotation, ExternalControllerConfiguration extContConfig) {
this.controllerAnnotation = controllerAnnotation;
this.extContConfig = extContConfig;
}

/**
* Extracts the appropriate configuration value for the controller checking first any annotation
* configuration, then potentially overriding it by a properties-provided value or returning a
* default value if neither is provided.
*
* @param extractor a Function extracting the optional value we're interested in from the
* external configuration
* @param annotationField the name of the {@link Controller} annotation we're want to retrieve
* if present
* @param converter a Function converting the annotation value to the type we're expecting
* @param defaultValue a Supplier that computes/retrieve a default value when needed
* @param <T> the expected type of the configuration value we're trying to extract
* @return the extracted configuration value
*/
<T> T extract(
Function<ExternalControllerConfiguration, Optional<T>> extractor,
String annotationField,
Function<AnnotationValue, T> converter,
Supplier<T> defaultValue) {
// first check if we have an external configuration
if (extContConfig != null) {
// extract value from config if present
return extractor
.apply(extContConfig)
// or get from the annotation or default
.orElse(annotationValueOrDefault(annotationField, converter, defaultValue));
} else {
// get from annotation or default
return annotationValueOrDefault(annotationField, converter, defaultValue);
}
}

private <T> T annotationValueOrDefault(
String name, Function<AnnotationValue, T> converter, Supplier<T> defaultValue) {
return QuarkusExtensionProcessor.annotationValueOrDefault(
controllerAnnotation, name, converter, defaultValue);
}
}

private static <T> T annotationValueOrDefault(
AnnotationInstance annotation,
String name,
Function<AnnotationValue, T> converter,
Supplier<T> defaultValue) {
return Optional.ofNullable(annotation.value(name)).map(converter).orElseGet(defaultValue);
return annotation != null
?
// get converted annotation value of get default
Optional.ofNullable(annotation.value(name)).map(converter).orElseGet(defaultValue)
:
// get default
defaultValue.get();
}

private Class<?> loadClass(String className) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.javaoperatorsdk.quarkus.extension.deployment;

import io.javaoperatorsdk.operator.api.config.RetryConfiguration;
import io.javaoperatorsdk.quarkus.extension.ExternalIntervalConfiguration;
import io.javaoperatorsdk.quarkus.extension.ExternalRetryConfiguration;
import io.javaoperatorsdk.quarkus.extension.PlainRetryConfiguration;
import java.util.Optional;

class RetryConfigurationResolver implements RetryConfiguration {

private final RetryConfiguration delegate;

private RetryConfigurationResolver(Optional<ExternalRetryConfiguration> retry) {
delegate =
retry
.<RetryConfiguration>map(ExternalRetryConfigurationAdapter::new)
.orElse(RetryConfiguration.DEFAULT);
}

public static RetryConfiguration resolve(Optional<ExternalRetryConfiguration> retry) {
final var delegate = new RetryConfigurationResolver(retry);
return new PlainRetryConfiguration(
delegate.getMaxAttempts(),
delegate.getInitialInterval(),
delegate.getIntervalMultiplier(),
delegate.getMaxInterval());
}

@Override
public int getMaxAttempts() {
return delegate.getMaxAttempts();
}

@Override
public long getInitialInterval() {
return delegate.getInitialInterval();
}

@Override
public double getIntervalMultiplier() {
return delegate.getIntervalMultiplier();
}

@Override
public long getMaxInterval() {
return delegate.getMaxInterval();
}

private static class ExternalRetryConfigurationAdapter implements RetryConfiguration {

private final int maxAttempts;
private final IntervalConfigurationAdapter interval;

public ExternalRetryConfigurationAdapter(ExternalRetryConfiguration config) {
maxAttempts = config.maxAttempts.orElse(RetryConfiguration.DEFAULT.getMaxAttempts());
interval =
config
.interval
.map(IntervalConfigurationAdapter::new)
.orElse(new IntervalConfigurationAdapter());
}

@Override
public int getMaxAttempts() {
return maxAttempts;
}

@Override
public long getInitialInterval() {
return interval.initial;
}

@Override
public double getIntervalMultiplier() {
return interval.multiplier;
}

@Override
public long getMaxInterval() {
return interval.max;
}
}

private static class IntervalConfigurationAdapter {

private final long initial;
private final double multiplier;
private final long max;

IntervalConfigurationAdapter(ExternalIntervalConfiguration config) {
initial = config.initial.orElse(RetryConfiguration.DEFAULT.getInitialInterval());
multiplier = config.multiplier.orElse(RetryConfiguration.DEFAULT.getIntervalMultiplier());
max = config.max.orElse(RetryConfiguration.DEFAULT.getMaxInterval());
}

IntervalConfigurationAdapter() {
this.initial = RetryConfiguration.DEFAULT.getInitialInterval();
this.multiplier = RetryConfiguration.DEFAULT.getIntervalMultiplier();
this.max = RetryConfiguration.DEFAULT.getMaxInterval();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.javaoperatorsdk.quarkus.extension;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import java.util.Map;

@ConfigRoot(name = "operator-sdk", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
public class ExternalConfiguration {

/** Maps a controller name to its configuration. */
@ConfigItem public Map<String, ExternalControllerConfiguration> controllers;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.javaoperatorsdk.quarkus.extension;

import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import java.util.List;
import java.util.Optional;

@ConfigGroup
public class ExternalControllerConfiguration {

/**
* An optional list of comma-separated namespace names the controller should watch. If the list
* contains {@link ControllerConfiguration#WATCH_ALL_NAMESPACES_MARKER} then the controller will
* watch all namespaces.
*/
@ConfigItem public Optional<List<String>> namespaces;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about if this list is empty then the controller watches all namespaces? I think watching all namespaces is a sensible default as this will mostly be controlled by RBAC anyway by cluster admins not by configuring the operator.

Copy link
Collaborator Author

@metacosm metacosm Jan 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment the default behavior is to watch the namespace the client is connected to (which should be the namespace the operator is deployed in), the idea being that you don't need to setup RBAC by default, which should make it easier to experiment with.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let's leave it like that for now. We could check what's the default in the operator-sdk and just run with that later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will address this in #303


/**
* The optional name of the finalizer for the controller. If none is provided, one will be
* automatically generated.
*/
@ConfigItem public Optional<String> finalizer;

/**
* Whether the controller should only process events if the associated resource generation has
* increased since last reconciliation, otherwise will process all events.
*/
@ConfigItem(defaultValue = "true")
public Optional<Boolean> generationAware;

/** The optional controller retry configuration */
public Optional<ExternalRetryConfiguration> retry;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.javaoperatorsdk.quarkus.extension;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import java.util.Optional;

@ConfigGroup
public class ExternalIntervalConfiguration {

/** The initial interval that the controller waits for before attempting the first retry */
@ConfigItem public Optional<Long> initial;

/** The value by which the initial interval is multiplied by for each retry */
@ConfigItem public Optional<Double> multiplier;

/**
* The maximum interval that the controller will wait for before attempting a retry, regardless of
* all other configuration
*/
@ConfigItem public Optional<Long> max;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.quarkus.extension;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import java.util.Optional;

@ConfigGroup
public class ExternalRetryConfiguration {

/** How many times an operation should be retried before giving up */
@ConfigItem public Optional<Integer> maxAttempts;

/** The configuration of the retry interval. */
@ConfigItem public Optional<ExternalIntervalConfiguration> interval;
}
Loading