├── .gitignore ├── gradle.properties ├── OSSMETADATA ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── scripts ├── deploy-operator.sh ├── get-rabbitmq-password.sh └── tail-operator.sh ├── rabbitmq-operator ├── bin │ └── push-to-local-registry.sh ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── indeed │ │ │ │ └── operators │ │ │ │ └── rabbitmq │ │ │ │ ├── OperatorException.java │ │ │ │ ├── model │ │ │ │ ├── ModelFieldLookups.java │ │ │ │ ├── Labels.java │ │ │ │ └── rabbitmq │ │ │ │ │ ├── RabbitMQUser.java │ │ │ │ │ └── RabbitMQConnectionInfo.java │ │ │ │ ├── reconciliation │ │ │ │ ├── validators │ │ │ │ │ ├── RabbitClusterValidator.java │ │ │ │ │ ├── OperatorPolicyValidator.java │ │ │ │ │ ├── PolicyValidator.java │ │ │ │ │ └── UserValidator.java │ │ │ │ ├── RabbitClusterConfigurationException.java │ │ │ │ ├── lock │ │ │ │ │ └── NamedSemaphores.java │ │ │ │ ├── ClusterReconciliationOrchestrator.java │ │ │ │ ├── Reconciliation.java │ │ │ │ └── rabbitmq │ │ │ │ │ ├── PolicyReconciler.java │ │ │ │ │ ├── OperatorPolicyReconciler.java │ │ │ │ │ └── ShovelReconciler.java │ │ │ │ ├── api │ │ │ │ ├── RabbitManagementApiException.java │ │ │ │ ├── RabbitManagementApiLogger.java │ │ │ │ ├── RabbitMQPasswordConverter.java │ │ │ │ └── RabbitManagementApiProvider.java │ │ │ │ ├── controller │ │ │ │ ├── WaitableResourceController.java │ │ │ │ ├── crd │ │ │ │ │ ├── CustomResourceController.java │ │ │ │ │ ├── RabbitMQResourceController.java │ │ │ │ │ └── NetworkPartitionResourceController.java │ │ │ │ ├── ResourceController.java │ │ │ │ ├── PodController.java │ │ │ │ ├── PodDisruptionBudgetController.java │ │ │ │ ├── PersistentVolumeClaimController.java │ │ │ │ ├── AbstractWaitableResourceController.java │ │ │ │ ├── SecretsController.java │ │ │ │ ├── StatefulSetController.java │ │ │ │ ├── ServicesController.java │ │ │ │ └── AbstractResourceController.java │ │ │ │ ├── Constants.java │ │ │ │ ├── operations │ │ │ │ └── AreQueuesEmptyOperation.java │ │ │ │ ├── config │ │ │ │ ├── AppConfig.java │ │ │ │ ├── RabbitConfig.java │ │ │ │ ├── ControllerConfig.java │ │ │ │ └── ReconcilerConfig.java │ │ │ │ ├── NetworkPartitionWatcher.java │ │ │ │ ├── resources │ │ │ │ ├── RabbitMQPods.java │ │ │ │ ├── RabbitMQContainers.java │ │ │ │ └── RabbitMQSecrets.java │ │ │ │ ├── RabbitMQEventWatcher.java │ │ │ │ ├── RabbitMQOperator.java │ │ │ │ └── executor │ │ │ │ └── ClusterAwareExecutor.java │ │ └── resources │ │ │ └── log4j2-spring.xml │ └── test │ │ └── java │ │ └── com │ │ └── indeed │ │ └── operators │ │ └── rabbitmq │ │ ├── model │ │ ├── api │ │ └── TestRabbitMQPasswordConverter.java │ │ ├── reconciliation │ │ └── validators │ │ │ ├── TestPolicyValidator.java │ │ │ ├── TestOperatorPolicyValidator.java │ │ │ └── TestUserValidator.java │ │ ├── operations │ │ └── TestAreQueuesEmptyOperation.java │ │ └── executor │ │ └── TestClusterAwareExecutor.java ├── Dockerfile └── build.gradle ├── examples ├── network-partition-resource.yaml └── rabbitmq_instance.yaml ├── rabbitmq-operator-model ├── src │ └── main │ │ └── java │ │ └── com │ │ └── indeed │ │ └── operators │ │ └── rabbitmq │ │ └── model │ │ └── crd │ │ ├── rabbitmq │ │ ├── RabbitMQCustomResourceList.java │ │ ├── SourceShovelSpec.java │ │ ├── RabbitMQStorageResources.java │ │ ├── AddressAndVhost.java │ │ ├── DestinationShovelSpec.java │ │ ├── ShovelSpec.java │ │ ├── RabbitMQCustomResource.java │ │ ├── OperatorPolicyDefinitionSpec.java │ │ ├── UserSpec.java │ │ ├── VhostPermissions.java │ │ ├── OperatorPolicySpec.java │ │ ├── VhostOperationPermissions.java │ │ ├── ClusterSpec.java │ │ ├── RabbitMQComputeResources.java │ │ ├── PolicySpec.java │ │ ├── RabbitMQCustomResourceSpec.java │ │ └── PolicyDefinitionSpec.java │ │ └── partition │ │ ├── RabbitMQNetworkPartitionCustomResourceList.java │ │ ├── RabbitMQNetworkPartitionCustomResourceSpec.java │ │ └── RabbitMQNetworkPartitionCustomResource.java └── build.gradle ├── CONTRIBUTING.md ├── release.sh ├── CHANGELOG.md ├── docs ├── logging.html └── storage.html ├── .travis.yml ├── gradlew.bat ├── CODE_OF_CONDUCT.md ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | indeed.oss=1 -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=inactive 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'rabbitmq-operator-model', 'rabbitmq-operator' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/indeedeng/rabbitmq-operator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scripts/deploy-operator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | gradle pushLocalImage && kubectl apply -f ./examples/rabbitmq_operator.yaml 3 | -------------------------------------------------------------------------------- /scripts/get-rabbitmq-password.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | kubectl get secret $1-runtime-secret -o json | jq -r '.data["password"]' | base64 --decode 3 | echo 4 | -------------------------------------------------------------------------------- /rabbitmq-operator/bin/push-to-local-registry.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | docker build -t ${LOCAL_DOCKER_REGISTRY}/rabbitmq-operator:latest . 6 | docker push ${LOCAL_DOCKER_REGISTRY}/rabbitmq-operator:latest -------------------------------------------------------------------------------- /scripts/tail-operator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This assumes that there's only one operator instance 4 | POD=`kubectl get pods --namespace rabbitmqs --no-headers | grep rabbitmq-operator | cut -d ' ' -f 1` 5 | echo "Tailing pod $POD" 6 | kubectl logs -f --namespace rabbitmqs $POD 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 16 15:19:22 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/OperatorException.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq; 2 | 3 | public class OperatorException extends RuntimeException { 4 | public OperatorException(final String description, final Throwable cause) { 5 | super(description, cause); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/model/ModelFieldLookups.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model; 2 | 3 | import io.fabric8.kubernetes.api.model.HasMetadata; 4 | 5 | public class ModelFieldLookups { 6 | 7 | public static String getName(final HasMetadata object) { 8 | return object.getMetadata().getName(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/validators/RabbitClusterValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 4 | 5 | import java.util.List; 6 | 7 | public interface RabbitClusterValidator { 8 | 9 | List validate(ClusterSpec clusterSpec); 10 | } 11 | -------------------------------------------------------------------------------- /examples/network-partition-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: indeed.com/v1alpha1 2 | kind: RabbitMQNetworkPartitionCustomResource 3 | metadata: 4 | name: myrabbitmq-network-partition 5 | namespace: rabbitmqs 6 | spec: 7 | clusterName: myrabbitmq 8 | partitions: 9 | - - myrabbitmq-0 10 | - myrabbitmq-1 11 | - - myrabbitmq-2 12 | drained: [] 13 | serviceName: myrabbitmq-svc-discovery.rabbitmqs.svc.cluster.local -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/api/RabbitManagementApiException.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.api; 2 | 3 | public class RabbitManagementApiException extends RuntimeException { 4 | 5 | RabbitManagementApiException(final String message) { 6 | super(message); 7 | } 8 | 9 | RabbitManagementApiException(final String message, final Throwable t) { 10 | super(message, t); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/RabbitClusterConfigurationException.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation; 2 | 3 | import com.google.common.base.Joiner; 4 | 5 | import java.util.List; 6 | 7 | public class RabbitClusterConfigurationException extends Exception { 8 | 9 | public RabbitClusterConfigurationException(final List errors) { 10 | super(Joiner.on("; ").join(errors)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rabbitmq-operator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11.0.1-jre 2 | RUN groupadd -r -g 999 rabbitmq-operator 3 | RUN useradd --no-log-init -r -u 999 -g rabbitmq-operator rabbitmq-operator 4 | COPY build/distributions/rabbitmq-operator.zip /app/rabbitmq-operator.zip 5 | RUN chown rabbitmq-operator:rabbitmq-operator -R /app 6 | USER rabbitmq-operator 7 | WORKDIR /app 8 | RUN unzip rabbitmq-operator.zip 9 | CMD ["java", "-cp", "rabbitmq-operator/lib/*", "com.indeed.operators.rabbitmq.RabbitMQOperator"] 10 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/RabbitMQCustomResourceList.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import io.fabric8.kubernetes.client.CustomResourceList; 4 | 5 | /** 6 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 7 | */ 8 | public class RabbitMQCustomResourceList extends CustomResourceList { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/WaitableResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public interface WaitableResourceController { 6 | 7 | void waitForReady(String name, String namespace, final long time, final TimeUnit timeUnit) throws InterruptedException; 8 | 9 | void waitForDeletion(String name, String namespace, final long time, final TimeUnit timeUnit) throws InterruptedException; 10 | } 11 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/partition/RabbitMQNetworkPartitionCustomResourceList.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.partition; 2 | 3 | import io.fabric8.kubernetes.client.CustomResourceList; 4 | 5 | /** 6 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 7 | */ 8 | public class RabbitMQNetworkPartitionCustomResourceList extends CustomResourceList { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions! Feel free to help make this better. 4 | 5 | ## Process 6 | 7 | 1. Please open an issue prior to making any changes. Provide a description of what you want to 8 | change and why. 9 | 2. Make your change. If this alters behavior or adds functionality, please add tests to exercise 10 | the new code. Please also add or update documentation as necessary. 11 | 3. Add your change to [CHANGELOG.md](CHANGELOG.md) 12 | 4. Open a pull request. 13 | 5. One of the core team members will review it and provide feedback. -------------------------------------------------------------------------------- /rabbitmq-operator-model/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | repositories { 6 | mavenLocal() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | compile 'io.fabric8:kubernetes-client:4.1.3' 12 | compile 'com.google.guava:guava:27.0.1-jre' 13 | compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.7' 14 | compile 'com.indeed:rabbitmq-admin:1.0.0' 15 | 16 | compileOnly 'io.sundr:builder-annotations:0.14.7' 17 | 18 | annotationProcessor 'io.sundr:builder-annotations:0.14.7' 19 | } 20 | 21 | ext['indeed.publish.name'] = 'rabbitmq-operator-model' -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | docker build -t indeedoss/rabbitmq-operator:latest rabbitmq-operator 5 | 6 | if [ "$TRAVIS_PULL_REQUEST" != "false" ] ; then 7 | echo "built pull request, nothing left to do" 8 | elif [[ ! -z "$TRAVIS_TAG" ]]; then 9 | echo "tag and push docker" 10 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 11 | docker tag indeedoss/rabbitmq-operator:latest indeedoss/rabbitmq-operator:$TRAVIS_TAG 12 | docker push indeedoss/rabbitmq-operator:latest 13 | docker push indeedoss/rabbitmq-operator:$TRAVIS_TAG 14 | else 15 | echo "nothing to do" 16 | fi 17 | 18 | docker images -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/crd/CustomResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller.crd; 2 | 3 | import io.fabric8.kubernetes.client.CustomResource; 4 | import io.fabric8.kubernetes.client.Watch; 5 | import io.fabric8.kubernetes.client.Watcher; 6 | 7 | import java.util.List; 8 | 9 | public interface CustomResourceController { 10 | 11 | T get(String name, String namespace); 12 | 13 | boolean delete(T resource); 14 | 15 | void patch(T resource); 16 | 17 | Watch watch(Watcher watcher, String namespace); 18 | 19 | List getAll(String namespace); 20 | } 21 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/ResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.HasMetadata; 4 | import io.fabric8.kubernetes.client.Watch; 5 | import io.fabric8.kubernetes.client.Watcher; 6 | 7 | import java.util.List; 8 | 9 | public interface ResourceController { 10 | 11 | T createOrUpdate(T resource); 12 | 13 | T get(String name, String namespace); 14 | 15 | boolean delete(String name, String namespace); 16 | 17 | T patch(T resource); 18 | 19 | Watch watch(Watcher watcher, String namespace); 20 | 21 | List getAll(String namespace); 22 | } 23 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/api/RabbitManagementApiLogger.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.api; 2 | 3 | import okhttp3.Interceptor; 4 | import okhttp3.Request; 5 | import okhttp3.Response; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.IOException; 10 | 11 | public class RabbitManagementApiLogger implements Interceptor { 12 | 13 | private final Logger log = LoggerFactory.getLogger(RabbitManagementApiLogger.class); 14 | 15 | @Override 16 | public Response intercept(final Chain chain) throws IOException { 17 | final Request req = chain.request(); 18 | 19 | log.debug("Executing {} call to {}", req.method(), req.url().toString()); 20 | 21 | return chain.proceed(req); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | #### 0.0.6 4 | Bugs 5 | ``` 6 | ``` 7 | 8 | Improvements 9 | ``` 10 | ``` 11 | 12 | New Features 13 | ``` 14 | ``` 15 | 16 | #### 0.0.5 17 | Bugs 18 | ``` 19 | #66 - Remove Indeed-specific flags from the Dockerfile 20 | ``` 21 | 22 | Improvements 23 | ``` 24 | ``` 25 | 26 | New Features 27 | ``` 28 | ``` 29 | 30 | #### 0.0.4 31 | 32 | Bugs 33 | ``` 34 | ``` 35 | 36 | Improvements 37 | ``` 38 | ``` 39 | 40 | New Features 41 | ``` 42 | #61 - Added createNodePort to spec that triggers creation of a NodePort service 43 | ``` 44 | 45 | #### 0.0.3 46 | 47 | Bugs 48 | ``` 49 | ``` 50 | 51 | Improvements 52 | ``` 53 | #36 - Improve preconditions and validation prior to interacting with Rabbit API 54 | ``` 55 | 56 | New Features 57 | ``` 58 | #58 - Add CHANGELOG.md 59 | ``` 60 | 61 | 62 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/lock/NamedSemaphores.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.lock; 2 | 3 | import com.google.common.cache.CacheBuilder; 4 | import com.google.common.cache.CacheLoader; 5 | import com.google.common.cache.LoadingCache; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.concurrent.Semaphore; 9 | 10 | public class NamedSemaphores { 11 | 12 | private final LoadingCache locks = CacheBuilder.newBuilder() 13 | .build(new CacheLoader() { 14 | @Override 15 | public Semaphore load(@Nonnull final String key) { 16 | return new Semaphore(1); 17 | } 18 | }); 19 | 20 | public Semaphore getSemaphore(final String lockName) { 21 | return locks.getUnchecked(lockName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/logging.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logging Configuration 5 | 6 | 7 |

Overview

8 |

This project uses Log4j 2 for logging. Logging is configured through the rabbitmq-operator/src/main/resources/log4j2-spring.xml file. This file is compiled in and is accessed from the classpath during operation.

9 | 10 |

To avoid needing to recompile to change logging levels, we make use of XInclude and a ConfigMap. An example of this is included in the provided rabbitmq-operator/examples/rabbitmq_operator.yaml file.

11 | 12 |

To pick up changes made to the ConfigMap, follow this procedure:

13 |
    14 |
  1. Edit the ConfigMap.
  2. 15 |
  3. Apply the change via kubectl.
  4. 16 |
  5. Restart the operator.
  6. 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/PodController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.DoneablePod; 4 | import io.fabric8.kubernetes.api.model.Pod; 5 | import io.fabric8.kubernetes.api.model.PodList; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 8 | import io.fabric8.kubernetes.client.dsl.PodResource; 9 | 10 | import java.util.Map; 11 | 12 | public class PodController extends AbstractWaitableResourceController> { 13 | 14 | public PodController( 15 | final KubernetesClient client, 16 | final Map labelsToWatch 17 | ) { 18 | super(client, labelsToWatch, Pod.class); 19 | } 20 | 21 | @Override 22 | protected MixedOperation> operation() { 23 | return getClient().pods(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/resources/log4j2-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/Constants.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq; 2 | 3 | public class Constants { 4 | 5 | public static final String RABBITMQ_CRD_NAME = "rabbitmqs.indeed.com"; 6 | public static final String RABBITMQ_NETWORK_PARTITION_CRD_NAME = "rabbitmqnetworkpartitions.indeed.com"; 7 | 8 | public static final String RABBITMQ_STORAGE_NAME = "rabbitmq-storage"; 9 | 10 | public static final String DEFAULT_USERNAME = "rabbit"; 11 | 12 | public static class Ports { 13 | public static final String EPMD = "epmd"; 14 | public static final String AMQP = "amqp"; 15 | public static final String MANAGEMENT = "management"; 16 | 17 | public static final int EPMD_PORT = 4369; 18 | public static final int AMQP_PORT = 5672; 19 | public static final int MANAGEMENT_PORT = 15672; 20 | } 21 | 22 | public static class Secrets { 23 | public static final String USERNAME_KEY = "username"; 24 | public static final String PASSWORD_KEY = "password"; 25 | public static final String ERLANG_COOKIE_KEY = "erlang-cookie"; 26 | } 27 | 28 | public static class Uris { 29 | public static final String AMQP_BASE = "amqp://"; 30 | } 31 | } -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/PodDisruptionBudgetController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.policy.DoneablePodDisruptionBudget; 4 | import io.fabric8.kubernetes.api.model.policy.PodDisruptionBudget; 5 | import io.fabric8.kubernetes.api.model.policy.PodDisruptionBudgetList; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 8 | import io.fabric8.kubernetes.client.dsl.Resource; 9 | 10 | import java.util.Map; 11 | 12 | public class PodDisruptionBudgetController extends AbstractResourceController> { 13 | 14 | public PodDisruptionBudgetController( 15 | final KubernetesClient client, 16 | final Map labelsToWatch 17 | ) { 18 | super(client, labelsToWatch, PodDisruptionBudget.class); 19 | } 20 | 21 | @Override 22 | protected MixedOperation> operation() { 23 | return getClient().policy().podDisruptionBudget(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/PersistentVolumeClaimController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.DoneablePersistentVolumeClaim; 4 | import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; 5 | import io.fabric8.kubernetes.api.model.PersistentVolumeClaimList; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 8 | import io.fabric8.kubernetes.client.dsl.Resource; 9 | 10 | import java.util.Map; 11 | 12 | public class PersistentVolumeClaimController extends AbstractResourceController> { 13 | 14 | public PersistentVolumeClaimController( 15 | final KubernetesClient client, 16 | final Map labelsToWatch 17 | ) { 18 | super(client, labelsToWatch, PersistentVolumeClaim.class); 19 | } 20 | 21 | @Override 22 | protected MixedOperation> operation() { 23 | return getClient().persistentVolumeClaims(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/SourceShovelSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.base.Preconditions; 9 | import com.google.common.base.Strings; 10 | 11 | @JsonPropertyOrder({"queue", "vhost"}) 12 | @JsonDeserialize(using = JsonDeserializer.None.class) 13 | public class SourceShovelSpec { 14 | 15 | private final String queue; 16 | private final String vhost; 17 | 18 | @JsonCreator 19 | public SourceShovelSpec( 20 | @JsonProperty("queue") final String queue, 21 | @JsonProperty("vhost") final String vhost 22 | ) { 23 | Preconditions.checkArgument(!Strings.isNullOrEmpty(queue), "Shovel source 'queue' must not be empty or null"); 24 | Preconditions.checkArgument(!Strings.isNullOrEmpty(queue), "Shovel source 'vhost' must not be empty or null"); 25 | this.queue = queue; 26 | this.vhost = vhost; 27 | } 28 | 29 | public String getQueue() { 30 | return queue; 31 | } 32 | 33 | public String getVhost() { 34 | return vhost; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/validators/OperatorPolicyValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.OperatorPolicySpec; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.regex.Pattern; 9 | import java.util.regex.PatternSyntaxException; 10 | 11 | public class OperatorPolicyValidator implements RabbitClusterValidator { 12 | @Override 13 | public List validate(final ClusterSpec clusterSpec) { 14 | final List policies = clusterSpec.getOperatorPolicies(); 15 | 16 | final List errors = new ArrayList<>(); 17 | 18 | policies.forEach(policy -> { 19 | try { 20 | Pattern.compile(policy.getPattern()); 21 | } catch (final PatternSyntaxException ex) { 22 | errors.add(String.format("Invalid pattern for policy %s: %s", policy.getName(), ex.getMessage())); 23 | } 24 | 25 | if (!policy.getApplyTo().equals("queues")) { 26 | errors.add(String.format("Operator policy applyTo value must be 'queues', but operator policy %s had applyTo: %s", policy.getName(), policy.getApplyTo())); 27 | } 28 | }); 29 | 30 | return errors; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/model: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import io.fabric8.kubernetes.api.model.HasMetadata; 5 | import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; 6 | import io.fabric8.kubernetes.api.model.PodBuilder; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.Map; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class TestLabels { 15 | 16 | @Test 17 | public void testGetIndeedLabels() { 18 | final Map labels = ImmutableMap.builder() 19 | .put("notindeed.com/label1", "label1") 20 | .put("indeed.com/label2", "label2") 21 | .put(" indeed.com/label3", "label3") 22 | .put("Indeed.com/label4", "label4") 23 | .put("indeed.comcom/label5", "label5") 24 | .build(); 25 | 26 | final HasMetadata obj = new PodBuilder().withMetadata(new ObjectMetaBuilder().withLabels(labels).build()).build(); 27 | 28 | final Map indeedLabels = Labels.Indeed.getIndeedLabels(obj); 29 | 30 | assertEquals(1, indeedLabels.size()); 31 | assertTrue(indeedLabels.containsKey("indeed.com/label2")); 32 | assertEquals("label2", indeedLabels.get("indeed.com/label2")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/validators/PolicyValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.PolicySpec; 5 | import com.indeed.rabbitmq.admin.pojo.Policy; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.regex.Pattern; 10 | import java.util.regex.PatternSyntaxException; 11 | 12 | public class PolicyValidator implements RabbitClusterValidator { 13 | @Override 14 | public List validate(final ClusterSpec clusterSpec) { 15 | final List policies = clusterSpec.getPolicies(); 16 | 17 | final List errors = new ArrayList<>(); 18 | 19 | policies.forEach(policy -> { 20 | try { 21 | Pattern.compile(policy.getPattern()); 22 | } catch (final PatternSyntaxException ex) { 23 | errors.add(String.format("Invalid pattern for policy %s: %s", policy.getName(), ex.getMessage())); 24 | } 25 | 26 | try { 27 | Policy.ApplyTo.fromValue(policy.getApplyTo()); 28 | } catch (final IllegalArgumentException ex) { 29 | errors.add(String.format("Invalid applyTo for policy %s: %s", policy.getName(), ex.getMessage())); 30 | } 31 | }); 32 | 33 | return errors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/RabbitMQStorageResources.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import io.fabric8.kubernetes.api.model.Quantity; 9 | import io.sundr.builder.annotations.Buildable; 10 | import io.sundr.builder.annotations.BuildableReference; 11 | 12 | @Buildable( 13 | builderPackage = "io.fabric8.kubernetes.api.builder", 14 | editableEnabled = false 15 | ) 16 | @JsonPropertyOrder({"storageClassName", "storage"}) 17 | @JsonDeserialize(using = JsonDeserializer.None.class) 18 | public class RabbitMQStorageResources { 19 | private final String storageClassName; 20 | private final Quantity storage; 21 | 22 | @JsonCreator 23 | public RabbitMQStorageResources( 24 | @JsonProperty("storageClassName") final String storageClassName, 25 | @JsonProperty("limit") final Quantity storage 26 | ) { 27 | 28 | this.storageClassName = storageClassName; 29 | this.storage = storage; 30 | } 31 | 32 | public String getStorageClassName() { 33 | return storageClassName; 34 | } 35 | 36 | public Quantity getStorage() { 37 | return storage; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: true 3 | jdk: 4 | - oraclejdk8 5 | services: 6 | - docker 7 | script: 8 | - "./gradlew build" 9 | - "./release.sh" 10 | env: 11 | global: 12 | - secure: aI+YbniPJLA7JGyH6oe08AQyNnj+rBxWY79venoG76F7you5hr7jFj+BIPoIi6LJAuNUdlDAakfabhBnyIQYiCoj1YQoCDO1O7ujlwuE0v3WZKibh5XnJVamMtDqYAzGMyE+azCGIQMzA5Jhno6wFxU37ZZLxdkHLfslY69v8MUA+W6SlWd0Fcqp2zAsKsWCFNj7mtDN8s+uvm9nwRyTCwLY3HhGyGAFUZ7NM6omUol/na+WaskGDGq+rSEyTiHGScza0z9S+JYaT6DF6ukCco/Tiu6g/wc6tNHB3FnsByoiqnhqqBoOmh++Pvx+ialgeMURDaBt30VyxlGyorvgUHjG0Op2k5i1dZQxGU7mes0Qee5ujCmmUXJmFXLc0/s5OfoIOP5vEbEKf1U2dtBMTTUIx6mi+V6XrWgJ8kR19qNHg0qFOxOydN27bVZ5xJ5mLjbTe4hbci9/0xUXClrq6Ngn1vXdqX1mHL0K6cAIHZL8Y5IfbR2Nf80KstMJKguj8J7mw7kOOl4Tw/f9dDUP7oCfXWaloO//NY9vIMUkDaHvDcxbB5AE56igua76OImWP1vLwMMFeScYQ7fYzp0hybdAv8m5AlrHwZXE/3+4253UydiCcPSRvGhndeH0YLAh4JaeW3s9FOIZn8xryXVenBGK0vXcfi9WsgpFkCk2nag= 13 | - secure: FQzYX68rtBUCMHv3EKBKoeicVSG59h136ifMYWG5NEqzqMQTw/MKEuXvibINy4HaQz8/q470cPxOH+wWUJ/Vt2TXO/RddALXa1ulaScJmvSjFUzmkNPpDOy0+i1XOhXgdmQcmxOUCvGE+QY9aVSj/G4kbwln9rhdijWBoJF+oOqOX4jJFu/vUbUO9lRjaJmUyLyO8qdM+DWM9v+yt0P86BHLT0g2SJ+JJzZIzpTDbOyznP4FercX9+zsvL/Awm3JEuJhUbrynHtN/aMYVRG2k0ptuTui/ILfBl+oT8DAk6B/Rc5faR/4r9N3qxMQ5Y61Vc9ma5fWuUrnnbyyO5jsINsLOus88nPl0W/z+7/9Ztx1vPK/mJeamc7TrBJJhvCvckTmh+RN5KNX24ufp+kNoZ4LEmvuMKe2B8iQyeCPsxh0Cglz5X2a3XFVzXqXtH6PlPUSULp/hmR5W23rWw+vpjaFv54guFq+LAcgUjNVOOhBxvjSGkTq5dK6pNRLLEqjpRIyCOF7XTbnQRGPbRuInCy36Ch+LazQhjtFgkIaWRr9OBxEoNyUv5MER7pOshs3Ees6MpQxcd2XQMyjyqfPRvufz01nJMgt/bDoQ//ovhhdVh4rDtXKny3ZBS27ImEvnRUFVV86sKB7zEi3cEM3LFA8jFE4FbjBVfhuRijodiA= 14 | -------------------------------------------------------------------------------- /rabbitmq-operator/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'application' 4 | id 'idea' 5 | id 'net.ltgt.apt-idea' version "0.20" 6 | } 7 | 8 | repositories { 9 | mavenLocal() 10 | mavenCentral() 11 | jcenter() 12 | } 13 | 14 | dependencies { 15 | compile project(':rabbitmq-operator-model') 16 | 17 | compile 'com.indeed:rabbitmq-admin:1.0.1' 18 | compile 'io.fabric8:kubernetes-client:4.1.3' 19 | compile 'com.google.guava:guava:27.0.1-jre' 20 | compile 'org.slf4j:slf4j-api' 21 | compile 'org.apache.commons:commons-collections4:4.2' 22 | compile 'org.apache.commons:commons-text:1.6' 23 | compile 'com.fasterxml.jackson.core:jackson-core:2.9.7' 24 | compile 'com.fasterxml.jackson.core:jackson-databind:2.9.7' 25 | compile('org.springframework.boot:spring-boot-starter:2.1.1.RELEASE') { 26 | exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' 27 | } 28 | compile 'org.springframework.boot:spring-boot-starter-log4j2:2.1.1.RELEASE' 29 | 30 | testCompile 'com.squareup.okhttp3:mockwebserver:3.14.1' 31 | testCompile 'org.mockito:mockito-core:2.24.0' 32 | testCompile 'org.mockito:mockito-junit-jupiter:2.24.0' 33 | testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.0' 34 | 35 | testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.4.0' 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | mainClassName = 'com.indeed.operators.rabbitmq.RabbitMQOperator' 43 | 44 | task pushLocalImage(type: Exec) { 45 | dependsOn 'build' 46 | commandLine 'bin/push-to-local-registry.sh' 47 | } -------------------------------------------------------------------------------- /examples/rabbitmq_instance.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: indeed.com/v1alpha1 2 | kind: RabbitMQCustomResource 3 | metadata: 4 | name: myrabbitmq 5 | namespace: rabbitmqs 6 | spec: 7 | rabbitMQImage: "rabbitmq:3.7.8-alpine" 8 | initContainerImage: "busybox:latest" 9 | createLoadBalancer: true 10 | replicas: 3 11 | compute: 12 | cpuRequest: "300m" 13 | memory: "512Mi" 14 | storage: 15 | storageClassName: rook-ceph-block 16 | limit: "1Gi" 17 | clusterSpec: 18 | highWatermarkFraction: 0.4 19 | policies: 20 | - name: "mypolicy" 21 | vhost: "/" 22 | pattern: ".*" 23 | applyTo: "queues" 24 | definition: 25 | ha-mode: "exactly" 26 | ha-params: 2 27 | ha-sync-mode: "automatic" 28 | operatorPolicies: 29 | - name: "myoperatorpolicy" 30 | vhost: "/" 31 | pattern: ".*" 32 | applyTo: "queues" 33 | definition: 34 | max-length: 1000 35 | shovels: 36 | - name: "myshovel" 37 | source: 38 | queue: "myqueue" 39 | vhost: "/" 40 | destination: 41 | addresses: 42 | - address: "myrabbitmq-svc" # shovel messages from this cluster to itself 43 | vhost: "/" 44 | secretName: "mynewadmin-myrabbitmq-user-secret" # user created below 45 | secretNamespace: "rabbitmqs" 46 | users: 47 | - username: "mynewadmin" 48 | tags: 49 | - "administrator" 50 | vhosts: 51 | - vhostName: "/" 52 | permissions: 53 | configure: ".*" 54 | write: ".*" 55 | read: ".*" -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/AddressAndVhost.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.common.base.Preconditions; 7 | import com.google.common.base.Strings; 8 | 9 | import java.io.UnsupportedEncodingException; 10 | import java.net.URLEncoder; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | public class AddressAndVhost { 14 | 15 | private final String address; 16 | private final String vhost; 17 | 18 | @JsonCreator 19 | public AddressAndVhost( 20 | @JsonProperty("address") final String address, 21 | @JsonProperty("vhost") final String vhost 22 | ) { 23 | Preconditions.checkArgument(!Strings.isNullOrEmpty(address), "'address' cannot be empty or null"); 24 | Preconditions.checkArgument(!Strings.isNullOrEmpty(address), "'vhost' cannot be empty or null"); 25 | 26 | this.address = address; 27 | this.vhost = vhost; 28 | } 29 | 30 | public String getAddress() { 31 | return address; 32 | } 33 | 34 | public String getVhost() { 35 | return vhost; 36 | } 37 | 38 | @JsonIgnore 39 | public String asRabbitUri() { 40 | try { 41 | return String.format("%s/%s", address, URLEncoder.encode(vhost, StandardCharsets.UTF_8.name())); 42 | } catch (final UnsupportedEncodingException e) { 43 | throw new RuntimeException(e); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/operations/AreQueuesEmptyOperation.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.operations; 2 | 3 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiFacade; 4 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 5 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQConnectionInfo; 6 | import com.indeed.rabbitmq.admin.pojo.Queue; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | public class AreQueuesEmptyOperation { 14 | private static final Logger log = LoggerFactory.getLogger(AreQueuesEmptyOperation.class); 15 | 16 | private final RabbitManagementApiProvider rabbitManagementApiProvider; 17 | 18 | public AreQueuesEmptyOperation( 19 | final RabbitManagementApiProvider rabbitManagementApiProvider 20 | ) { 21 | this.rabbitManagementApiProvider = rabbitManagementApiProvider; 22 | } 23 | 24 | public boolean execute(final RabbitMQConnectionInfo connectionInfo) { 25 | final RabbitManagementApiFacade api = rabbitManagementApiProvider.getApi(connectionInfo); 26 | final List queueStates; 27 | try { 28 | queueStates = api.listQueues(); 29 | } catch (final Exception e) { 30 | throw new RuntimeException(e); 31 | } 32 | 33 | final List nonEmptyQueues = queueStates.stream() 34 | .filter(queue -> queue.getMessages() > 0) 35 | .map(Queue::getName) 36 | .collect(Collectors.toList()); 37 | 38 | log.info("Non-empty queues: {}", nonEmptyQueues); 39 | 40 | return nonEmptyQueues.isEmpty(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/DestinationShovelSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.google.common.base.Preconditions; 7 | import com.google.common.base.Strings; 8 | 9 | import java.util.List; 10 | 11 | @JsonPropertyOrder({"addresses", "clusterName"}) 12 | public class DestinationShovelSpec { 13 | 14 | private final List addresses; 15 | private final String secretName; 16 | private final String secretNamespace; 17 | 18 | @JsonCreator 19 | public DestinationShovelSpec( 20 | @JsonProperty("addresses") final List addresses, 21 | @JsonProperty("secretName") final String secretName, 22 | @JsonProperty("secretNamespace") final String secretNamespace 23 | ) { 24 | Preconditions.checkArgument(addresses != null && !addresses.isEmpty(), "Shovel destination 'addresses' cannot be empty or null"); 25 | Preconditions.checkArgument(!Strings.isNullOrEmpty(secretName), "Shovel destination 'secretName' cannot be empty or null"); 26 | Preconditions.checkArgument(!Strings.isNullOrEmpty(secretNamespace), "Shovel destination 'secretNamespace' cannot be empty or null"); 27 | 28 | this.addresses = addresses; 29 | this.secretName = secretName; 30 | this.secretNamespace = secretNamespace; 31 | } 32 | 33 | public List getAddresses() { 34 | return addresses; 35 | } 36 | 37 | public String getSecretName() { 38 | return secretName; 39 | } 40 | 41 | public String getSecretNamespace() { 42 | return secretNamespace; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/model/Labels.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model; 2 | 3 | import com.google.common.collect.Maps; 4 | import io.fabric8.kubernetes.api.model.HasMetadata; 5 | 6 | import java.util.Collections; 7 | import java.util.Map; 8 | 9 | public class Labels { 10 | 11 | public static class Indeed { 12 | public static final String INDEED_PREFIX = "indeed.com/"; 13 | public static final String LOCKED_BY = INDEED_PREFIX + "locked-by"; 14 | 15 | private Indeed() {} 16 | 17 | public static Map getIndeedLabels(final HasMetadata object) { 18 | if (object.getMetadata().getLabels() == null || object.getMetadata().getLabels().isEmpty()) { 19 | return Collections.emptyMap(); 20 | } 21 | 22 | final Map indeedLabels = Maps.newHashMap(); 23 | for (final Map.Entry entry : object.getMetadata().getLabels().entrySet()) { 24 | if (entry.getKey().startsWith(INDEED_PREFIX)) { 25 | indeedLabels.put(entry.getKey(), entry.getValue()); 26 | } 27 | } 28 | 29 | return indeedLabels; 30 | } 31 | } 32 | 33 | public static class Kubernetes { 34 | public static final String KUBERNETES_PREFIX = "app.kubernetes.io/"; 35 | public static final String PART_OF = KUBERNETES_PREFIX + "part-of"; 36 | public static final String MANAGED_BY = KUBERNETES_PREFIX + "managed-by"; 37 | public static final String INSTANCE = KUBERNETES_PREFIX + "instance"; 38 | 39 | private Kubernetes() {} 40 | } 41 | 42 | public static class Values { 43 | 44 | public static final String RABBITMQ = "rabbitmq"; 45 | public static final String RABBITMQ_OPERATOR = "rabbitmq-operator"; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/ClusterReconciliationOrchestrator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation; 2 | 3 | import com.indeed.operators.rabbitmq.executor.ClusterAwareExecutor; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.slf4j.MDC; 7 | 8 | import java.util.function.Consumer; 9 | 10 | public class ClusterReconciliationOrchestrator { 11 | private static final Logger log = LoggerFactory.getLogger(ClusterReconciliationOrchestrator.class); 12 | 13 | private final ClusterAwareExecutor executor; 14 | 15 | public ClusterReconciliationOrchestrator( 16 | final ClusterAwareExecutor executor 17 | ) { 18 | this.executor = executor; 19 | } 20 | 21 | public void queueReconciliation(final Reconciliation reconciliation, final Consumer runner) { 22 | log.info("Queueing reconciliation {}", reconciliation); 23 | executor.submit(reconciliation.getClusterName(), reconciliation.getType(), () -> { 24 | MDC.put("clusterName", reconciliation.getClusterName()); 25 | MDC.put("namespace", reconciliation.getNamespace()); 26 | MDC.put("resourceName", reconciliation.getResourceName()); 27 | MDC.put("type", reconciliation.getType()); 28 | 29 | try { 30 | runner.accept(reconciliation); 31 | } catch (final Throwable t) { 32 | log.error("There was an error during reconciliation that the reconciler didn't handle", t); 33 | } finally { 34 | MDC.remove("type"); 35 | MDC.remove("resourceName"); 36 | MDC.remove("namespace"); 37 | MDC.remove("clusterName"); 38 | } 39 | }); 40 | 41 | log.info("Reconciliation {} successfully queued", reconciliation); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/Reconciliation.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation; 2 | 3 | import com.google.common.base.Objects; 4 | 5 | public class Reconciliation { 6 | 7 | private final String resourceName; 8 | private final String clusterName; 9 | private final String namespace; 10 | private final String type; 11 | 12 | public Reconciliation(final String resourceName, final String clusterName, final String namespace, final String type) { 13 | this.resourceName = resourceName; 14 | this.clusterName = clusterName; 15 | this.namespace = namespace; 16 | this.type = type; 17 | } 18 | 19 | public String getResourceName() { 20 | return resourceName; 21 | } 22 | 23 | public String getClusterName() { 24 | return clusterName; 25 | } 26 | 27 | public String getNamespace() { 28 | return namespace; 29 | } 30 | 31 | public String getType() { 32 | return type; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return String.format("{ resourceName: [%s], clusterName: [%s], namespace: [%s], type: [%s]", resourceName, clusterName, namespace, type); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) return true; 43 | if (o == null || getClass() != o.getClass()) return false; 44 | Reconciliation that = (Reconciliation) o; 45 | return Objects.equal(resourceName, that.resourceName) && 46 | Objects.equal(clusterName, that.clusterName) && 47 | Objects.equal(namespace, that.namespace) && 48 | Objects.equal(type, that.type); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hashCode(resourceName, clusterName, namespace, type); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/ShovelSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.base.Preconditions; 9 | import com.google.common.base.Strings; 10 | import io.sundr.builder.annotations.Buildable; 11 | 12 | @Buildable( 13 | builderPackage = "io.fabric8.kubernetes.api.builder", 14 | editableEnabled = false 15 | ) 16 | @JsonPropertyOrder({"name", "source", "destination"}) 17 | @JsonDeserialize(using = JsonDeserializer.None.class) 18 | public class ShovelSpec { 19 | 20 | private final String name; 21 | private final SourceShovelSpec source; 22 | private final DestinationShovelSpec destination; 23 | 24 | @JsonCreator 25 | public ShovelSpec( 26 | @JsonProperty("name") final String name, 27 | @JsonProperty("source") final SourceShovelSpec source, 28 | @JsonProperty("destination") final DestinationShovelSpec destination 29 | ) { 30 | Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Shovel 'name' cannot be empty or null"); 31 | 32 | this.name = name; 33 | this.source = Preconditions.checkNotNull(source, "Shovel 'source' cannot be null"); 34 | this.destination = Preconditions.checkNotNull(destination, "Shovel 'destination' cannot be null"); 35 | } 36 | 37 | public String getName() { 38 | return name; 39 | } 40 | 41 | public SourceShovelSpec getSource() { 42 | return source; 43 | } 44 | 45 | public DestinationShovelSpec getDestination() { 46 | return destination; 47 | } 48 | } -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/model/rabbitmq/RabbitMQUser.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.rabbitmq; 2 | 3 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.VhostPermissions; 4 | import io.fabric8.kubernetes.api.model.ObjectMeta; 5 | import io.fabric8.kubernetes.api.model.OwnerReference; 6 | import io.fabric8.kubernetes.api.model.Secret; 7 | 8 | import java.util.List; 9 | 10 | public class RabbitMQUser { 11 | 12 | private final String username; 13 | private final Secret userSecret; 14 | private final ObjectMeta clusterMetadata; 15 | private final OwnerReference clusterReference; 16 | private final List vhostPermissions; 17 | private final List tags; 18 | 19 | public RabbitMQUser( 20 | final String username, 21 | final Secret userSecret, 22 | final ObjectMeta clusterMetadata, 23 | final OwnerReference clusterReference, 24 | final List vhostPermissions, 25 | final List tags 26 | ) { 27 | this.username = username; 28 | this.userSecret = userSecret; 29 | this.clusterMetadata = clusterMetadata; 30 | this.clusterReference = clusterReference; 31 | this.vhostPermissions = vhostPermissions; 32 | this.tags = tags; 33 | } 34 | 35 | public String getUsername() { 36 | return username; 37 | } 38 | 39 | public Secret getUserSecret() { 40 | return userSecret; 41 | } 42 | 43 | public ObjectMeta getClusterMetadata() { 44 | return clusterMetadata; 45 | } 46 | 47 | public OwnerReference getClusterReference() { 48 | return clusterReference; 49 | } 50 | 51 | public List getVhostPermissions() { 52 | return vhostPermissions; 53 | } 54 | 55 | public List getTags() { 56 | return tags; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/RabbitMQCustomResource.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 10 | import io.fabric8.kubernetes.api.model.Doneable; 11 | import io.fabric8.kubernetes.api.model.ObjectMeta; 12 | import io.fabric8.kubernetes.client.CustomResource; 13 | import io.sundr.builder.annotations.Buildable; 14 | import io.sundr.builder.annotations.BuildableReference; 15 | import io.sundr.builder.annotations.Inline; 16 | 17 | /** 18 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 19 | */ 20 | @Buildable( 21 | builderPackage = "io.fabric8.kubernetes.api.builder", 22 | inline = @Inline(type = Doneable.class, prefix = "Doneable", value = "done"), 23 | editableEnabled = false, 24 | refs = @BuildableReference(CustomResource.class) 25 | ) 26 | @JsonDeserialize(using = JsonDeserializer.None.class) 27 | @JsonInclude(JsonInclude.Include.NON_NULL) 28 | @JsonPropertyOrder({"apiVersion", "kind", "metadata", "spec"}) 29 | public class RabbitMQCustomResource extends CustomResource { 30 | private RabbitMQCustomResourceSpec spec; 31 | 32 | @JsonCreator 33 | public RabbitMQCustomResource( 34 | @JsonProperty("spec") final RabbitMQCustomResourceSpec spec 35 | ) { 36 | this.spec = spec; 37 | } 38 | 39 | public RabbitMQCustomResourceSpec getSpec() { 40 | return spec; 41 | } 42 | 43 | @JsonIgnore 44 | public String getName() { 45 | return this.getMetadata().getName(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/crd/RabbitMQResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller.crd; 2 | 3 | import com.indeed.operators.rabbitmq.controller.AbstractResourceController; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.DoneableRabbitMQCustomResource; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQCustomResource; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQCustomResourceList; 7 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 8 | import io.fabric8.kubernetes.client.KubernetesClient; 9 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 10 | import io.fabric8.kubernetes.client.dsl.Resource; 11 | 12 | import java.util.Map; 13 | 14 | import static com.indeed.operators.rabbitmq.Constants.RABBITMQ_CRD_NAME; 15 | 16 | public class RabbitMQResourceController extends AbstractResourceController> { 17 | 18 | public RabbitMQResourceController( 19 | final KubernetesClient client, 20 | final Map labelsToWatch 21 | ) { 22 | super(client, labelsToWatch, RabbitMQCustomResource.class); 23 | } 24 | 25 | @Override 26 | protected MixedOperation> operation() { 27 | final CustomResourceDefinition rabbitCrd = getClient().customResourceDefinitions().withName(RABBITMQ_CRD_NAME).get(); 28 | 29 | if (rabbitCrd == null) { 30 | throw new RuntimeException(String.format("CustomResourceDefinition %s has not been defined", RABBITMQ_CRD_NAME)); 31 | } 32 | 33 | return getClient().customResources(rabbitCrd, RabbitMQCustomResource.class, RabbitMQCustomResourceList.class, DoneableRabbitMQCustomResource.class); 34 | } 35 | } -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/AbstractWaitableResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.Doneable; 4 | import io.fabric8.kubernetes.api.model.HasMetadata; 5 | import io.fabric8.kubernetes.api.model.KubernetesResourceList; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import io.fabric8.kubernetes.client.dsl.Resource; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.Map; 12 | import java.util.Objects; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public abstract class AbstractWaitableResourceController, R extends Resource> extends AbstractResourceController implements WaitableResourceController { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(AbstractWaitableResourceController.class); 18 | 19 | protected AbstractWaitableResourceController( 20 | final KubernetesClient client, 21 | final Map labelsToWatch, 22 | final Class resourceType 23 | ) { 24 | super(client, labelsToWatch, resourceType); 25 | } 26 | 27 | @Override 28 | public void waitForReady(final String name, final String namespace, final long time, final TimeUnit timeUnit) throws InterruptedException { 29 | log.info("Waiting {} {} for resource of type {} with name {} to be ready", time, timeUnit, getResourceType(), name); 30 | operation().inNamespace(namespace).withName(name).waitUntilReady(time, timeUnit); 31 | } 32 | 33 | @Override 34 | public void waitForDeletion(final String name, final String namespace, final long time, final TimeUnit timeUnit) throws InterruptedException { 35 | log.info("Waiting {} {} for resource of type {} with name {} to be deleted", time, timeUnit, getResourceType(), name); 36 | operation().inNamespace(namespace).withName(name).waitUntilCondition(Objects::isNull, time, timeUnit); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/api/TestRabbitMQPasswordConverter.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.api; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import java.security.MessageDigest; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.util.Base64; 11 | import java.util.Random; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertFalse; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | @ExtendWith(MockitoExtension.class) 18 | public class TestRabbitMQPasswordConverter { 19 | 20 | private static final String HASH_FROM_RABBIT = "awdOVA7vfvoxZZnftimygbY4kfrvQdv9vlESjpvmMQooWDV7"; 21 | private static final String PASSWORD = "rJCW15GyswdLMXOSo4fLvgMykf6Q3Q"; 22 | private static final int SALT = 1795640916; // first four bytes of HASH_FROM_RABBIT 23 | 24 | private RabbitMQPasswordConverter converter; 25 | 26 | @BeforeEach 27 | private void setup() throws NoSuchAlgorithmException { 28 | final Random myRandom = new Random() { 29 | @Override 30 | public int nextInt() { 31 | return SALT; 32 | } 33 | }; 34 | 35 | converter = new RabbitMQPasswordConverter(myRandom, MessageDigest.getInstance("SHA-256"), Base64.getEncoder(), Base64.getDecoder()); 36 | } 37 | 38 | @Test 39 | public void testConvertPasswordToHash() { 40 | final String hash = converter.convertPasswordToHash(PASSWORD); 41 | 42 | assertEquals(HASH_FROM_RABBIT, hash); 43 | } 44 | 45 | @Test 46 | public void testPasswordMatchesHashMatches() { 47 | assertTrue(converter.passwordMatchesHash(PASSWORD, HASH_FROM_RABBIT)); 48 | } 49 | 50 | @Test 51 | public void testPasswordMatchesHashDoesNotMatch() { 52 | assertFalse(converter.passwordMatchesHash(PASSWORD, "awdOVA7vfvoxZZnftimygbY4kfrvQdv9vlESjpvmMQooWDV8")); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/api/RabbitMQPasswordConverter.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.api; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.charset.StandardCharsets; 5 | import java.security.MessageDigest; 6 | import java.util.Arrays; 7 | import java.util.Base64; 8 | import java.util.Random; 9 | 10 | public class RabbitMQPasswordConverter { 11 | 12 | private final Random random; 13 | private final MessageDigest messageDigest; 14 | private final Base64.Encoder base64Encoder; 15 | private final Base64.Decoder base64Decoder; 16 | 17 | 18 | public RabbitMQPasswordConverter( 19 | final Random random, 20 | final MessageDigest messageDigest, 21 | final Base64.Encoder base64Encoder, 22 | final Base64.Decoder base64Decoder 23 | ) { 24 | this.random = random; 25 | this.messageDigest = messageDigest; 26 | this.base64Encoder = base64Encoder; 27 | this.base64Decoder = base64Decoder; 28 | } 29 | 30 | public String convertPasswordToHash(final String password) { 31 | return convertPasswordToHash(password, random.nextInt()); 32 | } 33 | 34 | public String convertPasswordToHash(final String password, final int salt) { 35 | final byte[] saltBytes = ByteBuffer.allocate(4).putInt(salt).array(); 36 | final byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8); 37 | 38 | final byte[] passwordWithSalt = ByteBuffer.allocate(saltBytes.length + passwordBytes.length).put(saltBytes).put(passwordBytes).array(); 39 | 40 | final byte[] hashedBytes = messageDigest.digest(passwordWithSalt); 41 | 42 | return base64Encoder.encodeToString(ByteBuffer.allocate(hashedBytes.length + saltBytes.length).put(saltBytes).put(hashedBytes).array()); 43 | } 44 | 45 | public boolean passwordMatchesHash(final String password, final String hash) { 46 | final byte[] decodedHash = base64Decoder.decode(hash); 47 | final int salt = ByteBuffer.wrap(decodedHash).getInt(); 48 | 49 | return convertPasswordToHash(password, salt).equals(hash); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/OperatorPolicyDefinitionSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.indeed.rabbitmq.admin.pojo.OperatorPolicyDefinition; 6 | 7 | public class OperatorPolicyDefinitionSpec { 8 | 9 | private final Long expires; 10 | private final Long maxLength; 11 | private final Long maxLengthBytes; 12 | private final Long messageTtl; 13 | 14 | public OperatorPolicyDefinitionSpec( 15 | @JsonProperty("expires") final Long expires, 16 | @JsonProperty("max-length") final Long maxLength, 17 | @JsonProperty("max-length-bytes") final Long maxLengthBytes, 18 | @JsonProperty("message-ttl") final Long messageTtl 19 | ) { 20 | this.expires = expires; 21 | this.maxLength = maxLength; 22 | this.maxLengthBytes = maxLengthBytes; 23 | this.messageTtl = messageTtl; 24 | } 25 | 26 | public Long getExpires() { 27 | return expires; 28 | } 29 | 30 | public Long getMaxLength() { 31 | return maxLength; 32 | } 33 | 34 | public Long getMaxLengthBytes() { 35 | return maxLengthBytes; 36 | } 37 | 38 | public Long getMessageTtl() { 39 | return messageTtl; 40 | } 41 | 42 | @JsonIgnore 43 | public OperatorPolicyDefinition asOperatorPolicyDefinition() { 44 | OperatorPolicyDefinition definition = new OperatorPolicyDefinition(); 45 | 46 | if (expires != null) { 47 | definition = definition.withExpires(getExpires()); 48 | } 49 | 50 | if (maxLength != null) { 51 | definition = definition.withExpires(getMaxLength()); 52 | } 53 | 54 | if (maxLengthBytes != null) { 55 | definition = definition.withExpires(getMaxLengthBytes()); 56 | } 57 | 58 | if (messageTtl != null) { 59 | definition = definition.withExpires(getMessageTtl()); 60 | } 61 | 62 | return definition; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/UserSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.base.Preconditions; 9 | import com.google.common.base.Strings; 10 | import com.google.common.collect.ImmutableList; 11 | import io.sundr.builder.annotations.Buildable; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | /** 17 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 18 | */ 19 | @Buildable( 20 | builderPackage = "io.fabric8.kubernetes.api.builder", 21 | editableEnabled = false 22 | ) 23 | @JsonPropertyOrder({"username", "vhosts", "tags"}) 24 | @JsonDeserialize(using = JsonDeserializer.None.class) 25 | public class UserSpec { 26 | private final String username; 27 | private final List vhosts; 28 | private final List tags; 29 | 30 | @JsonCreator 31 | public UserSpec( 32 | @JsonProperty("username") final String username, 33 | @JsonProperty("vhosts") final List vhosts, 34 | @JsonProperty("tags") final List tags 35 | ) { 36 | Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "User 'username' cannot be empty or null"); 37 | 38 | this.username = username; 39 | this.vhosts = vhosts == null ? Collections.emptyList() : ImmutableList.copyOf(vhosts); 40 | this.tags = tags == null ? Collections.emptyList() : ImmutableList.copyOf(tags); 41 | } 42 | 43 | public String getUsername() { 44 | return username; 45 | } 46 | 47 | public List getVhosts() { 48 | return vhosts; 49 | } 50 | 51 | public List getTags() { 52 | return tags; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/partition/RabbitMQNetworkPartitionCustomResourceSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.partition; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import io.sundr.builder.annotations.Buildable; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 15 | */ 16 | @Buildable( 17 | builderPackage = "io.fabric8.kubernetes.api.builder", 18 | editableEnabled = false 19 | ) 20 | @JsonPropertyOrder({"clusterName", "partitions", "drained", "serviceName"}) 21 | @JsonDeserialize(using = JsonDeserializer.None.class) 22 | public class RabbitMQNetworkPartitionCustomResourceSpec { 23 | private final String clusterName; 24 | private final List> partitions; 25 | private final List> drained; 26 | private final String serviceName; 27 | 28 | @JsonCreator 29 | public RabbitMQNetworkPartitionCustomResourceSpec( 30 | @JsonProperty("clusterName") final String clusterName, 31 | @JsonProperty("partitions") final List> partitions, 32 | @JsonProperty("drained") final List> drained, 33 | @JsonProperty("serviceName") final String serviceName 34 | ) { 35 | this.clusterName = clusterName; 36 | this.partitions = partitions; 37 | this.drained = drained; 38 | this.serviceName = serviceName; 39 | } 40 | 41 | public String getClusterName() { 42 | return clusterName; 43 | } 44 | 45 | public List> getPartitions() { 46 | return partitions; 47 | } 48 | 49 | public List> getDrained() { 50 | return drained; 51 | } 52 | 53 | public String getServiceName() { 54 | return serviceName; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/partition/RabbitMQNetworkPartitionCustomResource.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.partition; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonInclude; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 8 | import com.fasterxml.jackson.databind.JsonDeserializer; 9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 10 | import io.fabric8.kubernetes.api.model.Doneable; 11 | import io.fabric8.kubernetes.api.model.ObjectMeta; 12 | import io.fabric8.kubernetes.client.CustomResource; 13 | import io.sundr.builder.annotations.Buildable; 14 | import io.sundr.builder.annotations.BuildableReference; 15 | import io.sundr.builder.annotations.Inline; 16 | 17 | /** 18 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 19 | */ 20 | @Buildable( 21 | builderPackage = "io.fabric8.kubernetes.api.builder", 22 | inline = @Inline(type = Doneable.class, prefix = "Doneable", value = "done"), 23 | editableEnabled = false, 24 | refs = @BuildableReference(CustomResource.class) 25 | ) 26 | @JsonInclude(JsonInclude.Include.NON_NULL) 27 | @JsonDeserialize(using = JsonDeserializer.None.class) 28 | @JsonPropertyOrder({"apiVersion", "kind", "metadata", "spec"}) 29 | public class RabbitMQNetworkPartitionCustomResource extends CustomResource { 30 | private RabbitMQNetworkPartitionCustomResourceSpec spec; 31 | 32 | @JsonCreator 33 | public RabbitMQNetworkPartitionCustomResource( 34 | @JsonProperty("spec") final RabbitMQNetworkPartitionCustomResourceSpec spec 35 | ) { 36 | this.spec = spec; 37 | } 38 | 39 | public RabbitMQNetworkPartitionCustomResourceSpec getSpec() { 40 | return spec; 41 | } 42 | 43 | public void setSpec(final RabbitMQNetworkPartitionCustomResourceSpec spec) { 44 | this.spec = spec; 45 | } 46 | 47 | @JsonIgnore 48 | public String getName() { 49 | return this.getMetadata().getName(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/VhostPermissions.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.base.Objects; 9 | import com.google.common.base.Preconditions; 10 | import com.google.common.base.Strings; 11 | import io.sundr.builder.annotations.Buildable; 12 | 13 | @Buildable( 14 | builderPackage = "io.fabric8.kubernetes.api.builder", 15 | editableEnabled = false 16 | ) 17 | @JsonPropertyOrder({"vhostName", "permissions"}) 18 | @JsonDeserialize(using = JsonDeserializer.None.class) 19 | public class VhostPermissions { 20 | 21 | private final String vhostName; 22 | private final VhostOperationPermissions permissions; 23 | 24 | @JsonCreator 25 | public VhostPermissions( 26 | @JsonProperty("vhostName") final String vhostName, 27 | @JsonProperty("permissions") final VhostOperationPermissions permissions 28 | ) { 29 | Preconditions.checkArgument(!Strings.isNullOrEmpty(vhostName), "vhost permissions 'vhostName' cannot be empty or null"); 30 | 31 | this.vhostName = vhostName; 32 | this.permissions = Preconditions.checkNotNull(permissions, "vhost permissions 'permissions' cannot be null"); 33 | } 34 | 35 | public String getVhostName() { 36 | return vhostName; 37 | } 38 | 39 | public VhostOperationPermissions getPermissions() { 40 | return permissions; 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) return true; 46 | if (o == null || getClass() != o.getClass()) return false; 47 | VhostPermissions that = (VhostPermissions) o; 48 | return Objects.equal(vhostName, that.vhostName) && 49 | Objects.equal(permissions, that.permissions); 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | return Objects.hashCode(vhostName, permissions); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/model/rabbitmq/RabbitMQConnectionInfo.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.rabbitmq; 2 | 3 | import com.google.common.base.Objects; 4 | 5 | import java.util.Optional; 6 | 7 | public class RabbitMQConnectionInfo { 8 | 9 | private final String clusterName; 10 | private final String namespace; 11 | private final String serviceName; 12 | private final Optional nodeName; 13 | 14 | public RabbitMQConnectionInfo( 15 | final String clusterName, 16 | final String namespace, 17 | final String serviceName 18 | ) { 19 | this(clusterName, namespace, serviceName, null); 20 | } 21 | 22 | public RabbitMQConnectionInfo( 23 | final String clusterName, 24 | final String namespace, 25 | final String serviceName, 26 | final String nodeName 27 | ) { 28 | this.clusterName = clusterName; 29 | this.namespace = namespace; 30 | this.serviceName = serviceName; 31 | this.nodeName = Optional.ofNullable(nodeName); 32 | } 33 | 34 | public String getClusterName() { 35 | return clusterName; 36 | } 37 | 38 | public String getNamespace() { 39 | return namespace; 40 | } 41 | 42 | public String getServiceName() { 43 | return serviceName; 44 | } 45 | 46 | public Optional getNodeName() { 47 | return nodeName; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | RabbitMQConnectionInfo that = (RabbitMQConnectionInfo) o; 55 | return Objects.equal(clusterName, that.clusterName) && 56 | Objects.equal(namespace, that.namespace) && 57 | Objects.equal(serviceName, that.serviceName) && 58 | Objects.equal(nodeName, that.nodeName); 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | return Objects.hashCode(clusterName, namespace, serviceName, nodeName); 64 | } 65 | 66 | public static RabbitMQConnectionInfo fromCluster(final RabbitMQCluster cluster) { 67 | return new RabbitMQConnectionInfo(cluster.getName(), cluster.getNamespace(), cluster.getDiscoveryService().getMetadata().getName()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/OperatorPolicySpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.common.base.Preconditions; 5 | import com.google.common.base.Strings; 6 | 7 | public class OperatorPolicySpec { 8 | 9 | private String vhost; 10 | private String name; 11 | private String pattern; 12 | private String applyTo; 13 | private OperatorPolicyDefinitionSpec definition; 14 | private long priority; 15 | 16 | public OperatorPolicySpec( 17 | @JsonProperty("vhost") final String vhost, 18 | @JsonProperty("name") final String name, 19 | @JsonProperty("pattern") final String pattern, 20 | @JsonProperty("applyTo") final String applyTo, 21 | @JsonProperty("definition") final OperatorPolicyDefinitionSpec definition, 22 | @JsonProperty("priority") final long priority 23 | ) { 24 | Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Operator policy 'name' cannot be empty or null"); 25 | Preconditions.checkArgument(!Strings.isNullOrEmpty(vhost), "Operator policy 'vhost' cannot be empty or null"); 26 | Preconditions.checkArgument(!Strings.isNullOrEmpty(pattern), "Operator policy 'pattern' cannot be empty or null"); 27 | Preconditions.checkArgument(!Strings.isNullOrEmpty(applyTo), "Operator policy 'applyTo' cannot be empty or null"); 28 | Preconditions.checkArgument(priority >= 0, "Operator policy 'priority' must be greater than or equal to 0"); 29 | 30 | this.vhost = vhost; 31 | this.name = name; 32 | this.pattern = pattern; 33 | this.applyTo = applyTo; 34 | this.definition = Preconditions.checkNotNull(definition, "Operator policy 'definition' cannot be null"); 35 | this.priority = priority; 36 | } 37 | 38 | public String getVhost() { 39 | return vhost; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public String getPattern() { 47 | return pattern; 48 | } 49 | 50 | public String getApplyTo() { 51 | return applyTo; 52 | } 53 | 54 | public OperatorPolicyDefinitionSpec getDefinition() { 55 | return definition; 56 | } 57 | 58 | public long getPriority() { 59 | return priority; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/VhostOperationPermissions.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.base.Objects; 9 | import com.google.common.base.Preconditions; 10 | import com.google.common.base.Strings; 11 | 12 | @JsonDeserialize(using = JsonDeserializer.None.class) 13 | public class VhostOperationPermissions { 14 | 15 | private final String configure; 16 | private final String write; 17 | private final String read; 18 | 19 | @JsonCreator 20 | public VhostOperationPermissions( 21 | @JsonProperty("configure") final String configure, 22 | @JsonProperty("write") final String write, 23 | @JsonProperty("read") final String read 24 | ) { 25 | Preconditions.checkArgument(!Strings.isNullOrEmpty(configure), "vhost 'configure' permissions cannot be null"); 26 | Preconditions.checkArgument(!Strings.isNullOrEmpty(write), "vhost 'write' permissions cannot be null"); 27 | Preconditions.checkArgument(!Strings.isNullOrEmpty(read), "vhost 'read' permissions cannot be null"); 28 | this.configure = configure; 29 | this.write = write; 30 | this.read = read; 31 | } 32 | 33 | public String getConfigure() { 34 | return configure; 35 | } 36 | 37 | public String getWrite() { 38 | return write; 39 | } 40 | 41 | public String getRead() { 42 | return read; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | VhostOperationPermissions that = (VhostOperationPermissions) o; 50 | return Objects.equal(configure, that.configure) && 51 | Objects.equal(write, that.write) && 52 | Objects.equal(read, that.read); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return Objects.hashCode(configure, write, read); 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return String.format("{%s, %s, %s}", configure, write, read); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/ClusterSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.collect.Lists; 9 | import io.sundr.builder.annotations.Buildable; 10 | 11 | import java.util.List; 12 | 13 | @Buildable( 14 | builderPackage = "io.fabric8.kubernetes.api.builder", 15 | editableEnabled = false 16 | ) 17 | @JsonPropertyOrder({"highWatermarkFraction", "users", "shovels", "policies"}) 18 | @JsonDeserialize(using = JsonDeserializer.None.class) 19 | public class ClusterSpec { 20 | 21 | private final double highWatermarkFraction; 22 | private final List users; 23 | private final List shovels; 24 | private final List policies; 25 | private final List operatorPolicies; 26 | 27 | @JsonCreator 28 | public ClusterSpec( 29 | @JsonProperty("highWatermarkFraction") final double highWatermarkFraction, 30 | @JsonProperty("users") final List users, 31 | @JsonProperty("shovels") final List shovels, 32 | @JsonProperty("policies") final List policies, 33 | @JsonProperty("operatorPolicies") final List operatorPolicies 34 | ) { 35 | this.highWatermarkFraction = highWatermarkFraction; 36 | this.users = (users == null ? Lists.newArrayList() : users); 37 | this.shovels = (shovels == null ? Lists.newArrayList() : shovels); 38 | this.policies = (policies == null ? Lists.newArrayList() : policies); 39 | this.operatorPolicies = (operatorPolicies == null ? Lists.newArrayList() : operatorPolicies); 40 | } 41 | 42 | public double getHighWatermarkFraction() { 43 | return highWatermarkFraction; 44 | } 45 | 46 | public List getUsers() { 47 | return users; 48 | } 49 | 50 | public List getShovels() { 51 | return shovels; 52 | } 53 | 54 | public List getPolicies() { 55 | return policies; 56 | } 57 | 58 | public List getOperatorPolicies() { 59 | return operatorPolicies; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.config; 2 | 3 | import com.indeed.operators.rabbitmq.executor.ClusterAwareExecutor; 4 | import com.indeed.operators.rabbitmq.reconciliation.lock.NamedSemaphores; 5 | import io.fabric8.kubernetes.client.DefaultKubernetesClient; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import okhttp3.OkHttpClient; 8 | import org.apache.commons.text.RandomStringGenerator; 9 | import org.springframework.beans.factory.annotation.Qualifier; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | import java.util.concurrent.ExecutorService; 14 | import java.util.concurrent.Executors; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.function.Function; 17 | 18 | @Configuration 19 | public class AppConfig { 20 | 21 | // TODO: We've discussed making this configurable via a ConfigMap so that users can easily tune 22 | // it to match their needs. 23 | private static final int RECONCILIATION_THREAD_POOL_SIZE = 4; 24 | private static final int SCHEDULED_THREAD_POOL_SIZE = 4; 25 | 26 | @Bean 27 | public KubernetesClient kubernetesClient() { 28 | return new DefaultKubernetesClient(); 29 | } 30 | 31 | @Bean 32 | public OkHttpClient okHttpClient() { 33 | return new OkHttpClient(); 34 | } 35 | 36 | @Bean 37 | public NamedSemaphores namedSemaphores() { 38 | return new NamedSemaphores(); 39 | } 40 | 41 | @Bean 42 | public Function randomStringFunction() { 43 | return new RandomStringGenerator.Builder().withinRange(new char[][]{{'A','Z'}, {'a', 'z'}, {'0', '9'}}).build()::generate; 44 | } 45 | 46 | @Bean 47 | public String namespace(final KubernetesClient client) { 48 | return client.getNamespace(); 49 | } 50 | 51 | @Bean 52 | @Qualifier("STANDARD_EXECUTOR") 53 | public ExecutorService executorService() { 54 | return Executors.newFixedThreadPool(RECONCILIATION_THREAD_POOL_SIZE); 55 | } 56 | 57 | @Bean 58 | public ClusterAwareExecutor clusterAwareExecutor( 59 | @Qualifier("STANDARD_EXECUTOR") final ExecutorService executor, 60 | final NamedSemaphores namedSemaphores 61 | ) { 62 | return new ClusterAwareExecutor(executor, namedSemaphores); 63 | } 64 | 65 | @Bean 66 | @Qualifier("SCHEDULED_EXECUTOR") 67 | public ScheduledExecutorService scheduledExecutorService() { 68 | return Executors.newScheduledThreadPool(SCHEDULED_THREAD_POOL_SIZE); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/RabbitMQComputeResources.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import com.google.common.collect.ImmutableMap; 9 | import io.fabric8.kubernetes.api.model.Quantity; 10 | import io.sundr.builder.annotations.Buildable; 11 | import io.sundr.builder.annotations.BuildableReference; 12 | 13 | import java.util.Map; 14 | 15 | @Buildable( 16 | builderPackage = "io.fabric8.kubernetes.api.builder", 17 | editableEnabled = false 18 | ) 19 | @JsonPropertyOrder({"cpuRequest", "cpuLimit", "memory"}) 20 | @JsonDeserialize(using = JsonDeserializer.None.class) 21 | public class RabbitMQComputeResources { 22 | private final Quantity cpuRequest; 23 | private final Quantity cpuLimit; 24 | private final Quantity memory; 25 | 26 | @JsonCreator 27 | public RabbitMQComputeResources( 28 | @JsonProperty("cpuRequest") final Quantity cpuRequest, 29 | @JsonProperty("cpuLimit") final Quantity cpuLimit, 30 | @JsonProperty("memory") final Quantity memory) { 31 | 32 | if (cpuRequest == null && cpuLimit == null) { 33 | throw new RuntimeException("Must specify one of 'cpuRequest' or 'cpuLimit'"); 34 | } 35 | 36 | this.cpuRequest = cpuRequest; 37 | this.cpuLimit = cpuLimit; 38 | this.memory = memory; 39 | } 40 | 41 | public Quantity getCpuRequest() { 42 | return cpuRequest; 43 | } 44 | 45 | public Quantity getCpuLimit() { 46 | return cpuLimit; 47 | } 48 | 49 | public Quantity getMemory() { 50 | return memory; 51 | } 52 | 53 | public Map asResourceRequestQuantityMap() { 54 | 55 | final Quantity cpu = cpuRequest == null ? cpuLimit : cpuRequest; 56 | 57 | return ImmutableMap.builder() 58 | .put("cpu", cpu) 59 | .put("memory", memory) 60 | .build(); 61 | } 62 | 63 | public Map asResourceLimitQuantityMap() { 64 | final ImmutableMap.Builder limitsBuilder = ImmutableMap.builder(); 65 | if (cpuLimit != null) { 66 | limitsBuilder.put("cpu", cpuLimit); 67 | } 68 | limitsBuilder.put("memory", memory); 69 | 70 | return limitsBuilder.build(); 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/SecretsController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.DoneableSecret; 4 | import io.fabric8.kubernetes.api.model.Secret; 5 | import io.fabric8.kubernetes.api.model.SecretBuilder; 6 | import io.fabric8.kubernetes.api.model.SecretList; 7 | import io.fabric8.kubernetes.client.KubernetesClient; 8 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 9 | import io.fabric8.kubernetes.client.dsl.Resource; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.Map; 14 | import java.util.function.Function; 15 | 16 | public class SecretsController extends AbstractResourceController> { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(SecretsController.class); 19 | 20 | private final Function decoder; 21 | 22 | public SecretsController( 23 | final KubernetesClient client, 24 | final Map labelsToWatch, 25 | final Function decoder 26 | ) { 27 | super(client, labelsToWatch, Secret.class); 28 | 29 | this.decoder = decoder; 30 | } 31 | 32 | @Override 33 | protected MixedOperation> operation() { 34 | return getClient().secrets(); 35 | } 36 | 37 | @Override 38 | public Secret createOrUpdate(final Secret resource) { 39 | final Secret maybeExistingResource = get(resource.getMetadata().getName(), resource.getMetadata().getNamespace()); 40 | 41 | if (maybeExistingResource == null) { 42 | log.info("Creating resource of type {} with name {}", getResourceType(), resource.getMetadata().getName()); 43 | return operation().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()).create(resource); 44 | } else { 45 | log.info("Patching metadata for resource type Secret with name {} - leaving Secret payload alone", resource.getMetadata().getName()); 46 | final Secret patchedSecret = new SecretBuilder(resource) 47 | .withStringData(maybeExistingResource.getStringData()) 48 | .withData(maybeExistingResource.getData()) 49 | .build(); 50 | 51 | return patch(patchedSecret); 52 | } 53 | } 54 | 55 | public Secret createOrForceUpdate(final Secret resource) { 56 | return super.createOrUpdate(resource); 57 | } 58 | 59 | public String decodeSecretPayload(final String secretText) { 60 | return decoder.apply(secretText); 61 | } 62 | } -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/PolicySpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.google.common.base.Preconditions; 8 | import com.google.common.base.Strings; 9 | import io.sundr.builder.annotations.Buildable; 10 | 11 | @Buildable( 12 | builderPackage = "io.fabric8.kubernetes.api.builder", 13 | editableEnabled = false 14 | ) 15 | @JsonPropertyOrder({"vhost", "name", "pattern", "applyTo", "definition", "priority"}) 16 | @JsonDeserialize(using = JsonDeserializer.None.class) 17 | public class PolicySpec { 18 | 19 | private String vhost; 20 | private String name; 21 | private String pattern; 22 | private String applyTo; 23 | private PolicyDefinitionSpec definition; 24 | private long priority; 25 | 26 | public PolicySpec( 27 | @JsonProperty("vhost") final String vhost, 28 | @JsonProperty("name") final String name, 29 | @JsonProperty("pattern") final String pattern, 30 | @JsonProperty("applyTo") final String applyTo, 31 | @JsonProperty("definition") final PolicyDefinitionSpec definition, 32 | @JsonProperty("priority") final long priority 33 | ) { 34 | Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "Policy 'name' cannot be empty or null"); 35 | Preconditions.checkArgument(!Strings.isNullOrEmpty(vhost), "Policy 'vhost' cannot be empty or null"); 36 | Preconditions.checkArgument(!Strings.isNullOrEmpty(pattern), "Policy 'pattern' cannot be empty or null"); 37 | Preconditions.checkArgument(!Strings.isNullOrEmpty(applyTo), "Policy 'applyTo' cannot be empty or null"); 38 | Preconditions.checkArgument(priority >= 0, "Policy 'priority' must be greater than or equal to 0"); 39 | 40 | this.vhost = vhost; 41 | this.name = name; 42 | this.pattern = pattern; 43 | this.applyTo = applyTo; 44 | this.definition = Preconditions.checkNotNull(definition, "Policy 'definition' cannot be null"); 45 | this.priority = priority; 46 | } 47 | 48 | public String getVhost() { 49 | return vhost; 50 | } 51 | 52 | public String getName() { 53 | return name; 54 | } 55 | 56 | public String getPattern() { 57 | return pattern; 58 | } 59 | 60 | public String getApplyTo() { 61 | return applyTo; 62 | } 63 | 64 | public PolicyDefinitionSpec getDefinition() { 65 | return definition; 66 | } 67 | 68 | public long getPriority() { 69 | return priority; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/crd/NetworkPartitionResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller.crd; 2 | 3 | import com.indeed.operators.rabbitmq.controller.AbstractResourceController; 4 | import com.indeed.operators.rabbitmq.model.crd.partition.DoneableRabbitMQNetworkPartitionCustomResource; 5 | import com.indeed.operators.rabbitmq.model.crd.partition.RabbitMQNetworkPartitionCustomResource; 6 | import com.indeed.operators.rabbitmq.model.crd.partition.RabbitMQNetworkPartitionCustomResourceList; 7 | import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; 8 | import io.fabric8.kubernetes.client.KubernetesClient; 9 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 10 | import io.fabric8.kubernetes.client.dsl.Resource; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.Map; 15 | 16 | import static com.indeed.operators.rabbitmq.Constants.RABBITMQ_NETWORK_PARTITION_CRD_NAME; 17 | 18 | public class NetworkPartitionResourceController extends AbstractResourceController> { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(NetworkPartitionResourceController.class); 21 | 22 | public NetworkPartitionResourceController( 23 | final KubernetesClient client, 24 | final Map labelsToWatch 25 | ) { 26 | super(client, labelsToWatch, RabbitMQNetworkPartitionCustomResource.class); 27 | } 28 | 29 | @Override 30 | public boolean delete(final String name, final String namespace) { 31 | log.info("Deleting resource"); 32 | operation().inNamespace(namespace).withName(name).cascading(true).delete(); 33 | 34 | return true; 35 | } 36 | 37 | @Override 38 | protected MixedOperation> operation() { 39 | final CustomResourceDefinition networkPartitionCrd = getClient().customResourceDefinitions().withName(RABBITMQ_NETWORK_PARTITION_CRD_NAME).get(); 40 | 41 | if (networkPartitionCrd == null) { 42 | throw new RuntimeException(String.format("CustomResourceDefinition %s has not been defined", RABBITMQ_NETWORK_PARTITION_CRD_NAME)); 43 | } 44 | 45 | return getClient().customResources(networkPartitionCrd, RabbitMQNetworkPartitionCustomResource.class, RabbitMQNetworkPartitionCustomResourceList.class, DoneableRabbitMQNetworkPartitionCustomResource.class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/validators/UserValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Sets; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.UserSpec; 7 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.VhostOperationPermissions; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.regex.Pattern; 12 | import java.util.regex.PatternSyntaxException; 13 | 14 | public class UserValidator implements RabbitClusterValidator { 15 | 16 | private static final Set VALID_USER_TAGS = Sets.newHashSet("administrator", "monitoring", "policymaker", "management", "impersonator"); 17 | 18 | @Override 19 | public List validate(final ClusterSpec clusterSpec) { 20 | final List users = clusterSpec.getUsers(); 21 | 22 | final List errors = Lists.newArrayList(); 23 | 24 | users.forEach(user -> { 25 | if (user.getVhosts() != null && !user.getVhosts().isEmpty()) { 26 | user.getVhosts().forEach(vhost -> { 27 | final VhostOperationPermissions permissions = vhost.getPermissions(); 28 | 29 | try { 30 | Pattern.compile(permissions.getRead()); 31 | } catch (final PatternSyntaxException ex) { 32 | errors.add(String.format("vhost 'read' permissions for user [%s] on vhost [%s] were invalid: %s", user.getUsername(), vhost.getVhostName(), ex.getMessage())); 33 | } 34 | 35 | try { 36 | Pattern.compile(permissions.getWrite()); 37 | } catch (final PatternSyntaxException ex) { 38 | errors.add(String.format("vhost 'write' permissions for user [%s] on vhost [%s] were invalid: %s", user.getUsername(), vhost.getVhostName(), ex.getMessage())); 39 | } 40 | 41 | try { 42 | Pattern.compile(permissions.getConfigure()); 43 | } catch (final PatternSyntaxException ex) { 44 | errors.add(String.format("vhost 'configure' permissions for user [%s] on vhost [%s] were invalid: %s", user.getUsername(), vhost.getVhostName(), ex.getMessage())); 45 | } 46 | }); 47 | } 48 | 49 | if (user.getTags() != null && !user.getTags().isEmpty()) { 50 | user.getTags().stream() 51 | .filter(tag -> !VALID_USER_TAGS.contains(tag)) 52 | .forEach(tag -> errors.add(String.format("Tag [%s] for user [%s] is invalid", tag, user.getUsername()))); 53 | } 54 | }); 55 | 56 | return errors; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/StatefulSetController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import io.fabric8.kubernetes.api.model.apps.DoneableStatefulSet; 4 | import io.fabric8.kubernetes.api.model.apps.StatefulSet; 5 | import io.fabric8.kubernetes.api.model.apps.StatefulSetList; 6 | import io.fabric8.kubernetes.client.KubernetesClient; 7 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 8 | import io.fabric8.kubernetes.client.dsl.RollableScalableResource; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.Map; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public class StatefulSetController extends AbstractWaitableResourceController> { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(StatefulSetController.class); 18 | 19 | private final PodController podController; 20 | 21 | public StatefulSetController( 22 | final KubernetesClient client, 23 | final Map labelsToWatch, 24 | final PodController podController 25 | ) { 26 | super(client, labelsToWatch, StatefulSet.class); 27 | 28 | this.podController = podController; 29 | } 30 | 31 | @Override 32 | protected MixedOperation> operation() { 33 | return getClient().apps().statefulSets(); 34 | } 35 | 36 | @Override 37 | public StatefulSet patch(final StatefulSet resource) { 38 | log.info("Patching resource of type {} with name {}", getResourceType(), resource.getMetadata().getName()); 39 | return operation().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()).cascading(false).patch(resource); 40 | } 41 | 42 | @Override 43 | public void waitForReady(final String name, final String namespace, final long time, final TimeUnit timeUnit) throws InterruptedException { 44 | log.info("Waiting {} {} for StatefulSet with name {} to be ready", time, timeUnit, name); 45 | operation().inNamespace(namespace).withName(name).waitUntilReady(time, timeUnit); 46 | operation().inNamespace(namespace).withName(name).waitUntilCondition(ss -> ss.getSpec().getReplicas().equals(ss.getStatus().getCurrentReplicas()), time, timeUnit); 47 | 48 | log.info("StatefulSet with name {} reported as ready - checking pod statuses", name); 49 | 50 | final StatefulSet statefulSet = operation().inNamespace(namespace).withName(name).get(); 51 | 52 | for (int i = statefulSet.getSpec().getReplicas() - 1; i >= 0; i--) { 53 | final String podName = String.format("%s-%s", statefulSet.getMetadata().getName(), i); 54 | 55 | podController.waitForReady(podName, statefulSet.getMetadata().getNamespace(), time, timeUnit); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/ServicesController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.fabric8.kubernetes.api.model.DoneableService; 5 | import io.fabric8.kubernetes.api.model.Service; 6 | import io.fabric8.kubernetes.api.model.ServiceList; 7 | import io.fabric8.kubernetes.api.model.ServicePort; 8 | import io.fabric8.kubernetes.api.model.ServicePortBuilder; 9 | import io.fabric8.kubernetes.client.KubernetesClient; 10 | import io.fabric8.kubernetes.client.dsl.MixedOperation; 11 | import io.fabric8.kubernetes.client.dsl.ServiceResource; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class ServicesController extends AbstractResourceController> { 17 | 18 | public ServicesController( 19 | final KubernetesClient client, 20 | final Map labelsToWatch 21 | ) { 22 | super(client, labelsToWatch, Service.class); 23 | } 24 | 25 | @Override 26 | protected MixedOperation> operation() { 27 | return getClient().services(); 28 | } 29 | 30 | // adapted from https://github.com/strimzi/strimzi-kafka-operator/blob/bfd5402733fdc50cb3ff3876bf28c455cb2fb845/operator-common/src/main/java/io/strimzi/operator/common/operator/resource/ServiceOperator.java#L54 31 | @Override 32 | public Service patch(final Service resource) { 33 | final Service current = get(resource.getMetadata().getName(), resource.getMetadata().getNamespace()); 34 | 35 | if (shouldUpdateServicePorts(current, resource)) { 36 | final List updatedPorts = getUpdatedServicePorts(current, resource); 37 | resource.getSpec().setPorts(updatedPorts); 38 | } 39 | 40 | return super.patch(resource); 41 | } 42 | 43 | private List getUpdatedServicePorts(final Service current, final Service desired) { 44 | final List finalPorts = Lists.newArrayList(); 45 | for (final ServicePort desiredPort : desired.getSpec().getPorts()) { 46 | for (final ServicePort currentPort : current.getSpec().getPorts()) { 47 | if (desiredPort.getNodePort() == null && desiredPort.getName().equals(currentPort.getName()) && currentPort.getNodePort() != null) { 48 | finalPorts.add(new ServicePortBuilder(desiredPort).withNodePort(currentPort.getNodePort()).build()); 49 | } 50 | } 51 | } 52 | 53 | return finalPorts; 54 | } 55 | 56 | private boolean shouldUpdateServicePorts(final Service current, final Service desired) { 57 | return ("NodePort".equals(current.getSpec().getType()) && "NodePort".equals(desired.getSpec().getType())) || 58 | ("LoadBalancer".equals(current.getSpec().getType()) && "LoadBalancer".equals(desired.getSpec().getType())); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/reconciliation/validators/TestPolicyValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.PolicyDefinitionSpec; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.PolicySpec; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | public class TestPolicyValidator { 16 | private static final PolicyDefinitionSpec POLICY_DEFINITION = new PolicyDefinitionSpec(null, null, null, null, null, null, null, null, null, null, null, null, null); 17 | 18 | private final PolicyValidator validator = new PolicyValidator(); 19 | 20 | @Test 21 | public void testValidate_validSpec() { 22 | final List policies = Lists.newArrayList( 23 | new PolicySpec("vhost1", "name1", "pattern", "queues", POLICY_DEFINITION, 1L), 24 | new PolicySpec("////", "name2", "[a-z]", "queues", POLICY_DEFINITION, 10L), 25 | new PolicySpec("////", "name2", "[a-z]", "exchanges", POLICY_DEFINITION, 10L) 26 | ); 27 | 28 | final List errors = validator.validate(buildClusterSpec(policies)); 29 | 30 | assertTrue(errors.isEmpty()); 31 | } 32 | 33 | @Test 34 | public void testValidate_invalidPattern() { 35 | final List policies = Lists.newArrayList( 36 | new PolicySpec("vhost1", "name1", "[[[[[[", "queues", POLICY_DEFINITION, 1L) 37 | ); 38 | 39 | final List errors = validator.validate(buildClusterSpec(policies)); 40 | 41 | assertEquals(1, errors.size()); 42 | assertTrue(errors.get(0).contains("pattern")); 43 | } 44 | 45 | @Test 46 | public void testValidate_invalidApplyTo() { 47 | final List policies = Lists.newArrayList( 48 | new PolicySpec("vhost1", "name1", "pattern", "blah", POLICY_DEFINITION, 1L) 49 | ); 50 | 51 | final List errors = validator.validate(buildClusterSpec(policies)); 52 | 53 | assertEquals(1, errors.size()); 54 | assertTrue(errors.get(0).contains("applyTo")); 55 | } 56 | 57 | @Test 58 | public void testValidate_multipleInvalid() { 59 | final List policies = Lists.newArrayList( 60 | new PolicySpec("vhost1", "name1", "[[[[[[", "queues", POLICY_DEFINITION, 1L), 61 | new PolicySpec("vhost1", "name1", "pattern", "blah", POLICY_DEFINITION, 1L) 62 | ); 63 | 64 | final List errors = validator.validate(buildClusterSpec(policies)); 65 | 66 | assertEquals(2, errors.size()); 67 | } 68 | 69 | private ClusterSpec buildClusterSpec(final List policies) { 70 | return new ClusterSpec(0.0, Collections.emptyList(), Collections.emptyList(), policies, Collections.emptyList()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/api/RabbitManagementApiProvider.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.api; 2 | 3 | import com.indeed.operators.rabbitmq.Constants; 4 | import com.indeed.operators.rabbitmq.controller.SecretsController; 5 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQCluster; 6 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQConnectionInfo; 7 | import com.indeed.operators.rabbitmq.resources.RabbitMQSecrets; 8 | import com.indeed.operators.rabbitmq.resources.RabbitMQServices; 9 | import com.indeed.rabbitmq.admin.RabbitManagementApi; 10 | import com.indeed.rabbitmq.admin.RabbitManagementApiFactory; 11 | import io.fabric8.kubernetes.api.model.Secret; 12 | import okhttp3.OkHttpClient; 13 | 14 | import java.net.URI; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | public class RabbitManagementApiProvider { 19 | 20 | private final Map rabbitApis; 21 | private final SecretsController secretsController; 22 | 23 | public RabbitManagementApiProvider( 24 | final SecretsController secretsController 25 | ) { 26 | rabbitApis = new HashMap<>(); 27 | this.secretsController = secretsController; 28 | } 29 | 30 | public RabbitManagementApiFacade getApi(final RabbitMQConnectionInfo connectionInfo) { 31 | if (rabbitApis.containsKey(connectionInfo)) { 32 | return rabbitApis.get(connectionInfo); 33 | } 34 | 35 | synchronized (rabbitApis) { 36 | if (rabbitApis.containsKey(connectionInfo)) { 37 | return rabbitApis.get(connectionInfo); 38 | } 39 | 40 | final Secret adminSecret = secretsController.get(RabbitMQSecrets.getClusterSecretName(connectionInfo.getClusterName()), connectionInfo.getNamespace()); 41 | final OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder().addInterceptor(new RabbitManagementApiLogger()); 42 | final RabbitManagementApi api = RabbitManagementApiFactory.newInstance( 43 | okHttpClientBuilder, 44 | buildApiUri(connectionInfo), 45 | secretsController.decodeSecretPayload(adminSecret.getData().get(Constants.Secrets.USERNAME_KEY)), 46 | secretsController.decodeSecretPayload(adminSecret.getData().get(Constants.Secrets.PASSWORD_KEY)) 47 | ); 48 | 49 | final RabbitManagementApiFacade facade = new RabbitManagementApiFacade(api); 50 | rabbitApis.put(connectionInfo, facade); 51 | 52 | return facade; 53 | } 54 | } 55 | 56 | public RabbitManagementApiFacade getApi(final RabbitMQCluster rabbitMQCluster) { 57 | return getApi(RabbitMQConnectionInfo.fromCluster(rabbitMQCluster)); 58 | } 59 | 60 | private URI buildApiUri(final RabbitMQConnectionInfo connectionInfo) { 61 | final String serviceName = RabbitMQServices.getDiscoveryServiceName(connectionInfo.getClusterName()); 62 | 63 | if (connectionInfo.getNodeName().isPresent()) { 64 | return URI.create(String.format("%s.%s:15672", connectionInfo.getNodeName().get(), serviceName)); 65 | } 66 | 67 | return URI.create(String.format("http://%s:15672", serviceName)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/reconciliation/validators/TestOperatorPolicyValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.OperatorPolicyDefinitionSpec; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.OperatorPolicySpec; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | public class TestOperatorPolicyValidator { 16 | private static final OperatorPolicyDefinitionSpec POLICY_DEFINITION = new OperatorPolicyDefinitionSpec(0L, 0L, 0L, 0L); 17 | 18 | private final OperatorPolicyValidator validator = new OperatorPolicyValidator(); 19 | 20 | @Test 21 | public void testValidate_validSpec() { 22 | final List policies = Lists.newArrayList( 23 | new OperatorPolicySpec("vhost1", "name1", "pattern", "queues", POLICY_DEFINITION, 1L), 24 | new OperatorPolicySpec("////", "name2", "[a-z]", "queues", POLICY_DEFINITION, 10L) 25 | ); 26 | 27 | final ClusterSpec clusterSpec = buildClusterSpec(policies); 28 | 29 | assertTrue(validator.validate(clusterSpec).isEmpty()); 30 | } 31 | 32 | @Test 33 | public void testValidate_invalidPattern() { 34 | final List policies = Lists.newArrayList( 35 | new OperatorPolicySpec("vhost1", "name1", "[[[[[[", "queues", POLICY_DEFINITION, 1L) 36 | ); 37 | 38 | final ClusterSpec clusterSpec = buildClusterSpec(policies); 39 | 40 | final List errors = validator.validate(clusterSpec); 41 | 42 | assertEquals(1, errors.size()); 43 | assertTrue(errors.get(0).contains("pattern")); 44 | } 45 | 46 | @Test 47 | public void testValidate_invalidApplyTo() { 48 | final List policies = Lists.newArrayList( 49 | new OperatorPolicySpec("vhost1", "name1", "pattern", "exchanges", POLICY_DEFINITION, 1L) 50 | ); 51 | 52 | final ClusterSpec clusterSpec = buildClusterSpec(policies); 53 | 54 | final List errors = validator.validate(clusterSpec); 55 | 56 | assertEquals(1, errors.size()); 57 | assertTrue(errors.get(0).contains("applyTo")); 58 | } 59 | 60 | @Test 61 | public void testValidate_multipleInvalid() { 62 | final List policies = Lists.newArrayList( 63 | new OperatorPolicySpec("vhost1", "name1", "[[[[[[", "queues", POLICY_DEFINITION, 1L), 64 | new OperatorPolicySpec("vhost1", "name1", "pattern", "exchanges", POLICY_DEFINITION, 1L) 65 | ); 66 | 67 | final ClusterSpec clusterSpec = buildClusterSpec(policies); 68 | 69 | final List errors = validator.validate(clusterSpec); 70 | 71 | assertEquals(2, errors.size()); 72 | } 73 | 74 | private ClusterSpec buildClusterSpec(final List policies) { 75 | return new ClusterSpec(0.0, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), policies); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/NetworkPartitionWatcher.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.indeed.operators.rabbitmq.controller.crd.NetworkPartitionResourceController; 5 | import com.indeed.operators.rabbitmq.model.crd.partition.RabbitMQNetworkPartitionCustomResource; 6 | import com.indeed.operators.rabbitmq.reconciliation.ClusterReconciliationOrchestrator; 7 | import com.indeed.operators.rabbitmq.reconciliation.NetworkPartitionReconciler; 8 | import com.indeed.operators.rabbitmq.reconciliation.Reconciliation; 9 | import io.fabric8.kubernetes.client.KubernetesClientException; 10 | import io.fabric8.kubernetes.client.Watcher; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.List; 15 | 16 | public class NetworkPartitionWatcher implements Watcher { 17 | private static final Logger log = LoggerFactory.getLogger(NetworkPartitionWatcher.class); 18 | 19 | private final NetworkPartitionReconciler reconciler; 20 | private final NetworkPartitionResourceController controller; 21 | private final ClusterReconciliationOrchestrator orchestrator; 22 | 23 | public NetworkPartitionWatcher( 24 | final NetworkPartitionReconciler reconciler, 25 | final NetworkPartitionResourceController controller, 26 | final ClusterReconciliationOrchestrator orchestrator 27 | ) { 28 | this.reconciler = Preconditions.checkNotNull(reconciler); 29 | this.controller = controller; 30 | this.orchestrator = orchestrator; 31 | } 32 | 33 | @Override 34 | public void eventReceived(final Action action, final RabbitMQNetworkPartitionCustomResource resource) { 35 | try { 36 | switch (action) { 37 | case ADDED: 38 | reconcile(resource); 39 | break; 40 | case MODIFIED: 41 | case DELETED: 42 | break; 43 | default: 44 | log.error("Unsupported action: {}", action); 45 | } 46 | } catch (final Exception ex) { 47 | log.error("Exception during network partition processing - aborting this attempt", ex); 48 | } 49 | } 50 | 51 | private void reconcile(final RabbitMQNetworkPartitionCustomResource resource) { 52 | final Reconciliation rec = new Reconciliation(resource.getName(), resource.getSpec().getClusterName(), resource.getMetadata().getNamespace(), resource.getKind()); 53 | 54 | orchestrator.queueReconciliation(rec, (reconciliation) -> { 55 | try { 56 | reconciler.reconcile(reconciliation); 57 | } catch (final InterruptedException e) { 58 | log.error("Interrupted during reconciliation", e); 59 | } 60 | }); 61 | } 62 | 63 | public void reconcileAll(final String namespace) { 64 | log.info("Reconciling all NetworkPartition resources in namespace {}", namespace); 65 | final List allResources = controller.getAll(namespace); 66 | 67 | allResources.forEach(this::reconcile); 68 | } 69 | 70 | @Override 71 | public void onClose(final KubernetesClientException cause) { 72 | log.info("closing watcher"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/resources/RabbitMQPods.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.resources; 2 | 3 | import com.indeed.operators.rabbitmq.model.Labels; 4 | import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; 5 | import io.fabric8.kubernetes.api.model.Container; 6 | import io.fabric8.kubernetes.api.model.EmptyDirVolumeSource; 7 | import io.fabric8.kubernetes.api.model.PodSpec; 8 | import io.fabric8.kubernetes.api.model.PodSpecBuilder; 9 | import io.fabric8.kubernetes.api.model.WeightedPodAffinityTermBuilder; 10 | 11 | public class RabbitMQPods { 12 | 13 | public PodSpec buildPodSpec( 14 | final String rabbitName, 15 | final String initContainerImage, 16 | final Container container 17 | ) { 18 | return new PodSpecBuilder() 19 | .withServiceAccountName("rabbitmq-user") 20 | .addNewInitContainer() 21 | .withName("copy-rabbitmq-config") 22 | .withImage(initContainerImage) 23 | .withCommand(new String[]{"sh", "-c", "cp /configmap/* /etc/rabbitmq"}) 24 | .addNewVolumeMount().withName("config").withMountPath("/etc/rabbitmq").endVolumeMount() 25 | .addNewVolumeMount().withName("configmap").withMountPath("/configmap").endVolumeMount() 26 | .endInitContainer() 27 | .withContainers(container) 28 | .addNewVolume().withName("config").withEmptyDir(new EmptyDirVolumeSource()).endVolume() 29 | .addNewVolume().withName("configmap").withConfigMap( 30 | new ConfigMapVolumeSourceBuilder() 31 | .withName("rabbitmq-config") 32 | .addNewItem("rabbitmq.conf", 0644, "rabbitmq.conf") 33 | .addNewItem("enabled_plugins", 0644, "enabled_plugins") 34 | .build() 35 | ).endVolume() 36 | .addNewVolume().withName("probes").withConfigMap( 37 | new ConfigMapVolumeSourceBuilder() 38 | .withName("rabbitmq-probes") 39 | .addNewItem("readiness.sh", 0755, "readiness.sh") 40 | .build() 41 | ).endVolume() 42 | .addNewVolume().withName("startup-scripts").withConfigMap( 43 | new ConfigMapVolumeSourceBuilder() 44 | .withName("rabbitmq-startup-scripts") 45 | .addNewItem("users.sh", 0755, "users.sh") 46 | .build() 47 | ).endVolume() 48 | .withNewAffinity().withNewPodAntiAffinity().withPreferredDuringSchedulingIgnoredDuringExecution( 49 | new WeightedPodAffinityTermBuilder() 50 | .withNewWeight(1) 51 | .withNewPodAffinityTerm() 52 | .withNewLabelSelector() 53 | .addToMatchLabels(Labels.Kubernetes.INSTANCE, rabbitName) 54 | .endLabelSelector() 55 | .withTopologyKey("kubernetes.io/hostname") 56 | .endPodAffinityTerm() 57 | .build() 58 | ).endPodAntiAffinity().endAffinity() 59 | .build(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/RabbitMQEventWatcher.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.indeed.operators.rabbitmq.controller.crd.RabbitMQResourceController; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQCustomResource; 6 | import com.indeed.operators.rabbitmq.reconciliation.ClusterReconciliationOrchestrator; 7 | import com.indeed.operators.rabbitmq.reconciliation.RabbitClusterConfigurationException; 8 | import com.indeed.operators.rabbitmq.reconciliation.RabbitMQClusterReconciler; 9 | import com.indeed.operators.rabbitmq.reconciliation.Reconciliation; 10 | import io.fabric8.kubernetes.client.KubernetesClientException; 11 | import io.fabric8.kubernetes.client.Watcher; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | 17 | public class RabbitMQEventWatcher implements Watcher { 18 | private static final Logger log = LoggerFactory.getLogger(RabbitMQEventWatcher.class); 19 | 20 | private final RabbitMQClusterReconciler reconciler; 21 | private final RabbitMQResourceController controller; 22 | private final ClusterReconciliationOrchestrator orchestrator; 23 | 24 | public RabbitMQEventWatcher( 25 | final RabbitMQClusterReconciler reconciler, 26 | final RabbitMQResourceController controller, 27 | final ClusterReconciliationOrchestrator orchestrator 28 | ) { 29 | this.reconciler = Preconditions.checkNotNull(reconciler); 30 | this.controller = controller; 31 | this.orchestrator = orchestrator; 32 | } 33 | 34 | @Override 35 | public void eventReceived(final Action action, final RabbitMQCustomResource resource) { 36 | try { 37 | switch (action) { 38 | case ADDED: 39 | case MODIFIED: 40 | reconcile(resource); 41 | break; 42 | case DELETED: 43 | log.info("rabbit {} deleted", resource.getName()); 44 | break; 45 | default: 46 | log.error("Unsupported action: {}", action); 47 | } 48 | } catch (final Exception exception) { 49 | log.error("Exception during rabbit cluster processing - aborting this attempt", exception); 50 | } 51 | } 52 | 53 | private void reconcile(final RabbitMQCustomResource resource) { 54 | final Reconciliation rec = new Reconciliation(resource.getName(), resource.getName(), resource.getMetadata().getNamespace(), resource.getKind()); 55 | 56 | orchestrator.queueReconciliation(rec, (reconciliation) -> { 57 | try { 58 | reconciler.reconcile(reconciliation); 59 | } catch (final InterruptedException e) { 60 | log.error("Interrupted during reconciliation", e); 61 | } catch (final RabbitClusterConfigurationException e) { 62 | log.error("Rabbit cluster configuration is invalid", e); 63 | } 64 | }); 65 | } 66 | 67 | public void reconcileAll(final String namespace) { 68 | log.info("Reconciling all RabbitMQ cluster resources in namespace {}", namespace); 69 | final List allResources = controller.getAll(namespace); 70 | 71 | allResources.forEach(this::reconcile); 72 | } 73 | 74 | @Override 75 | public void onClose(final KubernetesClientException cause) { 76 | log.info("Closing watcher", cause); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/operations/TestAreQueuesEmptyOperation.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.operations; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.Lists; 5 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiFacade; 6 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 7 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQConnectionInfo; 8 | import com.indeed.rabbitmq.admin.pojo.Queue; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertFalse; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | import static org.mockito.Mockito.mock; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class TestAreQueuesEmptyOperation { 22 | 23 | @Mock 24 | private RabbitManagementApiProvider apiCache; 25 | 26 | @InjectMocks 27 | private AreQueuesEmptyOperation operation; 28 | 29 | @Test 30 | public void testAllQueuesEmpty() { 31 | final RabbitMQConnectionInfo connectionInfo = new RabbitMQConnectionInfo("username", "password", "nodename", "servicename"); 32 | 33 | final Queue queue1 = new Queue().withName("queue1").withMessages(0L); 34 | final Queue queue2 = new Queue().withName("queue2").withMessages(0L); 35 | final Queue queue3 = new Queue().withName("queue3").withMessages(0L); 36 | 37 | final RabbitManagementApiFacade apiClient = mock(RabbitManagementApiFacade.class); 38 | 39 | when(apiCache.getApi(connectionInfo)).thenReturn(apiClient); 40 | when(apiClient.listQueues()).thenReturn(Lists.newArrayList(queue1, queue2, queue3)); 41 | 42 | assertTrue(operation.execute(connectionInfo)); 43 | } 44 | 45 | @Test 46 | public void testOneQueuesHasMessages() { 47 | final RabbitMQConnectionInfo connectionInfo = new RabbitMQConnectionInfo("username", "password", "nodename", "servicename"); 48 | 49 | final Queue queue1 = new Queue().withName("queue1").withMessages(0L); 50 | final Queue queue2 = new Queue().withName("queue2").withMessages(1L); 51 | final Queue queue3 = new Queue().withName("queue1").withMessages(0L); 52 | 53 | final RabbitManagementApiFacade apiClient = mock(RabbitManagementApiFacade.class); 54 | 55 | when(apiCache.getApi(connectionInfo)).thenReturn(apiClient); 56 | when(apiClient.listQueues()).thenReturn(Lists.newArrayList(queue1, queue2, queue3)); 57 | 58 | assertFalse(operation.execute(connectionInfo)); 59 | } 60 | 61 | @Test 62 | public void testMultipleQueuesHaveMessages() { 63 | final RabbitMQConnectionInfo connectionInfo = new RabbitMQConnectionInfo("username", "password", "nodename", "servicename"); 64 | 65 | final Queue queue1 = new Queue().withName("queue1").withMessages(0L); 66 | final Queue queue2 = new Queue().withName("queue2").withMessages(1L); 67 | final Queue queue3 = new Queue().withName("queue3").withMessages(1L); 68 | 69 | final RabbitManagementApiFacade apiClient = mock(RabbitManagementApiFacade.class); 70 | 71 | when(apiCache.getApi(connectionInfo)).thenReturn(apiClient); 72 | when(apiClient.listQueues()).thenReturn(Lists.newArrayList(queue1, queue2, queue3)); 73 | 74 | assertFalse(operation.execute(connectionInfo)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@indeed.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/RabbitMQCustomResourceSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 6 | import com.fasterxml.jackson.databind.JsonDeserializer; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | import io.sundr.builder.annotations.Buildable; 9 | 10 | /** 11 | * See https://github.com/fabric8io/kubernetes-client/tree/master/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/crds 12 | */ 13 | @Buildable( 14 | builderPackage = "io.fabric8.kubernetes.api.builder", 15 | editableEnabled = false 16 | ) 17 | @JsonPropertyOrder({"rabbitMQImage", "initContainerImage", "createLoadBalancer", "createNodePort", "replicas", "compute", "storage", "highWatermarkFraction"}) 18 | @JsonDeserialize(using = JsonDeserializer.None.class) 19 | public class RabbitMQCustomResourceSpec { 20 | private final String rabbitMQImage; 21 | private final String initContainerImage; 22 | private final boolean createLoadBalancer; 23 | private final boolean createNodePort; 24 | private final int replicas; 25 | private final RabbitMQComputeResources computeResources; 26 | private final RabbitMQStorageResources storageResources; 27 | private final ClusterSpec clusterSpec; 28 | private final boolean preserveOrphanPVCs; 29 | 30 | @JsonCreator 31 | public RabbitMQCustomResourceSpec( 32 | @JsonProperty("rabbitMQImage") final String rabbitMQImage, 33 | @JsonProperty("initContainerImage") final String initContainerImage, 34 | @JsonProperty("createLoadBalancer") final boolean createLoadBalancer, 35 | @JsonProperty("createNodePort") final boolean createNodePort, 36 | @JsonProperty("replicas") final int replicas, 37 | @JsonProperty("compute") final RabbitMQComputeResources computeResources, 38 | @JsonProperty("storage") final RabbitMQStorageResources storageResources, 39 | @JsonProperty("clusterSpec") final ClusterSpec clusterSpec, 40 | @JsonProperty(value = "preserveOrphanPVCs", defaultValue = "false") final boolean preserveOrphanPVCs 41 | ) { 42 | this.rabbitMQImage = rabbitMQImage; 43 | this.initContainerImage = initContainerImage; 44 | this.createLoadBalancer = createLoadBalancer; 45 | this.createNodePort = createNodePort; 46 | this.replicas = replicas; 47 | this.computeResources = computeResources; 48 | this.clusterSpec = clusterSpec; 49 | this.storageResources = storageResources; 50 | this.preserveOrphanPVCs = preserveOrphanPVCs; 51 | } 52 | 53 | public String getRabbitMQImage() { 54 | return rabbitMQImage; 55 | } 56 | 57 | public String getInitContainerImage() { 58 | return initContainerImage; 59 | } 60 | 61 | public boolean isCreateLoadBalancer() { 62 | return createLoadBalancer; 63 | } 64 | 65 | public boolean isCreateNodePort() { 66 | return createNodePort; 67 | } 68 | 69 | public int getReplicas() { 70 | return replicas; 71 | } 72 | 73 | public RabbitMQComputeResources getComputeResources() { 74 | return computeResources; 75 | } 76 | 77 | public RabbitMQStorageResources getStorageResources() { 78 | return storageResources; 79 | } 80 | 81 | public ClusterSpec getClusterSpec() { 82 | return clusterSpec; 83 | } 84 | 85 | public boolean isPreserveOrphanPVCs() { 86 | return preserveOrphanPVCs; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/controller/AbstractResourceController.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.controller; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import io.fabric8.kubernetes.api.model.Doneable; 5 | import io.fabric8.kubernetes.api.model.HasMetadata; 6 | import io.fabric8.kubernetes.api.model.KubernetesResourceList; 7 | import io.fabric8.kubernetes.client.KubernetesClient; 8 | import io.fabric8.kubernetes.client.Watch; 9 | import io.fabric8.kubernetes.client.Watcher; 10 | import io.fabric8.kubernetes.client.dsl.Operation; 11 | import io.fabric8.kubernetes.client.dsl.Resource; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public abstract class AbstractResourceController, R extends Resource> implements ResourceController { 19 | private static final Logger log = LoggerFactory.getLogger(AbstractResourceController.class); 20 | 21 | private final KubernetesClient client; 22 | private final Map labelsToWatch; 23 | private final String resourceType; 24 | 25 | protected AbstractResourceController( 26 | final KubernetesClient client, 27 | final Map labelsToWatch, 28 | final Class resourceType 29 | ) { 30 | this.client = client; 31 | this.labelsToWatch = ImmutableMap.copyOf(labelsToWatch); 32 | this.resourceType = resourceType.getSimpleName(); 33 | } 34 | 35 | protected abstract Operation operation(); 36 | 37 | @Override 38 | public T createOrUpdate(final T resource) { 39 | final T maybeExistingResource = get(resource.getMetadata().getName(), resource.getMetadata().getNamespace()); 40 | 41 | if (maybeExistingResource == null) { 42 | log.info("Creating resource of type {} with name {}", resourceType, resource.getMetadata().getName()); 43 | return operation().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()).create(resource); 44 | } else { 45 | return patch(resource); 46 | } 47 | } 48 | 49 | @Override 50 | public T get(final String name, final String namespace) { 51 | return operation().inNamespace(namespace).withName(name).get(); 52 | } 53 | 54 | @Override 55 | public boolean delete(final String name, final String namespace) { 56 | log.info("Deleting resource of type {} with name {}", resourceType, name); 57 | return operation().inNamespace(namespace).withName(name).delete(); 58 | } 59 | 60 | @Override 61 | public T patch(final T resource) { 62 | log.info("Patching resource of type {} with name {}", resourceType, resource.getMetadata().getName()); 63 | return operation().inNamespace(resource.getMetadata().getNamespace()).withName(resource.getMetadata().getName()).patch(resource); 64 | } 65 | 66 | @Override 67 | public Watch watch(final Watcher watcher, final String namespace) { 68 | log.info("Watching resources of type {} in namespace {}", resourceType, namespace); 69 | return operation().inNamespace(namespace).withLabels(labelsToWatch).watch(watcher); 70 | } 71 | 72 | @Override 73 | public List getAll(final String namespace) { 74 | log.info("Getting all resources of type {} in namespace {}", resourceType, namespace); 75 | return operation().inNamespace(namespace).withLabels(labelsToWatch).list().getItems(); 76 | } 77 | 78 | protected KubernetesClient getClient() { 79 | return client; 80 | } 81 | 82 | protected final String getResourceType() { 83 | return resourceType; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.config; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.indeed.operators.rabbitmq.api.RabbitMQPasswordConverter; 5 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 6 | import com.indeed.operators.rabbitmq.controller.SecretsController; 7 | import com.indeed.operators.rabbitmq.operations.AreQueuesEmptyOperation; 8 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.RabbitMQClusterFactory; 9 | import com.indeed.operators.rabbitmq.reconciliation.validators.OperatorPolicyValidator; 10 | import com.indeed.operators.rabbitmq.reconciliation.validators.PolicyValidator; 11 | import com.indeed.operators.rabbitmq.reconciliation.validators.RabbitClusterValidator; 12 | import com.indeed.operators.rabbitmq.reconciliation.validators.UserValidator; 13 | import com.indeed.operators.rabbitmq.resources.RabbitMQContainers; 14 | import com.indeed.operators.rabbitmq.resources.RabbitMQPods; 15 | import com.indeed.operators.rabbitmq.resources.RabbitMQSecrets; 16 | import com.indeed.operators.rabbitmq.resources.RabbitMQServices; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Configuration; 19 | 20 | import java.security.MessageDigest; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.util.Base64; 23 | import java.util.List; 24 | import java.util.Random; 25 | import java.util.function.Function; 26 | 27 | @Configuration 28 | public class RabbitConfig { 29 | 30 | @Bean 31 | public RabbitMQPods rabbitMQPods() { 32 | return new RabbitMQPods(); 33 | } 34 | 35 | @Bean 36 | public RabbitMQContainers rabbitMQContainers() { 37 | return new RabbitMQContainers(); 38 | } 39 | 40 | @Bean 41 | public RabbitMQSecrets rabbitSecrets( 42 | final Function randomStringGenerator 43 | ) { 44 | return new RabbitMQSecrets(randomStringGenerator, (str) -> Base64.getEncoder().encodeToString(str.getBytes())); 45 | } 46 | 47 | @Bean 48 | public RabbitMQServices rabbitMQServices() { 49 | return new RabbitMQServices(); 50 | } 51 | 52 | @Bean 53 | public RabbitManagementApiProvider managementApiCache(final SecretsController secretsController) { 54 | return new RabbitManagementApiProvider(secretsController); 55 | } 56 | 57 | @Bean 58 | public AreQueuesEmptyOperation queuesEmptyOperation( 59 | final RabbitManagementApiProvider managementApiCache 60 | ) { 61 | return new AreQueuesEmptyOperation(managementApiCache); 62 | } 63 | 64 | @Bean 65 | public RabbitMQPasswordConverter passwordConverter() throws NoSuchAlgorithmException { 66 | return new RabbitMQPasswordConverter(new Random(), MessageDigest.getInstance("SHA-256"), Base64.getEncoder(), Base64.getDecoder()); 67 | } 68 | 69 | @Bean 70 | public List rabbitClusterValidators() { 71 | return ImmutableList.of( 72 | new OperatorPolicyValidator(), 73 | new PolicyValidator(), 74 | new UserValidator() 75 | ); 76 | } 77 | 78 | @Bean 79 | public RabbitMQClusterFactory clusterFactory( 80 | final List rabbitClusterValidators, 81 | final RabbitMQContainers rabbitMQContainers, 82 | final RabbitMQPods rabbitMQPods, 83 | final RabbitMQSecrets rabbitMQSecrets, 84 | final RabbitMQServices rabbitMQServices, 85 | final SecretsController secretsController 86 | ) { 87 | return new RabbitMQClusterFactory(rabbitClusterValidators, rabbitMQContainers, rabbitMQPods, rabbitMQSecrets, rabbitMQServices, secretsController); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /docs/storage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Storage Examples 5 | 6 | 7 |

Storage Examples

8 |

Overview

9 |

This page provides examples of different storage configurations.

10 | 11 |

Default Storage

12 |

Removing the storageClassName field from the RabbitMQ instance spec will result in each pod attempting to claim a PersistentVolume with the default storage class.

13 | 14 |

For this to work seamlessly, you'll need:

15 |
    16 |
  • A default storage class.
  • 17 |
  • Dynamic provisioning, or a sufficient number of pre-made PersistentVolumes of appropriate size, in the default storage class.
  • 18 |
19 | 20 |

Local Storage

21 |

Local storage doesn't support dynamic provisioning, so you'll need to explicitly create PersistentVolumes.

22 | 23 |

Create Volumes

24 |

Local storage uses directories on the host machine. Any path should work as long as the volumes provisioned below are also updated. You'll need one directory for each RabbitMQ pod.

25 | 26 | $ mkdir -p /data/volume-0001 27 | $ mkdir -p /data/volume-0002 28 | $ mkdir -p /data/volume-0003 29 | 30 | 31 |

Provision Volumes

32 |

Apply the following configuration to your cluster to provision the necessary volumes. Again, if you're running more or less than three RabbitMQ instances, adjust the number appropriately.

33 | 34 | apiVersion: storage.k8s.io/v1 35 | kind: StorageClass 36 | metadata: 37 | name: local-storage 38 | provisioner: kubernetes.io/no-provisioner 39 | volumeBindingMode: WaitForFirstConsumer 40 | --- 41 | apiVersion: v1 42 | kind: PersistentVolume 43 | metadata: 44 | name: pv0001 45 | spec: 46 | capacity: 47 | storage: 1Gi 48 | accessModes: 49 | - ReadWriteOnce 50 | persistentVolumeReclaimPolicy: Retain 51 | storageClassName: local-storage 52 | hostPath: 53 | path: "/data/volume-0001" 54 | --- 55 | apiVersion: v1 56 | kind: PersistentVolume 57 | metadata: 58 | name: pv0002 59 | spec: 60 | capacity: 61 | storage: 1Gi 62 | accessModes: 63 | - ReadWriteOnce 64 | persistentVolumeReclaimPolicy: Retain 65 | storageClassName: local-storage 66 | hostPath: 67 | path: "/data/volume-0002" 68 | --- 69 | apiVersion: v1 70 | kind: PersistentVolume 71 | metadata: 72 | name: pv0003 73 | spec: 74 | capacity: 75 | storage: 1Gi 76 | accessModes: 77 | - ReadWriteOnce 78 | persistentVolumeReclaimPolicy: Retain 79 | storageClassName: local-storage 80 | hostPath: 81 | path: "/data/volume-0003" 82 | --- 83 | 84 | 85 |

Update RabbitMQ Spec

86 |

Change the storageClassName in your RabbitMQ instance spec to reference local-storage rather than rook-ceph-block.

87 | 88 |

Rook-Managed Ceph Block Storage

89 |

The example provided in this package assumes that Rook is being used to provide Ceph block storage. We found these examples to be a good starting point for getting this up and running in our Kubernetes cluster.

90 | 91 | 92 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/reconciliation/validators/TestUserValidator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.validators; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.ClusterSpec; 5 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.UserSpec; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.VhostOperationPermissions; 7 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.VhostPermissions; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | public class TestUserValidator { 17 | 18 | private final UserValidator validator = new UserValidator(); 19 | 20 | @Test 21 | public void testValidate_validUser() { 22 | final VhostPermissions permissions = new VhostPermissions("vhostname", new VhostOperationPermissions(".*", ".*", ".*")); 23 | final UserSpec userSpec = new UserSpec("username", Lists.newArrayList(permissions), Lists.newArrayList("administrator")); 24 | 25 | final ClusterSpec clusterSpec = buildClusterSpec(Lists.newArrayList(userSpec)); 26 | 27 | assertTrue(validator.validate(clusterSpec).isEmpty()); 28 | } 29 | 30 | @Test 31 | public void testValidate_invalidTag() { 32 | final VhostPermissions permissions = new VhostPermissions("vhostname", new VhostOperationPermissions(".*", ".*", ".*")); 33 | final UserSpec userSpec = new UserSpec("username", Lists.newArrayList(permissions), Lists.newArrayList("a random tag")); 34 | 35 | final ClusterSpec clusterSpec = buildClusterSpec(Lists.newArrayList(userSpec)); 36 | 37 | final List errors = validator.validate(clusterSpec); 38 | 39 | assertEquals(1, errors.size()); 40 | assertTrue(errors.get(0).contains("tag")); 41 | } 42 | 43 | @Test 44 | public void testValidate_invalidConfigure() { 45 | final VhostPermissions permissions = new VhostPermissions("vhostname", new VhostOperationPermissions("[[[", ".*", ".*")); 46 | final UserSpec userSpec = new UserSpec("username", Lists.newArrayList(permissions), Lists.newArrayList("administrator")); 47 | 48 | final ClusterSpec clusterSpec = buildClusterSpec(Lists.newArrayList(userSpec)); 49 | 50 | final List errors = validator.validate(clusterSpec); 51 | 52 | assertEquals(1, errors.size()); 53 | assertTrue(errors.get(0).contains("configure")); 54 | } 55 | 56 | @Test 57 | public void testValidate_invalidWrite() { 58 | final VhostPermissions permissions = new VhostPermissions("vhostname", new VhostOperationPermissions(".*", "[[[", ".*")); 59 | final UserSpec userSpec = new UserSpec("username", Lists.newArrayList(permissions), Lists.newArrayList("administrator")); 60 | 61 | final ClusterSpec clusterSpec = buildClusterSpec(Lists.newArrayList(userSpec)); 62 | 63 | final List errors = validator.validate(clusterSpec); 64 | 65 | assertEquals(1, errors.size()); 66 | assertTrue(errors.get(0).contains("write")); 67 | } 68 | 69 | @Test 70 | public void testValidate_invalidRead() { 71 | final VhostPermissions permissions = new VhostPermissions("vhostname", new VhostOperationPermissions(".*", ".*", "[[[")); 72 | final UserSpec userSpec = new UserSpec("username", Lists.newArrayList(permissions), Lists.newArrayList("administrator")); 73 | 74 | final ClusterSpec clusterSpec = buildClusterSpec(Lists.newArrayList(userSpec)); 75 | 76 | final List errors = validator.validate(clusterSpec); 77 | 78 | assertEquals(1, errors.size()); 79 | assertTrue(errors.get(0).contains("read")); 80 | } 81 | 82 | private ClusterSpec buildClusterSpec(final List users) { 83 | return new ClusterSpec(0.0, users, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/test/java/com/indeed/operators/rabbitmq/executor/TestClusterAwareExecutor.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.executor; 2 | 3 | import com.indeed.operators.rabbitmq.reconciliation.lock.NamedSemaphores; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | class TestClusterAwareExecutor { 15 | 16 | private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(2); 17 | private static final NamedSemaphores NAMED_LOCKS = new NamedSemaphores(); 18 | 19 | // Ensures that we don't enqueue duplicate tasks. "Enqueue" is the key term - we don't consider 20 | // tasks that are currently being executed. So the behavior is that we try to enqueue ten 21 | // copies of the same task. The first one we enqueue should start executing, leaving the queue 22 | // empty. Another task should then get enqueued and subsequent ones should get discarded. 23 | @Test 24 | void deduplication() throws InterruptedException { 25 | final ClusterAwareExecutor executor = new ClusterAwareExecutor(EXECUTOR_SERVICE, NAMED_LOCKS); 26 | final AtomicLong executionCount = new AtomicLong(); 27 | final CountDownLatch startLatch = new CountDownLatch(1); 28 | final CountDownLatch completionLatch = new CountDownLatch(2); 29 | 30 | for (int index = 0; index < 10; index++) { 31 | executor.submit("cluster", "operation", () -> { 32 | try { 33 | startLatch.await(10, TimeUnit.SECONDS); 34 | } catch (final InterruptedException ignored) {} 35 | executionCount.incrementAndGet(); 36 | completionLatch.countDown(); 37 | }); 38 | } 39 | 40 | startLatch.countDown(); 41 | completionLatch.await(10, TimeUnit.SECONDS); 42 | 43 | // Since we only wait for two tasks to complete, if there's a bug that breaks the 44 | // deduplication logic it's possible that this test may continue to pass. However, it would 45 | // likely be unstable, because this assertion would fail if more than the two tasks managed 46 | // to complete by the time we got here. 47 | 48 | assertEquals(2, executionCount.get()); 49 | } 50 | 51 | // Ensures that we don't execute multiple tasks for a given cluster at the same time. It 52 | // enqueues two tasks for one cluster, then a single task for another cluster. That single task 53 | // should not be blocked - we have two execution threads and only one of them should be occupied 54 | // with the first cluster. The task task for the first cluster must wait. 55 | @Test 56 | void oneActiveTaskPerCluster() throws InterruptedException { 57 | final ClusterAwareExecutor executor = new ClusterAwareExecutor(EXECUTOR_SERVICE, NAMED_LOCKS); 58 | final CountDownLatch cluster1StartLatch = new CountDownLatch(1); 59 | final CountDownLatch cluster1CompletionLatch = new CountDownLatch(2); 60 | final CountDownLatch cluster2CompletionLatch = new CountDownLatch(1); 61 | 62 | for (int index = 0; index < 2; index++) { 63 | executor.submit("cluster1", String.format("operation-%d", index), () -> { 64 | try { 65 | cluster1StartLatch.await(10, TimeUnit.SECONDS); 66 | } catch (final InterruptedException ignored) {} 67 | cluster1CompletionLatch.countDown(); 68 | }); 69 | } 70 | executor.submit("cluster2", "operation", cluster2CompletionLatch::countDown); 71 | 72 | // cluster1 has one task running and another waiting. Since we have a second execution 73 | // thread, the cluster2 task should have already completed. 74 | cluster2CompletionLatch.await(10, TimeUnit.SECONDS); 75 | assertEquals(2, cluster1CompletionLatch.getCount()); 76 | 77 | cluster1StartLatch.countDown(); 78 | cluster1CompletionLatch.await(10, TimeUnit.SECONDS); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/RabbitMQOperator.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq; 2 | 3 | import com.indeed.operators.rabbitmq.controller.crd.NetworkPartitionResourceController; 4 | import com.indeed.operators.rabbitmq.controller.crd.RabbitMQResourceController; 5 | import com.indeed.operators.rabbitmq.model.crd.partition.RabbitMQNetworkPartitionCustomResource; 6 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQCustomResource; 7 | import io.fabric8.kubernetes.internal.KubernetesDeserializer; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.Banner; 12 | import org.springframework.boot.CommandLineRunner; 13 | import org.springframework.boot.WebApplicationType; 14 | import org.springframework.boot.autoconfigure.SpringBootApplication; 15 | import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; 16 | import org.springframework.boot.builder.SpringApplicationBuilder; 17 | 18 | import java.util.concurrent.ScheduledExecutorService; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @SpringBootApplication(exclude = GsonAutoConfiguration.class) 22 | public class RabbitMQOperator implements CommandLineRunner { 23 | private static final Logger log = LoggerFactory.getLogger(RabbitMQOperator.class); 24 | 25 | private final RabbitMQResourceController rabbitMQResourceController; 26 | private final NetworkPartitionResourceController networkPartitionResourceController; 27 | private final RabbitMQEventWatcher rabbitMQEventWatcher; 28 | private final NetworkPartitionWatcher networkPartitionWatcher; 29 | private final ScheduledExecutorService scheduledExecutor; 30 | private final String namespace; 31 | 32 | @Autowired 33 | public RabbitMQOperator( 34 | final RabbitMQResourceController rabbitMQResourceController, 35 | final NetworkPartitionResourceController networkPartitionResourceController, 36 | final RabbitMQEventWatcher rabbitMQEventWatcher, 37 | final NetworkPartitionWatcher networkPartitionWatcher, 38 | final ScheduledExecutorService scheduledExecutor, 39 | final String namespace 40 | ) { 41 | this.rabbitMQResourceController = rabbitMQResourceController; 42 | this.networkPartitionResourceController = networkPartitionResourceController; 43 | this.rabbitMQEventWatcher = rabbitMQEventWatcher; 44 | this.networkPartitionWatcher = networkPartitionWatcher; 45 | this.scheduledExecutor = scheduledExecutor; 46 | this.namespace = namespace; 47 | } 48 | 49 | public static void main(final String[] args) { 50 | new SpringApplicationBuilder(RabbitMQOperator.class).web(WebApplicationType.NONE).bannerMode(Banner.Mode.OFF).run(args); 51 | } 52 | 53 | @Override 54 | public void run(final String[] args) { 55 | log.info("Starting {}", RabbitMQOperator.class.getName()); 56 | 57 | registerCrdDeserializationTypes(); 58 | 59 | rabbitMQResourceController.watch(rabbitMQEventWatcher, namespace); 60 | networkPartitionResourceController.watch(networkPartitionWatcher, namespace); 61 | 62 | 63 | scheduledExecutor.scheduleAtFixedRate(() -> { 64 | try { 65 | rabbitMQEventWatcher.reconcileAll(namespace); 66 | } catch (final Throwable t) { 67 | log.error("Got an error while reconciling all clusters", t); 68 | } 69 | }, 10, 60, TimeUnit.SECONDS); 70 | 71 | scheduledExecutor.scheduleAtFixedRate(() -> { 72 | try { 73 | networkPartitionWatcher.reconcileAll(namespace); 74 | } catch (final Throwable t) { 75 | log.error("Got an error while reconciling all network partitions", t); 76 | } 77 | 78 | }, 10, 60, TimeUnit.SECONDS); 79 | } 80 | 81 | private void registerCrdDeserializationTypes() { 82 | KubernetesDeserializer.registerCustomKind("indeed.com/v1alpha1", "RabbitMQCustomResource", RabbitMQCustomResource.class); 83 | KubernetesDeserializer.registerCustomKind("indeed.com/v1alpha1", "RabbitMQNetworkPartitionCustomResource", RabbitMQNetworkPartitionCustomResource.class); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/config/ControllerConfig.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.config; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.indeed.operators.rabbitmq.controller.PersistentVolumeClaimController; 5 | import com.indeed.operators.rabbitmq.controller.PodController; 6 | import com.indeed.operators.rabbitmq.controller.PodDisruptionBudgetController; 7 | import com.indeed.operators.rabbitmq.controller.SecretsController; 8 | import com.indeed.operators.rabbitmq.controller.ServicesController; 9 | import com.indeed.operators.rabbitmq.controller.StatefulSetController; 10 | import com.indeed.operators.rabbitmq.controller.crd.NetworkPartitionResourceController; 11 | import com.indeed.operators.rabbitmq.controller.crd.RabbitMQResourceController; 12 | import io.fabric8.kubernetes.client.KubernetesClient; 13 | import org.springframework.beans.factory.annotation.Qualifier; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.Base64; 19 | import java.util.Map; 20 | 21 | @Configuration 22 | public class ControllerConfig { 23 | 24 | private static final String WATCH_LABELS_ENV_VAR = "WATCH_LABELS"; 25 | 26 | @Bean 27 | @Qualifier("LABELS_TO_WATCH") 28 | public Map labelsToWatch() { 29 | final ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); 30 | 31 | if (System.getenv().containsKey(WATCH_LABELS_ENV_VAR)) { 32 | final String watchLabels = System.getenv().get(WATCH_LABELS_ENV_VAR); 33 | for (final String label : watchLabels.split(",")) { 34 | if (label.contains("=")) { 35 | final String[] keyValue = label.split("="); 36 | mapBuilder.put(keyValue[0], keyValue[1]); 37 | } 38 | } 39 | } 40 | 41 | return mapBuilder.build(); 42 | } 43 | 44 | @Bean 45 | public RabbitMQResourceController rabbitResourceController( 46 | final KubernetesClient client, 47 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 48 | ) { 49 | return new RabbitMQResourceController(client, labelsToWatch); 50 | } 51 | 52 | @Bean 53 | public NetworkPartitionResourceController networkPartitionResourceController( 54 | final KubernetesClient client, 55 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 56 | ) { 57 | return new NetworkPartitionResourceController(client, labelsToWatch); 58 | } 59 | 60 | @Bean 61 | public SecretsController secretsController( 62 | final KubernetesClient client, 63 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 64 | ) { 65 | return new SecretsController(client, labelsToWatch, (val) -> new String(Base64.getDecoder().decode(val), StandardCharsets.UTF_8)); 66 | } 67 | 68 | @Bean 69 | public ServicesController servicesController( 70 | final KubernetesClient client, 71 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 72 | ) { 73 | return new ServicesController(client, labelsToWatch); 74 | } 75 | 76 | @Bean 77 | public StatefulSetController statefulSetController( 78 | final KubernetesClient client, 79 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch, 80 | final PodController podController 81 | ) { 82 | return new StatefulSetController(client, labelsToWatch, podController); 83 | } 84 | 85 | @Bean 86 | public PodController podController( 87 | final KubernetesClient client, 88 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 89 | ) { 90 | return new PodController(client, labelsToWatch); 91 | } 92 | 93 | @Bean 94 | public PodDisruptionBudgetController podDisruptionBudgetController( 95 | final KubernetesClient client, 96 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 97 | ) { 98 | return new PodDisruptionBudgetController(client, labelsToWatch); 99 | } 100 | 101 | @Bean 102 | public PersistentVolumeClaimController persistentVolumeClaimController( 103 | final KubernetesClient client, 104 | @Qualifier("LABELS_TO_WATCH") final Map labelsToWatch 105 | ) { 106 | return new PersistentVolumeClaimController(client, labelsToWatch); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/resources/RabbitMQContainers.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.resources; 2 | 3 | import com.indeed.operators.rabbitmq.Constants; 4 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQComputeResources; 5 | import io.fabric8.kubernetes.api.model.Container; 6 | import io.fabric8.kubernetes.api.model.ContainerBuilder; 7 | import io.fabric8.kubernetes.api.model.Lifecycle; 8 | import io.fabric8.kubernetes.api.model.LifecycleBuilder; 9 | import io.fabric8.kubernetes.api.model.Probe; 10 | import io.fabric8.kubernetes.api.model.ProbeBuilder; 11 | 12 | import static com.indeed.operators.rabbitmq.Constants.Ports.AMQP_PORT; 13 | import static com.indeed.operators.rabbitmq.Constants.Ports.EPMD_PORT; 14 | import static com.indeed.operators.rabbitmq.Constants.Ports.MANAGEMENT_PORT; 15 | import static com.indeed.operators.rabbitmq.Constants.RABBITMQ_STORAGE_NAME; 16 | 17 | public class RabbitMQContainers { 18 | public Container buildContainer( 19 | final String namespace, 20 | final String rabbitName, 21 | final String rabbitMQImage, 22 | final RabbitMQComputeResources resources, 23 | final double highWatermark 24 | ) { 25 | final String discoveryServiceName = RabbitMQServices.getDiscoveryServiceName(rabbitName); 26 | final String adminSecretName = RabbitMQSecrets.getClusterSecretName(rabbitName); 27 | final String erlangCookieSecretName = RabbitMQSecrets.getErlangCookieSecretName(rabbitName); 28 | 29 | return new ContainerBuilder() 30 | .addNewPort().withName("epmd").withContainerPort(EPMD_PORT).withProtocol("TCP").endPort() 31 | .addNewPort().withName("amqp").withContainerPort(AMQP_PORT).withProtocol("TCP").endPort() 32 | .addNewPort().withName("management").withContainerPort(MANAGEMENT_PORT).withProtocol("TCP").endPort() 33 | .withImage(rabbitMQImage) 34 | .withName(rabbitName) 35 | .withNewResources() 36 | .withRequests(resources.asResourceRequestQuantityMap()) 37 | .withLimits(resources.asResourceLimitQuantityMap()) 38 | .endResources() 39 | .addNewEnv().withName("MY_POD_NAME").withNewValueFrom().withNewFieldRef().withFieldPath("metadata.name").endFieldRef().endValueFrom().endEnv() 40 | .addNewEnv().withName("RABBITMQ_VM_MEMORY_HIGH_WATERMARK").withValue(highWatermark > 0 ? String.valueOf(highWatermark) : "0Mib").endEnv() 41 | .addNewEnv().withName("RABBITMQ_ERLANG_COOKIE").withNewValueFrom().withNewSecretKeyRef(Constants.Secrets.ERLANG_COOKIE_KEY, erlangCookieSecretName, false).endValueFrom().endEnv() 42 | .addNewEnv().withName("RABBITMQ_DEFAULT_USER").withNewValueFrom().withNewSecretKeyRef(Constants.Secrets.USERNAME_KEY, adminSecretName, false).endValueFrom().endEnv() 43 | .addNewEnv().withName("RABBITMQ_DEFAULT_PASS").withNewValueFrom().withNewSecretKeyRef(Constants.Secrets.PASSWORD_KEY, adminSecretName, false).endValueFrom().endEnv() 44 | .addNewEnv().withName("K8S_SERVICE_NAME").withValue(discoveryServiceName).endEnv() 45 | .addNewEnv().withName("RABBITMQ_USE_LONGNAME").withValue("true").endEnv() 46 | .addNewEnv().withName("RABBITMQ_NODENAME").withValue(String.format("rabbit@$(MY_POD_NAME).%s.%s.svc.cluster.local", discoveryServiceName, namespace)).endEnv() 47 | .addNewEnv().withName("K8S_HOSTNAME_SUFFIX").withValue(String.format(".%s.%s.svc.cluster.local", discoveryServiceName, namespace)).endEnv() 48 | .addNewVolumeMount().withName("config").withMountPath("/etc/rabbitmq").endVolumeMount() 49 | .addNewVolumeMount().withName(RABBITMQ_STORAGE_NAME).withMountPath("/var/lib/rabbitmq").endVolumeMount() 50 | .addNewVolumeMount().withName("probes").withMountPath("/probes").endVolumeMount() 51 | .addNewVolumeMount().withName("startup-scripts").withMountPath("/startup-scripts").endVolumeMount() 52 | .withReadinessProbe(buildReadinessProbe()) 53 | .withLifecycle(buildLifeCycle()) 54 | .build(); 55 | } 56 | 57 | private Lifecycle buildLifeCycle() { 58 | return new LifecycleBuilder() 59 | .withNewPostStart() 60 | .withNewExec().withCommand("/startup-scripts/users.sh").endExec() 61 | .endPostStart() 62 | .build(); 63 | } 64 | 65 | private Probe buildReadinessProbe() { 66 | return new ProbeBuilder() 67 | .withNewExec().withCommand("/probes/readiness.sh").endExec() 68 | .withInitialDelaySeconds(20) 69 | .withTimeoutSeconds(5) 70 | .withPeriodSeconds(5) 71 | .withFailureThreshold(3) 72 | .withSuccessThreshold(1) 73 | .build(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/executor/ClusterAwareExecutor.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.executor; 2 | 3 | import com.google.common.base.Objects; 4 | import com.indeed.operators.rabbitmq.reconciliation.lock.NamedSemaphores; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.annotation.Nonnull; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | import java.util.concurrent.ExecutorService; 12 | import java.util.concurrent.Semaphore; 13 | 14 | public class ClusterAwareExecutor { 15 | private static final Logger log = LoggerFactory.getLogger(ClusterAwareExecutor.class); 16 | 17 | private final Map taskQueue = new LinkedHashMap<>(); 18 | 19 | private final ExecutorService executorService; 20 | private final NamedSemaphores namedSemaphores; 21 | 22 | public ClusterAwareExecutor(@Nonnull final ExecutorService executorService, @Nonnull final NamedSemaphores namedSemaphores) { 23 | this.executorService = executorService; 24 | this.namedSemaphores = namedSemaphores; 25 | } 26 | 27 | public synchronized void submit( 28 | @Nonnull final String clusterName, 29 | @Nonnull final String operation, 30 | @Nonnull final Runnable runnable) { 31 | // Only call dispatch if this task wasn't already present - if it was, then dispatch would 32 | // be a no-op anyway. 33 | if (null == taskQueue.putIfAbsent(new ClusterOperation(clusterName, operation), runnable)) { 34 | dispatch(); 35 | } 36 | } 37 | 38 | private synchronized void dispatch() { 39 | for (final Map.Entry task : taskQueue.entrySet()) { 40 | final ClusterOperation clusterOperation = task.getKey(); 41 | final String clusterName = clusterOperation.getClusterName(); 42 | final String operation = clusterOperation.getOperation(); 43 | 44 | // We want to prevent multiple tasks for the same cluster from attempting to run 45 | // simultaneously - if they did they could easily conflict with one another. To rule 46 | // this out we use a semaphore per cluster as a lock. 47 | final Semaphore semaphore = namedSemaphores.getSemaphore(clusterName); 48 | log.debug("Attempting to acquire semaphore for cluster {}", clusterName); 49 | if (semaphore.tryAcquire()) { 50 | log.info("Acquired semaphore for cluster {}, running operation {}", clusterName, operation); 51 | 52 | executorService.submit(() -> { 53 | try { 54 | task.getValue().run(); 55 | } finally { 56 | log.info("Finished operation {} for cluster {}, releasing semaphore", operation, clusterName); 57 | 58 | // Important - release the semaphore before dispatching the next task, 59 | // otherwise we may inadvertently make a task for this cluster wait longer 60 | // than is strictly necessary. 61 | semaphore.release(); 62 | 63 | // Check for additional work, unless we were interrupted (which usually 64 | // indicates that we're being shut down). 65 | if (!Thread.currentThread().isInterrupted()) { 66 | dispatch(); 67 | } 68 | } 69 | }); 70 | taskQueue.remove(clusterOperation); 71 | return; 72 | } else { 73 | log.debug("Semaphore for cluster {} was unacquirable", clusterName); 74 | } 75 | } 76 | } 77 | 78 | private static class ClusterOperation { 79 | 80 | private final String clusterName; 81 | private final String operation; 82 | 83 | ClusterOperation(@Nonnull final String clusterName, @Nonnull final String operation) { 84 | this.clusterName = clusterName; 85 | this.operation = operation; 86 | } 87 | 88 | String getClusterName() { 89 | return clusterName; 90 | } 91 | 92 | String getOperation() { 93 | return operation; 94 | } 95 | 96 | @Override 97 | public boolean equals(final Object o) { 98 | if (this == o) return true; 99 | if (o == null || getClass() != o.getClass()) return false; 100 | final ClusterOperation that = (ClusterOperation) o; 101 | return Objects.equal(clusterName, that.clusterName) && 102 | Objects.equal(operation, that.operation); 103 | } 104 | 105 | @Override 106 | public int hashCode() { 107 | return Objects.hashCode(clusterName, operation); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Operator for RabbitMQ 2 | 3 | ## Overview 4 | ### Status: **Indeed is no longer maintaining this application and the repo will be archived 2/22/21 ** 5 | 6 | [![Build Status](https://travis-ci.org/indeedeng/rabbitmq-operator.svg?branch=master)](https://travis-ci.org/indeedeng/rabbitmq-operator) 7 | ![OSS Lifecycle](https://img.shields.io/osslifecycle/indeedeng/rabbitmq-operator.svg) 8 | 9 | 10 | Provision and manage RabbitMQ clusters on Kubernetes! This operator currently has the following features: 11 | * Deploy N-node RabbitMQ clusters, utilizing auto-discovery for automatic clustering 12 | * Scale cluster replicas, storage, and CPU 13 | * Specify persistent volume storage class 14 | * Expose clusters to external clients using a LoadBalancer 15 | * Datadog auto-discovery annotations 16 | * Safely resolve network partitions without dropping messages (experimental, requires manual custom resource creation) 17 | 18 | # Getting Started 19 | ## Prerequisites 20 | You must have a Kubernetes cluster. Standard Pod and Service networking must work. 21 | 22 | You must also have a Docker registry that both your development environment and the Kubernetes cluster can access via the CNAME `registry.local.tld` 23 | 24 | The example assumes you have Rook-managed storage deployed. You can read about Rook at https://rook.io/. 25 | 26 | ## Deploying the operator 27 | Use the script `deploy-operator.sh` to build and push the operator image. At the end you should see a `rabbitmq-operator` pod spin up in the `rabbitmqs` namespace. 28 | ``` 29 | LOCAL_DOCKER_REGISTRY=registry.local.tld ./scripts/deploy-operator.sh 30 | ``` 31 | 32 | ## Deploying a cluster 33 | Apply the [example RabbitMQCustomResource](examples/rabbitmq_instance.yaml). By default, this deploys a cluster with 3 instances in the `rabbitmqs` namespace. 34 | ``` 35 | kubectl apply -f examples/rabbitmq_instance.yaml 36 | ``` 37 | 38 | ## Connecting to the cluster 39 | For each cluster, a service called `-svc` will be created. This is a standard (non-headless) service. Nodes will be added to the relevant Endpoints as soon as their healthcheck returns ok. A cluster named `myrabbitmq` in namespace `rabbitmqs` can be internally accessed at `myrabbitmq.rabbitmqs.svc.cluster.local`. Standard RabbitMQ ports are exposed. 40 | 41 | To access a RabbitMQ cluster from outside the Kubernetes cluster, you need to either expose the Rabbit cluster using a NodePort or set `createLoadBalancer` to `true`. This will provision a LoadBalancer service with name `-svc-lb` (assuming your environment supports it). You can then access your cluster using the LoadBalancer IP and standard RabbitMQ ports. 42 | 43 | For more information on Service DNS and routing, see https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/. 44 | 45 | # Custom Resource Schema 46 | ## RabbitMQCustomResource spec - [example](examples/rabbitmq_instance.yaml) 47 | | Field | Type | Description | 48 | |----------------------|-----------|-------------| 49 | | `rabbitMQImage` | string | Name of RabbitMQ image | 50 | | `initContainerImage` | string | Name of initContainer image| 51 | | `createLoadBalancer` | boolean | Whether to create a LoadBalancer service | 52 | | `preserveOrphanPVCs` | boolean | When scaling down a cluster, whether to preserve "orphaned" PVCs; this field is optional and defaults to false | 53 | | `replicas` | number | Number of cluster nodes | 54 | | `compute.cpuRequest` | string | CPU request per node, ex: "500m" | 55 | | `compute.memory` | string | Memory request per node, ex: "512Mi" | 56 | | `storage.storageClassName` | string | Storage class to use for persistent storage (immutable) | 57 | | `storage.limit` | string | PersistentVolume size per cluster node (immutable) | 58 | | `clusterSpec.highWatermarkFraction` | string | RabbitMQ high watermark, ex: 0.4 | 59 | 60 | **Note:** Scaling replicas down is a dangerous operation. The operator does not currently make any safety guarantees when scaling down replicas. 61 | 62 | # Roadmap 63 | This operator is very much a work-in-progress. Features that we want to implement in the near future include: 64 | * Shovel configuration 65 | * Policy configuration 66 | * Improvements to user management 67 | 68 | # Code of Conduct 69 | Operator for RabbitMQ is governed by the [Contributor Covenant v 1.4.1](CODE_OF_CONDUCT.md). 70 | 71 | # License 72 | Operator for RabbitMQ is licensed under the [Apache 2 License](LICENSE). 73 | 74 | # Creating a Release 75 | Check the GitHub [releases page](https://github.com/indeedeng/rabbitmq-operator/releases) for the latest version and from that determine the next version to release. On master, create a tag (`git tag `) and push it (`git push --tags`). [release.sh](release.sh) will see the tag is on master and push the new version to DockerHub automatically (via Travis). 76 | 77 | [Draft a new release](https://github.com/indeedeng/rabbitmq-operator/releases/new), put the tag you just created in the "tag version" box, and copy everything from [CHANGELOG.md](CHANGELOG.md) into the release description. 78 | 79 | Finally, add a `Bugs`, `Improvements`, and `New Features` section for the next version. 80 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/rabbitmq/PolicyReconciler.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.rabbitmq; 2 | 3 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiException; 4 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiFacade; 5 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 6 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQCluster; 7 | import com.indeed.rabbitmq.admin.pojo.Policy; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.regex.Pattern; 14 | import java.util.stream.Collectors; 15 | 16 | public class PolicyReconciler { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(PolicyReconciler.class); 19 | 20 | private final RabbitManagementApiProvider apiProvider; 21 | 22 | public PolicyReconciler( 23 | final RabbitManagementApiProvider apiProvider 24 | ) { 25 | this.apiProvider = apiProvider; 26 | } 27 | 28 | public void reconcile(final RabbitMQCluster cluster) { 29 | final RabbitManagementApiFacade apiClient = apiProvider.getApi(cluster); 30 | 31 | final Map desiredPolicies = cluster.getPolicies().stream() 32 | .map(policySpec -> new Policy() 33 | .withName(policySpec.getName()) 34 | .withVhost(policySpec.getVhost()) 35 | .withApplyTo(Policy.ApplyTo.fromValue(policySpec.getApplyTo())) 36 | .withDefinition(policySpec.getDefinition().asDefinition()) 37 | .withPattern(Pattern.compile(policySpec.getPattern())) 38 | .withPriority(policySpec.getPriority())) 39 | .collect(Collectors.toMap(Policy::getName, policy -> policy)); 40 | final Map existingPolicies = apiClient.listPolicies().stream() 41 | .collect(Collectors.toMap(Policy::getName, policy -> policy)); 42 | 43 | deleteObsoletePolicies(desiredPolicies, existingPolicies, apiClient); 44 | createMissingPolicies(desiredPolicies, existingPolicies, apiClient); 45 | updateExistingPolicies(desiredPolicies, existingPolicies, apiClient); 46 | } 47 | 48 | private void createMissingPolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 49 | final List policiesToCreate = desiredPolicies.entrySet().stream() 50 | .filter(desiredPolicy -> !existingPolicies.containsKey(desiredPolicy.getKey())) 51 | .map(Map.Entry::getValue) 52 | .collect(Collectors.toList()); 53 | 54 | for (final Policy policy : policiesToCreate) { 55 | try { 56 | apiClient.createPolicy(policy.getVhost(), policy.getName(), policy); 57 | } catch (final RabbitManagementApiException e) { 58 | log.error(String.format("Failed to create policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 59 | } 60 | } 61 | } 62 | 63 | private void updateExistingPolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 64 | final List policiesToUpdate = desiredPolicies.entrySet().stream() 65 | .filter(desiredPolicy -> existingPolicies.containsKey(desiredPolicy.getKey()) && !policiesMatch(desiredPolicy.getValue(), existingPolicies.get(desiredPolicy.getKey()))) 66 | .map(Map.Entry::getValue) 67 | .collect(Collectors.toList()); 68 | 69 | for (final Policy policy : policiesToUpdate) { 70 | try { 71 | apiClient.createPolicy(policy.getVhost(), policy.getName(), policy); 72 | } catch (final RabbitManagementApiException e) { 73 | log.error(String.format("Failed to update policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 74 | } 75 | } 76 | } 77 | 78 | private void deleteObsoletePolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 79 | final List policiesToDelete = existingPolicies.entrySet().stream() 80 | .filter(existingPolicy -> !desiredPolicies.containsKey(existingPolicy.getKey())) 81 | .map(Map.Entry::getValue) 82 | .collect(Collectors.toList()); 83 | 84 | for (final Policy policy : policiesToDelete) { 85 | try { 86 | apiClient.deletePolicy(policy.getVhost(), policy.getName()); 87 | } catch (final RabbitManagementApiException e) { 88 | log.error(String.format("Failed to delete policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 89 | } 90 | } 91 | } 92 | 93 | private boolean policiesMatch(final Policy desired, final Policy existing) { 94 | return existing != null && 95 | desired.getName().equals(existing.getName()) && 96 | desired.getVhost().equals(existing.getVhost()) && 97 | desired.getApplyTo().equals(existing.getApplyTo()) && 98 | desired.getDefinition().equals(existing.getDefinition()) && 99 | desired.getPattern().pattern().equals(existing.getPattern().pattern()) && 100 | desired.getPriority().equals(existing.getPriority()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/rabbitmq/OperatorPolicyReconciler.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.rabbitmq; 2 | 3 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiException; 4 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiFacade; 5 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 6 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQCluster; 7 | import com.indeed.rabbitmq.admin.pojo.OperatorPolicy; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.regex.Pattern; 14 | import java.util.stream.Collectors; 15 | 16 | public class OperatorPolicyReconciler { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(OperatorPolicyReconciler.class); 19 | 20 | private final RabbitManagementApiProvider apiProvider; 21 | 22 | public OperatorPolicyReconciler( 23 | final RabbitManagementApiProvider apiProvider 24 | ) { 25 | this.apiProvider = apiProvider; 26 | } 27 | 28 | public void reconcile(final RabbitMQCluster cluster) { 29 | final RabbitManagementApiFacade apiClient = apiProvider.getApi(cluster); 30 | 31 | final Map desiredPolicies = cluster.getOperatorPolicies().stream() 32 | .map(operatorPolicySpec -> new OperatorPolicy() 33 | .withName(operatorPolicySpec.getName()) 34 | .withVhost(operatorPolicySpec.getVhost()) 35 | .withApplyTo(OperatorPolicy.ApplyTo.fromValue(operatorPolicySpec.getApplyTo())) 36 | .withOperatorPolicyDefinition(operatorPolicySpec.getDefinition().asOperatorPolicyDefinition()) 37 | .withPattern(Pattern.compile(operatorPolicySpec.getPattern())) 38 | .withPriority(operatorPolicySpec.getPriority())) 39 | .collect(Collectors.toMap(OperatorPolicy::getName, policy -> policy)); 40 | final Map existingPolicies = apiClient.listOperatorPolicies().stream() 41 | .collect(Collectors.toMap(OperatorPolicy::getName, policy -> policy)); 42 | 43 | deleteObsoletePolicies(desiredPolicies, existingPolicies, apiClient); 44 | createMissingPolicies(desiredPolicies, existingPolicies, apiClient); 45 | updateExistingPolicies(desiredPolicies, existingPolicies, apiClient); 46 | } 47 | 48 | private void createMissingPolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 49 | final List policiesToCreate = desiredPolicies.entrySet().stream() 50 | .filter(desiredPolicy -> !existingPolicies.containsKey(desiredPolicy.getKey())) 51 | .map(Map.Entry::getValue) 52 | .collect(Collectors.toList()); 53 | 54 | for (final OperatorPolicy policy : policiesToCreate) { 55 | try { 56 | apiClient.createOperatorPolicy(policy.getVhost(), policy.getName(), policy); 57 | } catch (final RabbitManagementApiException e) { 58 | log.error(String.format("Failed to create operator policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 59 | } 60 | } 61 | } 62 | 63 | private void updateExistingPolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 64 | final List policiesToUpdate = desiredPolicies.entrySet().stream() 65 | .filter(desiredPolicy -> existingPolicies.containsKey(desiredPolicy.getKey()) && !policiesMatch(desiredPolicy.getValue(), existingPolicies.get(desiredPolicy.getKey()))) 66 | .map(Map.Entry::getValue) 67 | .collect(Collectors.toList()); 68 | 69 | for (final OperatorPolicy policy : policiesToUpdate) { 70 | try { 71 | apiClient.createOperatorPolicy(policy.getVhost(), policy.getName(), policy); 72 | } catch (final RabbitManagementApiException e) { 73 | log.error(String.format("Failed to update operator policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 74 | } 75 | } 76 | } 77 | 78 | private void deleteObsoletePolicies(final Map desiredPolicies, final Map existingPolicies, final RabbitManagementApiFacade apiClient) { 79 | final List policiesToDelete = existingPolicies.entrySet().stream() 80 | .filter(existingPolicy -> !desiredPolicies.containsKey(existingPolicy.getKey())) 81 | .map(Map.Entry::getValue) 82 | .collect(Collectors.toList()); 83 | 84 | for (final OperatorPolicy policy : policiesToDelete) { 85 | try { 86 | apiClient.deleteOperatorPolicy(policy.getVhost(), policy.getName()); 87 | } catch (final RabbitManagementApiException e) { 88 | log.error(String.format("Failed to delete operator policy with name %s in vhost %s", policy.getName(), policy.getVhost()), e); 89 | } 90 | } 91 | } 92 | 93 | private boolean policiesMatch(final OperatorPolicy desired, final OperatorPolicy existing) { 94 | return existing != null && 95 | desired.getName().equals(existing.getName()) && 96 | desired.getVhost().equals(existing.getVhost()) && 97 | desired.getApplyTo().equals(existing.getApplyTo()) && 98 | desired.getOperatorPolicyDefinition().equals(existing.getOperatorPolicyDefinition()) && 99 | desired.getPattern().pattern().equals(existing.getPattern().pattern()) && 100 | desired.getPriority().equals(existing.getPriority()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /rabbitmq-operator-model/src/main/java/com/indeed/operators/rabbitmq/model/crd/rabbitmq/PolicyDefinitionSpec.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.model.crd.rabbitmq; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.indeed.rabbitmq.admin.pojo.Definition; 6 | 7 | public class PolicyDefinitionSpec { 8 | 9 | private final String alternateExchange; 10 | private final String deadLetterExchange; 11 | private final String deadLetterRoutingKey; 12 | private final Long expires; 13 | private final String haMode; 14 | private final Long haParams; 15 | private final String haPromoteOnShutdown; 16 | private final Long haSyncBatchSize; 17 | private final String haSyncMode; 18 | private final Long maxLength; 19 | private final Long maxLengthBytes; 20 | private final Long messageTtl; 21 | private final String queueMasterLocator; 22 | 23 | public PolicyDefinitionSpec( 24 | @JsonProperty("alternate-exchange") final String alternateExchange, 25 | @JsonProperty("dead-letter-exchange") final String deadLetterExchange, 26 | @JsonProperty("dead-letter-routing-key") final String deadLetterRoutingKey, 27 | @JsonProperty("expires") final Long expires, 28 | @JsonProperty("ha-mode") final String haMode, 29 | @JsonProperty("ha-params") final Long haParams, 30 | @JsonProperty("ha-promote-on-shutdown") final String haPromoteOnShutdown, 31 | @JsonProperty("ha-sync-batch-size") final Long haSyncBatchSize, 32 | @JsonProperty("ha-sync-mode") final String haSyncMode, 33 | @JsonProperty("max-length") final Long maxLength, 34 | @JsonProperty("max-length-bytes") final Long maxLengthBytes, 35 | @JsonProperty("message-ttl") final Long messageTtl, 36 | @JsonProperty("queue-master-locator") final String queueMasterLocator 37 | ) { 38 | this.alternateExchange = alternateExchange; 39 | this.deadLetterExchange = deadLetterExchange; 40 | this.deadLetterRoutingKey = deadLetterRoutingKey; 41 | this.expires = expires; 42 | this.haMode = haMode; 43 | this.haParams = haParams; 44 | this.haPromoteOnShutdown = haPromoteOnShutdown; 45 | this.haSyncBatchSize = haSyncBatchSize; 46 | this.haSyncMode = haSyncMode; 47 | this.maxLength = maxLength; 48 | this.maxLengthBytes = maxLengthBytes; 49 | this.messageTtl = messageTtl; 50 | this.queueMasterLocator = queueMasterLocator; 51 | } 52 | 53 | public String getAlternateExchange() { 54 | return alternateExchange; 55 | } 56 | 57 | public String getDeadLetterExchange() { 58 | return deadLetterExchange; 59 | } 60 | 61 | public String getDeadLetterRoutingKey() { 62 | return deadLetterRoutingKey; 63 | } 64 | 65 | public Long getExpires() { 66 | return expires; 67 | } 68 | 69 | public String getHaMode() { 70 | return haMode; 71 | } 72 | 73 | public Long getHaParams() { 74 | return haParams; 75 | } 76 | 77 | public String getHaPromoteOnShutdown() { 78 | return haPromoteOnShutdown; 79 | } 80 | 81 | public Long getHaSyncBatchSize() { 82 | return haSyncBatchSize; 83 | } 84 | 85 | public String getHaSyncMode() { 86 | return haSyncMode; 87 | } 88 | 89 | public Long getMaxLength() { 90 | return maxLength; 91 | } 92 | 93 | public Long getMaxLengthBytes() { 94 | return maxLengthBytes; 95 | } 96 | 97 | public Long getMessageTtl() { 98 | return messageTtl; 99 | } 100 | 101 | public String getQueueMasterLocator() { 102 | return queueMasterLocator; 103 | } 104 | 105 | @JsonIgnore 106 | public Definition asDefinition() { 107 | Definition definition = new Definition(); 108 | 109 | if (getAlternateExchange() != null) { 110 | definition = definition.withAlternateExchange(getAlternateExchange()); 111 | } 112 | 113 | if (getDeadLetterExchange() != null) { 114 | definition = definition.withAlternateExchange(getDeadLetterExchange()); 115 | } 116 | 117 | if (getDeadLetterRoutingKey() != null) { 118 | definition = definition.withAlternateExchange(getDeadLetterRoutingKey()); 119 | } 120 | 121 | if (getExpires() != null) { 122 | definition = definition.withExpires(getExpires()); 123 | } 124 | 125 | if (getHaMode() != null) { 126 | definition = definition.withHaMode(Definition.HaMode.fromValue(getHaMode())); 127 | } 128 | 129 | if (getHaParams() != null) { 130 | definition = definition.withHaParams(getHaParams()); 131 | } 132 | 133 | if (getHaPromoteOnShutdown() != null) { 134 | definition = definition.withHaPromoteOnShutdown(Definition.HaPromoteOnShutdown.fromValue(getHaPromoteOnShutdown())); 135 | } 136 | 137 | if (getHaSyncBatchSize() != null) { 138 | definition = definition.withHaSyncBatchSize(getHaSyncBatchSize()); 139 | } 140 | 141 | if (getHaSyncMode() != null) { 142 | definition = definition.withHaSyncMode(Definition.HaSyncMode.fromValue(getHaSyncMode())); 143 | } 144 | 145 | if (getMaxLength() != null) { 146 | definition = definition.withMaxLength(getMaxLength()); 147 | } 148 | 149 | if (getMaxLengthBytes() != null) { 150 | definition = definition.withMaxLengthBytes(getMaxLengthBytes()); 151 | } 152 | 153 | if (getMessageTtl() != null) { 154 | definition = definition.withMessageTtl(getMessageTtl()); 155 | } 156 | 157 | if (getQueueMasterLocator() != null) { 158 | definition = definition.withQueueMasterLocator(Definition.QueueMasterLocator.fromValue(getQueueMasterLocator())); 159 | } 160 | 161 | return definition; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/resources/RabbitMQSecrets.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.resources; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.indeed.operators.rabbitmq.Constants; 5 | import com.indeed.operators.rabbitmq.model.Labels; 6 | import com.indeed.operators.rabbitmq.model.ModelFieldLookups; 7 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.RabbitMQCustomResource; 8 | import io.fabric8.kubernetes.api.model.OwnerReference; 9 | import io.fabric8.kubernetes.api.model.Secret; 10 | import io.fabric8.kubernetes.api.model.SecretBuilder; 11 | 12 | import java.util.function.Function; 13 | 14 | import static com.indeed.operators.rabbitmq.Constants.DEFAULT_USERNAME; 15 | 16 | public class RabbitMQSecrets { 17 | 18 | private final Function randomStringGenerator; 19 | private final Function secretDataEncoder; 20 | 21 | public RabbitMQSecrets( 22 | final Function randomStringGenerator, 23 | final Function secretDataEncoder 24 | ) { 25 | this.randomStringGenerator = Preconditions.checkNotNull(randomStringGenerator); 26 | this.secretDataEncoder = Preconditions.checkNotNull(secretDataEncoder); 27 | } 28 | 29 | public Secret createClusterSecret(final RabbitMQCustomResource rabbit) { 30 | final String clusterName = ModelFieldLookups.getName(rabbit); 31 | 32 | final String password = randomStringGenerator.apply(30); 33 | 34 | return new SecretBuilder() 35 | .addToData(Constants.Secrets.USERNAME_KEY, secretDataEncoder.apply(DEFAULT_USERNAME)) 36 | .addToData(Constants.Secrets.PASSWORD_KEY, secretDataEncoder.apply(password)) 37 | .withNewMetadata() 38 | .withName(getClusterSecretName(clusterName)) 39 | .withNamespace(rabbit.getMetadata().getNamespace()) 40 | .addToLabels(Labels.Kubernetes.INSTANCE, clusterName) 41 | .addToLabels(Labels.Kubernetes.MANAGED_BY, Labels.Values.RABBITMQ_OPERATOR) 42 | .addToLabels(Labels.Kubernetes.PART_OF, Labels.Values.RABBITMQ) 43 | .addToLabels(Labels.Indeed.getIndeedLabels(rabbit)) 44 | .withOwnerReferences( 45 | new OwnerReference( 46 | rabbit.getApiVersion(), 47 | true, 48 | true, 49 | rabbit.getKind(), 50 | rabbit.getName(), 51 | rabbit.getMetadata().getUid() 52 | ) 53 | ) 54 | .endMetadata() 55 | .build(); 56 | } 57 | 58 | public Secret createUserSecret( 59 | final String username, 60 | final RabbitMQCustomResource rabbit 61 | ) { 62 | final String clusterName = rabbit.getName(); 63 | final String password = randomStringGenerator.apply(30); 64 | 65 | return new SecretBuilder() 66 | .addToData(Constants.Secrets.USERNAME_KEY, secretDataEncoder.apply(username)) 67 | .addToData(Constants.Secrets.PASSWORD_KEY, secretDataEncoder.apply(password)) 68 | .withNewMetadata() 69 | .withName(getUserSecretName(username, clusterName)) 70 | .withNamespace(rabbit.getMetadata().getNamespace()) 71 | .addToLabels(Labels.Kubernetes.INSTANCE, clusterName) 72 | .addToLabels(Labels.Kubernetes.MANAGED_BY, Labels.Values.RABBITMQ_OPERATOR) 73 | .addToLabels(Labels.Kubernetes.PART_OF, Labels.Values.RABBITMQ) 74 | .addToLabels(Labels.Indeed.getIndeedLabels(rabbit)) 75 | .withOwnerReferences( 76 | new OwnerReference( 77 | rabbit.getApiVersion(), 78 | true, 79 | true, 80 | rabbit.getKind(), 81 | rabbit.getName(), 82 | rabbit.getMetadata().getUid() 83 | ) 84 | ) 85 | .endMetadata() 86 | .build(); 87 | } 88 | 89 | public Secret createErlangCookieSecret(final RabbitMQCustomResource rabbit) { 90 | final String clusterName = ModelFieldLookups.getName(rabbit); 91 | 92 | final String erlangCookie = randomStringGenerator.apply(50); 93 | 94 | return new SecretBuilder() 95 | .addToData(Constants.Secrets.ERLANG_COOKIE_KEY, secretDataEncoder.apply(erlangCookie)) 96 | .withNewMetadata() 97 | .withName(getErlangCookieSecretName(clusterName)) 98 | .withNamespace(rabbit.getMetadata().getNamespace()) 99 | .addToLabels(Labels.Kubernetes.INSTANCE, clusterName) 100 | .addToLabels(Labels.Kubernetes.MANAGED_BY, Labels.Values.RABBITMQ_OPERATOR) 101 | .addToLabels(Labels.Kubernetes.PART_OF, Labels.Values.RABBITMQ) 102 | .addToLabels(Labels.Indeed.getIndeedLabels(rabbit)) 103 | .withOwnerReferences( 104 | new OwnerReference( 105 | rabbit.getApiVersion(), 106 | true, 107 | true, 108 | rabbit.getKind(), 109 | rabbit.getName(), 110 | rabbit.getMetadata().getUid() 111 | ) 112 | ) 113 | .endMetadata() 114 | .build(); 115 | } 116 | 117 | public static String getClusterSecretName(final String rabbitName) { 118 | return String.format("%s-runtime-secret", rabbitName); 119 | } 120 | 121 | public static String getUserSecretName(final String username, final String rabbitName) { 122 | return String.format("%s-%s-user-secret", username, rabbitName); 123 | } 124 | 125 | public static String getErlangCookieSecretName(final String rabbitName) { 126 | return String.format("%s-erlang-cookie", rabbitName); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/config/ReconcilerConfig.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.config; 2 | 3 | import com.indeed.operators.rabbitmq.NetworkPartitionWatcher; 4 | import com.indeed.operators.rabbitmq.RabbitMQEventWatcher; 5 | import com.indeed.operators.rabbitmq.api.RabbitMQPasswordConverter; 6 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 7 | import com.indeed.operators.rabbitmq.controller.PersistentVolumeClaimController; 8 | import com.indeed.operators.rabbitmq.controller.PodController; 9 | import com.indeed.operators.rabbitmq.controller.PodDisruptionBudgetController; 10 | import com.indeed.operators.rabbitmq.controller.SecretsController; 11 | import com.indeed.operators.rabbitmq.controller.ServicesController; 12 | import com.indeed.operators.rabbitmq.controller.StatefulSetController; 13 | import com.indeed.operators.rabbitmq.controller.crd.NetworkPartitionResourceController; 14 | import com.indeed.operators.rabbitmq.controller.crd.RabbitMQResourceController; 15 | import com.indeed.operators.rabbitmq.executor.ClusterAwareExecutor; 16 | import com.indeed.operators.rabbitmq.operations.AreQueuesEmptyOperation; 17 | import com.indeed.operators.rabbitmq.reconciliation.ClusterReconciliationOrchestrator; 18 | import com.indeed.operators.rabbitmq.reconciliation.NetworkPartitionReconciler; 19 | import com.indeed.operators.rabbitmq.reconciliation.RabbitMQClusterReconciler; 20 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.UserReconciler; 21 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.OperatorPolicyReconciler; 22 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.PolicyReconciler; 23 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.RabbitMQClusterFactory; 24 | import com.indeed.operators.rabbitmq.reconciliation.rabbitmq.ShovelReconciler; 25 | import com.indeed.operators.rabbitmq.resources.RabbitMQContainers; 26 | import com.indeed.operators.rabbitmq.resources.RabbitMQPods; 27 | import org.springframework.context.annotation.Bean; 28 | import org.springframework.context.annotation.Configuration; 29 | 30 | @Configuration 31 | public class ReconcilerConfig { 32 | 33 | @Bean 34 | public RabbitMQEventWatcher rabbitEventWatcher( 35 | final RabbitMQClusterReconciler reconciler, 36 | final RabbitMQResourceController controller, 37 | final ClusterReconciliationOrchestrator orchestrator 38 | ) { 39 | return new RabbitMQEventWatcher(reconciler, controller, orchestrator); 40 | } 41 | 42 | @Bean 43 | public NetworkPartitionWatcher networkPartitionWatcher( 44 | final NetworkPartitionReconciler partitionReconciler, 45 | final NetworkPartitionResourceController controller, 46 | final ClusterReconciliationOrchestrator orchestrator 47 | ) { 48 | return new NetworkPartitionWatcher(partitionReconciler, controller, orchestrator); 49 | } 50 | 51 | @Bean 52 | public ClusterReconciliationOrchestrator clusterReconciliationOrchestrator( 53 | final ClusterAwareExecutor executor 54 | ) { 55 | return new ClusterReconciliationOrchestrator(executor); 56 | } 57 | 58 | @Bean 59 | public RabbitMQClusterReconciler rabbitClusterReconciler( 60 | final RabbitMQClusterFactory clusterFactory, 61 | final RabbitMQResourceController controller, 62 | final SecretsController secretsController, 63 | final ServicesController servicesController, 64 | final StatefulSetController statefulSetController, 65 | final PodDisruptionBudgetController podDisruptionBudgetController, 66 | final PersistentVolumeClaimController persistentVolumeClaimController, 67 | final ShovelReconciler shovelReconciler, 68 | final UserReconciler usersReconciler, 69 | final PolicyReconciler policyReconciler, 70 | final OperatorPolicyReconciler operatorPolicyReconciler 71 | ) { 72 | return new RabbitMQClusterReconciler( 73 | clusterFactory, 74 | controller, 75 | secretsController, 76 | servicesController, 77 | statefulSetController, 78 | podDisruptionBudgetController, 79 | persistentVolumeClaimController, 80 | shovelReconciler, 81 | usersReconciler, 82 | policyReconciler, 83 | operatorPolicyReconciler 84 | ); 85 | } 86 | 87 | @Bean 88 | public ShovelReconciler shovelReconciler( 89 | final RabbitManagementApiProvider apiProvider, 90 | final SecretsController secretsController 91 | ) { 92 | return new ShovelReconciler(apiProvider, secretsController); 93 | } 94 | 95 | @Bean 96 | public UserReconciler rabbitMQUserReconciler( 97 | final SecretsController secretsController, 98 | final RabbitManagementApiProvider managementApiProvider, 99 | final RabbitMQPasswordConverter passwordConverter 100 | ) { 101 | return new UserReconciler(secretsController, managementApiProvider, passwordConverter); 102 | } 103 | 104 | @Bean 105 | public NetworkPartitionReconciler networkPartitionReconciler( 106 | final RabbitMQResourceController rabbitMQResourceController, 107 | final NetworkPartitionResourceController networkPartitionResourceController, 108 | final AreQueuesEmptyOperation queuesEmptyOperation, 109 | final RabbitMQPods rabbitMQPods, 110 | final RabbitMQContainers rabbitMQContainers, 111 | final StatefulSetController statefulSetController, 112 | final PodController podController, 113 | final String namespace 114 | ) { 115 | return new NetworkPartitionReconciler(rabbitMQResourceController, networkPartitionResourceController, queuesEmptyOperation, rabbitMQPods, rabbitMQContainers, statefulSetController, podController, namespace); 116 | } 117 | 118 | @Bean 119 | public PolicyReconciler policyReconciler( 120 | final RabbitManagementApiProvider apiProvider 121 | ) { 122 | return new PolicyReconciler(apiProvider); 123 | } 124 | 125 | @Bean 126 | public OperatorPolicyReconciler operatorPolicyReconciler( 127 | final RabbitManagementApiProvider apiProvider 128 | ) { 129 | return new OperatorPolicyReconciler(apiProvider); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /rabbitmq-operator/src/main/java/com/indeed/operators/rabbitmq/reconciliation/rabbitmq/ShovelReconciler.java: -------------------------------------------------------------------------------- 1 | package com.indeed.operators.rabbitmq.reconciliation.rabbitmq; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.collect.Lists; 5 | import com.indeed.operators.rabbitmq.Constants; 6 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiException; 7 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiFacade; 8 | import com.indeed.operators.rabbitmq.api.RabbitManagementApiProvider; 9 | import com.indeed.operators.rabbitmq.controller.SecretsController; 10 | import com.indeed.operators.rabbitmq.model.crd.rabbitmq.AddressAndVhost; 11 | import com.indeed.operators.rabbitmq.model.rabbitmq.RabbitMQCluster; 12 | import com.indeed.rabbitmq.admin.pojo.Shovel; 13 | import com.indeed.rabbitmq.admin.pojo.ShovelArguments; 14 | import io.fabric8.kubernetes.api.model.Secret; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.stream.Collectors; 21 | 22 | import static com.indeed.operators.rabbitmq.Constants.Uris.AMQP_BASE; 23 | 24 | public class ShovelReconciler { 25 | private static final Logger log = LoggerFactory.getLogger(ShovelReconciler.class); 26 | 27 | private final RabbitManagementApiProvider apiProvider; 28 | private final SecretsController secretsController; 29 | 30 | public ShovelReconciler( 31 | final RabbitManagementApiProvider apiProvider, 32 | final SecretsController secretsController 33 | ) { 34 | this.apiProvider = apiProvider; 35 | this.secretsController = secretsController; 36 | } 37 | 38 | public void reconcile(final RabbitMQCluster cluster) { 39 | final RabbitManagementApiFacade apiClient = apiProvider.getApi(cluster); 40 | 41 | final Map desiredShovels = cluster.getShovels().stream() 42 | .map(shovelSpec -> { 43 | final String destSecretName = shovelSpec.getDestination().getSecretName(); 44 | final String destSecretNamespace = shovelSpec.getDestination().getSecretNamespace(); 45 | final Secret secret = secretsController.get(destSecretName, destSecretNamespace); 46 | 47 | Preconditions.checkNotNull(secret, String.format("Could not find secret with name [%s] in namespace [%s]", destSecretName, destSecretNamespace)); 48 | 49 | final List uris = shovelSpec.getDestination().getAddresses().stream() 50 | .map(addr -> buildShovelUri(secret, addr)) 51 | .collect(Collectors.toList()); 52 | 53 | final ShovelArguments shovelArguments = new ShovelArguments() 54 | .withSrcUri(Lists.newArrayList(AMQP_BASE)) 55 | .withSrcQueue(shovelSpec.getSource().getQueue()) 56 | .withDestUri(uris); 57 | return new Shovel().withValue(shovelArguments).withVhost(shovelSpec.getSource().getVhost()).withName(shovelSpec.getName()); 58 | }) 59 | .collect(Collectors.toMap(Shovel::getName, shovel -> shovel)); 60 | final Map existingShovels = apiClient.listShovels().stream() 61 | .collect(Collectors.toMap(Shovel::getName, shovel -> shovel)); 62 | 63 | deleteObsoleteShovels(desiredShovels, existingShovels, apiClient); 64 | createMissingShovels(desiredShovels, existingShovels, apiClient); 65 | updateExistingShovels(desiredShovels, existingShovels, apiClient); 66 | } 67 | 68 | private void createMissingShovels(final Map desiredShovels, final Map existingShovels, final RabbitManagementApiFacade apiClient) { 69 | final List shovelsToCreate = desiredShovels.entrySet().stream() 70 | .filter(desiredShovel -> !existingShovels.containsKey(desiredShovel.getKey())) 71 | .map(Map.Entry::getValue) 72 | .collect(Collectors.toList()); 73 | 74 | for (final Shovel shovel : shovelsToCreate) { 75 | try { 76 | apiClient.createShovel(shovel.getVhost(), shovel.getName(), shovel); 77 | } catch (final RabbitManagementApiException e) { 78 | log.error(String.format("Failed to create shovel with name %s in vhost %s", shovel.getName(), shovel.getVhost()), e); 79 | } 80 | } 81 | } 82 | 83 | private void updateExistingShovels(final Map desiredShovels, final Map existingShovels, final RabbitManagementApiFacade apiClient) { 84 | final List shovelsToUpdate = desiredShovels.entrySet().stream() 85 | .filter(desiredShovel -> existingShovels.containsKey(desiredShovel.getKey()) && !shovelsMatch(desiredShovel.getValue(), existingShovels.get(desiredShovel.getKey()))) 86 | .map(Map.Entry::getValue) 87 | .collect(Collectors.toList()); 88 | 89 | for (final Shovel shovel : shovelsToUpdate) { 90 | try { 91 | apiClient.createShovel(shovel.getVhost(), shovel.getName(), shovel); 92 | } catch (final RabbitManagementApiException e) { 93 | log.error(String.format("Failed to update shovel with name %s in vhost %s", shovel.getName(), shovel.getVhost()), e); 94 | } 95 | } 96 | } 97 | 98 | private void deleteObsoleteShovels(final Map desiredShovels, final Map existingShovels, final RabbitManagementApiFacade apiClient) { 99 | final List shovelsToDelete = existingShovels.entrySet().stream() 100 | .filter(existingShovel -> !desiredShovels.containsKey(existingShovel.getKey())) 101 | .map(Map.Entry::getValue) 102 | .collect(Collectors.toList()); 103 | 104 | for (final Shovel existingShovel : shovelsToDelete) { 105 | try { 106 | apiClient.deleteShovel(existingShovel.getVhost(), existingShovel.getName()); 107 | } catch (final Exception e) { 108 | log.error(String.format("Failed to delete shovel with name %s in vhost %s", existingShovel.getName(), existingShovel.getVhost()), e); 109 | } 110 | } 111 | } 112 | 113 | private String buildShovelUri(final Secret shovelSecret, final AddressAndVhost rabbitAddress) { 114 | final String username = secretsController.decodeSecretPayload(shovelSecret.getData().get(Constants.Secrets.USERNAME_KEY)); 115 | final String password = secretsController.decodeSecretPayload(shovelSecret.getData().get(Constants.Secrets.PASSWORD_KEY)); 116 | 117 | return String.format("%s%s:%s@%s", AMQP_BASE, username, password, rabbitAddress.asRabbitUri()); 118 | } 119 | 120 | private boolean shovelsMatch(final Shovel desired, final Shovel existing) { 121 | return existing != null && 122 | desired.getValue().equals(existing.getValue()) && 123 | desired.getVhost().equals(existing.getVhost()) && 124 | desired.getName().equals(existing.getName()); 125 | } 126 | } 127 | --------------------------------------------------------------------------------