Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.zeebe.shared.management;
import io.atomix.cluster.MemberId;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.AddMembersRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.BrokerScaleRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.CancelChangeRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.ClusterPatchRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.ClusterScaleRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.ForceRemoveBrokersRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.JoinPartitionRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.LeavePartitionRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequest.RemoveMembersRequest;
import io.camunda.zeebe.dynamic.config.api.ClusterConfigurationManagementRequestSender;
import io.camunda.zeebe.management.cluster.ClusterConfigPatchRequest;
import io.camunda.zeebe.management.cluster.ClusterConfigPatchRequestBrokers;
import io.camunda.zeebe.management.cluster.ClusterConfigPatchRequestPartitions;
import io.camunda.zeebe.management.cluster.Error;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
@Component
@RestControllerEndpoint(id = "cluster")
public class ClusterEndpoint {
private final ClusterConfigurationManagementRequestSender requestSender;
@Autowired
public ClusterEndpoint(final ClusterConfigurationManagementRequestSender requestSender) {
this.requestSender = requestSender;
}
@GetMapping(produces = "application/json")
public ResponseEntity> clusterTopology() {
try {
return ClusterApiUtils.mapClusterTopologyResponse(requestSender.getTopology().join());
} catch (final Exception error) {
return ClusterApiUtils.mapError(error);
}
}
private ResponseEntity invalidRequest(final String message) {
final var errorResponse = new Error();
errorResponse.setMessage(message);
return ResponseEntity.status(400).body(errorResponse);
}
@PostMapping(path = "/{resource}/{id}")
public ResponseEntity> add(
@PathVariable("resource") final Resource resource,
@PathVariable final int id,
@RequestParam(defaultValue = "false") final boolean dryRun) {
return switch (resource) {
case brokers ->
ClusterApiUtils.mapOperationResponse(
requestSender
.addMembers(
new AddMembersRequest(Set.of(new MemberId(String.valueOf(id))), dryRun))
.join());
case partitions -> ResponseEntity.status(501).body("Adding partitions is not supported");
case changes ->
ResponseEntity.status(501)
.body(
"Changing cluster directly is not supported. Use POST /cluster/brokers for scaling the cluster");
};
}
@DeleteMapping(path = "/{resource}/{id}")
public ResponseEntity> remove(
@PathVariable("resource") final Resource resource,
@PathVariable final String id,
@RequestParam(defaultValue = "false") final boolean dryRun) {
return switch (resource) {
case brokers ->
ClusterApiUtils.mapOperationResponse(
requestSender
.removeMembers(new RemoveMembersRequest(Set.of(new MemberId(id)), dryRun))
.join());
case partitions -> ResponseEntity.status(501).body("Removing partitions is not supported");
case changes -> {
if (dryRun) {
yield ResponseEntity.status(501).body("Dry run is not supported for cancelling changes");
} else {
yield cancelChange(id);
}
}
};
}
/**
* Cancels a change with the given id. This is a dangerous operation and should only be used when
* the change is stuck and cannot make progress on its own. Cancelling a change will not revert
* already applied operations, so the cluster will be in an intermediate state with partially
* applied changes. For example, a partition might have been added to a broker, but not removed
* from another one; so it has a higher number of replicas than the configured value. In another
* case, the configuration in raft might differ from what is reflected in the ClusterTopology. So
* a manual intervention would be required to clean up the state.
*/
private ResponseEntity> cancelChange(final String changeId) {
try {
return ClusterApiUtils.mapClusterTopologyResponse(
requestSender
.cancelTopologyChange(new CancelChangeRequest(Long.parseLong(changeId)))
.join());
} catch (final NumberFormatException ignore) {
return invalidRequest("Change id must be a number");
} catch (final Exception error) {
return ClusterApiUtils.mapError(error);
}
}
@PostMapping(path = "/{resource}", consumes = "application/json")
public ResponseEntity> scale(
@PathVariable("resource") final Resource resource,
@RequestBody final List ids,
@RequestParam(defaultValue = "false") final boolean dryRun,
@RequestParam(defaultValue = "false") final boolean force,
@RequestParam final Optional replicationFactor) {
return switch (resource) {
case brokers -> scaleBrokers(ids, dryRun, force, replicationFactor);
case partitions ->
new ResponseEntity<>("Scaling partitions is not supported", HttpStatusCode.valueOf(501));
case changes ->
ResponseEntity.status(501)
.body(
"Changing cluster directly is not supported. Use POST /cluster/brokers for scaling the cluster");
};
}
private ResponseEntity> scaleBrokers(
final List ids,
final boolean dryRun,
final boolean force,
final Optional replicationFactor) {
try {
final BrokerScaleRequest scaleRequest =
new BrokerScaleRequest(
ids.stream().map(String::valueOf).map(MemberId::from).collect(Collectors.toSet()),
replicationFactor,
dryRun);
// here we assume, if it force request it is always force scale down. The coordinator will
// reject the request if that is not the case.
final var response =
force
? requestSender.forceScaleDown(scaleRequest).join()
: requestSender.scaleMembers(scaleRequest).join();
return ClusterApiUtils.mapOperationResponse(response);
} catch (final Exception error) {
return ClusterApiUtils.mapError(error);
}
}
@PatchMapping(consumes = "application/json", produces = "application/json")
public ResponseEntity> updateClusterConfiguration(
@RequestParam(defaultValue = "false") final boolean dryRun,
@RequestParam(defaultValue = "false") final boolean force,
@RequestBody final ClusterConfigPatchRequest request) {
try {
final var brokers = request.getBrokers();
final var partitions = request.getPartitions();
if (force) {
return forceRemoveBrokers(dryRun, brokers, partitions);
}
final boolean isScale = brokers != null && brokers.getCount() != null;
final boolean shouldAddBrokers =
brokers != null && brokers.getAdd() != null && !brokers.getAdd().isEmpty();
final boolean shouldRemoveBrokers =
brokers != null && brokers.getRemove() != null && !brokers.getRemove().isEmpty();
if (isScale && (shouldAddBrokers || shouldRemoveBrokers)) {
return invalidRequest(
"Cannot change brokers count and add/remove brokers at the same time. Specify either the new brokers count or brokers to add and remove.");
}
final Optional newPartitionCount =
Optional.ofNullable(partitions).map(ClusterConfigPatchRequestPartitions::getCount);
final Optional newReplicationFactor =
Optional.ofNullable(partitions)
.map(ClusterConfigPatchRequestPartitions::getReplicationFactor);
if (isScale) {
final var scaleRequest =
new ClusterScaleRequest(
Optional.of(brokers.getCount()), newPartitionCount, newReplicationFactor, dryRun);
return ClusterApiUtils.mapOperationResponse(
requestSender.scaleCluster(scaleRequest).join());
} else {
return patchCluster(dryRun, request, brokers, newPartitionCount, newReplicationFactor);
}
} catch (final Exception error) {
return ClusterApiUtils.mapError(error);
}
}
private ResponseEntity> patchCluster(
final boolean dryRun,
final ClusterConfigPatchRequest request,
final ClusterConfigPatchRequestBrokers brokers,
final Optional newPartitionCount,
final Optional newReplicationFactor) {
final Set brokersToAdd =
brokers != null
? request.getBrokers().getAdd().stream()
.map(String::valueOf)
.map(MemberId::from)
.collect(Collectors.toSet())
: Set.of();
final Set brokersToRemove =
brokers != null
? request.getBrokers().getRemove().stream()
.map(String::valueOf)
.map(MemberId::from)
.collect(Collectors.toSet())
: Set.of();
final var patchRequest =
new ClusterPatchRequest(
brokersToAdd, brokersToRemove, newPartitionCount, newReplicationFactor, dryRun);
return ClusterApiUtils.mapOperationResponse(requestSender.patchCluster(patchRequest).join());
}
private ResponseEntity> forceRemoveBrokers(
final boolean dryRun,
final ClusterConfigPatchRequestBrokers brokers,
final ClusterConfigPatchRequestPartitions partitions) {
if (brokers == null) {
return invalidRequest("Must provide a set of brokers to force remove.");
}
if (brokers.getCount() != null) {
return invalidRequest("Cannot force change the broker count.");
}
if (brokers.getAdd() != null && !brokers.getAdd().isEmpty()) {
return invalidRequest("Cannot force add brokers");
}
if (partitions != null) {
if (partitions.getCount() != null) {
return invalidRequest("Cannot force change the partition count.");
}
if (partitions.getReplicationFactor() != null) {
return invalidRequest("Cannot force change the replication factor.");
}
}
if (brokers.getRemove() == null || brokers.getRemove().isEmpty()) {
return invalidRequest("Must provide a set of brokers to force remove.");
}
final var forceRemoveRequest =
new ForceRemoveBrokersRequest(
brokers.getRemove().stream()
.map(String::valueOf)
.map(MemberId::from)
.collect(Collectors.toSet()),
dryRun);
return ClusterApiUtils.mapOperationResponse(
requestSender.forceRemoveBrokers(forceRemoveRequest).join());
}
@PostMapping(
path = "/{resource}/{resourceId}/{subResource}/{subResourceId}",
consumes = "application/json")
public ResponseEntity> addSubResource(
@PathVariable("resource") final Resource resource,
@PathVariable final int resourceId,
@PathVariable("subResource") final Resource subResource,
@PathVariable final int subResourceId,
@RequestBody final PartitionAddRequest request,
@RequestParam(defaultValue = "false") final boolean dryRun) {
final int priority = request.priority();
return switch (resource) {
case brokers ->
switch (subResource) {
// POST /cluster/brokers/1/partitions/2
case partitions ->
ClusterApiUtils.mapOperationResponse(
requestSender
.joinPartition(
new JoinPartitionRequest(
MemberId.from(String.valueOf(resourceId)),
subResourceId,
priority,
dryRun))
.join());
case brokers, changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
case partitions ->
switch (subResource) {
// POST /cluster/partitions/2/brokers/1
case brokers ->
ClusterApiUtils.mapOperationResponse(
requestSender
.joinPartition(
new JoinPartitionRequest(
MemberId.from(String.valueOf(subResourceId)),
resourceId,
priority,
dryRun))
.join());
case partitions, changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
case changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
}
@DeleteMapping(
path = "/{resource}/{resourceId}/{subResource}/{subResourceId}",
consumes = "application/json")
public ResponseEntity> removeSubResource(
@PathVariable("resource") final Resource resource,
@PathVariable final int resourceId,
@PathVariable("subResource") final Resource subResource,
@PathVariable final int subResourceId,
@RequestParam(defaultValue = "false") final boolean dryRun) {
return switch (resource) {
case brokers ->
switch (subResource) {
case partitions ->
ClusterApiUtils.mapOperationResponse(
requestSender
.leavePartition(
new LeavePartitionRequest(
MemberId.from(String.valueOf(resourceId)), subResourceId, dryRun))
.join());
case brokers, changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
case partitions ->
switch (subResource) {
case brokers ->
ClusterApiUtils.mapOperationResponse(
requestSender
.leavePartition(
new LeavePartitionRequest(
MemberId.from(String.valueOf(subResourceId)), resourceId, dryRun))
.join());
case partitions, changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
case changes -> new ResponseEntity<>(HttpStatusCode.valueOf(404));
};
}
public record PartitionAddRequest(int priority) {}
public enum Resource {
brokers,
partitions,
changes
}
}