├── .gitignore ├── Dockerfile ├── README.md ├── application.properties ├── build.gradle ├── dist ├── shared-kafka-admin-micro-0.9.0.jar └── shared-kafka-admin-micro-0.9.1.jar ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── avroProducer.png ├── basicProducer.png ├── consumer.png ├── consumerTab.png └── manager.png ├── src ├── main │ ├── java │ │ └── com │ │ │ └── bettercloud │ │ │ ├── kadmin │ │ │ ├── AppConfiguration.java │ │ │ ├── Application.java │ │ │ ├── api │ │ │ │ ├── kafka │ │ │ │ │ ├── AvrifyConverter.java │ │ │ │ │ ├── JsonToAvroConverter.java │ │ │ │ │ ├── KadminConsumerConfig.java │ │ │ │ │ ├── KadminConsumerGroup.java │ │ │ │ │ ├── KadminConsumerGroupContainer.java │ │ │ │ │ ├── KadminProducer.java │ │ │ │ │ ├── KadminProducerConfig.java │ │ │ │ │ ├── MessageHandler.java │ │ │ │ │ ├── MessageHandlerRegistry.java │ │ │ │ │ └── exception │ │ │ │ │ │ └── SchemaRegistryRestException.java │ │ │ │ ├── models │ │ │ │ │ ├── DeserializerInfoModel.java │ │ │ │ │ ├── Model.java │ │ │ │ │ └── SerializerInfoModel.java │ │ │ │ └── services │ │ │ │ │ ├── BundledKadminConsumerGroupProviderService.java │ │ │ │ │ ├── DeserializerRegistryService.java │ │ │ │ │ ├── FeaturesService.java │ │ │ │ │ ├── IntrospectionService.java │ │ │ │ │ ├── KadminConsumerGroupProviderService.java │ │ │ │ │ ├── KadminProducerProviderService.java │ │ │ │ │ ├── RegistryService.java │ │ │ │ │ ├── SchemaRegistryService.java │ │ │ │ │ └── SerializerRegistryService.java │ │ │ ├── io │ │ │ │ ├── network │ │ │ │ │ ├── dto │ │ │ │ │ │ ├── ConsumerInfoModel.java │ │ │ │ │ │ ├── KafkaProduceMessageMetaModel.java │ │ │ │ │ │ ├── KafkaProduceRequestModel.java │ │ │ │ │ │ ├── ProducerInfoModel.java │ │ │ │ │ │ ├── ProducerRepsonseModel.java │ │ │ │ │ │ └── SchemaInfoModel.java │ │ │ │ │ └── rest │ │ │ │ │ │ ├── KafkaConsumerResource.java │ │ │ │ │ │ ├── KafkaProducerResource.java │ │ │ │ │ │ ├── ManagerResource.java │ │ │ │ │ │ ├── SchemaProxyResource.java │ │ │ │ │ │ └── utils │ │ │ │ │ │ ├── PropertiesBackedFeaturesService.java │ │ │ │ │ │ └── ResponseUtil.java │ │ │ │ └── ui │ │ │ │ │ └── KadminUiController.java │ │ │ ├── kafka │ │ │ │ ├── BasicKafkaConsumerGroup.java │ │ │ │ ├── BasicKafkaProducer.java │ │ │ │ ├── DefaultJsonToAvroConverter.java │ │ │ │ ├── LoggedKafkaMessageHandler.java │ │ │ │ ├── QueuedKafkaMessageHandler.java │ │ │ │ ├── SerializersProperties.java │ │ │ │ └── avro │ │ │ │ │ └── ErrorTolerantAvroObjectDeserializer.java │ │ │ └── services │ │ │ │ ├── BasicKafkaConsumerProviderService.java │ │ │ │ ├── BasicKafkaProducerProviderService.java │ │ │ │ ├── DefaultIntrospectionService.java │ │ │ │ ├── DefaultSchemaRegistryService.java │ │ │ │ ├── DefultBundledKadminConsumerGroupProviderService.java │ │ │ │ ├── KafkaDeserializerRegistryService.java │ │ │ │ ├── KafkaSerializerRegistryService.java │ │ │ │ └── SimpleRegistryService.java │ │ │ └── util │ │ │ ├── LoggerUtils.java │ │ │ ├── Opt.java │ │ │ ├── Page.java │ │ │ ├── StreamUtils.java │ │ │ └── TimedWrapper.java │ └── resources │ │ ├── application-kadmin.properties │ │ ├── application-readonly.properties │ │ ├── application.properties │ │ ├── local.properties │ │ ├── logger.properties │ │ ├── service.properties │ │ ├── static │ │ ├── basicproducer │ │ │ ├── main.html │ │ │ ├── main.js │ │ │ ├── sent-error-template.html │ │ │ └── sent-info-template.html │ │ ├── consumer │ │ │ ├── main.html │ │ │ ├── main.js │ │ │ └── message-tile-template.html │ │ ├── css │ │ │ └── bootstrap.min.css │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── js │ │ │ ├── ace.min.js │ │ │ ├── bootstrap.min.js │ │ │ ├── clipboard.min.js │ │ │ ├── jquery.min.js │ │ │ ├── moment.min.js │ │ │ └── underscore-min.js │ │ ├── logo.png │ │ ├── manager │ │ │ ├── consumer-template.html │ │ │ ├── main.html │ │ │ ├── main.js │ │ │ └── producer-template.html │ │ ├── mode-json.js │ │ ├── mode-text.js │ │ ├── producer │ │ │ ├── main.html │ │ │ ├── main.js │ │ │ ├── sent-error-template.html │ │ │ └── sent-info-template.html │ │ ├── theme-chrome.js │ │ ├── theme-kuroir.js │ │ ├── theme-monokai.js │ │ └── worker-json.js │ │ └── templates │ │ ├── basicproducer.html │ │ ├── consumer.html │ │ ├── index.html │ │ ├── manager.html │ │ └── producer.html └── test │ ├── java │ └── com │ │ └── bettercloud │ │ └── kadmin │ │ ├── KadminApplicationTests.java │ │ └── kafka │ │ └── DefaultJsonToAvroConverterTest.java │ └── resources │ └── test │ └── avro │ ├── CanonicalPayload.01.expected.json │ ├── CanonicalPayload.01.json │ ├── CanonicalPayload.schema.json │ ├── EventCall.01.expected.json │ ├── EventCall.01.json │ └── EventCall.schema.json └── updateClient.sh /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | out/ 3 | dist/application.properties 4 | classes 5 | 6 | .idea 7 | .DS_Store 8 | 9 | .gradle 10 | 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk as builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN ./gradlew build -x test 5 | 6 | FROM openjdk:8-jre 7 | LABEL maintainer="Eimar Fandino" 8 | 9 | WORKDIR /app 10 | 11 | COPY --from=builder /app/build/libs/shared-kafka-admin-micro-*.jar /app/app.jar 12 | COPY application.properties /app/application.properties 13 | 14 | EXPOSE 8080 15 | 16 | ENTRYPOINT [ "java", "-jar", "/app/app.jar" , "--spring.profiles.active=kadmin,local"] 17 | -------------------------------------------------------------------------------- /application.properties: -------------------------------------------------------------------------------- 1 | # The following config sets the spring context path. You will access the application at 2 | # http:/// e.g. http://localhost:8080/kadmin 3 | # 4 | #server.contextPath=/kadmin 5 | 6 | 7 | # This active profile sets the application context so that all requests are 8 | # served under /kadmin. See application-kadmin.properties for more information 9 | # or if you need to change the context. 10 | # 11 | spring.profiles.include=kadmin 12 | 13 | 14 | # Toggles read only mode i.e. Kafka producers are disabled. You can use the following 15 | # Spring profile or the raw config. 16 | # 17 | #spring.profiles.include=readonly 18 | ff.producer.enabled=true 19 | 20 | 21 | # Allows custom urls to be used for Kafka and Service Registry for each producer and consumer. 22 | # 23 | # 24 | ff.customKafkaUrl.enabled=true 25 | 26 | 27 | # If ff.customKafkaUrl.enabled is disabled then you need to configure the default endpoints using the following configs 28 | # kafka.host is a comma seperated list of kafka brokers. 29 | # 30 | #kafka.host=host1.bettercloud:6667,host2.bettercloud:6667 31 | #schema.registry.url=http://host3.bettercloud:8081 -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url 'http://packages.confluent.io/maven/' } 8 | maven { url "https://plugins.gradle.org/m2/" } 9 | jcenter() 10 | } 11 | dependencies { 12 | classpath "io.spring.gradle:dependency-management-plugin:0.5.2.RELEASE" 13 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.2.RELEASE") 14 | classpath("org.springframework:springloaded:1.2.8.RELEASE") 15 | } 16 | } 17 | 18 | apply plugin: 'eclipse' 19 | apply plugin: 'idea' 20 | apply plugin: 'spring-boot' 21 | apply plugin: 'java' 22 | apply plugin: 'war' 23 | apply plugin: "io.spring.dependency-management" 24 | 25 | jar { 26 | baseName = 'shared-kafka-admin-micro' 27 | version = '0.9.1' 28 | } 29 | 30 | war { 31 | baseName = 'shared-kafka-admin-micro' 32 | version = '0.9.1' 33 | } 34 | 35 | sourceCompatibility = 1.8 36 | targetCompatibility = 1.8 37 | 38 | repositories { 39 | jcenter() 40 | mavenCentral() 41 | mavenLocal() 42 | maven { url 'http://packages.confluent.io/maven/' } 43 | } 44 | 45 | 46 | dependencies { 47 | 48 | //Spring Boot 49 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: spring_boot_version 50 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: spring_boot_version 51 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: spring_boot_version 52 | 53 | 54 | //Apache 55 | compile group: 'org.apache.commons', name: 'commons-lang3', version: apache_commons_lang_version 56 | compile group: 'org.apache.httpcomponents', name: 'httpclient', version: apache_httpclient_version 57 | 58 | 59 | //Utils 60 | compile group: 'com.google.guava', name: 'guava', version: guava_version 61 | compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.1.7' 62 | compile group: 'org.projectlombok', name: 'lombok', version: lombok_version 63 | 64 | 65 | //confluent 66 | compile group: 'org.apache.avro', name: 'avro', version: avro_version 67 | compile group: 'org.apache.kafka', name: 'kafka-clients', version: kafka_version 68 | compile group: 'io.confluent', name: 'kafka-avro-serializer', version: confluent_version 69 | 70 | 71 | testCompile('org.springframework.boot:spring-boot-starter-test') 72 | } 73 | 74 | dependencyManagement { 75 | dependencies { 76 | dependency 'com.netflix.archaius:archaius-core:0.7.1' 77 | dependency ('org.springframework.integration:spring-integration-kafka:${spring_integration_version}') { 78 | exclude 'org.apache.kafka:kafka_2.10' 79 | } 80 | } 81 | } 82 | 83 | task wrapper(type: Wrapper) { 84 | gradleVersion = '2.11' 85 | } 86 | 87 | configurations { 88 | integTestCompile.extendsFrom testCompile 89 | integTestRuntime.extendsFrom testRuntime 90 | unitTestCompile.extendsFrom testCompile 91 | unitTestRuntime.extendsFrom testRuntime 92 | all*.exclude module : 'slf4j-log4j12' 93 | } 94 | -------------------------------------------------------------------------------- /dist/shared-kafka-admin-micro-0.9.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/dist/shared-kafka-admin-micro-0.9.0.jar -------------------------------------------------------------------------------- /dist/shared-kafka-admin-micro-0.9.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/dist/shared-kafka-admin-micro-0.9.1.jar -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | 4 | services: 5 | kadmin: 6 | hostname: kadmin 7 | image: bettercloud/kadmin 8 | ports: 9 | - "8080:8080" 10 | environment: 11 | ZOOKEEPER_HOST: zookeeper.localtest.me:2181 12 | KAFKA_HOST: kafka.localtest.me:9093 13 | SECURITY_PROTOCOL: SSL 14 | TRUST_STORE_LOCATION: ssl/client.truststore.jks 15 | TRUST_STORE_PASSWORD: password 16 | KEY_STORE_LOCATION: ssl/server.keystore.jks 17 | KEY_STORE_PASSWORD: password 18 | KEY_PASSWORD: password 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Spring 2 | spring_boot_version=1.3.3.RELEASE 3 | spring_integration_version=1.2.1.RELEASE 4 | spring_cloud_version=1.1.1.RELEASE 5 | 6 | #Utils 7 | guava_version=18.0 8 | jackson_version=2.4.4 9 | lombok_version=1.16.4 10 | findbugs_version=3.0.1 11 | apache_commons_lang_version=3.3.2 12 | apache_httpclient_version=4.5 13 | spring_redis_version=1.4.1.RELEASE 14 | jedis_version=2.6.1 15 | 16 | #Bettercloud 17 | modelAdapterVersion=1.+ 18 | logger_version=1.+ 19 | docker_plugin_version=2.+ 20 | wfAccessControlVersion=1.+ 21 | hmac_lib=1.0.0 22 | environment_loader=1.+ 23 | bc_kafka_lib=1.+ 24 | 25 | #confluent 26 | avro_version=1.8+ 27 | kafka_version=1.0.1 28 | confluent_version=4.0.0 29 | 30 | #Data 31 | postgres_jdbc_version=9.3-1102-jdbc41 32 | tomcat_jdbc_version=8.0.15 33 | h2_version=1.4.183 34 | mysql_connector_version=5.1.35 35 | avro_model_version=1.+ 36 | 37 | #metrics 38 | statsd_client_version=3.1.0 39 | codahale_version=3.0.2 40 | dropwizard_version=3.1.2 41 | 42 | #Testing 43 | junit_version=4.11 44 | power_mock_version=1.6.2 45 | meanbean_version=2.0.3 46 | equals_verifier_version=1.7.3 47 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 29 12:21:55 EDT 2016 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-2.13-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 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="" 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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= 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 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /images/avroProducer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/images/avroProducer.png -------------------------------------------------------------------------------- /images/basicProducer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/images/basicProducer.png -------------------------------------------------------------------------------- /images/consumer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/images/consumer.png -------------------------------------------------------------------------------- /images/consumerTab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/images/consumerTab.png -------------------------------------------------------------------------------- /images/manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/images/manager.png -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/AppConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 5 | import com.bettercloud.kadmin.api.services.DeserializerRegistryService; 6 | import com.bettercloud.kadmin.api.services.SerializerRegistryService; 7 | import com.bettercloud.kadmin.kafka.avro.ErrorTolerantAvroObjectDeserializer; 8 | import com.bettercloud.kadmin.services.KafkaDeserializerRegistryService; 9 | import com.bettercloud.kadmin.services.KafkaSerializerRegistryService; 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.node.TextNode; 13 | import com.google.common.collect.Maps; 14 | import org.apache.kafka.common.serialization.*; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | 18 | import java.io.IOException; 19 | import java.util.UUID; 20 | import java.util.function.Function; 21 | 22 | /** 23 | * Created by davidesposito on 7/21/16. 24 | */ 25 | @Configuration 26 | public class AppConfiguration { 27 | 28 | private static final ObjectMapper mapper = new ObjectMapper(); 29 | 30 | private static final String JSON_REPLACE; 31 | private static final String JSON_REPLACE_VAL = (char)191 + ""; 32 | 33 | static { 34 | // http://stackoverflow.com/a/3020108 35 | String temp = "["; 36 | for (int i=0;i<=20;i++) { 37 | temp += (char)i; 38 | } 39 | JSON_REPLACE = temp + "]"; 40 | } 41 | 42 | @Bean 43 | public SerializerRegistryService serializerRegistryService() { 44 | SerializerRegistryService registry = new KafkaSerializerRegistryService(); 45 | 46 | registry.register(sim("string", StringSerializer.class, s -> s)); 47 | registry.register(sim("bytes", ByteArraySerializer.class, s -> s)); 48 | registry.register(sim("int", IntegerSerializer.class, s -> Integer.parseInt(s))); 49 | registry.register(sim("long", LongSerializer.class, s -> Long.parseLong(s))); 50 | 51 | return registry; 52 | } 53 | 54 | private SerializerInfoModel sim(String id, Class serializerClass, Function rawPrep) { 55 | return sim(id, serializerClass.getSimpleName(), serializerClass, rawPrep); 56 | } 57 | 58 | private SerializerInfoModel sim(String id, String name, Class serializerClass, Function rawPrep) { 59 | return SerializerInfoModel.builder() 60 | .id(UUID.randomUUID().toString()) 61 | .name(name) 62 | .className(serializerClass.getName()) 63 | .meta(Maps.newHashMap()) 64 | .prepareRawFunc(rawPrep) 65 | .build(); 66 | } 67 | 68 | @Bean 69 | public DeserializerRegistryService deserializerRegistryService() { 70 | DeserializerRegistryService registry = new KafkaDeserializerRegistryService(); 71 | 72 | registry.register(dim("Avro Object Deserializer", ErrorTolerantAvroObjectDeserializer.class, "avro", (o) -> toNode(o + ""))); 73 | registry.register(dim(StringDeserializer.class, "string", (o) -> new TextNode(o + ""))); 74 | registry.register(dim(ByteArrayDeserializer.class, "bytes", (o) -> toNode(w(o + "")))); 75 | registry.register(dim(IntegerDeserializer.class, "int", (o) -> toNode(o + ""))); 76 | registry.register(dim(LongDeserializer.class, "long", (o) -> toNode(o + ""))); 77 | 78 | return registry; 79 | } 80 | 81 | private String w(String s) { 82 | return s == null ? "null" : "\"" + s + "\""; 83 | } 84 | 85 | private JsonNode toNode(String s) { 86 | try { 87 | return mapper.readTree(s.replace("\n", "\\n") 88 | .replaceAll(JSON_REPLACE, JSON_REPLACE_VAL)); 89 | } catch (IOException e) { 90 | e.printStackTrace(); 91 | } 92 | return null; 93 | } 94 | 95 | private DeserializerInfoModel dim(Class deserializerClass, String id, Function prepOutput) { 96 | return dim(deserializerClass.getSimpleName(), deserializerClass, id, prepOutput); 97 | } 98 | 99 | private DeserializerInfoModel dim(String name, Class deserializerClass, String id, Function prepOutput) { 100 | return DeserializerInfoModel.builder() 101 | .id(id) 102 | .name(name) 103 | .className(deserializerClass.getName()) 104 | .prepareOutputFunc(prepOutput) 105 | .build(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/Application.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin; 2 | 3 | import org.apache.http.client.HttpClient; 4 | import org.apache.http.impl.client.HttpClients; 5 | import org.springframework.boot.CommandLineRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.ComponentScan; 11 | 12 | @EnableAutoConfiguration 13 | @ComponentScan 14 | @SpringBootApplication 15 | public class Application { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(Application.class, args); 19 | } 20 | 21 | @Bean(name = "defaultClient") 22 | public HttpClient defaultHttpClient() { 23 | return HttpClients.createDefault(); 24 | } 25 | 26 | @Bean 27 | public CommandLineRunner logConfigurationRunner() { 28 | return (args) -> { 29 | // Logger kafkaClientLogger = LoggerFactory.getLogger(NetworkClient.class); 30 | // ((ch.qos.logback.classic.Logger)kafkaClientLogger).setLevel(Level.OFF); 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/AvrifyConverter.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | /** 4 | * Created by davidesposito on 7/11/16. 5 | */ 6 | public interface AvrifyConverter { 7 | 8 | String avrify(String json, String schema); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/JsonToAvroConverter.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | /** 4 | * Created by davidesposito on 7/6/16. 5 | */ 6 | public interface JsonToAvroConverter { 7 | 8 | Object convert(String json, String schemaStr); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/KadminConsumerConfig.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NonNull; 7 | 8 | /** 9 | * Created by davidesposito on 7/19/16. 10 | */ 11 | @Data 12 | @Builder 13 | public class KadminConsumerConfig { 14 | 15 | @NonNull private final String topic; 16 | private String kafkaHost; 17 | private String schemaRegistryUrl; 18 | private String keyDeserializer; 19 | private DeserializerInfoModel valueDeserializer; 20 | private String securityProtocol; 21 | private String trustStoreLocation; 22 | private String trustStorePassword; 23 | private String keyStoreLocation; 24 | private String keyStorePassword; 25 | private String keyPassword; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/KadminConsumerGroup.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | /** 4 | * Created by davidesposito on 7/19/16. 5 | */ 6 | public interface KadminConsumerGroup extends Runnable, MessageHandlerRegistry { 7 | 8 | KadminConsumerConfig getConfig(); 9 | 10 | String getClientId(); 11 | 12 | String getGroupId(); 13 | 14 | void shutdown(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/KadminConsumerGroupContainer.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | import com.bettercloud.kadmin.kafka.QueuedKafkaMessageHandler; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NonNull; 7 | 8 | import java.util.Collection; 9 | 10 | /** 11 | * Created by davidesposito on 7/23/16. 12 | */ 13 | @Data 14 | @Builder 15 | public class KadminConsumerGroupContainer { 16 | 17 | @NonNull private final KadminConsumerGroup consumer; 18 | @NonNull private final QueuedKafkaMessageHandler queue; 19 | 20 | /** 21 | * Must return the id of the contained consumer. Merely a delegate method for {@link KadminConsumerGroup#getClientId()}. 22 | * @return The consumers unique client id 23 | */ 24 | public String getId() { 25 | return getConsumer().getClientId(); 26 | } 27 | 28 | public KadminConsumerGroup getConsumer() { 29 | return consumer; 30 | } 31 | 32 | /** 33 | * The Cached queue of the contained consumer. Note that this {@link QueuedKafkaMessageHandler} will be included in the 34 | * {@link KadminConsumerGroupContainer#getHandlers()} return values. 35 | * @return 36 | */ 37 | public QueuedKafkaMessageHandler getQueue() { 38 | return queue; 39 | } 40 | 41 | /** 42 | * Must return the handlers list of the contained consumer. Merely a delegate method for {@link KadminConsumerGroup#getHandlers()}. 43 | * @return The consumers handlers 44 | */ 45 | public Collection getHandlers() { 46 | return getConsumer().getHandlers(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/KadminProducer.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | /** 4 | * Created by davidesposito on 7/19/16. 5 | */ 6 | public interface KadminProducer { 7 | 8 | long getSentCount(); 9 | 10 | long getErrorCount(); 11 | 12 | long getLastUsedTime(); 13 | 14 | KadminProducerConfig getConfig(); 15 | 16 | String getId(); 17 | 18 | void send(KeyT key, ValueT val); 19 | 20 | void shutdown(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/KadminProducerConfig.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NonNull; 7 | 8 | /** 9 | * Created by davidesposito on 7/19/16. 10 | */ 11 | @Data 12 | @Builder 13 | public class KadminProducerConfig { 14 | 15 | @NonNull private final String topic; 16 | private String kafkaHost; 17 | private String schemaRegistryUrl; 18 | private String keySerializer; 19 | private SerializerInfoModel valueSerializer; 20 | private String securityProtocol; 21 | private String trustStoreLocation; 22 | private String trustStorePassword; 23 | private String keyStoreLocation; 24 | private String keyStorePassword; 25 | private String keyPassword; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | import org.apache.kafka.clients.consumer.ConsumerRecord; 4 | 5 | /** 6 | * Created by davidesposito on 7/19/16. 7 | */ 8 | public interface MessageHandler { 9 | 10 | void handle(ConsumerRecord record); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/MessageHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * Created by davidesposito on 7/20/16. 7 | */ 8 | public interface MessageHandlerRegistry { 9 | 10 | void register(MessageHandler handler); 11 | 12 | boolean remove(MessageHandler handler); 13 | 14 | Collection getHandlers(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/kafka/exception/SchemaRegistryRestException.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.kafka.exception; 2 | 3 | /** 4 | * Created by davidesposito on 7/18/16. 5 | */ 6 | public class SchemaRegistryRestException extends Exception { 7 | 8 | private final int statusCode; 9 | 10 | public SchemaRegistryRestException(String message, int statusCode) { 11 | super(message); 12 | this.statusCode = statusCode; 13 | } 14 | 15 | public SchemaRegistryRestException(String message, Throwable cause, int statusCode) { 16 | super(message, cause); 17 | this.statusCode = statusCode; 18 | } 19 | 20 | public int getStatusCode() { 21 | return statusCode; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/models/DeserializerInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.util.Map; 11 | import java.util.function.Function; 12 | 13 | /** 14 | * Created by davidesposito on 7/21/16. 15 | */ 16 | @Data 17 | @Builder 18 | @NoArgsConstructor 19 | @AllArgsConstructor 20 | public class DeserializerInfoModel implements Model { 21 | 22 | private String id; 23 | private String name; 24 | private String className; 25 | 26 | @JsonIgnore 27 | private Function prepareOutputFunc; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/models/Model.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.models; 2 | 3 | /** 4 | * Created by davidesposito on 7/21/16. 5 | */ 6 | public interface Model { 7 | 8 | String getId(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/models/SerializerInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.models; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.Map; 10 | import java.util.function.Function; 11 | 12 | /** 13 | * Created by davidesposito on 7/21/16. 14 | */ 15 | @Data 16 | @Builder 17 | @NoArgsConstructor 18 | @AllArgsConstructor 19 | public class SerializerInfoModel implements Model { 20 | 21 | private String id; 22 | private String name; 23 | private String className; 24 | private Map meta; 25 | 26 | @JsonIgnore 27 | private Function prepareRawFunc; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/BundledKadminConsumerGroupProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerConfig; 4 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 5 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroupContainer; 6 | import com.bettercloud.util.Page; 7 | 8 | /** 9 | * Created by davidesposito on 7/19/16. 10 | */ 11 | public interface BundledKadminConsumerGroupProviderService { 12 | 13 | void start(String consumerId); 14 | 15 | /** 16 | * Starts the consumer with the provided configurations 17 | * @param config 18 | * @return 19 | */ 20 | KadminConsumerGroupContainer get(KadminConsumerConfig config, boolean start, int queueSize); 21 | 22 | Page findAll(); 23 | 24 | Page findAll(int page, int size); 25 | 26 | KadminConsumerGroupContainer findById(String consumerId); 27 | 28 | long count(); 29 | 30 | KadminConsumerGroupContainer dispose(String consumerId); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/DeserializerRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 5 | import com.bettercloud.util.Page; 6 | 7 | /** 8 | * Created by davidesposito on 7/21/16. 9 | */ 10 | public interface DeserializerRegistryService extends RegistryService { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/FeaturesService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import java.util.Optional; 4 | 5 | /** 6 | * Created by davidesposito on 7/25/16. 7 | */ 8 | public interface FeaturesService { 9 | 10 | boolean customUrlsEnabled(); 11 | 12 | boolean producersEnabeled(); 13 | 14 | String getCustomUrl(String url); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/IntrospectionService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | 4 | import java.util.Collection; 5 | import java.util.Optional; 6 | 7 | public interface IntrospectionService { 8 | 9 | Collection getAllTopicNames(Optional kafkaUrl); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/KadminConsumerGroupProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerConfig; 4 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 5 | import com.bettercloud.util.Page; 6 | 7 | /** 8 | * Created by davidesposito on 7/19/16. 9 | */ 10 | public interface KadminConsumerGroupProviderService { 11 | 12 | void start(KadminConsumerGroup consumer); 13 | 14 | /** 15 | * Starts the consumer with the provided configurations 16 | * @param config 17 | * @return 18 | */ 19 | KadminConsumerGroup get(KadminConsumerConfig config, boolean start); 20 | 21 | Page findAll(); 22 | 23 | Page findAll(int page, int size); 24 | 25 | KadminConsumerGroup findById(String consumerId); 26 | 27 | long count(); 28 | 29 | void dispose(String Id); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/KadminProducerProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminProducer; 4 | import com.bettercloud.kadmin.api.kafka.KadminProducerConfig; 5 | import com.bettercloud.util.Page; 6 | import com.bettercloud.util.TimedWrapper; 7 | 8 | /** 9 | * Created by davidesposito on 7/19/16. 10 | */ 11 | public interface KadminProducerProviderService { 12 | 13 | /** 14 | * Starts the consumer with the provided configurations 15 | * @param config 16 | * @return 17 | */ 18 | KadminProducer get(KadminProducerConfig config); 19 | 20 | Page> findAll(); 21 | 22 | Page> findAll(int page, int size); 23 | 24 | KadminProducer findById(String producerId); 25 | 26 | long count(); 27 | 28 | boolean dispose(String producerId); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/RegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import com.bettercloud.util.Page; 5 | 6 | /** 7 | * Created by davidesposito on 7/21/16. 8 | */ 9 | public interface RegistryService { 10 | 11 | void register(ModelT model); 12 | 13 | Page findAll(); 14 | 15 | ModelT findById(String id); 16 | 17 | ModelT remove(String id); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/SchemaRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.exception.SchemaRegistryRestException; 4 | import com.bettercloud.kadmin.io.network.dto.SchemaInfoModel; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Created by davidesposito on 7/18/16. 12 | */ 13 | public interface SchemaRegistryService { 14 | 15 | List findAll(String oUrl) throws SchemaRegistryRestException; 16 | 17 | List guessAllTopics(String oUrl) throws SchemaRegistryRestException; 18 | 19 | SchemaInfoModel getInfo(String name, String oUrl) throws SchemaRegistryRestException; 20 | 21 | JsonNode getVersion(String name, int version, String oUrl) throws SchemaRegistryRestException; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/api/services/SerializerRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.api.services; 2 | 3 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 4 | import com.bettercloud.util.Page; 5 | 6 | /** 7 | * Created by davidesposito on 7/21/16. 8 | */ 9 | public interface SerializerRegistryService extends RegistryService { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/ConsumerInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Created by davidesposito on 7/20/16. 10 | */ 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class ConsumerInfoModel { 16 | 17 | private String topic; 18 | private long lastUsedTime; 19 | private long lastMessageTime; 20 | private String consumerGroupId; 21 | private long queueSize; 22 | private long total; 23 | private String deserializerName; 24 | private String deserializerId; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/KafkaProduceMessageMetaModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Created by davidesposito on 7/1/16. 10 | */ 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class KafkaProduceMessageMetaModel { 16 | 17 | private String schema; 18 | private String rawSchema; 19 | private String topic; 20 | private String serializerId; 21 | private String kafkaUrl; 22 | private String schemaRegistryUrl; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/KafkaProduceRequestModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Created by davidesposito on 7/1/16. 10 | */ 11 | @Data 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class KafkaProduceRequestModel { 15 | 16 | private String key; 17 | private String rawMessage; 18 | private KafkaProduceMessageMetaModel meta; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/ProducerInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Created by davidesposito on 7/20/16. 10 | */ 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class ProducerInfoModel { 16 | 17 | private String id; 18 | private String topic; 19 | private long lastUsedTime; 20 | private long totalMessagesSent; 21 | private long totalErrors; 22 | private String serializerName; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/ProducerRepsonseModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * Created by davidesposito on 7/21/16. 10 | */ 11 | @Data 12 | @Builder 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class ProducerRepsonseModel { 16 | private int count; 17 | private long duration; 18 | private double rate; 19 | private boolean success; 20 | private boolean sent; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/dto/SchemaInfoModel.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.dto; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * Created by davidesposito on 7/18/16. 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class SchemaInfoModel { 19 | private String name; 20 | private List versions; 21 | private JsonNode currSchema; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/rest/KafkaConsumerResource.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.rest; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerConfig; 4 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 5 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroupContainer; 6 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 7 | import com.bettercloud.kadmin.api.services.BundledKadminConsumerGroupProviderService; 8 | import com.bettercloud.kadmin.api.services.DeserializerRegistryService; 9 | import com.bettercloud.kadmin.api.services.FeaturesService; 10 | import com.bettercloud.kadmin.io.network.rest.utils.ResponseUtil; 11 | import com.bettercloud.kadmin.kafka.QueuedKafkaMessageHandler; 12 | import com.bettercloud.util.Page; 13 | import com.fasterxml.jackson.databind.JsonNode; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.fasterxml.jackson.databind.node.ObjectNode; 16 | import com.google.common.base.Joiner; 17 | import lombok.Builder; 18 | import lombok.Data; 19 | import org.apache.kafka.common.serialization.StringDeserializer; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.core.env.Environment; 22 | import org.springframework.http.HttpStatus; 23 | import org.springframework.http.MediaType; 24 | import org.springframework.http.ResponseEntity; 25 | import org.springframework.web.bind.annotation.*; 26 | 27 | import java.util.List; 28 | import java.util.Optional; 29 | import java.util.stream.Collectors; 30 | 31 | /** 32 | * Created by davidesposito on 7/5/16. 33 | */ 34 | @RestController 35 | @RequestMapping("/api") 36 | public class KafkaConsumerResource { 37 | 38 | private static final ObjectMapper mapper = new ObjectMapper(); 39 | private static final Joiner keyBuilder = Joiner.on(':'); 40 | 41 | private final BundledKadminConsumerGroupProviderService kafkaConsumerProvider; 42 | private final DeserializerRegistryService deserializerRegistryService; 43 | private final FeaturesService featuresService; 44 | 45 | @Autowired 46 | public KafkaConsumerResource(BundledKadminConsumerGroupProviderService kafkaConsumerProvider, 47 | DeserializerRegistryService deserializerRegistryService, 48 | FeaturesService featuresService) { 49 | this.kafkaConsumerProvider = kafkaConsumerProvider; 50 | this.deserializerRegistryService = deserializerRegistryService; 51 | this.featuresService = featuresService; 52 | 53 | } 54 | 55 | @RequestMapping( 56 | path = "/kafka/read/{topic}", 57 | method = RequestMethod.GET, 58 | produces = MediaType.APPLICATION_JSON_VALUE 59 | ) 60 | public ResponseEntity readKafka(@PathVariable("topic") String topic, 61 | @RequestParam("since") Optional oSince, 62 | @RequestParam("window") Optional oWindow, 63 | @RequestParam("kafkaUrl") Optional kafkaUrl, 64 | @RequestParam("schemaUrl") Optional schemaUrl, 65 | @RequestParam("size") Optional queueSize, 66 | @RequestParam("deserializerId") String deserializerId) { 67 | DeserializerInfoModel des = deserializerRegistryService.findById(deserializerId); 68 | if (des == null) { 69 | return ResponseUtil.error("Invalid deserializer id", HttpStatus.NOT_FOUND); 70 | } 71 | KadminConsumerConfig config = KadminConsumerConfig.builder() 72 | .topic(topic) 73 | .kafkaHost(featuresService.getCustomUrl(kafkaUrl.orElse(null))) 74 | .schemaRegistryUrl(featuresService.getCustomUrl(schemaUrl.orElse(null))) 75 | .keyDeserializer(StringDeserializer.class.getName()) 76 | .valueDeserializer(des) 77 | .build(); 78 | int maxSize = queueSize.filter(s -> s < 100).orElse(50); 79 | KadminConsumerGroupContainer consumerGroupContainer = kafkaConsumerProvider.get(config, true, maxSize); 80 | 81 | QueuedKafkaMessageHandler handler = consumerGroupContainer.getQueue(); 82 | Long since = getSince(oSince, oWindow); 83 | Page page = null; 84 | try { 85 | List messages = handler.get(since).stream() 86 | .map(m -> (QueuedKafkaMessageHandler.MessageContainer) m) 87 | .map(mc -> { 88 | ObjectNode node = mapper.createObjectNode(); 89 | node.put("key", mc.getKey()); 90 | node.put("writeTime", mc.getWriteTime()); 91 | node.put("offset", mc.getOffset()); 92 | node.put("topic", mc.getTopic()); 93 | node.replace("message", des.getPrepareOutputFunc().apply(mc.getMessage())); 94 | return node; 95 | }) 96 | .map(n -> (JsonNode) n) 97 | .collect(Collectors.toList()); 98 | page = new Page<>(); 99 | page.setPage(0); 100 | page.setSize(messages.size()); 101 | page.setTotalElements(handler.total()); 102 | page.setContent(messages); 103 | } catch (RuntimeException e) { 104 | e.printStackTrace(); 105 | return ResponseUtil.error(e); 106 | } 107 | return ResponseEntity.ok(ConsumerResponse.builder() 108 | .consumerId(consumerGroupContainer.getId()) 109 | .page(page) 110 | .build()); 111 | } 112 | 113 | protected long getSince(Optional oSince, Optional oWindow) { 114 | return oSince.isPresent() ? oSince.get() : 115 | oWindow.map(win -> System.currentTimeMillis() - win * 1000).orElse(-1L); 116 | } 117 | 118 | @Data 119 | @Builder 120 | private static class ConsumerResponse { 121 | private final String consumerId; 122 | private final Page page; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/rest/ManagerResource.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.rest; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 4 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 5 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 6 | import com.bettercloud.kadmin.api.services.BundledKadminConsumerGroupProviderService; 7 | import com.bettercloud.kadmin.api.services.DeserializerRegistryService; 8 | import com.bettercloud.kadmin.api.services.SerializerRegistryService; 9 | import com.bettercloud.kadmin.io.network.dto.ConsumerInfoModel; 10 | import com.bettercloud.kadmin.io.network.rest.utils.ResponseUtil; 11 | import com.bettercloud.kadmin.kafka.QueuedKafkaMessageHandler; 12 | import com.bettercloud.util.Opt; 13 | import com.bettercloud.util.Page; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.http.ResponseEntity; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | import org.springframework.web.bind.annotation.RestController; 22 | 23 | import java.util.List; 24 | import java.util.stream.Collectors; 25 | 26 | /** 27 | * Created by davidesposito on 7/21/16. 28 | */ 29 | @RestController 30 | @RequestMapping("/api/manager") 31 | public class ManagerResource { 32 | 33 | private final SerializerRegistryService serializerRegistryService; 34 | private final DeserializerRegistryService deserializerRegistryService; 35 | private final BundledKadminConsumerGroupProviderService consumerGroupProviderService; 36 | 37 | @Autowired 38 | public ManagerResource(SerializerRegistryService serializerRegistryService, 39 | DeserializerRegistryService deserializerRegistryService, 40 | BundledKadminConsumerGroupProviderService consumerGroupProviderService) { 41 | this.serializerRegistryService = serializerRegistryService; 42 | this.deserializerRegistryService = deserializerRegistryService; 43 | this.consumerGroupProviderService = consumerGroupProviderService; 44 | } 45 | 46 | @RequestMapping( 47 | path = "/serializers", 48 | method = RequestMethod.GET, 49 | produces = MediaType.APPLICATION_JSON_VALUE 50 | ) 51 | public ResponseEntity> serializers() { 52 | return ResponseEntity.ok(serializerRegistryService.findAll()); 53 | } 54 | 55 | @RequestMapping( 56 | path = "/deserializers", 57 | method = RequestMethod.GET, 58 | produces = MediaType.APPLICATION_JSON_VALUE 59 | ) 60 | public ResponseEntity> deserializers() { 61 | return ResponseEntity.ok(deserializerRegistryService.findAll()); 62 | } 63 | 64 | @RequestMapping( 65 | path = "/consumers", 66 | method = RequestMethod.GET, 67 | produces = MediaType.APPLICATION_JSON_VALUE 68 | ) 69 | public ResponseEntity> getAllConsumers() { 70 | Page consumers = new Page<>(); 71 | List content = consumerGroupProviderService.findAll(0, 100).getContent().stream() 72 | .map(container -> { 73 | KadminConsumerGroup consumer = container.getConsumer(); 74 | QueuedKafkaMessageHandler queue = container.getQueue(); 75 | return ConsumerInfoModel.builder() 76 | .consumerGroupId(consumer.getGroupId()) 77 | .deserializerName(consumer.getConfig().getValueDeserializer().getName()) 78 | .deserializerId(consumer.getConfig().getValueDeserializer().getId()) 79 | .lastMessageTime(queue.getLastMessageTime()) 80 | .lastUsedTime(queue.getLastReadTime()) 81 | .queueSize(queue.getQueueSize()) 82 | .topic(consumer.getConfig().getTopic()) 83 | .total(queue.total()) 84 | .build(); 85 | }) 86 | .collect(Collectors.toList()); 87 | consumers.setContent(content); 88 | consumers.setPage(0); 89 | consumers.setTotalElements(content.size()); 90 | consumers.setTotalElements(content.size()); 91 | return ResponseEntity.ok(consumers); 92 | } 93 | 94 | @RequestMapping( 95 | path = "/consumers/{consumerId}", 96 | method = RequestMethod.DELETE, 97 | produces = MediaType.APPLICATION_JSON_VALUE 98 | ) 99 | public ResponseEntity kill(@PathVariable("consumerId") String id) { 100 | if (!Opt.of(consumerGroupProviderService.dispose(id)).isPresent()) { 101 | return ResponseUtil.error(HttpStatus.NOT_FOUND); 102 | } 103 | return ResponseEntity.ok(true); 104 | } 105 | 106 | @RequestMapping( 107 | path = "/consumers/{consumerId}/truncate", 108 | method = RequestMethod.DELETE, 109 | produces = MediaType.APPLICATION_JSON_VALUE 110 | ) 111 | public ResponseEntity truncate(@PathVariable("consumerId") String id) { 112 | if (!Opt.of(consumerGroupProviderService.dispose(id)) 113 | .ifPresent(container -> container.getQueue().clear()) 114 | .isPresent()) { 115 | return ResponseUtil.error(HttpStatus.NOT_FOUND); 116 | } 117 | return ResponseEntity.ok(true); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/rest/SchemaProxyResource.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.rest; 2 | 3 | import com.bettercloud.kadmin.api.kafka.exception.SchemaRegistryRestException; 4 | import com.bettercloud.kadmin.api.services.IntrospectionService; 5 | import com.bettercloud.kadmin.api.services.SchemaRegistryService; 6 | import com.bettercloud.kadmin.io.network.dto.SchemaInfoModel; 7 | import com.bettercloud.util.LoggerUtils; 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import org.slf4j.Logger; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.Collection; 17 | import java.util.List; 18 | import java.util.Optional; 19 | 20 | /** 21 | * Created by davidesposito on 7/6/16. 22 | */ 23 | @RestController 24 | @RequestMapping(path = "/api") 25 | public class SchemaProxyResource { 26 | 27 | private static final ObjectMapper mapper = new ObjectMapper(); 28 | private static final Logger LOGGER = LoggerUtils.get(SchemaProxyResource.class); 29 | 30 | private final SchemaRegistryService schemaRegistryService; 31 | private final IntrospectionService introspectionService; 32 | 33 | @Autowired 34 | public SchemaProxyResource(SchemaRegistryService schemaRegistryService, IntrospectionService introspectionService) { 35 | this.schemaRegistryService = schemaRegistryService; 36 | this.introspectionService = introspectionService; 37 | } 38 | 39 | @RequestMapping( 40 | path = "/schemas", 41 | method = RequestMethod.GET, 42 | produces = MediaType.APPLICATION_JSON_VALUE 43 | ) 44 | public ResponseEntity> schemas(@RequestParam("url") Optional oUrl) { 45 | try { 46 | return ResponseEntity.ok(schemaRegistryService.findAll(oUrl.orElse(null))); 47 | } catch (SchemaRegistryRestException e) { 48 | return ResponseEntity.status(e.getStatusCode()) 49 | .header("error-message", e.getMessage()) 50 | .body(null); 51 | } 52 | } 53 | 54 | @RequestMapping( 55 | path = "/topics", 56 | method = RequestMethod.GET, 57 | produces = MediaType.APPLICATION_JSON_VALUE 58 | ) 59 | public ResponseEntity> topics(@RequestParam("kafka-url") Optional kafkaUrl) { 60 | try { 61 | return ResponseEntity.ok(introspectionService.getAllTopicNames(kafkaUrl)); 62 | } catch (Exception e) { 63 | return ResponseEntity.status(500) 64 | .header("error-message", e.getMessage()) 65 | .body(null); 66 | } 67 | } 68 | 69 | @RequestMapping( 70 | path = "/schemas/{name}", 71 | method = RequestMethod.GET, 72 | produces = MediaType.APPLICATION_JSON_VALUE 73 | ) 74 | public ResponseEntity info(@PathVariable("name") String name, 75 | @RequestParam("url") Optional oUrl) { 76 | try { 77 | return ResponseEntity.ok(schemaRegistryService.getInfo(name, oUrl.orElse(null))); 78 | } catch (SchemaRegistryRestException e) { 79 | return ResponseEntity.status(e.getStatusCode()) 80 | .header("error-message", e.getMessage()) 81 | .body(null); 82 | } 83 | } 84 | 85 | @RequestMapping( 86 | path = "/schemas/{name}/{version}", 87 | method = RequestMethod.GET, 88 | produces = MediaType.APPLICATION_JSON_VALUE 89 | ) 90 | public ResponseEntity version(@PathVariable("name") String name, 91 | @PathVariable("version") Integer version, 92 | @RequestParam("url") Optional oUrl) { 93 | try { 94 | return ResponseEntity.ok(schemaRegistryService.getVersion(name, version, oUrl.orElse(null))); 95 | } catch (SchemaRegistryRestException e) { 96 | return ResponseEntity.status(e.getStatusCode()) 97 | .header("error-message", e.getMessage()) 98 | .body(null); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/rest/utils/PropertiesBackedFeaturesService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.rest.utils; 2 | 3 | import com.bettercloud.kadmin.api.services.FeaturesService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.core.env.Environment; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * Created by davidesposito on 7/25/16. 12 | */ 13 | @Service 14 | public class PropertiesBackedFeaturesService implements FeaturesService { 15 | 16 | private final Environment env; 17 | 18 | @Autowired 19 | public PropertiesBackedFeaturesService(Environment env) { 20 | this.env = env; 21 | } 22 | 23 | @Override 24 | public boolean customUrlsEnabled() { 25 | return env.getProperty("ff.customKafkaUrl.enabled", Boolean.class, false); 26 | } 27 | 28 | @Override 29 | public boolean producersEnabeled() { 30 | return env.getProperty("ff.producer.enabled", Boolean.class, false); 31 | } 32 | 33 | @Override 34 | public String getCustomUrl(String url) { 35 | return Optional.ofNullable(url) 36 | .filter(u -> customUrlsEnabled()) 37 | .orElse(null); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/network/rest/utils/ResponseUtil.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.network.rest.utils; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | 6 | /** 7 | * Created by davidesposito on 7/11/16. 8 | */ 9 | public class ResponseUtil { 10 | 11 | public static ResponseEntity error(Throwable e) { 12 | return error(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); 13 | } 14 | 15 | public static ResponseEntity error(HttpStatus status) { 16 | return error("", status); 17 | } 18 | 19 | public static ResponseEntity error(String message, HttpStatus status) { 20 | return ResponseEntity.status(status) 21 | .header("error-message", message) 22 | .body(null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/io/ui/KadminUiController.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.io.ui; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import com.bettercloud.kadmin.api.services.DeserializerRegistryService; 5 | import com.bettercloud.kadmin.api.services.FeaturesService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | /** 14 | * Created by davidesposito on 7/15/16. 15 | */ 16 | @Controller 17 | public class KadminUiController { 18 | 19 | private final Environment env; 20 | private final DeserializerRegistryService deserializerRegistryService; 21 | private final FeaturesService featuresService; 22 | 23 | @Autowired 24 | public KadminUiController(Environment env, DeserializerRegistryService deserializerRegistryService, 25 | FeaturesService featuresService) { 26 | this.env = env; 27 | this.deserializerRegistryService = deserializerRegistryService; 28 | this.featuresService = featuresService; 29 | } 30 | 31 | @RequestMapping("/") 32 | public String home(Model model) { 33 | setEnvProps(model); 34 | return "index"; 35 | } 36 | 37 | @RequestMapping("/consumer") 38 | public String consumer(Model model) { 39 | setEnvProps(model); 40 | return "consumer"; 41 | } 42 | 43 | @RequestMapping("/consumer/topic/{topic}/{deserializerId}") 44 | public String consumer(Model model, 45 | @PathVariable("topic") String topic, 46 | @PathVariable("deserializerId") String deserializerId) { 47 | setEnvProps(model); 48 | DeserializerInfoModel info = deserializerRegistryService.findById(deserializerId); 49 | if (info != null) { 50 | model.addAttribute("deserializerId", deserializerId); 51 | model.addAttribute("deserializerName", info.getName()); 52 | } 53 | model.addAttribute("defaultTopicName", topic); 54 | return "consumer"; 55 | } 56 | 57 | @RequestMapping("/producer") 58 | public String producer(Model model) { 59 | setEnvProps(model); 60 | return featuresService.producersEnabeled() ? "producer" : "index"; 61 | } 62 | 63 | @RequestMapping("/basicproducer") 64 | public String basicProducer(Model model) { 65 | setEnvProps(model); 66 | return featuresService.producersEnabeled() ? "basicproducer" : "index"; 67 | } 68 | 69 | @RequestMapping("/manager") 70 | public String manager(Model model) { 71 | setEnvProps(model); 72 | return "manager"; 73 | } 74 | 75 | private void setEnvProps(Model model) { 76 | model.addAttribute("contextPath", env.getProperty("server.contextPath", "")); 77 | model.addAttribute("producerEnabled", featuresService.producersEnabeled()); 78 | model.addAttribute("customKafkaUrlEnabled", featuresService.customUrlsEnabled()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/kafka/BasicKafkaProducer.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.kafka; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminProducer; 4 | import com.bettercloud.kadmin.api.kafka.KadminProducerConfig; 5 | import lombok.Getter; 6 | import org.apache.kafka.clients.CommonClientConfigs; 7 | import org.apache.kafka.clients.producer.KafkaProducer; 8 | import org.apache.kafka.clients.producer.ProducerRecord; 9 | import org.apache.kafka.common.config.SslConfigs; 10 | 11 | import java.util.Properties; 12 | import java.util.UUID; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | 15 | /** 16 | * Created by davidesposito on 7/20/16. 17 | */ 18 | public class BasicKafkaProducer implements KadminProducer { 19 | 20 | private static final String SSL = "SSL"; 21 | @Getter private final KadminProducerConfig config; 22 | @Getter private final String id; 23 | @Getter private long lastUsedTime = -1; 24 | private KafkaProducer producer; 25 | private final AtomicLong sentCount; 26 | private final AtomicLong errorCount; 27 | 28 | public BasicKafkaProducer(KadminProducerConfig config) { 29 | this.config = config; 30 | this.id = UUID.randomUUID().toString(); 31 | this.sentCount = new AtomicLong(0); 32 | this.errorCount = new AtomicLong(0); 33 | 34 | init(); 35 | } 36 | 37 | public long getSentCount() { 38 | return sentCount.get(); 39 | } 40 | 41 | public long getErrorCount() { 42 | return errorCount.get(); 43 | } 44 | 45 | /** 46 | * Gets called before init(). Set any default configs here 47 | * @param config 48 | */ 49 | protected void initConfig(KadminProducerConfig config) { } 50 | 51 | protected void init() { 52 | initConfig(config); 53 | 54 | final Properties properties = new Properties(); 55 | properties.put("acks", "all"); 56 | properties.put("bootstrap.servers", config.getKafkaHost()); 57 | properties.put("schema.registry.url", config.getSchemaRegistryUrl()); 58 | properties.put("key.serializer", config.getKeySerializer()); 59 | properties.put("value.serializer", config.getValueSerializer().getClassName()); 60 | 61 | if(config.getSecurityProtocol().equals(SSL)) { 62 | //configure the following three settings for SSL Encryption 63 | properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); 64 | properties.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, config.getTrustStoreLocation()); 65 | properties.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, config.getTrustStorePassword()); 66 | 67 | // configure the following three settings for SSL Authentication 68 | properties.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, config.getKeyStoreLocation()); 69 | properties.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, config.getKeyStorePassword()); 70 | properties.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, config.getKeyPassword()); 71 | } 72 | 73 | this.producer = new KafkaProducer<>(properties); 74 | } 75 | 76 | @Override 77 | public void send(String key, ValueT val) { 78 | if (this.producer != null) { 79 | ProducerRecord pr = new ProducerRecord<>(config.getTopic(), key, val); 80 | try { 81 | lastUsedTime = System.currentTimeMillis(); 82 | producer.send(pr); 83 | sentCount.getAndIncrement(); 84 | } catch (Exception e) { 85 | errorCount.getAndIncrement(); 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | } 90 | 91 | @Override 92 | public void shutdown() { 93 | if (producer != null) { 94 | producer.close(); 95 | producer = null; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/kafka/LoggedKafkaMessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.kafka; 2 | 3 | import com.bettercloud.kadmin.api.kafka.MessageHandler; 4 | import com.bettercloud.util.LoggerUtils; 5 | import org.apache.kafka.clients.consumer.ConsumerRecord; 6 | import org.slf4j.Logger; 7 | 8 | /** 9 | * Created by davidesposito on 7/5/16. 10 | */ 11 | public class LoggedKafkaMessageHandler implements MessageHandler { 12 | 13 | private static final Logger logger = LoggerUtils.get(LoggedKafkaMessageHandler.class); 14 | 15 | @Override 16 | public void handle(ConsumerRecord record) { 17 | logger.info("({}@{}) {}: {}", record.offset(), record.topic(), record.key(), record.value()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/kafka/QueuedKafkaMessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.kafka; 2 | 3 | import com.bettercloud.kadmin.api.kafka.MessageHandler; 4 | import com.bettercloud.util.LoggerUtils; 5 | import com.google.common.collect.Lists; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.Getter; 9 | import org.apache.kafka.clients.consumer.ConsumerRecord; 10 | import org.slf4j.Logger; 11 | 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.concurrent.atomic.AtomicLong; 15 | import java.util.stream.Collectors; 16 | import java.util.stream.Stream; 17 | 18 | /** 19 | * Created by davidesposito on 7/1/16. 20 | */ 21 | public class QueuedKafkaMessageHandler implements MessageHandler { 22 | 23 | private static final Logger LOGGER = LoggerUtils.get(QueuedKafkaMessageHandler.class); 24 | 25 | private final FixedSizeList messageQueue; 26 | private final AtomicLong total = new AtomicLong(0L); 27 | @Getter private long lastReadTime; 28 | @Getter private long lastMessageTime; 29 | 30 | public QueuedKafkaMessageHandler(int maxSize) { 31 | messageQueue = new FixedSizeList<>(maxSize); 32 | } 33 | 34 | @Override 35 | public void handle(ConsumerRecord record) { 36 | LOGGER.debug("receiving => {}, queued => {}",total.get() + 1, messageQueue.spine.size()); 37 | total.incrementAndGet(); 38 | long currTime = System.currentTimeMillis(); 39 | lastMessageTime = currTime; 40 | this.messageQueue.add(MessageContainer.builder() 41 | .key(record.key()) 42 | .message(record.value()) 43 | .offset(record.offset()) 44 | .partition(record.partition()) 45 | .topic(record.topic()) 46 | .writeTime(currTime) 47 | .build()); 48 | } 49 | 50 | public List get(Long since) { 51 | lastReadTime = System.currentTimeMillis(); 52 | return messageQueue.stream() 53 | .filter(c -> isValidDate(since, c.getWriteTime())) 54 | .collect(Collectors.toList()); 55 | } 56 | 57 | public int count(Long since) { 58 | return (int)messageQueue.stream() 59 | .filter(c -> isValidDate(since, c.getWriteTime())) 60 | .count(); 61 | } 62 | 63 | public long total() { 64 | return total.get(); 65 | } 66 | 67 | public long getQueueSize() { 68 | return messageQueue.maxSize; 69 | } 70 | 71 | public void clear() { 72 | total.set(0L); 73 | messageQueue.clear(); 74 | } 75 | 76 | protected boolean isValidDate(Long since, Long writeTime) { 77 | return since < 0 || writeTime > since; 78 | } 79 | 80 | @Data 81 | @Builder 82 | public static class MessageContainer { 83 | 84 | private final long writeTime; 85 | private final String key; 86 | private final Object message; 87 | private final String topic; 88 | private final int partition; 89 | private final long offset; 90 | } 91 | 92 | protected static class FixedSizeList { 93 | 94 | private final LinkedList spine; 95 | private final int maxSize; 96 | 97 | public FixedSizeList(int maxSize) { 98 | spine = Lists.newLinkedList(); 99 | this.maxSize = Math.min(Math.max(maxSize, 1), 2000); 100 | } 101 | 102 | public synchronized void add(E ele) { 103 | if (spine.size() >= maxSize) { 104 | spine.removeFirst(); 105 | } 106 | spine.add(ele); 107 | } 108 | 109 | public void clear() { 110 | spine.clear(); 111 | } 112 | 113 | public synchronized Stream stream() { 114 | return spine.stream(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/kafka/SerializersProperties.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.kafka; 2 | 3 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 4 | import com.bettercloud.kadmin.kafka.avro.ErrorTolerantAvroObjectDeserializer; 5 | import com.google.common.collect.Maps; 6 | import org.apache.kafka.common.serialization.ByteArraySerializer; 7 | import org.apache.kafka.common.serialization.IntegerSerializer; 8 | import org.apache.kafka.common.serialization.LongSerializer; 9 | import org.apache.kafka.common.serialization.StringSerializer; 10 | 11 | import java.util.UUID; 12 | import java.util.function.Function; 13 | 14 | /** 15 | * Created by davidesposito on 7/23/16. 16 | */ 17 | public class SerializersProperties { 18 | 19 | 20 | public static final SerializerInfoModel STRING = sim("string", StringSerializer.class, s -> s); 21 | public static final SerializerInfoModel BYTES = sim("bytes", ByteArraySerializer.class, s -> s); 22 | public static final SerializerInfoModel INTEGER = sim("int", IntegerSerializer.class, s -> Integer.parseInt(s)); 23 | public static final SerializerInfoModel LONG = sim("long", LongSerializer.class, s -> Long.parseLong(s)); 24 | public static final SerializerInfoModel AVRO = sim("avro", ErrorTolerantAvroObjectDeserializer.class, s -> Long.parseLong(s)); 25 | 26 | private static SerializerInfoModel sim(String id, Class serializerClass, Function rawPrep) { 27 | return sim(id, serializerClass.getSimpleName(), serializerClass, rawPrep); 28 | } 29 | 30 | private static SerializerInfoModel sim(String id, String name, Class serializerClass, Function rawPrep) { 31 | return SerializerInfoModel.builder() 32 | .id(UUID.randomUUID().toString()) 33 | .name(name) 34 | .className(serializerClass.getName()) 35 | .meta(Maps.newHashMap()) 36 | .prepareRawFunc(rawPrep) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/kafka/avro/ErrorTolerantAvroObjectDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.kafka.avro; 2 | 3 | import com.bettercloud.util.LoggerUtils; 4 | import com.bettercloud.util.Opt; 5 | import io.confluent.kafka.serializers.KafkaAvroDeserializer; 6 | import org.apache.kafka.common.errors.SerializationException; 7 | import org.slf4j.Logger; 8 | 9 | /** 10 | * Created by davidesposito on 7/20/16. 11 | */ 12 | public class ErrorTolerantAvroObjectDeserializer extends KafkaAvroDeserializer { 13 | 14 | private static final Logger LOGGER = LoggerUtils.get(ErrorTolerantAvroObjectDeserializer.class); 15 | 16 | protected Object deserialize(byte[] payload) throws SerializationException { 17 | String error; 18 | try { 19 | return super.deserialize(payload); 20 | } catch (SerializationException e) { 21 | LOGGER.warn("There was an error deserializing avro payload: {}, caused by: {}", e.getMessage(), e.getCause()); 22 | error = String.format("\" \nThere was an error deserializing the avro payload. \n \nError: %s %s \n\"", 23 | e.getMessage(), Opt.of(e.getCause()).map(cause -> "\n \nCause: " + cause.getMessage()).orElse("")); 24 | } 25 | return error; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/BasicKafkaConsumerProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerConfig; 4 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 5 | import com.bettercloud.kadmin.api.services.KadminConsumerGroupProviderService; 6 | import com.bettercloud.kadmin.kafka.BasicKafkaConsumerGroup; 7 | import com.bettercloud.util.Opt; 8 | import com.bettercloud.util.Page; 9 | import com.google.common.base.Joiner; 10 | import com.google.common.collect.Maps; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Value; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.Collections; 17 | import java.util.LinkedHashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * Created by davidesposito on 7/19/16. 26 | */ 27 | @Service(value = "kafkaConsumerGroupProvider") 28 | public class BasicKafkaConsumerProviderService implements KadminConsumerGroupProviderService { 29 | 30 | private static final Joiner KEY_BUILDER = Joiner.on("<==>"); 31 | 32 | private final ExecutorService consumerExecutor; 33 | private final String defaultKafkaHost; 34 | private final String defaultSchemaRegistryUrl; 35 | private final String defaultSecurityProtocol; 36 | private final String defaultTrustStoreLocation; 37 | private final String defaultTrustStorePassword; 38 | private final String defaultKeyStoreLocation; 39 | private final String defaultKeyStorePassword; 40 | private final String defaultKeyPassword; 41 | 42 | private final Environment env; 43 | 44 | private final LinkedHashMap consumerMap; 45 | private final Map correlationMap; 46 | 47 | @Autowired 48 | public BasicKafkaConsumerProviderService( 49 | @Value("${kafka.host:localhost:9092}") 50 | String defaultKafkaHost, 51 | @Value("${schema.registry.url:http://localhost:8081}") 52 | String defaultSchemaRegistryUrl, 53 | @Value("${security.protocol:PLAINTEXT}") 54 | String defaultSecurityProtocol, 55 | Environment env) { 56 | this.consumerExecutor = Executors.newCachedThreadPool(); 57 | this.defaultKafkaHost = defaultKafkaHost; 58 | this.defaultSchemaRegistryUrl = defaultSchemaRegistryUrl; 59 | this.defaultSecurityProtocol = defaultSecurityProtocol; 60 | this.defaultTrustStoreLocation = env.getProperty("trust.store.location"); 61 | this.defaultTrustStorePassword = env.getProperty("trust.store.password"); 62 | this.defaultKeyStoreLocation = env.getProperty("key.store.location"); 63 | this.defaultKeyStorePassword = env.getProperty("key.store.password"); 64 | this.defaultKeyPassword = env.getProperty("key.password"); 65 | 66 | this.env = env; 67 | 68 | this.consumerMap = Maps.newLinkedHashMap(); 69 | this.correlationMap = Maps.newConcurrentMap(); 70 | } 71 | 72 | private String getConfigKey(KadminConsumerConfig config) { 73 | return KEY_BUILDER.join(config.getKafkaHost(), 74 | config.getSchemaRegistryUrl(), 75 | config.getTopic(), 76 | config.getValueDeserializer()); 77 | } 78 | 79 | @Override 80 | public void start(KadminConsumerGroup consumer) { 81 | if (consumer != null && !consumerMap.containsKey(consumer.getClientId())) { 82 | consumerMap.put(consumer.getClientId(), consumer); 83 | consumerExecutor.submit(consumer); 84 | } 85 | } 86 | 87 | @Override 88 | public KadminConsumerGroup get(KadminConsumerConfig config, boolean start) { 89 | Opt.of(config.getKafkaHost()).notPresent(() -> config.setKafkaHost(defaultKafkaHost)); 90 | Opt.of(config.getSchemaRegistryUrl()).notPresent(() -> config.setSchemaRegistryUrl(defaultSchemaRegistryUrl)); 91 | Opt.of(config.getSecurityProtocol()).notPresent(() -> config.setSecurityProtocol(defaultSecurityProtocol)); 92 | Opt.of(config.getTrustStoreLocation()).notPresent(() -> config.setTrustStoreLocation(defaultTrustStoreLocation)); 93 | Opt.of(config.getTrustStorePassword()).notPresent(() -> config.setTrustStorePassword(defaultTrustStorePassword)); 94 | Opt.of(config.getKeyStoreLocation()).notPresent(() -> config.setKeyStoreLocation(defaultKeyStoreLocation)); 95 | Opt.of(config.getKeyStorePassword()).notPresent(() -> config.setKeyStorePassword(defaultKeyStorePassword)); 96 | Opt.of(config.getKeyPassword()).notPresent(() -> config.setKeyPassword(defaultKeyPassword)); 97 | 98 | KadminConsumerGroup consumerGroup = new BasicKafkaConsumerGroup(config, env); 99 | String key = getConfigKey(consumerGroup.getConfig()); 100 | if (!correlationMap.containsKey(key)) { 101 | correlationMap.put(key, consumerGroup.getClientId()); 102 | if (start) { 103 | this.start(consumerGroup); 104 | } 105 | } else { 106 | consumerGroup = consumerMap.get(correlationMap.get(key)); 107 | } 108 | return consumerGroup; 109 | } 110 | 111 | @Override 112 | public Page findAll() { 113 | return findAll(-1 ,- 1); 114 | } 115 | 116 | @Override 117 | public Page findAll(int page, int size) { 118 | if (page < 0) { 119 | page = 0; 120 | } 121 | if (size <= 0 || size > 100) { 122 | size = 20; 123 | } 124 | int skip = page * size; 125 | List consumers = consumerMap.values().stream() 126 | .skip(skip) 127 | .limit(size) 128 | .collect(Collectors.toList()); 129 | Page consumerPage = new Page<>(); 130 | consumerPage.setPage(page); 131 | consumerPage.setSize(size); 132 | consumerPage.setTotalElements(count()); 133 | consumerPage.setContent(consumers); 134 | return consumerPage; 135 | } 136 | 137 | @Override 138 | public KadminConsumerGroup findById(String consumerId) { 139 | return consumerMap.get(consumerId); 140 | } 141 | 142 | @Override 143 | public long count() { 144 | return consumerMap.size(); 145 | } 146 | 147 | @Override 148 | public void dispose(String consumerId) { 149 | Opt.of(consumerMap.get(consumerId)).ifPresent(c -> { 150 | c.shutdown(); 151 | consumerMap.remove(consumerId); 152 | correlationMap.remove(getConfigKey(c.getConfig())); 153 | }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/BasicKafkaProducerProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminProducer; 4 | import com.bettercloud.kadmin.api.kafka.KadminProducerConfig; 5 | import com.bettercloud.kadmin.api.services.KadminProducerProviderService; 6 | import com.bettercloud.kadmin.kafka.BasicKafkaProducer; 7 | import com.bettercloud.util.LoggerUtils; 8 | import com.bettercloud.util.Opt; 9 | import com.bettercloud.util.Page; 10 | import com.google.common.collect.Maps; 11 | import org.slf4j.Logger; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.core.env.Environment; 15 | import org.springframework.scheduling.annotation.Scheduled; 16 | import org.springframework.stereotype.Service; 17 | 18 | import java.util.LinkedHashMap; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | /** 23 | * Created by davidesposito on 7/20/16. 24 | */ 25 | @Service 26 | public class BasicKafkaProducerProviderService implements KadminProducerProviderService { 27 | 28 | private static final Logger LOGGER = LoggerUtils.get(BasicKafkaProducerProviderService.class); 29 | private static final long IDLE_THRESHOLD = 15L * 60 * 1000; // 15 minutes 30 | private static final long IDLE_CHECK_DELAY = 60L * 60 * 1000; // 60 minutes 31 | 32 | private final String defaultKafkaHost; 33 | private final String defaultSchemaRegistryUrl; 34 | private final String defaultSecurityProtocol; 35 | private final String defaultTrustStoreLocation; 36 | private final String defaultTrustStorePassword; 37 | private final String defaultKeyStoreLocation; 38 | private final String defaultKeyStorePassword; 39 | private final String defaultKeyPassword; 40 | 41 | private final LinkedHashMap> producerMap; 42 | 43 | @Autowired 44 | public BasicKafkaProducerProviderService( 45 | @Value("${kafka.host:localhost:9092}") 46 | String defaultKafkaHost, 47 | @Value("${schema.registry.url:http://localhost:8081}") 48 | String defaultSchemaRegistryUrl, 49 | @Value("${security.protocol:PLAINTEXT}") 50 | String defaultSecurityProtocol, 51 | Environment env) { 52 | this.defaultKafkaHost = defaultKafkaHost; 53 | this.defaultSchemaRegistryUrl = defaultSchemaRegistryUrl; 54 | this.defaultSecurityProtocol = defaultSecurityProtocol; 55 | this.defaultTrustStoreLocation = env.getProperty("trust.store.location"); 56 | this.defaultTrustStorePassword = env.getProperty("trust.store.password"); 57 | this.defaultKeyStoreLocation = env.getProperty("key.store.location"); 58 | this.defaultKeyStorePassword = env.getProperty("key.store.password"); 59 | this.defaultKeyPassword = env.getProperty("key.password"); 60 | 61 | this.producerMap = Maps.newLinkedHashMap(); 62 | } 63 | 64 | @Scheduled(fixedRate = IDLE_CHECK_DELAY) 65 | private void clearMemory() { 66 | LOGGER.info("Cleaning up producers connections"); 67 | long currTime = System.currentTimeMillis(); 68 | List keys = producerMap.keySet().stream() 69 | .filter(k -> currTime - producerMap.get(k).getLastUsedTime() > IDLE_THRESHOLD) 70 | .collect(Collectors.toList()); 71 | keys.stream() 72 | .forEach(k -> { 73 | KadminProducer p = producerMap.get(k); 74 | LOGGER.debug("Disposing old consumer ({}) with timeout {}", k, System.currentTimeMillis() - p.getLastUsedTime()); 75 | p.shutdown(); 76 | }); 77 | System.gc(); 78 | } 79 | 80 | @Override 81 | public KadminProducer get(KadminProducerConfig config) { 82 | Opt.of(config.getKafkaHost()).notPresent(() -> config.setKafkaHost(defaultKafkaHost)); 83 | Opt.of(config.getSchemaRegistryUrl()).notPresent(() -> config.setSchemaRegistryUrl(defaultSchemaRegistryUrl)); 84 | Opt.of(config.getSecurityProtocol()).notPresent(() -> config.setSecurityProtocol(defaultSecurityProtocol)); 85 | Opt.of(config.getTrustStoreLocation()).notPresent(() -> config.setTrustStoreLocation(defaultTrustStoreLocation)); 86 | Opt.of(config.getTrustStorePassword()).notPresent(() -> config.setTrustStorePassword(defaultTrustStorePassword)); 87 | Opt.of(config.getKeyStoreLocation()).notPresent(() -> config.setKeyStoreLocation(defaultKeyStoreLocation)); 88 | Opt.of(config.getKeyStorePassword()).notPresent(() -> config.setKeyStorePassword(defaultKeyStorePassword)); 89 | Opt.of(config.getKeyPassword()).notPresent(() -> config.setKeyPassword(defaultKeyPassword)); 90 | 91 | BasicKafkaProducer producer = new BasicKafkaProducer<>(config); 92 | producerMap.put(producer.getId(), producer); 93 | return producer; 94 | } 95 | 96 | @Override 97 | public Page> findAll() { 98 | return findAll(-1 ,- 1); 99 | } 100 | 101 | @Override 102 | public Page> findAll(int page, int size) { 103 | if (page < 0) { 104 | page = 0; 105 | } 106 | if (size <= 0 || size > 100) { 107 | size = 20; 108 | } 109 | int skip = page * size; 110 | List> consumers = producerMap.values().stream() 111 | .skip(skip) 112 | .limit(size) 113 | .collect(Collectors.toList()); 114 | Page> consumerPage = new Page<>(); 115 | consumerPage.setPage(page); 116 | consumerPage.setSize(size); 117 | consumerPage.setTotalElements(count()); 118 | consumerPage.setContent(consumers); 119 | return consumerPage; 120 | } 121 | 122 | @Override 123 | public KadminProducer findById(String consumerId) { 124 | return producerMap.get(consumerId); 125 | } 126 | 127 | @Override 128 | public long count() { 129 | return producerMap.size(); 130 | } 131 | 132 | @Override 133 | public boolean dispose(String producerId) { 134 | KadminProducer p = producerMap.get(producerId); 135 | if (p == null) { 136 | return false; 137 | } 138 | p.shutdown(); 139 | producerMap.remove(producerId); 140 | return true; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/DefaultIntrospectionService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.services.IntrospectionService; 4 | import org.apache.kafka.clients.consumer.KafkaConsumer; 5 | import org.apache.kafka.common.serialization.StringDeserializer; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.Collection; 11 | import java.util.Optional; 12 | import java.util.Properties; 13 | import java.util.Set; 14 | 15 | @Service 16 | public class DefaultIntrospectionService implements IntrospectionService { 17 | 18 | private String defaultKafkaHost; 19 | 20 | @Autowired 21 | public DefaultIntrospectionService(@Value("${kafka.host:localhost:9092}") String defaultKafkaHost) { 22 | this.defaultKafkaHost = defaultKafkaHost; 23 | } 24 | 25 | @Override 26 | public Collection getAllTopicNames(Optional kafkaUrl) { 27 | Set topics; 28 | 29 | Properties props = new Properties(); 30 | props.put("bootstrap.servers", kafkaUrl.orElse(defaultKafkaHost)); 31 | props.put("group.id", "kadmin-topic-listing-group"); 32 | props.put("key.deserializer", StringDeserializer.class); 33 | props.put("value.deserializer", StringDeserializer.class); 34 | 35 | KafkaConsumer consumer = new KafkaConsumer<>(props); 36 | topics = consumer.listTopics().keySet(); 37 | consumer.close(); 38 | return topics; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/DefaultSchemaRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.exception.SchemaRegistryRestException; 4 | import com.bettercloud.kadmin.api.services.FeaturesService; 5 | import com.bettercloud.kadmin.io.network.dto.SchemaInfoModel; 6 | import com.bettercloud.kadmin.api.services.SchemaRegistryService; 7 | import com.bettercloud.kadmin.io.network.rest.SchemaProxyResource; 8 | import com.bettercloud.util.LoggerUtils; 9 | import com.fasterxml.jackson.databind.JsonNode; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import com.fasterxml.jackson.databind.node.ArrayNode; 12 | import com.google.common.cache.Cache; 13 | import com.google.common.cache.CacheBuilder; 14 | import org.apache.http.HttpResponse; 15 | import org.apache.http.client.HttpClient; 16 | import org.apache.http.client.methods.HttpGet; 17 | import org.slf4j.Logger; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.stereotype.Service; 21 | 22 | import java.io.IOException; 23 | import java.util.List; 24 | import java.util.Optional; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.stream.Collectors; 27 | import java.util.stream.StreamSupport; 28 | 29 | /** 30 | * Created by davidesposito on 7/18/16. 31 | */ 32 | @Service 33 | public class DefaultSchemaRegistryService implements SchemaRegistryService { 34 | 35 | private static final ObjectMapper MAPPER = new ObjectMapper(); 36 | private static final Logger LOGGER = LoggerUtils.get(SchemaProxyResource.class); 37 | 38 | private final String schemaRegistryUrl; 39 | 40 | private final HttpClient client; 41 | private final FeaturesService featuresService; 42 | 43 | private final Cache> schemasCache; 44 | private final Cache schemaInfoCache; 45 | private final Cache schemaVersionCache; 46 | 47 | @Autowired 48 | public DefaultSchemaRegistryService(HttpClient defaultClient, 49 | @Value("${schema.registry.url:http://localhost:8081}") 50 | String schemaRegistryUrl, 51 | FeaturesService featuresService) { 52 | this.client = defaultClient; 53 | this.schemaRegistryUrl = schemaRegistryUrl; 54 | this.featuresService = featuresService; 55 | schemasCache = defaultCache(); 56 | schemaInfoCache = defaultCache(); 57 | schemaVersionCache = defaultCache(); 58 | } 59 | 60 | private Cache defaultCache() { 61 | return CacheBuilder.newBuilder() 62 | .expireAfterAccess(30, TimeUnit.SECONDS) 63 | .expireAfterWrite(90, TimeUnit.SECONDS) 64 | .build(); 65 | } 66 | 67 | @Override 68 | public List findAll(String url) throws SchemaRegistryRestException { 69 | String tempUrl = String.format("%s/subjects", 70 | Optional.ofNullable(featuresService.getCustomUrl(url)).orElse(this.schemaRegistryUrl) 71 | ); 72 | if (schemasCache.getIfPresent(tempUrl) == null) { 73 | NodeConverter> c = (node) -> { 74 | if (node.isArray()) { 75 | ArrayNode arr = (ArrayNode) node; 76 | return StreamSupport.stream(arr.spliterator(), false) 77 | .map(n -> n.asText()) 78 | .sorted() 79 | .collect(Collectors.toList()); 80 | } 81 | return null; 82 | }; 83 | schemasCache.put(tempUrl, proxyResponse(tempUrl, c, null)); 84 | } else { 85 | LOGGER.debug("Hit schema cache for: {}", tempUrl); 86 | } 87 | return schemasCache.getIfPresent(tempUrl); 88 | } 89 | 90 | @Override 91 | public List guessAllTopics(String url) throws SchemaRegistryRestException { 92 | return findAll(url).stream() 93 | .map(schemaName -> schemaName.replaceAll("-value", "")) 94 | .collect(Collectors.toList()); 95 | } 96 | 97 | @Override 98 | public SchemaInfoModel getInfo(String name, String url) throws SchemaRegistryRestException { 99 | String tempUrl = String.format("%s/subjects/%s/versions", 100 | Optional.ofNullable(featuresService.getCustomUrl(url)).orElse(this.schemaRegistryUrl), 101 | name 102 | ); 103 | if (schemaInfoCache.getIfPresent(tempUrl) == null) { 104 | NodeConverter> c = (node) -> { 105 | if (node.isArray()) { 106 | ArrayNode arr = (ArrayNode) node; 107 | return StreamSupport.stream(arr.spliterator(), false) 108 | .map(n -> n.asInt()) 109 | .collect(Collectors.toList()); 110 | } 111 | return null; 112 | }; 113 | List versions = proxyResponse(tempUrl, c, null); 114 | JsonNode info = getVersion(name, versions.get(versions.size() - 1), url); 115 | JsonNode currSchema = info; 116 | schemaInfoCache.put(tempUrl, SchemaInfoModel.builder() 117 | .name(name) 118 | .versions(versions) 119 | .currSchema(currSchema) 120 | .build()); 121 | } else { 122 | LOGGER.debug("Hit info cache for: {}", tempUrl); 123 | } 124 | return schemaInfoCache.getIfPresent(tempUrl); 125 | } 126 | 127 | @Override 128 | public JsonNode getVersion(String name, int version, String url) throws SchemaRegistryRestException { 129 | url = String.format("%s/subjects/%s/versions/%d", 130 | Optional.ofNullable(featuresService.getCustomUrl(url)).orElse(this.schemaRegistryUrl), 131 | name, 132 | version 133 | ); 134 | if (schemaVersionCache.getIfPresent(url) == null) { 135 | schemaVersionCache.put(url, proxyResponse(url, n -> n, null)); 136 | } else { 137 | LOGGER.debug("Hit version cache for: {}", url); 138 | } 139 | return schemaVersionCache.getIfPresent(url); 140 | } 141 | 142 | private ResponseT proxyResponse(String url, NodeConverter c, ResponseT defaultVal) 143 | throws SchemaRegistryRestException { 144 | HttpGet get = new HttpGet(url); 145 | try { 146 | HttpResponse res = client.execute(get); 147 | int statusCode = res.getStatusLine().getStatusCode(); 148 | if (statusCode != 200) { 149 | LOGGER.error("Non 200 status: {}", statusCode); 150 | throw new SchemaRegistryRestException("Non 200 status: " + statusCode, statusCode); 151 | } 152 | ResponseT val = c.convert(MAPPER.readTree(res.getEntity().getContent())); 153 | if (val == null) { 154 | return defaultVal; 155 | } 156 | return val; 157 | } catch (IOException e) { 158 | LOGGER.error("There was an error: {}", e.getMessage()); 159 | throw new SchemaRegistryRestException(e.getMessage(), e, 500); 160 | } 161 | } 162 | 163 | public interface NodeConverter extends Converter { } 164 | 165 | public interface Converter { 166 | ToT convert(FromT o); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/DefultBundledKadminConsumerGroupProviderService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.kafka.KadminConsumerConfig; 4 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroup; 5 | import com.bettercloud.kadmin.api.kafka.KadminConsumerGroupContainer; 6 | import com.bettercloud.kadmin.api.services.BundledKadminConsumerGroupProviderService; 7 | import com.bettercloud.kadmin.api.services.KadminConsumerGroupProviderService; 8 | import com.bettercloud.kadmin.kafka.QueuedKafkaMessageHandler; 9 | import com.bettercloud.util.Opt; 10 | import com.bettercloud.util.Page; 11 | import com.google.common.collect.Maps; 12 | import org.apache.kafka.common.serialization.StringDeserializer; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * Created by davidesposito on 7/19/16. 22 | */ 23 | @Service 24 | public class DefultBundledKadminConsumerGroupProviderService implements BundledKadminConsumerGroupProviderService { 25 | 26 | private final KadminConsumerGroupProviderService providerService; 27 | private final Map containerMap; 28 | 29 | @Autowired 30 | public DefultBundledKadminConsumerGroupProviderService(KadminConsumerGroupProviderService providerService) { 31 | this.providerService = providerService; 32 | this.containerMap = Maps.newLinkedHashMap(); 33 | } 34 | 35 | @Override 36 | public void start(String consumerId) { 37 | Opt.of(containerMap.get(consumerId)).ifPresent(cont -> providerService.start(cont.getConsumer())); 38 | } 39 | 40 | @Override 41 | public KadminConsumerGroupContainer get(KadminConsumerConfig config, boolean start, int queueSize) { 42 | KadminConsumerGroup consumer = providerService.get(config, start); 43 | KadminConsumerGroupContainer container = containerMap.get(consumer.getClientId()); 44 | if (container == null) { 45 | QueuedKafkaMessageHandler queue = new QueuedKafkaMessageHandler(queueSize); 46 | consumer.register(queue); 47 | container = KadminConsumerGroupContainer.builder() 48 | .consumer(consumer) 49 | .queue(queue) 50 | .build(); 51 | containerMap.put(container.getId(), container); 52 | } 53 | return container; 54 | } 55 | 56 | @Override 57 | public Page findAll() { 58 | return findAll(-1, -1); 59 | } 60 | 61 | @Override 62 | public Page findAll(int page, int size) { 63 | if (page < 0) { 64 | page = 0; 65 | } 66 | if (size <= 0 || size > 100) { 67 | size = 20; 68 | } 69 | int skip = page * size; 70 | List consumers = containerMap.values().stream() 71 | .skip(skip) 72 | .limit(size) 73 | .collect(Collectors.toList()); 74 | Page consumerPage = new Page<>(); 75 | consumerPage.setPage(page); 76 | consumerPage.setSize(size); 77 | consumerPage.setTotalElements(count()); 78 | consumerPage.setContent(consumers); 79 | return consumerPage; 80 | } 81 | 82 | @Override 83 | public KadminConsumerGroupContainer findById(String consumerId) { 84 | return containerMap.get(consumerId); 85 | } 86 | 87 | @Override 88 | public long count() { 89 | return containerMap.size(); 90 | } 91 | 92 | @Override 93 | public KadminConsumerGroupContainer dispose(String consumerId) { 94 | return Opt.of(containerMap.get(consumerId)) 95 | .ifPresent(container -> { 96 | providerService.dispose(consumerId); 97 | containerMap.remove(consumerId); 98 | }) 99 | .orElse(null); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/KafkaDeserializerRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.models.DeserializerInfoModel; 4 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 5 | import com.bettercloud.kadmin.api.services.DeserializerRegistryService; 6 | import com.bettercloud.kadmin.api.services.SerializerRegistryService; 7 | 8 | /** 9 | * Created by davidesposito on 7/21/16. 10 | */ 11 | public class KafkaDeserializerRegistryService extends SimpleRegistryService implements DeserializerRegistryService { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/KafkaSerializerRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.models.Model; 4 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 5 | import com.bettercloud.kadmin.api.services.RegistryService; 6 | import com.bettercloud.kadmin.api.services.SerializerRegistryService; 7 | import com.bettercloud.util.Page; 8 | import com.google.common.collect.Maps; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * Created by davidesposito on 7/21/16. 16 | */ 17 | public class KafkaSerializerRegistryService extends SimpleRegistryService implements SerializerRegistryService { 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/kadmin/services/SimpleRegistryService.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin.services; 2 | 3 | import com.bettercloud.kadmin.api.models.Model; 4 | import com.bettercloud.kadmin.api.models.SerializerInfoModel; 5 | import com.bettercloud.kadmin.api.services.RegistryService; 6 | import com.bettercloud.kadmin.api.services.SerializerRegistryService; 7 | import com.bettercloud.util.Page; 8 | import com.google.common.collect.Maps; 9 | 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * Created by davidesposito on 7/21/16. 17 | */ 18 | public class SimpleRegistryService implements RegistryService { 19 | 20 | private final Map infoMap = Collections.synchronizedMap(Maps.newLinkedHashMap()); 21 | 22 | @Override 23 | public void register(ModelT model) { 24 | if (model != null && model.getId() != null) { 25 | infoMap.put(model.getId(), model); 26 | } 27 | } 28 | 29 | @Override 30 | public Page findAll() { 31 | Page page = new Page<>(); 32 | List content = infoMap.values().stream().collect(Collectors.toList()); 33 | page.setContent(content); 34 | page.setPage(0); 35 | page.setSize(content.size()); 36 | page.setTotalElements(content.size()); 37 | return page; 38 | } 39 | 40 | @Override 41 | public ModelT findById(String id) { 42 | return infoMap.get(id); 43 | } 44 | 45 | @Override 46 | public ModelT remove(String id) { 47 | return infoMap.remove(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/util/LoggerUtils.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.util; 2 | 3 | import ch.qos.logback.classic.Level; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | /** 8 | * Created by davidesposito on 7/16/16. 9 | */ 10 | public final class LoggerUtils { 11 | 12 | private LoggerUtils() { } 13 | 14 | public static Logger get(Class logClass) { 15 | return LoggerFactory.getLogger(logClass); 16 | } 17 | 18 | public static Logger get(Class logClass, Level level) { 19 | Logger logger = get(logClass); 20 | ((ch.qos.logback.classic.Logger)logger).setLevel(level); 21 | return logger; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/util/Opt.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.util; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Function; 5 | 6 | /** 7 | * Created by davidesposito on 7/8/16. 8 | */ 9 | public class Opt { 10 | 11 | private static final Runnable NOOP = () -> { }; 12 | 13 | private final T o; 14 | 15 | private Opt(T o) { 16 | this.o = o; 17 | } 18 | 19 | public boolean isPresent() { 20 | return o != null; 21 | } 22 | 23 | public T get() { 24 | return o; 25 | } 26 | 27 | public T orElse(T other) { 28 | return isPresent() ? o : other; 29 | } 30 | 31 | public Opt ifPresent(Consumer _if) { 32 | if (isPresent()) { 33 | _if.accept(get()); 34 | } 35 | return this; 36 | } 37 | 38 | public Opt ifPresent(Consumer _if, Runnable _else) { 39 | if (isPresent()) { 40 | _if.accept(get()); 41 | } else { 42 | _else.run(); 43 | } 44 | return this; 45 | } 46 | 47 | public void notPresent(Runnable _else) { 48 | if (!isPresent()) { 49 | _else.run(); 50 | } 51 | } 52 | 53 | public Opt map(Function _map) { 54 | return isPresent() ? Opt.of(_map.apply(get())) : Opt.empty(); 55 | } 56 | 57 | public Opt flatMap(Function> _map) { 58 | return isPresent() ? Opt.of(_map.apply(get()).get()) : Opt.empty(); 59 | } 60 | 61 | public Opt filter(Function _filter) { 62 | return isPresent() && _filter.apply(get()) ? this : Opt.empty(); 63 | } 64 | 65 | public Opt filter(Function _filter, Consumer _else) { 66 | boolean filtered = isPresent() && _filter.apply(get()); 67 | if (filtered) { 68 | _else.accept(get()); 69 | } 70 | return filtered ? this : Opt.empty(); 71 | } 72 | 73 | public static Opt empty() { 74 | return of(null); 75 | } 76 | 77 | public static Opt of(StaticT o) { 78 | return new Opt<>(o); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/util/Page.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.util; 2 | 3 | import lombok.*; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by davidesposito on 7/20/16. 9 | */ 10 | @Data 11 | @NoArgsConstructor 12 | public class Page { 13 | 14 | private List content; 15 | private int page; 16 | private int size; 17 | private long totalElements; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/util/StreamUtils.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.stream.Stream; 5 | import java.util.stream.StreamSupport; 6 | 7 | /** 8 | * Created by davidesposito on 7/8/16. 9 | */ 10 | public class StreamUtils { 11 | 12 | public static Stream asStream(Iterator sourceIterator) { 13 | return asStream(sourceIterator, false); 14 | } 15 | 16 | public static Stream asStream(Iterator sourceIterator, boolean parallel) { 17 | return asStream(() -> sourceIterator, parallel); 18 | } 19 | 20 | public static Stream asStream(Iterable sourceIterable) { 21 | return asStream(sourceIterable, false); 22 | } 23 | 24 | public static Stream asStream(Iterable sourceIterable, boolean parallel) { 25 | return StreamSupport.stream(sourceIterable.spliterator(), parallel); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/bettercloud/util/TimedWrapper.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.util; 2 | 3 | /** 4 | * Created by davidesposito on 7/11/16. 5 | */ 6 | public class TimedWrapper { 7 | 8 | private long lastUsed; 9 | private final DataT data; 10 | 11 | private TimedWrapper(DataT data) { 12 | this.data = data; 13 | this.lastUsed = System.currentTimeMillis(); 14 | } 15 | 16 | public DataT getData() { 17 | return getData(true); 18 | } 19 | 20 | public DataT getData(boolean used) { 21 | if (used) { 22 | lastUsed = System.currentTimeMillis(); 23 | } 24 | return data; 25 | } 26 | 27 | public long getLastUsed() { 28 | return lastUsed; 29 | } 30 | 31 | public long getIdleTime() { 32 | return System.currentTimeMillis() - getLastUsed(); 33 | } 34 | 35 | public static TimedWrapper of(StaticDataT data) { 36 | return new TimedWrapper<>(data); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/resources/application-kadmin.properties: -------------------------------------------------------------------------------- 1 | server.contextPath=/kadmin -------------------------------------------------------------------------------- /src/main/resources/application-readonly.properties: -------------------------------------------------------------------------------- 1 | ff.producer.enabled=false 2 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # The following config sets the spring context path. You will access the application at 2 | # http:/// e.g. http://localhost:8080/kadmin 3 | # 4 | #server.contextPath=/kadmin 5 | 6 | 7 | # This active profile sets the application context so that all requests are 8 | # served under /kadmin. See application-kadmin.properties for more information 9 | # or if you need to change the context. 10 | # 11 | #spring.profiles.include=kadmin 12 | 13 | 14 | # Toggles read only mode i.e. Kafka producers are disabled. You can use the following 15 | # Spring profile or the raw config. 16 | # 17 | #spring.profiles.include=readonly 18 | #ff.producer.enabled=false 19 | 20 | 21 | # Allows custom urls to be used for Kafka and Service Registry for each producer and consumer. 22 | # 23 | # 24 | #ff.customKafkaUrl.enabled=false 25 | 26 | 27 | # If ff.customKafkaUrl.enabled is disabled then you need to configure the default endpoints using the following configs 28 | # kafka.host is a comma seperated list of kafka brokers. 29 | # 30 | #kafka.host=host1.bettercloud:6667,host2.bettercloud:6667 31 | #schema.registry.url=http://host3.bettercloud:8081 32 | 33 | security.protocol=PLAINTEXT 34 | #trust.store.location= 35 | #trust.store.password= 36 | #key.store.location= 37 | #key.store.password= 38 | #key.password= -------------------------------------------------------------------------------- /src/main/resources/local.properties: -------------------------------------------------------------------------------- 1 | zookeeper.host=localhost:2181 2 | kafka.host=localhost:9092 3 | schema.registry.url=http://localhost:8081 4 | 5 | 6 | spring.cloud.config.uri = http://localhost:8888 -------------------------------------------------------------------------------- /src/main/resources/logger.properties: -------------------------------------------------------------------------------- 1 | MICROSERVICE_NAME=kadmin 2 | FEATURE_TYPE=devops 3 | LOG_FORMAT=v1 4 | DOCKER_PORT=8080 -------------------------------------------------------------------------------- /src/main/resources/service.properties: -------------------------------------------------------------------------------- 1 | # The name of this service 2 | serviceName=Kadmin 3 | 4 | # The domain supported by this module (or * for all) 5 | domain=* 6 | 7 | # The vendor the service deals with (Google, Dropbox, Salesforce, etc) 8 | vendor=Generic 9 | 10 | # The key used to retrieve the overlay from the database 11 | overlayKey=kadmin 12 | modelPackage=com.bettercloud.kadmin -------------------------------------------------------------------------------- /src/main/resources/static/basicproducer/main.html: -------------------------------------------------------------------------------- 1 | 7 |
8 |
9 |
10 |
11 | 12 |
13 |
14 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | 30 | 33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | 52 |
53 |
-------------------------------------------------------------------------------- /src/main/resources/static/basicproducer/main.js: -------------------------------------------------------------------------------- 1 | function initMain() { 2 | if (!(App.pageLoaded && App.mainTemplateLoaded && App.infoTemplateLoaded && App.errorTemplateLoaded)) { 3 | // not ready yet 4 | return; 5 | } 6 | App.producer = _.extend(App.producer, { 7 | $results: $('#sent-results-row'), 8 | $aceMessage: ace.edit("ace-message"), 9 | $topicsSelect: $('#topic-dd'), 10 | $serializerSelect: $('#serializer-dd') 11 | }); 12 | 13 | $('#send-message-btn').click(function(e) { 14 | e.preventDefault(); 15 | sendMessage(); 16 | }); 17 | App.producer.$topicsSelect.change(function() { 18 | var val = App.producer.$topicsSelect.val(); 19 | // TODO: regex check 20 | // if (val.matches(/[\w-]+/)) { 21 | $('#topic').val(val); 22 | // } 23 | }); 24 | refreshTopics(); 25 | 26 | refreshSerializers(); 27 | 28 | App.producer.$aceMessage.setTheme("ace/theme/kuroir"); 29 | App.producer.$aceMessage.getSession().setMode("ace/mode/text"); 30 | } 31 | 32 | function getKafkaHost() { 33 | var kafkaHost = $('#kafkahost').val(); 34 | return kafkaHost !== "" ? kafkaHost : undefined; 35 | } 36 | 37 | function refreshTopics() { 38 | $.get(App.contextPath + "/api/topics", {"kafka-url": getKafkaHost()}, handleNewTopics); 39 | } 40 | 41 | function handleNewTopics(data) { 42 | var $topicsSelect = App.producer.$topicsSelect; 43 | $topicsSelect.html(""); 44 | _.each(data, function(topicName) { 45 | $topicsSelect.append(""); 46 | }); 47 | } 48 | 49 | function refreshSerializers() { 50 | $.get(App.contextPath + "/api/manager/serializers", handleSerializers); 51 | } 52 | 53 | function handleSerializers(data) { 54 | var $serializerSelect = App.producer.$serializerSelect; 55 | $serializerSelect.html(''); 56 | _.each(data.content, function(info) { 57 | $serializerSelect.append(""); 58 | }); 59 | } 60 | 61 | function sendMessage() { 62 | var req = buildRequest(); 63 | $.ajax({ 64 | type: "POST", 65 | url: App.contextPath + "/api/kafka/publish?count=" + getCount(), 66 | headers: { 67 | "Content-Type": "application/json", 68 | Accept: "application/json;charset=UTF-8" 69 | }, 70 | data: JSON.stringify(req), 71 | success: handleResponse 72 | }); 73 | $('#send-message-btn').addClass("disabled"); 74 | } 75 | 76 | function buildRequest() { 77 | var req = { 78 | meta: { 79 | kafkaUrl: $('#kafkahost').val(), 80 | topic: $('#topic').val(), 81 | serializerId: App.producer.$serializerSelect.val() 82 | }, 83 | rawMessage: App.producer.$aceMessage.getValue(), 84 | key: $('#message-key').val() 85 | }; 86 | if (req.meta.kafkaUrl === "") { 87 | req.meta.kafkaUrl = null; 88 | } 89 | if (req.meta.schemaRegistryUrl === "") { 90 | req.meta.schemaRegistryUrl = null; 91 | } 92 | return req; 93 | } 94 | 95 | function getCount() { 96 | var count = $('#count').val(); 97 | if (count !== '') { 98 | count = parseInt(count); 99 | if (count !== NaN && count > 0 && count < 10000) { 100 | return count; 101 | } 102 | } 103 | return 1; 104 | } 105 | 106 | function handleResponse(data) { 107 | var timeText = moment().format('LTS'), 108 | template = null; 109 | if (!!data && !!data.sent && data.sent && !!data.success && data.success) { 110 | // request was successful 111 | template = App.producer.sentInfoTemplate; 112 | } else { 113 | data = {}; 114 | template = App.producer.sentErrorTemplate; 115 | } 116 | data.timeText = timeText; 117 | App.producer.$results.html(template(data)); 118 | $('#send-message-btn').removeClass("disabled"); 119 | window.scrollTo(0, document.body.scrollHeight); 120 | } 121 | -------------------------------------------------------------------------------- /src/main/resources/static/basicproducer/sent-error-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
There was an error
4 |
<%= timeText %>
5 |
6 |
-------------------------------------------------------------------------------- /src/main/resources/static/basicproducer/sent-info-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Sent
4 |
<%= timeText %>
5 |
    6 |
  • Success: <%= success %>
  • 7 |
  • Count: <%= count %>
  • 8 |
  • Duration: <%= duration %>ms
  • 9 |
  • Rate: <%= rate %> messages/second
  • 10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/main/resources/static/consumer/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 | 33 | 36 |
37 |
38 |
39 |
40 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 56 |
57 |
58 |
59 | 62 | 65 | 68 | 71 |
72 |
73 |
74 |
75 |

Messages

76 |
77 |
NOTE: Collapse works weird with auto-refresh intervals. You should set the refresh interval to manual before using the collapse button.
78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /src/main/resources/static/consumer/main.js: -------------------------------------------------------------------------------- 1 | function initMain() { 2 | if (!(App.pageLoaded && App.mainTemplateLoaded && App.messageTemplateLoaded)) { 3 | // not ready yet 4 | return; 5 | } 6 | var $refreshRate = $('#refresh-rate'); 7 | App.consumer = _.extend(App.consumer, { 8 | count : 0, 9 | $refreshRate: $refreshRate, 10 | consumerConfig: { 11 | started: false, 12 | kafkaUrl: null, 13 | schemaUrl: null, 14 | topic: null, 15 | since: null, 16 | refreshHandle: null, 17 | refreshRate: $refreshRate.val() 18 | }, 19 | $messageList: $('#message-list'), 20 | clipboard: null, 21 | $topicsSelect: $('#topic-dd'), 22 | $desSelect: $('#deserializer-dd') 23 | }); 24 | 25 | $('#refresh-rate').change(function(e) { 26 | var consumerConfig = App.consumer.consumerConfig; 27 | consumerConfig.refreshRate = $refreshRate.val(); 28 | if (!!consumerConfig.refreshHandle) { 29 | clearTimeout(consumerConfig.refreshHandle); 30 | } 31 | if (consumerConfig.refreshRate > 0 && consumerConfig.started) { 32 | refresh(); 33 | } 34 | console.log("Refresh Rate: " + consumerConfig.refreshRate); 35 | }); 36 | $('#start-consumer-btn').click(function(e) { 37 | e.preventDefault(); 38 | refresh(); 39 | }); 40 | $('#refresh-topics-btn').click(refreshTopics); 41 | App.consumer.$topicsSelect.change(function() { 42 | var val = App.consumer.$topicsSelect.val(); 43 | // TODO: regex check 44 | // if (val.matches(/[\w-]+/)) { 45 | $('#topic').val(val); 46 | // } 47 | }); 48 | if (!!App.defaultTopic && App.des.id && App.des.name) { 49 | $('#topic').val(App.defaultTopic); 50 | $('#deserializer-dd').html(''); 51 | refresh(); 52 | } else { 53 | refreshTopics(); 54 | refreshDeserializers(); 55 | } 56 | } 57 | 58 | 59 | function getKafkaHost() { 60 | var kafkaHost = $('#kafkahost').val(); 61 | return kafkaHost !== "" ? kafkaHost : undefined; 62 | } 63 | 64 | function refreshTopics() { 65 | $.get(App.contextPath + "/api/topics", {"kafka-url": getKafkaHost()}, handleNewTopics); 66 | } 67 | 68 | function handleNewTopics(data) { 69 | App.consumer.$topicsSelect.html(""); 70 | _.each(data, function(topicName) { 71 | App.consumer.$topicsSelect.append(""); 72 | }); 73 | } 74 | 75 | function refreshDeserializers() { 76 | $.get(App.contextPath + "/api/manager/deserializers", handleNewDeserializers); 77 | } 78 | 79 | function handleNewDeserializers(data) { 80 | App.consumer.$desSelect.html(''); 81 | _.each(data.content, function(des) { 82 | App.consumer.$desSelect.append(""); 83 | }); 84 | } 85 | 86 | function initConfig() { 87 | var topic = $('#topic').val(); 88 | if (!topic || topic === "") { 89 | alert("Topic is required. Aborting creating consumer."); 90 | return false; 91 | } 92 | consumerConfig = { 93 | started: true, 94 | kafkaUrl: $('#kafkahost').val(), 95 | schemaUrl: $('#schemaurl').val(), 96 | topic: $('#topic').val(), 97 | since: -1, 98 | autoRefresh: null, 99 | refreshRate: App.consumer.$refreshRate.val(), 100 | desClass: App.consumer.$desSelect.val() 101 | }; 102 | if (consumerConfig.kafkaUrl === "") { 103 | consumerConfig.kafkaUrl = null; 104 | } 105 | if (consumerConfig.schemaUrl === "") { 106 | consumerConfig.schemaUrl = null; 107 | } 108 | App.consumer.consumerConfig = consumerConfig; 109 | disableForm(); 110 | initMessageList(); 111 | return true; 112 | } 113 | 114 | function disableForm() { 115 | $("#kafkahost").prop("disabled", true); 116 | $("#schemaurl").prop("disabled", true); 117 | $("#topic").prop("disabled", true); 118 | App.consumer.$topicsSelect.prop("disabled", true); 119 | $("#start-consumer-btn").addClass("disabled"); 120 | App.consumer.$desSelect.prop("disabled", true); 121 | } 122 | 123 | function initMessageList() { 124 | $('#message-list-title').html("Messages - " + consumerConfig.topic); 125 | var $refresh = $("#refresh-btn"); 126 | $refresh.removeClass("disabled"); 127 | $refresh.click(function() { refresh(true); }); 128 | var $clear = $("#clear-btn"); 129 | $clear.removeClass("disabled"); 130 | $clear.click(truncateList); 131 | var $dispose = $("#dispose-btn"); 132 | $dispose.removeClass("disabled"); 133 | $dispose.click(disposeConsumer); 134 | var $permalink = $("#permalink-btn"); 135 | $permalink.removeClass("disabled"); 136 | $permalink.attr("data-clipboard-text", window.location.origin + App.contextPath + "/consumer/topic/" + 137 | consumerConfig.topic + "/" + consumerConfig.desClass); 138 | new Clipboard("#permalink-btn"); 139 | } 140 | 141 | function truncateList() { 142 | if (!App.consumer.consumerConfig.started) { 143 | if (!initConfig()) { 144 | return; 145 | } 146 | } 147 | $.ajax({ 148 | type: "DELETE", 149 | url: App.contextPath + "/api/manager/consumers/" + App.consumer.consumerConfig.id + "/truncate", 150 | success: refresh 151 | }); 152 | } 153 | 154 | function disposeConsumer() { 155 | if (!App.consumer.consumerConfig.started) { 156 | if (!initConfig()) { 157 | return; 158 | } 159 | } 160 | $.ajax({ 161 | type: "DELETE", 162 | url: App.contextPath + "/api/manager/consumers/" + App.consumer.consumerConfig.id, 163 | success: function() { 164 | window.location.href = App.contextPath; 165 | } 166 | }); 167 | } 168 | 169 | function refresh(manualRefresh) { 170 | var consumerConfig = App.consumer.consumerConfig; 171 | if (!consumerConfig.started) { 172 | if (!initConfig()) { 173 | return; 174 | } 175 | } 176 | var url = buildUrl(); 177 | consumerConfig.since = new Date().getTime(); 178 | $.get(url, handleResults); 179 | $("#since-row").html("Updated: " + moment(consumerConfig.since).format('LTS')); 180 | if (consumerConfig.refreshRate > 0 && !manualRefresh) { 181 | consumerConfig.refreshHandle = setTimeout(refresh, consumerConfig.refreshRate); 182 | } 183 | } 184 | 185 | function buildUrl() { 186 | var consumerConfig = App.consumer.consumerConfig, 187 | url = App.contextPath + "/api/kafka/read/" + consumerConfig.topic + "?deserializerId=" + consumerConfig.desClass + "&"; 188 | if (!!consumerConfig.kafkaUrl) { 189 | url += "kafkaUrl=" + consumerConfig.kafkaUrl + "&"; 190 | } 191 | if (!!consumerConfig.schemaUrl) { 192 | url += "schemaUrl=" + consumerConfig.schemaUrl; 193 | } 194 | return url; 195 | } 196 | 197 | function handleResults(res) { 198 | var count = 0, 199 | consumerConfig = App.consumer.consumerConfig, 200 | data = res.page; 201 | App.consumer.consumerConfig.id = res.consumerId; 202 | if (!!App.consumer.clipboard) { 203 | App.consumer.clipboard.destroy(); 204 | } 205 | App.consumer.$messageList.html(''); 206 | document.title = "(" + data.totalElements + ") " + consumerConfig.topic; 207 | _.each(data.content, function(ele) { 208 | var html = ""; 209 | ele.uuid = uniqueId(); 210 | ele.writeTimeText = moment(ele.writeTime).format('LTS'); 211 | ele.messageText = "null"; 212 | if (!!ele.message) { 213 | ele.rawMessage = JSON.stringify(ele.message, null, 2).replace(/([^\\])\\n/g, "$1\n"); 214 | ele.messageText = syntaxHighlight(ele.rawMessage); 215 | } else { 216 | ele.rawMessage = "null"; 217 | ele.messageText = "null"; 218 | } 219 | ele.timestamp = "_" + count++; 220 | html = App.consumer.messageTemplate(ele); 221 | App.consumer.$messageList.prepend(html); 222 | }); 223 | App.consumer.clipboard = new Clipboard('.copy-btn'); 224 | } 225 | 226 | function uniqueId() { 227 | return 'xxxxxxxx'.replace(/[xy]/g, function(c) { 228 | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 229 | return v.toString(16); 230 | }); 231 | } 232 | 233 | function syntaxHighlight(json) { 234 | json = json.replace(/&/g, '&').replace(//g, '>'); 235 | return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { 236 | var cls = 'number'; 237 | if (/^"/.test(match)) { 238 | if (/:$/.test(match)) { 239 | cls = 'key'; 240 | } else { 241 | cls = 'string'; 242 | } 243 | } else if (/true|false/.test(match)) { 244 | cls = 'boolean'; 245 | } else if (/null/.test(match)) { 246 | cls = 'null'; 247 | } 248 | return '' + match + ''; 249 | }); 250 | } 251 | -------------------------------------------------------------------------------- /src/main/resources/static/consumer/message-tile-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 9 |
10 | Key: "<%= key %>" 11 |
12 |
13 | Consumed @<%= writeTimeText %> 14 |
15 |
16 | offset: <%= offset %> 17 |
18 |
19 | topic: <%= topic %> 20 |
21 |
22 |
<%= messageText %>
23 |
24 |
25 |
-------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/js/clipboard.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v1.5.12 3 | * https://zenorocha.github.io/clipboard.js 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,o){function i(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;ao;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],i=[];if(o&&e)for(var r=0,a=o.length;a>r;r++)o[r].fn!==e&&o[r].fn._!==e&&i.push(o[r]);return i.length?n[t]=i:delete n[t],this}},e.exports=o},{}],8:[function(e,n,o){!function(i,r){if("function"==typeof t&&t.amd)t(["module","select"],r);else if("undefined"!=typeof o)r(n,e("select"));else{var a={exports:{}};r(a,i.select),i.clipboardAction=a.exports}}(this,function(t,e){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=n(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},a=function(){function t(t,e){for(var n=0;n 2 | <%= topic %> 3 | <%= lastUsedTimeText %> 4 | <%= lastMessageTimeText %> 5 | <%= consumerGroupId %> 6 | <%= deserializerName %> 7 | <%= queueSize %> 8 | <%= total %> 9 | 10 | 11 | View 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/static/manager/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Manage Consumers 4 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
TopicLast UsedLast MessageConsumer Group IdDeserializerCached Message Queue SizeMessages ConsumedActions
29 |
30 |
31 |
32 |
33 |
34 | Manage Producers 35 | 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
TopicSerializerLast UsedTotal SentTotal ErrorsActions
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /src/main/resources/static/manager/main.js: -------------------------------------------------------------------------------- 1 | function initMain() { 2 | if (!(App.pageLoaded && App.mainTemplateLoaded && App.consumerTemplateLoaded && App.producerTemplateLoaded)) { 3 | // not ready yet 4 | return; 5 | } 6 | App.manager = _.extend(App.manager, { 7 | $consumerUpdated: $("#consumer-list-updated"), 8 | $consumerTotal: $("#consumer-list-total"), 9 | $consumerTableBody: $("#consumer-table-body"), 10 | $producerUpdated: $("#producer-list-updated"), 11 | $producerTotal: $("#producer-list-total"), 12 | $producerTableBody: $("#producer-table-body"), 13 | }); 14 | 15 | $("#refresh-consumers-btn").click(refreshConsumers); 16 | $("#refresh-producers-btn").click(refreshProducers); 17 | 18 | refreshConsumers(); 19 | refreshProducers(); 20 | } 21 | 22 | function refreshConsumers() { 23 | $.get(App.contextPath + "/api/manager/consumers", handleConsumers); 24 | } 25 | 26 | function handleConsumers(data) { 27 | var table = App.manager.$consumerTableBody, 28 | template = App.manager.consumerTemplate; 29 | table.html(''); 30 | _.each(data.content, function(c) { 31 | c.contextPath = App.contextPath; 32 | c.lastUsedTimeText = c.lastUsedTime < 0 ? 'never' : moment(c.lastUsedTime).fromNow(); 33 | c.lastMessageTimeText = c.lastMessageTime < 0 ? 'never' : moment(c.lastMessageTime).fromNow(); 34 | table.append(template(c)); 35 | $('#delete-' + c.consumerGroupId + '-btn').click(function() { disposeConsumer(c.consumerGroupId); }); 36 | }); 37 | } 38 | 39 | function disposeConsumer(consumerId) { 40 | $.ajax({ 41 | type: "DELETE", 42 | url: App.contextPath + "/api/manager/consumers/" + consumerId, 43 | success: refreshConsumers 44 | }); 45 | } 46 | 47 | function refreshProducers() { 48 | $.get(App.contextPath + "/api/manager/producers", handleProducers); 49 | } 50 | 51 | function handleProducers(data) { 52 | console.log(data); 53 | var table = App.manager.$producerTableBody, 54 | template = App.manager.producerTemplate; 55 | table.html(''); 56 | _.each(data.content, function(p) { 57 | p.contextPath = App.contextPath; 58 | p.lastUsedTimeText = p.lastUsedTime < 0 ? 'never' : moment(p.lastUsedTime).fromNow(); 59 | table.append(template(p)); 60 | $('#delete-' + p.id + '-btn').click(function() { disposeProducer(p.id); }); 61 | }); 62 | } 63 | 64 | function disposeProducer(producerId) { 65 | $.ajax({ 66 | type: "DELETE", 67 | url: App.contextPath + "/api/manager/producers/" + producerId, 68 | success: refreshProducers 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/static/manager/producer-template.html: -------------------------------------------------------------------------------- 1 | 2 | <%= topic %> 3 | <%= serializerName %> 4 | <%= lastUsedTimeText %> 5 | <%= totalMessagesSent %> 6 | <%= totalErrors %> 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/static/mode-text.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BetterCloud/kadmin/9d709d3a90f1d010f005711630c7f8e3b922bb66/src/main/resources/static/mode-text.js -------------------------------------------------------------------------------- /src/main/resources/static/producer/main.html: -------------------------------------------------------------------------------- 1 | 18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | 51 |
52 |
53 | 54 | 57 |
58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 82 |
83 |
84 | -------------------------------------------------------------------------------- /src/main/resources/static/producer/main.js: -------------------------------------------------------------------------------- 1 | function initMain() { 2 | if (!(App.pageLoaded && App.mainTemplateLoaded && App.infoTemplateLoaded && App.errorTemplateLoaded)) { 3 | // not ready yet 4 | return; 5 | } 6 | App.producer = _.extend(App.producer, { 7 | $results: $('#sent-results-row'), 8 | $schemas: $('#schema'), 9 | $schemaVersions: $('#schema-version'), 10 | $aceMessage: ace.edit("ace-message"), 11 | $aceSchema: ace.edit("ace-schema"), 12 | $topicsSelect: $('#topic-dd') 13 | }); 14 | 15 | $('#send-message-btn').click(function(e) { 16 | e.preventDefault(); 17 | sendMessage(); 18 | }); 19 | $('#refresh-schemas-btn').click(function(e) { 20 | e.preventDefault(); 21 | refreshSchemas(); 22 | refreshTopics(); 23 | }); 24 | refreshSchemas(); 25 | App.producer.$schemas.change(handleNewSchema); 26 | App.producer.$schemaVersions.change(handleNewVersion); 27 | App.producer.$topicsSelect.change(function() { 28 | var val = App.producer.$topicsSelect.val(); 29 | // TODO: regex check 30 | // if (val.matches(/[\w-]+/)) { 31 | $('#topic').val(val); 32 | // } 33 | }); 34 | refreshTopics(); 35 | 36 | App.producer.$aceMessage.setTheme("ace/theme/chrome"); 37 | App.producer.$aceMessage.getSession().setMode("ace/mode/json"); 38 | App.producer.$aceSchema.setTheme("ace/theme/chrome"); 39 | App.producer.$aceSchema.getSession().setMode("ace/mode/json"); 40 | } 41 | 42 | 43 | function getKafkaHost() { 44 | var kafkaHost = $('#kafkahost').val(); 45 | return kafkaHost !== "" ? kafkaHost : undefined; 46 | } 47 | 48 | function refreshTopics() { 49 | $.get(App.contextPath + "/api/topics", {"kafka-url": getKafkaHost()}, handleNewTopics); 50 | } 51 | 52 | function handleNewTopics(data) { 53 | var $topicsSelect = App.producer.$topicsSelect; 54 | $topicsSelect.html(""); 55 | _.each(data, function(topicName) { 56 | $topicsSelect.append(""); 57 | }); 58 | } 59 | 60 | function sendMessage() { 61 | var req = buildRequest(); 62 | $.ajax({ 63 | type: "POST", 64 | url: App.contextPath + "/api/avro/publish?count=" + getCount(), 65 | headers: { 66 | "Content-Type": "application/json", 67 | Accept: "application/json;charset=UTF-8" 68 | }, 69 | data: JSON.stringify(req), 70 | success: handleResponse 71 | }); 72 | $('#send-message-btn').addClass("disabled"); 73 | } 74 | 75 | function buildRequest() { 76 | var req = { 77 | meta: { 78 | kafkaUrl: $('#kafkahost').val(), 79 | schemaRegistryUrl: $('#schemaurl').val(), 80 | schema: $('#schema').val(), 81 | rawSchema: App.producer.$aceSchema.getValue(), 82 | topic: $('#topic').val() 83 | }, 84 | rawMessage: App.producer.$aceMessage.getValue(), 85 | key: $('#message-key').val() 86 | }; 87 | if (req.meta.kafkaUrl === "") { 88 | req.meta.kafkaUrl = null; 89 | } 90 | if (req.meta.schemaRegistryUrl === "") { 91 | req.meta.schemaRegistryUrl = null; 92 | } 93 | return req; 94 | } 95 | 96 | function getCount() { 97 | var count = $('#count').val(); 98 | if (count !== '') { 99 | count = parseInt(count); 100 | if (count !== NaN && count > 0 && count < 10000) { 101 | return count; 102 | } 103 | } 104 | return 1; 105 | } 106 | 107 | function handleResponse(data) { 108 | var timeText = moment().format('LTS'), 109 | template = null; 110 | if (!!data && !!data.sent && data.sent && !!data.success && data.success) { 111 | // request was successful 112 | template = App.producer.sentInfoTemplate; 113 | } else { 114 | data = {}; 115 | template = App.producer.sentErrorTemplate; 116 | } 117 | data.timeText = timeText; 118 | App.producer.$results.html(template(data)); 119 | $('#send-message-btn').removeClass("disabled"); 120 | window.scrollTo(0, document.body.scrollHeight); 121 | } 122 | 123 | function refreshSchemas() { 124 | var url = App.contextPath + "/api/schemas", 125 | schemaUrl = $('#schemaurl').val(); 126 | if (schemaUrl !== '') { 127 | url += "?url=" + schemaUrl; 128 | } 129 | var $schemas = App.producer.$schemas; 130 | $.get(url) 131 | .done(function(data) { 132 | $schemas.html(''); 133 | _.each(data, function(schemaName) { 134 | $schemas.append(''); 135 | }); 136 | }) 137 | .fail(function(err) { 138 | $schemas.html(''); 139 | }); 140 | } 141 | 142 | function handleNewSchema() { 143 | var name = App.producer.$schemas.val(); 144 | if (name !== 'null') { 145 | $.get(App.contextPath + '/api/schemas/' + name, function(data) { 146 | var curr = data.currSchema, 147 | v = curr.version, 148 | rawSchema = JSON.stringify(JSON.parse(curr.schema), null, 2); 149 | 150 | App.producer.$schemaVersions.html(''); 151 | _.each(data.versions, function(ver) { 152 | App.producer.$schemaVersions.append(''); 153 | }); 154 | 155 | App.producer.$aceSchema.setValue(rawSchema); 156 | }); 157 | } 158 | } 159 | 160 | function handleNewVersion() { 161 | var name = App.producer.$schemas.val(), 162 | ver = App.producer.$schemaVersions.val(); 163 | if (!!name && name !== '' && !!ver && ver !== '') { 164 | $.get(App.contextPath + '/api/schemas/' + name + "/" + ver, function(data) { 165 | var rawSchema = JSON.stringify(JSON.parse(data.schema), null, 2); 166 | App.producer.$aceSchema.setValue(rawSchema); 167 | }); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/resources/static/producer/sent-error-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
There was an error
4 |
<%= timeText %>
5 |
6 |
-------------------------------------------------------------------------------- /src/main/resources/static/producer/sent-info-template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Sent
4 |
<%= timeText %>
5 |
    6 |
  • Success: <%= success %>
  • 7 |
  • Count: <%= count %>
  • 8 |
  • Duration: <%= duration %>ms
  • 9 |
  • Rate: <%= rate %> messages/second
  • 10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/main/resources/static/theme-chrome.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/chrome",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-chrome",t.cssText='.ace-chrome .ace_gutter {background: #ebebeb;color: #333;overflow : hidden;}.ace-chrome .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-chrome {background-color: #FFFFFF;color: black;}.ace-chrome .ace_cursor {color: black;}.ace-chrome .ace_invisible {color: rgb(191, 191, 191);}.ace-chrome .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-chrome .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-chrome .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-chrome .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-chrome .ace_fold {}.ace-chrome .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-chrome .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-chrome .ace_support.ace_type,.ace-chrome .ace_support.ace_class.ace-chrome .ace_support.ace_other {color: rgb(109, 121, 222);}.ace-chrome .ace_variable.ace_parameter {font-style:italic;color:#FD971F;}.ace-chrome .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-chrome .ace_comment {color: #236e24;}.ace-chrome .ace_comment.ace_doc {color: #236e24;}.ace-chrome .ace_comment.ace_doc.ace_tag {color: #236e24;}.ace-chrome .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-chrome .ace_variable {color: rgb(49, 132, 149);}.ace-chrome .ace_xml-pe {color: rgb(104, 104, 91);}.ace-chrome .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-chrome .ace_heading {color: rgb(12, 7, 255);}.ace-chrome .ace_list {color:rgb(185, 6, 144);}.ace-chrome .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-chrome .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-chrome .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-chrome .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-chrome .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-chrome .ace_gutter-active-line {background-color : #dcdcdc;}.ace-chrome .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-chrome .ace_storage,.ace-chrome .ace_keyword,.ace-chrome .ace_meta.ace_tag {color: rgb(147, 15, 128);}.ace-chrome .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-chrome .ace_string {color: #1A1AA6;}.ace-chrome .ace_entity.ace_other.ace_attribute-name {color: #994409;}.ace-chrome .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /src/main/resources/static/theme-kuroir.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/kuroir",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-kuroir",t.cssText=".ace-kuroir .ace_gutter {background: #e8e8e8;color: #333;}.ace-kuroir .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-kuroir {background-color: #E8E9E8;color: #363636;}.ace-kuroir .ace_cursor {color: #202020;}.ace-kuroir .ace_marker-layer .ace_selection {background: rgba(245, 170, 0, 0.57);}.ace-kuroir.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #E8E9E8;}.ace-kuroir .ace_marker-layer .ace_step {background: rgb(198, 219, 174);}.ace-kuroir .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(0, 0, 0, 0.29);}.ace-kuroir .ace_marker-layer .ace_active-line {background: rgba(203, 220, 47, 0.22);}.ace-kuroir .ace_gutter-active-line {background-color: rgba(203, 220, 47, 0.22);}.ace-kuroir .ace_marker-layer .ace_selected-word {border: 1px solid rgba(245, 170, 0, 0.57);}.ace-kuroir .ace_invisible {color: #BFBFBF}.ace-kuroir .ace_fold {border-color: #363636;}.ace-kuroir .ace_constant{color:#CD6839;}.ace-kuroir .ace_constant.ace_numeric{color:#9A5925;}.ace-kuroir .ace_support{color:#104E8B;}.ace-kuroir .ace_support.ace_function{color:#005273;}.ace-kuroir .ace_support.ace_constant{color:#CF6A4C;}.ace-kuroir .ace_storage{color:#A52A2A;}.ace-kuroir .ace_invalid.ace_illegal{color:#FD1224;background-color:rgba(255, 6, 0, 0.15);}.ace-kuroir .ace_invalid.ace_deprecated{text-decoration:underline;font-style:italic;color:#FD1732;background-color:#E8E9E8;}.ace-kuroir .ace_string{color:#639300;}.ace-kuroir .ace_string.ace_regexp{color:#417E00;background-color:#C9D4BE;}.ace-kuroir .ace_comment{color:rgba(148, 148, 148, 0.91);background-color:rgba(220, 220, 220, 0.56);}.ace-kuroir .ace_variable{color:#009ACD;}.ace-kuroir .ace_meta.ace_tag{color:#005273;}.ace-kuroir .ace_markup.ace_heading{color:#B8012D;background-color:rgba(191, 97, 51, 0.051);}.ace-kuroir .ace_markup.ace_list{color:#8F5B26;}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /src/main/resources/static/theme-monokai.js: -------------------------------------------------------------------------------- 1 | ace.define("ace/theme/monokai",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-monokai",t.cssText=".ace-monokai .ace_gutter {background: #2F3129;color: #8F908A}.ace-monokai .ace_print-margin {width: 1px;background: #555651}.ace-monokai {background-color: #272822;color: #F8F8F2}.ace-monokai .ace_cursor {color: #F8F8F0}.ace-monokai .ace_marker-layer .ace_selection {background: #49483E}.ace-monokai.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #272822;}.ace-monokai .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-monokai .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #49483E}.ace-monokai .ace_marker-layer .ace_active-line {background: #202020}.ace-monokai .ace_gutter-active-line {background-color: #272727}.ace-monokai .ace_marker-layer .ace_selected-word {border: 1px solid #49483E}.ace-monokai .ace_invisible {color: #52524d}.ace-monokai .ace_entity.ace_name.ace_tag,.ace-monokai .ace_keyword,.ace-monokai .ace_meta.ace_tag,.ace-monokai .ace_storage {color: #F92672}.ace-monokai .ace_punctuation,.ace-monokai .ace_punctuation.ace_tag {color: #fff}.ace-monokai .ace_constant.ace_character,.ace-monokai .ace_constant.ace_language,.ace-monokai .ace_constant.ace_numeric,.ace-monokai .ace_constant.ace_other {color: #AE81FF}.ace-monokai .ace_invalid {color: #F8F8F0;background-color: #F92672}.ace-monokai .ace_invalid.ace_deprecated {color: #F8F8F0;background-color: #AE81FF}.ace-monokai .ace_support.ace_constant,.ace-monokai .ace_support.ace_function {color: #66D9EF}.ace-monokai .ace_fold {background-color: #A6E22E;border-color: #F8F8F2}.ace-monokai .ace_storage.ace_type,.ace-monokai .ace_support.ace_class,.ace-monokai .ace_support.ace_type {font-style: italic;color: #66D9EF}.ace-monokai .ace_entity.ace_name.ace_function,.ace-monokai .ace_entity.ace_other,.ace-monokai .ace_entity.ace_other.ace_attribute-name,.ace-monokai .ace_variable {color: #A6E22E}.ace-monokai .ace_variable.ace_parameter {font-style: italic;color: #FD971F}.ace-monokai .ace_string {color: #E6DB74}.ace-monokai .ace_comment {color: #75715E}.ace-monokai .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}) -------------------------------------------------------------------------------- /src/main/resources/templates/basicproducer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kadmin @ BetterCloud 5 | 6 | 7 | 39 | 40 | 41 |
42 |
43 | 50 |
51 |
52 | 56 |
57 | 58 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/main/resources/templates/consumer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kadmin @ BetterCloud 5 | 6 | 7 | 49 | 50 | 51 |
52 |
53 | 60 |
61 |
62 | 66 |
67 | 68 | 69 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kadmin @ BetterCloud 5 | 6 | 7 | 30 | 31 | 32 |
33 |
34 | 41 |
42 |
43 |
44 | 45 |

kadmin by BetterCloud

46 |
47 |
48 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/templates/manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kadmin @ BetterCloud 5 | 6 | 7 | 33 | 34 | 35 |
36 |
37 | 44 |
45 |
46 | 50 |
51 | 52 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/main/resources/templates/producer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kadmin @ BetterCloud 5 | 6 | 7 | 39 | 40 | 41 |
42 |
43 | 50 |
51 |
52 | 56 |
57 | 58 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/test/java/com/bettercloud/kadmin/KadminApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.bettercloud.kadmin; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = Application.class) 11 | @WebAppConfiguration 12 | public class KadminApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/test/avro/CanonicalPayload.01.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "tenantId": "bd303621-c0d5-4625-8235-afb52488f277", 4 | "providerId": "7c7cb499-00ef-11e6-a8bb-09d7f4163a23", 5 | "operation": "UpdatedUser", 6 | "correlationId": "06233055-a797-4030-b899-43854afbc8f0", 7 | "timestamp": 1468869220294 8 | }, 9 | "data": { 10 | "com.bettercloud.directory.models.canonical.UpdatedEventData": { 11 | "changedData": { 12 | "com.bettercloud.directory.models.canonical.ChangedData": { 13 | "oldValues": { 14 | "map": { 15 | "orgUnitRelationalId": null 16 | } 17 | }, 18 | "newValues": { 19 | "map": { 20 | "orgUnitRelationalId": { 21 | "string": "/test OU" 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | "oldData": { 28 | "com.bettercloud.directory.models.canonical.CanonicalUser": { 29 | "userId": { 30 | "string": "3dc8b322-4542-11e6-9b9b-811164c15f64" 31 | }, 32 | "externalUserId": { 33 | "string": "108285540337714935123" 34 | }, 35 | "betterCloudUserId": { 36 | "string": "3dc8b323-4542-11e6-9b9b-7b556c73fd81" 37 | }, 38 | "orgUnit": null, 39 | "name": { 40 | "com.bettercloud.directory.models.canonical.CanonicalUserName": { 41 | "displayName": { 42 | "string": "Robert Plant" 43 | }, 44 | "familyName": { 45 | "string": "Plant" 46 | }, 47 | "givenName": { 48 | "string": "Robert" 49 | }, 50 | "middleName": null, 51 | "nickName": null, 52 | "honorificPrefix": null, 53 | "honorificSuffix": null 54 | } 55 | }, 56 | "addresses": null, 57 | "department": null, 58 | "title": null, 59 | "manager": null, 60 | "phoneNumbers": null, 61 | "active": { 62 | "boolean": true 63 | }, 64 | "language": null, 65 | "locale": null, 66 | "timezone": null, 67 | "userType": null, 68 | "emails": { 69 | "array": [{ 70 | "value": { 71 | "string": "robert.plant@autotestalias.cloud8labs.com" 72 | }, 73 | "type": null, 74 | "primary": { 75 | "boolean": false 76 | } 77 | }, { 78 | "value": { 79 | "string": "robert.plant@autotest.cloud8labs.com" 80 | }, 81 | "type": null, 82 | "primary": { 83 | "boolean": true 84 | } 85 | }] 86 | }, 87 | "admin": { 88 | "boolean": false 89 | }, 90 | "lastLogin": null, 91 | "meta": { 92 | "com.bettercloud.directory.models.canonical.CanonicalMeta": { 93 | "lastModified": null, 94 | "created": { 95 | "string": "2016-06-22T20:58:45Z" 96 | } 97 | } 98 | }, 99 | "raw": { 100 | "string": "{\"agreedToTerms\":true,\"changePasswordAtNextLogin\":true,\"creationTime\":\"2016-06-22T20:58:45.000Z\",\"customerId\":\"C025v3b7k\",\"emails\":[{\"address\":\"robert.plant@autotestalias.cloud8labs.com\"},{\"address\":\"robert.plant@autotest.cloud8labs.com\",\"primary\":true}],\"etag\":\"\\\"XGyyUMiAmCo7o31SWwFDDNla4RE/9J7W_5kOcDppzddRRXwUVR7qEQY\\\"\",\"id\":\"108285540337714935123\",\"includeInGlobalAddressList\":true,\"ipWhitelisted\":false,\"isAdmin\":false,\"isDelegatedAdmin\":false,\"isMailboxSetup\":true,\"kind\":\"admin#directory#user\",\"lastLoginTime\":\"1970-01-01T00:00:00.000Z\",\"name\":{\"familyName\":\"Plant\",\"fullName\":\"Robert Plant\",\"givenName\":\"Robert\"},\"nonEditableAliases\":[\"robert.plant@autotest.cloud8labs.com.test-google-a.com\",\"robert.plant@autotestalias.cloud8labs.com\"],\"orgUnitPath\":\"/test OU\",\"primaryEmail\":\"robert.plant@autotest.cloud8labs.com\",\"suspended\":false}" 101 | } 102 | } 103 | }, 104 | "newData": { 105 | "com.bettercloud.directory.models.canonical.CanonicalUser": { 106 | "userId": { 107 | "string": "3dc8b322-4542-11e6-9b9b-811164c15f64" 108 | }, 109 | "externalUserId": { 110 | "string": "108285540337714935123" 111 | }, 112 | "betterCloudUserId": { 113 | "string": "3dc8b323-4542-11e6-9b9b-7b556c73fd81" 114 | }, 115 | "orgUnit": { 116 | "com.bettercloud.directory.models.canonical.CanonicalOrgUnit": { 117 | "orgUnitId": null, 118 | "externalOrgUnitId": "/test OU", 119 | "name": null, 120 | "description": null, 121 | "parentOrgUnit": null, 122 | "memberCount": null, 123 | "inheritsSettings": null, 124 | "meta": null, 125 | "raw": null, 126 | "path": null 127 | } 128 | }, 129 | "name": { 130 | "com.bettercloud.directory.models.canonical.CanonicalUserName": { 131 | "displayName": { 132 | "string": "Robert Plant" 133 | }, 134 | "familyName": { 135 | "string": "Plant" 136 | }, 137 | "givenName": { 138 | "string": "Robert" 139 | }, 140 | "middleName": null, 141 | "nickName": null, 142 | "honorificPrefix": null, 143 | "honorificSuffix": null 144 | } 145 | }, 146 | "addresses": null, 147 | "department": null, 148 | "title": null, 149 | "manager": null, 150 | "phoneNumbers": null, 151 | "active": { 152 | "boolean": true 153 | }, 154 | "language": null, 155 | "locale": null, 156 | "timezone": null, 157 | "userType": null, 158 | "emails": { 159 | "array": [{ 160 | "value": { 161 | "string": "robert.plant@autotestalias.cloud8labs.com" 162 | }, 163 | "type": null, 164 | "primary": { 165 | "boolean": false 166 | } 167 | }, { 168 | "value": { 169 | "string": "robert.plant@autotest.cloud8labs.com" 170 | }, 171 | "type": null, 172 | "primary": { 173 | "boolean": true 174 | } 175 | }] 176 | }, 177 | "admin": { 178 | "boolean": false 179 | }, 180 | "lastLogin": null, 181 | "meta": { 182 | "com.bettercloud.directory.models.canonical.CanonicalMeta": { 183 | "lastModified": null, 184 | "created": { 185 | "string": "2016-06-22T20:58:45Z" 186 | } 187 | } 188 | }, 189 | "raw": { 190 | "string": "{\"agreedToTerms\":true,\"changePasswordAtNextLogin\":true,\"creationTime\":\"2016-06-22T20:58:45.000Z\",\"customerId\":\"C025v3b7k\",\"emails\":[{\"address\":\"robert.plant@autotestalias.cloud8labs.com\"},{\"address\":\"robert.plant@autotest.cloud8labs.com\",\"primary\":true}],\"etag\":\"\\\"XGyyUMiAmCo7o31SWwFDDNla4RE/nspdO-LT5tBniT3E1njNdLVh9cU\\\"\",\"id\":\"108285540337714935123\",\"includeInGlobalAddressList\":true,\"ipWhitelisted\":false,\"isAdmin\":false,\"isDelegatedAdmin\":false,\"isMailboxSetup\":true,\"kind\":\"admin#directory#user\",\"lastLoginTime\":\"1970-01-01T00:00:00.000Z\",\"name\":{\"familyName\":\"Plant\",\"fullName\":\"Robert Plant\",\"givenName\":\"Robert\"},\"nonEditableAliases\":[\"robert.plant@autotest.cloud8labs.com.test-google-a.com\",\"robert.plant@autotestalias.cloud8labs.com\"],\"orgUnitPath\":\"/test OU\",\"primaryEmail\":\"robert.plant@autotest.cloud8labs.com\",\"suspended\":false}" 191 | } 192 | } 193 | } 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /src/test/resources/test/avro/CanonicalPayload.01.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "tenantId": "bd303621-c0d5-4625-8235-afb52488f277", 4 | "providerId": "7c7cb499-00ef-11e6-a8bb-09d7f4163a23", 5 | "operation": "UpdatedUser", 6 | "correlationId": "06233055-a797-4030-b899-43854afbc8f0", 7 | "timestamp": 1468869220294 8 | }, 9 | "data": { 10 | "changedData": { 11 | "oldValues": { 12 | "orgUnitRelationalId": null 13 | }, 14 | "newValues": { 15 | "orgUnitRelationalId": "/test OU" 16 | } 17 | }, 18 | "oldData": { 19 | "userId": "3dc8b322-4542-11e6-9b9b-811164c15f64", 20 | "externalUserId": "108285540337714935123", 21 | "betterCloudUserId": "3dc8b323-4542-11e6-9b9b-7b556c73fd81", 22 | "orgUnit": null, 23 | "name": { 24 | "displayName": "Robert Plant", 25 | "familyName": "Plant", 26 | "givenName": "Robert", 27 | "middleName": null, 28 | "nickName": null, 29 | "honorificPrefix": null, 30 | "honorificSuffix": null 31 | }, 32 | "addresses": null, 33 | "department": null, 34 | "title": null, 35 | "manager": null, 36 | "phoneNumbers": null, 37 | "active": true, 38 | "language": null, 39 | "locale": null, 40 | "timezone": null, 41 | "userType": null, 42 | "emails": [ 43 | { 44 | "value": "robert.plant@autotestalias.cloud8labs.com", 45 | "type": null, 46 | "primary": false 47 | }, 48 | { 49 | "value": "robert.plant@autotest.cloud8labs.com", 50 | "type": null, 51 | "primary": true 52 | } 53 | ], 54 | "admin": false, 55 | "lastLogin": null, 56 | "meta": { 57 | "lastModified": null, 58 | "created": "2016-06-22T20:58:45Z" 59 | }, 60 | "raw": "{\"agreedToTerms\":true,\"changePasswordAtNextLogin\":true,\"creationTime\":\"2016-06-22T20:58:45.000Z\",\"customerId\":\"C025v3b7k\",\"emails\":[{\"address\":\"robert.plant@autotestalias.cloud8labs.com\"},{\"address\":\"robert.plant@autotest.cloud8labs.com\",\"primary\":true}],\"etag\":\"\\\"XGyyUMiAmCo7o31SWwFDDNla4RE/9J7W_5kOcDppzddRRXwUVR7qEQY\\\"\",\"id\":\"108285540337714935123\",\"includeInGlobalAddressList\":true,\"ipWhitelisted\":false,\"isAdmin\":false,\"isDelegatedAdmin\":false,\"isMailboxSetup\":true,\"kind\":\"admin#directory#user\",\"lastLoginTime\":\"1970-01-01T00:00:00.000Z\",\"name\":{\"familyName\":\"Plant\",\"fullName\":\"Robert Plant\",\"givenName\":\"Robert\"},\"nonEditableAliases\":[\"robert.plant@autotest.cloud8labs.com.test-google-a.com\",\"robert.plant@autotestalias.cloud8labs.com\"],\"orgUnitPath\":\"/test OU\",\"primaryEmail\":\"robert.plant@autotest.cloud8labs.com\",\"suspended\":false}" 61 | }, 62 | "newData": { 63 | "userId": "3dc8b322-4542-11e6-9b9b-811164c15f64", 64 | "externalUserId": "108285540337714935123", 65 | "betterCloudUserId": "3dc8b323-4542-11e6-9b9b-7b556c73fd81", 66 | "orgUnit": { 67 | "orgUnitId": null, 68 | "externalOrgUnitId": "/test OU", 69 | "name": null, 70 | "description": null, 71 | "parentOrgUnit": null, 72 | "memberCount": null, 73 | "inheritsSettings": null, 74 | "meta": null, 75 | "raw": null, 76 | "path": null 77 | }, 78 | "name": { 79 | "displayName": "Robert Plant", 80 | "familyName": "Plant", 81 | "givenName": "Robert", 82 | "middleName": null, 83 | "nickName": null, 84 | "honorificPrefix": null, 85 | "honorificSuffix": null 86 | }, 87 | "addresses": null, 88 | "department": null, 89 | "title": null, 90 | "manager": null, 91 | "phoneNumbers": null, 92 | "active": true, 93 | "language": null, 94 | "locale": null, 95 | "timezone": null, 96 | "userType": null, 97 | "emails": [ 98 | { 99 | "value": "robert.plant@autotestalias.cloud8labs.com", 100 | "type": null, 101 | "primary": false 102 | }, 103 | { 104 | "value": "robert.plant@autotest.cloud8labs.com", 105 | "type": null, 106 | "primary": true 107 | } 108 | ], 109 | "admin": false, 110 | "lastLogin": null, 111 | "meta": { 112 | "lastModified": null, 113 | "created": "2016-06-22T20:58:45Z" 114 | }, 115 | "raw": "{\"agreedToTerms\":true,\"changePasswordAtNextLogin\":true,\"creationTime\":\"2016-06-22T20:58:45.000Z\",\"customerId\":\"C025v3b7k\",\"emails\":[{\"address\":\"robert.plant@autotestalias.cloud8labs.com\"},{\"address\":\"robert.plant@autotest.cloud8labs.com\",\"primary\":true}],\"etag\":\"\\\"XGyyUMiAmCo7o31SWwFDDNla4RE/nspdO-LT5tBniT3E1njNdLVh9cU\\\"\",\"id\":\"108285540337714935123\",\"includeInGlobalAddressList\":true,\"ipWhitelisted\":false,\"isAdmin\":false,\"isDelegatedAdmin\":false,\"isMailboxSetup\":true,\"kind\":\"admin#directory#user\",\"lastLoginTime\":\"1970-01-01T00:00:00.000Z\",\"name\":{\"familyName\":\"Plant\",\"fullName\":\"Robert Plant\",\"givenName\":\"Robert\"},\"nonEditableAliases\":[\"robert.plant@autotest.cloud8labs.com.test-google-a.com\",\"robert.plant@autotestalias.cloud8labs.com\"],\"orgUnitPath\":\"/test OU\",\"primaryEmail\":\"robert.plant@autotest.cloud8labs.com\",\"suspended\":false}" 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/test/resources/test/avro/EventCall.01.expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "senderId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 4 | "domainId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 5 | "tenantId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 6 | "providerId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 7 | "correlationId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 8 | "externalCorrelationId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 9 | "receivedDate": {"long": 1467378058627}, 10 | "eventId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 11 | "userId": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"}, 12 | "eventMeta": { 13 | "array": [ 14 | { 15 | "key": "workflow", 16 | "value": {"boolean": true} 17 | }, { 18 | "key": "workflow_directory_integration", 19 | "value": {"boolean": false} 20 | } 21 | ] 22 | } 23 | }, 24 | "eventId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 25 | "eventParams": null, 26 | "values": { 27 | "array": [ 28 | { 29 | "key": "userId", 30 | "value": {"string": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa"} 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/resources/test/avro/EventCall.01.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "senderId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 4 | "domainId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 5 | "tenantId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 6 | "providerId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 7 | "correlationId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 8 | "externalCorrelationId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 9 | "receivedDate": 1467378058627, 10 | "eventId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 11 | "userId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 12 | "eventMeta": [{ 13 | "key": "workflow", 14 | "value": true 15 | }, { 16 | "key": "workflow_directory_integration", 17 | "value": false 18 | }] 19 | }, 20 | "eventId": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa", 21 | "eventParams": null, 22 | "values": [{ 23 | "key": "userId", 24 | "value": "f7d7e7c5-1a1f-4d2a-9ae0-ce07e83907fa" 25 | }] 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/test/avro/EventCall.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "record", 3 | "name": "EventCall", 4 | "namespace": "com.bettercloud.avro.workflow", 5 | "fields": [ 6 | { 7 | "name": "header", 8 | "type": { 9 | "type": "record", 10 | "name": "Header", 11 | "namespace": "com.bettercloud.avro", 12 | "fields": [ 13 | { 14 | "name": "senderId", 15 | "type": [ 16 | { 17 | "type": "string", 18 | "avro.java.string": "String" 19 | }, 20 | "null" 21 | ], 22 | "default": "null" 23 | }, 24 | { 25 | "name": "domainId", 26 | "type": [ 27 | { 28 | "type": "string", 29 | "avro.java.string": "String" 30 | }, 31 | "null" 32 | ] 33 | }, 34 | { 35 | "name": "tenantId", 36 | "type": { 37 | "type": "string", 38 | "avro.java.string": "String" 39 | } 40 | }, 41 | { 42 | "name": "providerId", 43 | "type": [ 44 | { 45 | "type": "string", 46 | "avro.java.string": "String" 47 | }, 48 | "null" 49 | ] 50 | }, 51 | { 52 | "name": "correlationId", 53 | "type": [ 54 | { 55 | "type": "string", 56 | "avro.java.string": "String" 57 | }, 58 | "null" 59 | ] 60 | }, 61 | { 62 | "name": "externalCorrelationId", 63 | "type": [ 64 | { 65 | "type": "string", 66 | "avro.java.string": "String" 67 | }, 68 | "null" 69 | ] 70 | }, 71 | { 72 | "name": "receivedDate", 73 | "type": [ 74 | "long", 75 | "null" 76 | ] 77 | }, 78 | { 79 | "name": "eventId", 80 | "type": [ 81 | { 82 | "type": "string", 83 | "avro.java.string": "String" 84 | }, 85 | "null" 86 | ] 87 | }, 88 | { 89 | "name": "userId", 90 | "type": [ 91 | { 92 | "type": "string", 93 | "avro.java.string": "String" 94 | }, 95 | "null" 96 | ] 97 | }, 98 | { 99 | "name": "eventMeta", 100 | "type": [ 101 | "null", 102 | { 103 | "type": "array", 104 | "items": { 105 | "type": "record", 106 | "name": "MapKeyValueEntry", 107 | "namespace": "com.bettercloud.avro.workflow", 108 | "fields": [ 109 | { 110 | "name": "key", 111 | "type": { 112 | "type": "string", 113 | "avro.java.string": "String" 114 | } 115 | }, 116 | { 117 | "name": "value", 118 | "type": [ 119 | "boolean", 120 | "int", 121 | "long", 122 | "float", 123 | "double", 124 | "bytes", 125 | { 126 | "type": "string", 127 | "avro.java.string": "String" 128 | }, 129 | "null", 130 | { 131 | "type": "array", 132 | "items": [ 133 | "MapKeyValueEntry", 134 | "boolean", 135 | "int", 136 | "long", 137 | "float", 138 | "double", 139 | "bytes", 140 | { 141 | "type": "string", 142 | "avro.java.string": "String" 143 | }, 144 | "null", 145 | { 146 | "type": "array", 147 | "items": "MapKeyValueEntry" 148 | } 149 | ] 150 | } 151 | ] 152 | } 153 | ] 154 | } 155 | } 156 | ], 157 | "default": null 158 | } 159 | ] 160 | } 161 | }, 162 | { 163 | "name": "eventId", 164 | "type": { 165 | "type": "string", 166 | "avro.java.string": "String" 167 | } 168 | }, 169 | { 170 | "name": "eventParams", 171 | "type": [ 172 | "null", 173 | { 174 | "type": "map", 175 | "values": [ 176 | "boolean", 177 | "int", 178 | "long", 179 | "float", 180 | "double", 181 | "bytes", 182 | { 183 | "type": "string", 184 | "avro.java.string": "String" 185 | }, 186 | "null", 187 | { 188 | "type": "map", 189 | "values": [ 190 | { 191 | "type": "string", 192 | "avro.java.string": "String" 193 | }, 194 | "null" 195 | ], 196 | "avro.java.string": "String" 197 | } 198 | ], 199 | "avro.java.string": "String" 200 | } 201 | ], 202 | "default": null 203 | }, 204 | { 205 | "name": "values", 206 | "type": [ 207 | "null", 208 | { 209 | "type": "array", 210 | "items": "MapKeyValueEntry" 211 | } 212 | ], 213 | "default": null 214 | } 215 | ] 216 | } -------------------------------------------------------------------------------- /updateClient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp -rf src/main/resources/static/ build/resources/main/static/ 4 | --------------------------------------------------------------------------------