Skip to content

feat: dependent resource context + my sql e2e test improvements #979

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 3 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -3,6 +3,7 @@
import java.util.Optional;

import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext;

public interface Context extends AttributeHolder {

Expand All @@ -22,4 +23,6 @@ default <T> T getMandatory(Object key, Class<T> expectedType) {
}

ConfigurationService getConfigurationService();

ManagedDependentResourceContext managedDependentResourceContext();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.api.reconciler.dependent.ManagedDependentResourceContext;
import io.javaoperatorsdk.operator.processing.Controller;

public class DefaultContext<P extends HasMetadata> extends MapAttributeHolder implements Context {
Expand All @@ -12,12 +13,15 @@ public class DefaultContext<P extends HasMetadata> extends MapAttributeHolder im
private final Controller<P> controller;
private final P primaryResource;
private final ConfigurationService configurationService;
private ManagedDependentResourceContext managedDependentResourceContext;

public DefaultContext(RetryInfo retryInfo, Controller<P> controller, P primaryResource) {
this.retryInfo = retryInfo;
this.controller = controller;
this.primaryResource = primaryResource;
this.configurationService = controller.getConfiguration().getConfigurationService();
this.managedDependentResourceContext = new ManagedDependentResourceContext(
controller.getDependents().getDependents());
}

@Override
Expand All @@ -36,4 +40,8 @@ public <T> Optional<T> getSecondaryResource(Class<T> expectedType, String eventS
public ConfigurationService getConfigurationService() {
return configurationService;
}

public ManagedDependentResourceContext managedDependentResourceContext() {
return managedDependentResourceContext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.javaoperatorsdk.operator.api.reconciler.dependent;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import io.javaoperatorsdk.operator.OperatorException;

public class ManagedDependentResourceContext {

private List<DependentResource> dependentResources;

public ManagedDependentResourceContext(List<DependentResource> dependentResources) {
this.dependentResources = dependentResources;
}

public List<DependentResource> getDependentResources() {
return Collections.unmodifiableList(dependentResources);
}

public <T extends DependentResource> T getDependentResource(Class<T> resourceClass) {
var resourceList =
dependentResources.stream()
.filter(dr -> dr.getClass().equals(resourceClass))
.collect(Collectors.toList());
if (resourceList.isEmpty()) {
throw new OperatorException(
"No dependent resource found for class: " + resourceClass.getName());
}
if (resourceList.size() > 1) {
throw new OperatorException(
"More than one dependent resource found for class: " + resourceClass.getName());
}
return (T) resourceList.get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,8 @@ public void stop() {
eventSourceManager.stop();
}
}

public DependentResourceManager<R> getDependents() {
return dependents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,8 @@ private DependentResource createAndConfigureFrom(DependentResourceSpec dependent
throw new IllegalStateException(e);
}
}

public List<DependentResource> getDependents() {
return dependents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider;
import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
import io.javaoperatorsdk.operator.monitoring.micrometer.MicrometerMetrics;
import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig;
import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource;
import io.micrometer.core.instrument.logging.LoggingMeterRegistry;

public class MySQLSchemaOperator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@

import java.util.Optional;

import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.Secret;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.RetryInfo;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.sample.schema.Schema;
import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource;
import io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource;

import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER;
import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME;
import static java.lang.String.format;

// todo handle this, should work with finalizer
Expand All @@ -26,42 +27,23 @@
@Dependent(type = SchemaDependentResource.class)
})
public class MySQLSchemaReconciler
implements Reconciler<MySQLSchema>, ErrorStatusHandler<MySQLSchema>,
ContextInitializer<MySQLSchema> {
implements Reconciler<MySQLSchema>, ErrorStatusHandler<MySQLSchema> {

static final String SECRET_FORMAT = "%s-secret";
static final String USERNAME_FORMAT = "%s-user";

static final String MYSQL_SECRET_NAME = "mysql.secret.name";
static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name";
static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password";
static final String BUILT_SCHEMA = "built schema";
static final Logger log = LoggerFactory.getLogger(MySQLSchemaReconciler.class);

public MySQLSchemaReconciler() {}

@Override
public void initContext(MySQLSchema primary, Context context) {
final var name = primary.getMetadata().getName();
final var password = RandomStringUtils
.randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here

final var secretName = String.format(SECRET_FORMAT, name);
final var userName = String.format(USERNAME_FORMAT, name);

// put information in context for other dependents and reconciler to use
context.put(MYSQL_SECRET_PASSWORD, password);
context.put(MYSQL_SECRET_NAME, secretName);
context.put(MYSQL_SECRET_USERNAME, userName);
}

@Override
public UpdateControl<MySQLSchema> reconcile(MySQLSchema schema, Context context) {
// we only need to update the status if we just built the schema, i.e. when it's present in the
// context
return context.get(BUILT_SCHEMA, Schema.class).map(s -> {
updateStatusPojo(schema, context.getMandatory(MYSQL_SECRET_NAME, String.class),
context.getMandatory(MYSQL_SECRET_USERNAME, String.class));
Secret secret = context.getSecondaryResource(Secret.class).orElseThrow();
SchemaDependentResource schemaDependentResource = context.managedDependentResourceContext()
.getDependentResource(SchemaDependentResource.class);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be better to get the dependent via its class in a way similar to what we do for secondary resources, i.e. getDependentResource(Schema.class), though I guess this way this side-steps the issue of what to do when there are multiple dependents with the same type? Maybe provide both versions?

Copy link
Collaborator Author

@csviri csviri Feb 28, 2022

Choose a reason for hiding this comment

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

I think it'd be better to get the dependent via its class in a way similar to what we do for secondary resources, i.e. getDependentResource(Schema.class)

That is how it's done, just hidden behind dedicated context. Or?

though I guess this way this side-steps the issue of what to do when there are multiple dependents with the same type? Maybe provide both versions?

I intentionally did not cover this, since on denepnds_on we plan to have names of the resources, so I would add that as part of it?!

return schemaDependentResource.getResource(schema).map(s -> {
updateStatusPojo(schema, secret.getMetadata().getName(),
secret.getData().get(MYSQL_SECRET_USERNAME));
log.info("Schema {} created - updating CR status", schema.getMetadata().getName());
return UpdateControl.updateStatus(schema);
}).orElse(UpdateControl.noUpdate());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package io.javaoperatorsdk.operator.sample;
package io.javaoperatorsdk.operator.sample.dependent;

import io.javaoperatorsdk.operator.sample.MySQLDbConfig;

public class ResourcePollerConfig {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package io.javaoperatorsdk.operator.sample;
package io.javaoperatorsdk.operator.sample.dependent;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Base64;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.fabric8.kubernetes.api.model.Secret;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource;
Expand All @@ -14,9 +19,12 @@
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource;
import io.javaoperatorsdk.operator.sample.*;
import io.javaoperatorsdk.operator.sample.schema.Schema;
import io.javaoperatorsdk.operator.sample.schema.SchemaService;

import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_PASSWORD;
import static io.javaoperatorsdk.operator.sample.dependent.SecretDependentResource.MYSQL_SECRET_USERNAME;
import static java.lang.String.format;

public class SchemaDependentResource
Expand All @@ -26,6 +34,8 @@ public class SchemaDependentResource
Creator<Schema, MySQLSchema>,
Deleter<MySQLSchema> {

private static final Logger log = LoggerFactory.getLogger(SchemaDependentResource.class);

private MySQLDbConfig dbConfig;
private int pollPeriod = 500;

Expand Down Expand Up @@ -53,25 +63,22 @@ public Schema desired(MySQLSchema primary, Context context) {
@Override
public void create(Schema target, MySQLSchema mySQLSchema, Context context) {
try (Connection connection = getConnection()) {
Secret secret = context.getSecondaryResource(Secret.class).orElseThrow();
var username = decode(secret.getData().get(MYSQL_SECRET_USERNAME));
var password = decode(secret.getData().get(MYSQL_SECRET_PASSWORD));
final var schema = SchemaService.createSchemaAndRelatedUser(
connection,
target.getName(),
target.getCharacterSet(),
context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_USERNAME, String.class),
context.getMandatory(MySQLSchemaReconciler.MYSQL_SECRET_PASSWORD, String.class));

// put the newly built schema in the context to let the reconciler know we just built it
context.put(MySQLSchemaReconciler.BUILT_SCHEMA, schema);
target.getCharacterSet(), username, password);
} catch (SQLException e) {
MySQLSchemaReconciler.log.error("Error while creating Schema", e);
log.error("Error while creating Schema", e);
throw new IllegalStateException(e);
}
}

private Connection getConnection() throws SQLException {
String connectURL = format("jdbc:mysql://%1$s:%2$s", dbConfig.getHost(), dbConfig.getPort());

MySQLSchemaReconciler.log.debug("Connecting to '{}' with user '{}'", connectURL,
log.debug("Connecting to '{}' with user '{}'", connectURL,
dbConfig.getUser());
return DriverManager.getConnection(connectURL, dbConfig.getUser(), dbConfig.getPassword());
}
Expand Down Expand Up @@ -99,4 +106,8 @@ public Optional<Schema> getResource(MySQLSchema primaryResource) {
}
}

private static String decode(String value) {
return new String(Base64.getDecoder().decode(value.getBytes()));
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package io.javaoperatorsdk.operator.sample;
package io.javaoperatorsdk.operator.sample.dependent;

import java.util.Optional;

import io.javaoperatorsdk.operator.processing.event.source.polling.PerResourcePollingEventSource;
import io.javaoperatorsdk.operator.sample.MySQLDbConfig;
import io.javaoperatorsdk.operator.sample.MySQLSchema;
import io.javaoperatorsdk.operator.sample.schema.Schema;
import io.javaoperatorsdk.operator.sample.schema.SchemaService;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,49 @@
package io.javaoperatorsdk.operator.sample;
package io.javaoperatorsdk.operator.sample.dependent;

import java.util.Base64;

import org.apache.commons.lang3.RandomStringUtils;

import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Updater;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier;
import io.javaoperatorsdk.operator.sample.MySQLSchema;

import static io.javaoperatorsdk.operator.sample.MySQLSchemaReconciler.*;

public class SecretDependentResource extends KubernetesDependentResource<Secret, MySQLSchema>
implements AssociatedSecondaryResourceIdentifier<MySQLSchema>, Updater<Secret, MySQLSchema> {
implements AssociatedSecondaryResourceIdentifier<MySQLSchema>, Creator<Secret, MySQLSchema> {

public static final String SECRET_FORMAT = "%s-secret";
public static final String USERNAME_FORMAT = "%s-user";
public static final String MYSQL_SECRET_USERNAME = "mysql.secret.user.name";
public static final String MYSQL_SECRET_PASSWORD = "mysql.secret.user.password";

private static String encode(String value) {
return Base64.getEncoder().encodeToString(value.getBytes());
}

@Override
protected Secret desired(MySQLSchema schema, Context context) {
final var password = RandomStringUtils
.randomAlphanumeric(16); // NOSONAR: we don't need cryptographically-strong randomness here
final var name = schema.getMetadata().getName();
final var secretName = String.format(SECRET_FORMAT, name);
final var userName = String.format(USERNAME_FORMAT, name);

return new SecretBuilder()
.withNewMetadata()
.withName(context.getMandatory(MYSQL_SECRET_NAME, String.class))
.withName(secretName)
.withNamespace(schema.getMetadata().getNamespace())
.endMetadata()
.addToData(
"MYSQL_USERNAME", encode(context.getMandatory(MYSQL_SECRET_USERNAME, String.class)))
MYSQL_SECRET_USERNAME, encode(userName))
.addToData(
"MYSQL_PASSWORD", encode(context.getMandatory(MYSQL_SECRET_PASSWORD, String.class)))
MYSQL_SECRET_PASSWORD, encode(password))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import io.javaoperatorsdk.operator.junit.AbstractOperatorExtension;
import io.javaoperatorsdk.operator.junit.E2EOperatorExtension;
import io.javaoperatorsdk.operator.junit.OperatorExtension;
import io.javaoperatorsdk.operator.sample.dependent.ResourcePollerConfig;
import io.javaoperatorsdk.operator.sample.dependent.SchemaDependentResource;

import static java.util.concurrent.TimeUnit.MINUTES;
import static org.awaitility.Awaitility.await;
Expand Down