use of io.strimzi.operator.cluster.operator.resource.cruisecontrol.CruiseControlApi in project strimzi by strimzi.
the class KafkaRebalanceStateMachineTest method checkTransition.
/**
* Checks the expected transition between two states of the Kafka Rebalance operator.
*
* @param vertx The vertx test instance.
* @param context The test context instance.
* @param currentState The current state of the resource before being passed to computeNextStatus.
* @param nextState The expected state of the resouce after computeNextStatus has been called.
* @param initialAnnotation The initial annotation attached to the Kafka Rebalance resource. For example none or refresh.
* @param kcRebalance The Kafka Rebalance instance that will be returned by the resourceSupplier.
* @return A future for the {@link KafkaRebalanceStatus} returned by the {@link KafkaRebalanceAssemblyOperator#computeNextStatus} method
*/
private Future<KafkaRebalanceStatus> checkTransition(Vertx vertx, VertxTestContext context, KafkaRebalanceState currentState, KafkaRebalanceState nextState, KafkaRebalanceAnnotation initialAnnotation, KafkaRebalance kcRebalance) {
CruiseControlApi client = new CruiseControlApiImpl(vertx, HTTP_DEFAULT_IDLE_TIMEOUT_SECONDS, MockCruiseControl.CC_SECRET, MockCruiseControl.CC_API_SECRET, true, true);
ResourceOperatorSupplier supplier = ResourceUtils.supplierWithMocks(true);
ConfigMapOperator mockCmOps = supplier.configMapOperations;
PlatformFeaturesAvailability pfa = new PlatformFeaturesAvailability(true, kubernetesVersion);
KafkaRebalanceAssemblyOperator kcrao = new KafkaRebalanceAssemblyOperator(vertx, pfa, supplier, ResourceUtils.dummyClusterOperatorConfig()) {
@Override
public String cruiseControlHost(String clusterName, String clusterNamespace) {
return HOST;
}
};
Reconciliation recon = new Reconciliation("test-trigger", KafkaRebalance.RESOURCE_KIND, CLUSTER_NAMESPACE, RESOURCE_NAME);
RebalanceOptions.RebalanceOptionsBuilder rbOptions = new RebalanceOptions.RebalanceOptionsBuilder();
if (kcRebalance.getSpec() != null) {
if (kcRebalance.getSpec().getGoals() != null) {
rbOptions.withGoals(kcRebalance.getSpec().getGoals());
}
if (kcRebalance.getSpec().isSkipHardGoalCheck()) {
rbOptions.withSkipHardGoalCheck();
}
}
CrdOperator<KubernetesClient, KafkaRebalance, KafkaRebalanceList> mockRebalanceOps = supplier.kafkaRebalanceOperator;
when(mockCmOps.getAsync(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(Future.succeededFuture(new ConfigMap()));
when(mockRebalanceOps.get(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(kcRebalance);
when(mockRebalanceOps.getAsync(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(Future.succeededFuture(kcRebalance));
return kcrao.computeNextStatus(recon, HOST, client, kcRebalance, currentState, initialAnnotation, rbOptions).compose(result -> {
context.verify(() -> {
assertThat(result.getStatus().getConditions(), StateMatchers.hasStateInConditions(nextState));
});
return Future.succeededFuture(result.getStatus());
});
}
use of io.strimzi.operator.cluster.operator.resource.cruisecontrol.CruiseControlApi in project strimzi by strimzi.
the class KafkaRebalanceAssemblyOperator method reconcileRebalance.
/**
* Reconcile loop for the KafkaRebalance
*/
/* test */
Future<Void> reconcileRebalance(Reconciliation reconciliation, KafkaRebalance kafkaRebalance) {
if (kafkaRebalance == null) {
LOGGER.infoCr(reconciliation, "Rebalance resource deleted");
return Future.succeededFuture();
}
String clusterName = kafkaRebalance.getMetadata().getLabels() == null ? null : kafkaRebalance.getMetadata().getLabels().get(Labels.STRIMZI_CLUSTER_LABEL);
String clusterNamespace = kafkaRebalance.getMetadata().getNamespace();
if (clusterName == null) {
LOGGER.warnCr(reconciliation, "Resource lacks label '{}': No cluster related to a possible rebalance.", Labels.STRIMZI_CLUSTER_LABEL);
return updateStatus(reconciliation, kafkaRebalance, new KafkaRebalanceStatus(), new InvalidResourceException("Resource lacks label '" + Labels.STRIMZI_CLUSTER_LABEL + "': No cluster related to a possible rebalance.")).mapEmpty();
}
// Get associated Kafka cluster state
return kafkaOperator.getAsync(clusterNamespace, clusterName).compose(kafka -> {
if (kafka == null) {
LOGGER.warnCr(reconciliation, "Kafka resource '{}' identified by label '{}' does not exist in namespace {}.", clusterName, Labels.STRIMZI_CLUSTER_LABEL, clusterNamespace);
return updateStatus(reconciliation, kafkaRebalance, new KafkaRebalanceStatus(), new NoSuchResourceException("Kafka resource '" + clusterName + "' identified by label '" + Labels.STRIMZI_CLUSTER_LABEL + "' does not exist in namespace " + clusterNamespace + ".")).mapEmpty();
} else if (!Util.matchesSelector(kafkaSelector, kafka)) {
LOGGER.debugCr(reconciliation, "{} {} in namespace {} belongs to a Kafka cluster {} which does not match label selector {} and will be ignored", kind(), kafkaRebalance.getMetadata().getName(), clusterNamespace, clusterName, kafkaSelector.get().getMatchLabels());
return Future.succeededFuture();
} else if (kafka.getSpec().getCruiseControl() == null) {
LOGGER.warnCr(reconciliation, "Kafka resource lacks 'cruiseControl' declaration : No deployed Cruise Control for doing a rebalance.");
return updateStatus(reconciliation, kafkaRebalance, new KafkaRebalanceStatus(), new InvalidResourceException("Kafka resource lacks 'cruiseControl' declaration " + ": No deployed Cruise Control for doing a rebalance.")).mapEmpty();
}
if (kafka.getSpec().getKafka().getStorage() instanceof JbodStorage) {
usingJbodStorage = true;
}
String ccSecretName = CruiseControlResources.secretName(clusterName);
String ccApiSecretName = CruiseControlResources.apiSecretName(clusterName);
Future<Secret> ccSecretFuture = secretOperations.getAsync(clusterNamespace, ccSecretName);
Future<Secret> ccApiSecretFuture = secretOperations.getAsync(clusterNamespace, ccApiSecretName);
return CompositeFuture.join(ccSecretFuture, ccApiSecretFuture).compose(compositeFuture -> {
Secret ccSecret = compositeFuture.resultAt(0);
if (ccSecret == null) {
return Future.failedFuture(Util.missingSecretException(clusterNamespace, ccSecretName));
}
Secret ccApiSecret = compositeFuture.resultAt(1);
if (ccApiSecret == null) {
return Future.failedFuture(Util.missingSecretException(clusterNamespace, ccApiSecretName));
}
CruiseControlConfiguration c = new CruiseControlConfiguration(reconciliation, kafka.getSpec().getCruiseControl().getConfig().entrySet());
boolean apiAuthEnabled = CruiseControl.isApiAuthEnabled(c);
boolean apiSslEnabled = CruiseControl.isApiSslEnabled(c);
CruiseControlApi apiClient = cruiseControlClientProvider(ccSecret, ccApiSecret, apiAuthEnabled, apiSslEnabled);
// get latest KafkaRebalance state as it may have changed
return kafkaRebalanceOperator.getAsync(kafkaRebalance.getMetadata().getNamespace(), kafkaRebalance.getMetadata().getName()).compose(currentKafkaRebalance -> {
KafkaRebalanceStatus kafkaRebalanceStatus = currentKafkaRebalance.getStatus();
KafkaRebalanceState currentState;
// cluster rebalance is new or it is in one of the others states
if (kafkaRebalanceStatus == null || kafkaRebalanceStatus.getConditions().stream().filter(cond -> "ReconciliationPaused".equals(cond.getType())).findAny().isPresent()) {
currentState = KafkaRebalanceState.New;
} else {
String rebalanceStateType = rebalanceStateConditionType(kafkaRebalanceStatus);
if (rebalanceStateType == null) {
throw new RuntimeException("Unable to find KafkaRebalance State in current KafkaRebalance status");
}
currentState = KafkaRebalanceState.valueOf(rebalanceStateType);
}
// Check annotation
KafkaRebalanceAnnotation rebalanceAnnotation = rebalanceAnnotation(reconciliation, currentKafkaRebalance);
return reconcile(reconciliation, cruiseControlHost(clusterName, clusterNamespace), apiClient, currentKafkaRebalance, currentState, rebalanceAnnotation).mapEmpty();
}, exception -> Future.failedFuture(exception).mapEmpty());
});
}, exception -> updateStatus(reconciliation, kafkaRebalance, new KafkaRebalanceStatus(), exception).mapEmpty());
}
use of io.strimzi.operator.cluster.operator.resource.cruisecontrol.CruiseControlApi in project strimzi by strimzi.
the class KafkaRebalanceAssemblyOperator method onRebalancing.
/**
* This method handles the transition from {@code Rebalancing} state.
* It starts a periodic timer in order to check the status of the ongoing rebalance processing on Cruise Control side.
* In order to do that, it calls the related Cruise Control REST API about asking the user task status.
* When the rebalance is finished, the next state is {@code Ready}.
* If the user sets the strimzi.io/rebalance annotation to 'stop', it calls the Cruise Control REST API for stopping the ongoing task
* and then transitions to the {@code Stopped} state.
* If the user sets any other values for the strimzi.io/rebalance annotation, it is just ignored and the user task checks continue.
* This method holds the lock until the rebalance is finished, the ongoing task is stopped or any exception is raised.
*
* @param reconciliation Reconciliation information
* @param host Cruise Control service to which sending the REST API requests
* @param apiClient Cruise Control REST API client instance
* @param kafkaRebalance Current {@code KafkaRebalance} resource
* @param rebalanceAnnotation The current value for the strimzi.io/rebalance annotation
* @return a Future with the next {@code MapAndStatus<ConfigMap, KafkaRebalanceStatus>} including the state
*/
private Future<MapAndStatus<ConfigMap, KafkaRebalanceStatus>> onRebalancing(Reconciliation reconciliation, String host, CruiseControlApi apiClient, KafkaRebalance kafkaRebalance, KafkaRebalanceAnnotation rebalanceAnnotation) {
Promise<MapAndStatus<ConfigMap, KafkaRebalanceStatus>> p = Promise.promise();
if (rebalanceAnnotation == KafkaRebalanceAnnotation.none) {
LOGGER.infoCr(reconciliation, "Starting Cruise Control rebalance user task status timer");
String sessionId = kafkaRebalance.getStatus().getSessionId();
AtomicInteger ccApiErrorCount = new AtomicInteger();
vertx.setPeriodic(REBALANCE_POLLING_TIMER_MS, t -> {
// Check that we have not already failed to contact the API beyond the allowed number of times.
if (ccApiErrorCount.get() >= MAX_API_RETRIES) {
vertx.cancelTimer(t);
p.fail(new CruiseControlRestException("Unable to reach Cruise Control API after " + MAX_API_RETRIES + " attempts"));
}
kafkaRebalanceOperator.getAsync(kafkaRebalance.getMetadata().getNamespace(), kafkaRebalance.getMetadata().getName()).onSuccess(currentKafkaRebalance -> {
// Checking that the resource was not deleted between periodic polls
if (currentKafkaRebalance != null) {
// Safety check as timer might be called again (from a delayed timer firing)
if (state(currentKafkaRebalance) == KafkaRebalanceState.Rebalancing) {
if (rebalanceAnnotation(reconciliation, currentKafkaRebalance) == KafkaRebalanceAnnotation.stop) {
LOGGER.debugCr(reconciliation, "Stopping current Cruise Control rebalance user task");
vertx.cancelTimer(t);
apiClient.stopExecution(host, CruiseControl.REST_API_PORT).onSuccess(r -> p.complete(buildRebalanceStatus(null, KafkaRebalanceState.Stopped, validate(reconciliation, kafkaRebalance)))).onFailure(e -> {
LOGGER.errorCr(reconciliation, "Cruise Control stopping execution failed", e.getCause());
p.fail(e.getCause());
});
} else {
LOGGER.infoCr(reconciliation, "Getting Cruise Control rebalance user task status");
apiClient.getUserTaskStatus(host, CruiseControl.REST_API_PORT, sessionId).onSuccess(cruiseControlResponse -> {
JsonObject taskStatusJson = cruiseControlResponse.getJson();
CruiseControlUserTaskStatus taskStatus = CruiseControlUserTaskStatus.lookup(taskStatusJson.getString("Status"));
switch(taskStatus) {
case COMPLETED:
vertx.cancelTimer(t);
LOGGER.infoCr(reconciliation, "Rebalance ({}) is now complete", sessionId);
p.complete(buildRebalanceStatus(kafkaRebalance, null, KafkaRebalanceState.Ready, taskStatusJson, validate(reconciliation, kafkaRebalance)));
break;
case COMPLETED_WITH_ERROR:
// TODO: There doesn't seem to be a way to retrieve the actual error message from the user tasks endpoint?
// We may need to propose an upstream PR for this.
// TODO: Once we can get the error details we need to add an error field to the Rebalance Status to hold
// details of any issues while rebalancing.
LOGGER.errorCr(reconciliation, "Rebalance ({}) optimization proposal has failed to complete", sessionId);
vertx.cancelTimer(t);
p.complete(buildRebalanceStatus(sessionId, KafkaRebalanceState.NotReady, validate(reconciliation, kafkaRebalance)));
break;
case // Rebalance is still in progress
IN_EXECUTION:
// the proposal is complete but the optimisation proposal summary will be missing.
if (currentKafkaRebalance.getStatus().getOptimizationResult() == null || currentKafkaRebalance.getStatus().getOptimizationResult().isEmpty()) {
LOGGER.infoCr(reconciliation, "Rebalance ({}) optimization proposal is now ready and has been added to the status", sessionId);
// Cancel the timer so that the status is returned and updated.
vertx.cancelTimer(t);
p.complete(buildRebalanceStatus(kafkaRebalance, sessionId, KafkaRebalanceState.Rebalancing, taskStatusJson, validate(reconciliation, kafkaRebalance)));
}
ccApiErrorCount.set(0);
// We can then update the status at this point.
break;
case // Rebalance proposal is still being calculated
ACTIVE:
// If a rebalance(dryrun=false) was called and the proposal is still being prepared then the task
// will be in an ACTIVE state. When the proposal is ready it will shift to IN_EXECUTION and we will
// check that the optimisation proposal is added to the status on the next reconcile.
LOGGER.infoCr(reconciliation, "Rebalance ({}) optimization proposal is still being prepared", sessionId);
ccApiErrorCount.set(0);
break;
default:
LOGGER.errorCr(reconciliation, "Unexpected state {}", taskStatus);
vertx.cancelTimer(t);
p.fail("Unexpected state " + taskStatus);
break;
}
}).onFailure(e -> {
LOGGER.errorCr(reconciliation, "Cruise Control getting rebalance task status failed", e.getCause());
// To make sure this error is not just a temporary problem with the network we retry several times.
// If the number of errors pass the MAX_API_ERRORS limit then the period method will fail the promise.
ccApiErrorCount.getAndIncrement();
});
}
} else {
p.complete(new MapAndStatus<>(null, currentKafkaRebalance.getStatus()));
}
} else {
LOGGER.debugCr(reconciliation, "Rebalance resource was deleted, stopping the request time");
vertx.cancelTimer(t);
p.complete();
}
}).onFailure(e -> {
LOGGER.errorCr(reconciliation, "Cruise Control getting rebalance resource failed", e.getCause());
vertx.cancelTimer(t);
p.fail(e.getCause());
});
});
} else {
p.complete(new MapAndStatus<>(null, kafkaRebalance.getStatus()));
}
return p.future();
}
use of io.strimzi.operator.cluster.operator.resource.cruisecontrol.CruiseControlApi in project strimzi-kafka-operator by strimzi.
the class KafkaRebalanceAssemblyOperatorTest method beforeEach.
@BeforeEach
public void beforeEach(Vertx vertx) {
ccServer.reset();
kubernetesClient = new MockKube().withCustomResourceDefinition(Crds.kafkaRebalance(), KafkaRebalance.class, KafkaRebalanceList.class).end().build();
ResourceOperatorSupplier supplier = ResourceUtils.supplierWithMocks(true);
PlatformFeaturesAvailability pfa = new PlatformFeaturesAvailability(true, kubernetesVersion);
// Override to inject mocked cruise control address so real cruise control not required
kcrao = new KafkaRebalanceAssemblyOperator(vertx, pfa, supplier, ResourceUtils.dummyClusterOperatorConfig()) {
@Override
public String cruiseControlHost(String clusterName, String clusterNamespace) {
return HOST;
}
@Override
public CruiseControlApi cruiseControlClientProvider(Secret ccSecret, Secret ccApiSecret, boolean apiAuthEnabled, boolean apiSslEnabled) {
return new CruiseControlApiImpl(vertx, 1, ccSecret, ccApiSecret, true, true);
}
};
mockRebalanceOps = supplier.kafkaRebalanceOperator;
mockKafkaOps = supplier.kafkaOperator;
mockCmOps = supplier.configMapOperations;
mockSecretOps = supplier.secretOperations;
}
use of io.strimzi.operator.cluster.operator.resource.cruisecontrol.CruiseControlApi in project strimzi-kafka-operator by strimzi.
the class KafkaRebalanceStateMachineTest method checkTransition.
/**
* Checks the expected transition between two states of the Kafka Rebalance operator.
*
* @param vertx The vertx test instance.
* @param context The test context instance.
* @param currentState The current state of the resource before being passed to computeNextStatus.
* @param nextState The expected state of the resouce after computeNextStatus has been called.
* @param initialAnnotation The initial annotation attached to the Kafka Rebalance resource. For example none or refresh.
* @param kcRebalance The Kafka Rebalance instance that will be returned by the resourceSupplier.
* @return A future for the {@link KafkaRebalanceStatus} returned by the {@link KafkaRebalanceAssemblyOperator#computeNextStatus} method
*/
private Future<KafkaRebalanceStatus> checkTransition(Vertx vertx, VertxTestContext context, KafkaRebalanceState currentState, KafkaRebalanceState nextState, KafkaRebalanceAnnotation initialAnnotation, KafkaRebalance kcRebalance) {
CruiseControlApi client = new CruiseControlApiImpl(vertx, HTTP_DEFAULT_IDLE_TIMEOUT_SECONDS, MockCruiseControl.CC_SECRET, MockCruiseControl.CC_API_SECRET, true, true);
ResourceOperatorSupplier supplier = ResourceUtils.supplierWithMocks(true);
ConfigMapOperator mockCmOps = supplier.configMapOperations;
PlatformFeaturesAvailability pfa = new PlatformFeaturesAvailability(true, kubernetesVersion);
KafkaRebalanceAssemblyOperator kcrao = new KafkaRebalanceAssemblyOperator(vertx, pfa, supplier, ResourceUtils.dummyClusterOperatorConfig()) {
@Override
public String cruiseControlHost(String clusterName, String clusterNamespace) {
return HOST;
}
};
Reconciliation recon = new Reconciliation("test-trigger", KafkaRebalance.RESOURCE_KIND, CLUSTER_NAMESPACE, RESOURCE_NAME);
RebalanceOptions.RebalanceOptionsBuilder rbOptions = new RebalanceOptions.RebalanceOptionsBuilder();
if (kcRebalance.getSpec() != null) {
if (kcRebalance.getSpec().getGoals() != null) {
rbOptions.withGoals(kcRebalance.getSpec().getGoals());
}
if (kcRebalance.getSpec().isSkipHardGoalCheck()) {
rbOptions.withSkipHardGoalCheck();
}
}
CrdOperator<KubernetesClient, KafkaRebalance, KafkaRebalanceList> mockRebalanceOps = supplier.kafkaRebalanceOperator;
when(mockCmOps.getAsync(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(Future.succeededFuture(new ConfigMap()));
when(mockRebalanceOps.get(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(kcRebalance);
when(mockRebalanceOps.getAsync(CLUSTER_NAMESPACE, RESOURCE_NAME)).thenReturn(Future.succeededFuture(kcRebalance));
return kcrao.computeNextStatus(recon, HOST, client, kcRebalance, currentState, initialAnnotation, rbOptions).compose(result -> {
context.verify(() -> {
assertThat(result.getStatus().getConditions(), StateMatchers.hasStateInConditions(nextState));
});
return Future.succeededFuture(result.getStatus());
});
}
Aggregations