From 683903846aab15c664431b86b91fd7b25de222c5 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Mon, 6 Jan 2025 11:12:33 -0500 Subject: [PATCH 01/13] Initial setup for data contracts with terraform module. --- data-contracts-with-terraform/.gitignore | 23 ++ data-contracts-with-terraform/README.md | 19 ++ data-contracts-with-terraform/build.gradle | 24 ++ .../gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + data-contracts-with-terraform/gradlew | 249 ++++++++++++++++++ data-contracts-with-terraform/gradlew.bat | 92 +++++++ data-contracts-with-terraform/settings.gradle | 3 + .../src/main/avro/.gitkeep | 0 .../src/main/protobuf/.gitkeep | 0 .../src/main/resources/.gitkeep | 0 .../src/test/java/.gitkeep | 0 .../src/test/resources/.gitkeep | 0 .../terraform/.gitkeep | 0 settings.gradle | 1 + 16 files changed, 419 insertions(+) create mode 100644 data-contracts-with-terraform/.gitignore create mode 100644 data-contracts-with-terraform/README.md create mode 100644 data-contracts-with-terraform/build.gradle create mode 100644 data-contracts-with-terraform/gradle.properties create mode 100644 data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar create mode 100644 data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties create mode 100755 data-contracts-with-terraform/gradlew create mode 100644 data-contracts-with-terraform/gradlew.bat create mode 100644 data-contracts-with-terraform/settings.gradle create mode 100644 data-contracts-with-terraform/src/main/avro/.gitkeep create mode 100644 data-contracts-with-terraform/src/main/protobuf/.gitkeep create mode 100644 data-contracts-with-terraform/src/main/resources/.gitkeep create mode 100644 data-contracts-with-terraform/src/test/java/.gitkeep create mode 100644 data-contracts-with-terraform/src/test/resources/.gitkeep create mode 100644 data-contracts-with-terraform/terraform/.gitkeep diff --git a/data-contracts-with-terraform/.gitignore b/data-contracts-with-terraform/.gitignore new file mode 100644 index 00000000..9a7afb8c --- /dev/null +++ b/data-contracts-with-terraform/.gitignore @@ -0,0 +1,23 @@ +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + diff --git a/data-contracts-with-terraform/README.md b/data-contracts-with-terraform/README.md new file mode 100644 index 00000000..1add1b64 --- /dev/null +++ b/data-contracts-with-terraform/README.md @@ -0,0 +1,19 @@ + + + +# Manage Data Contracts with Terraform + +Data contracts consists not only of the schemas to define the data, but also rulesets allowing for more fine-grained validations, +controls, and discovery.In this tutorial, we'll evolve a couple of schemas and add data quality and migration rules.We'll also +explore tagging those schemas, fields, and rules for data discovery. + +## Setup + +## Running the Example + +### Prerequisites + +### Executing Terraform + +### Using Schemas + diff --git a/data-contracts-with-terraform/build.gradle b/data-contracts-with-terraform/build.gradle new file mode 100644 index 00000000..208ad718 --- /dev/null +++ b/data-contracts-with-terraform/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } +} + +plugins { + id 'java' + id 'idea' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +version = "0.0.1" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation project(path: ':common', configuration: 'testArtifacts') +} diff --git a/data-contracts-with-terraform/gradle.properties b/data-contracts-with-terraform/gradle.properties new file mode 100644 index 00000000..ae0a05bb --- /dev/null +++ b/data-contracts-with-terraform/gradle.properties @@ -0,0 +1 @@ +confluentVersion=7.7.0 \ No newline at end of file diff --git a/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar b/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties b/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..1af9e093 --- /dev/null +++ b/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/data-contracts-with-terraform/gradlew b/data-contracts-with-terraform/gradlew new file mode 100755 index 00000000..1aa94a42 --- /dev/null +++ b/data-contracts-with-terraform/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/data-contracts-with-terraform/gradlew.bat b/data-contracts-with-terraform/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/data-contracts-with-terraform/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/data-contracts-with-terraform/settings.gradle b/data-contracts-with-terraform/settings.gradle new file mode 100644 index 00000000..c0a46c4e --- /dev/null +++ b/data-contracts-with-terraform/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name="data-contracts-terraform" +include ':common' +project(':common').projectDir = file('../../common') \ No newline at end of file diff --git a/data-contracts-with-terraform/src/main/avro/.gitkeep b/data-contracts-with-terraform/src/main/avro/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data-contracts-with-terraform/src/main/protobuf/.gitkeep b/data-contracts-with-terraform/src/main/protobuf/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data-contracts-with-terraform/src/main/resources/.gitkeep b/data-contracts-with-terraform/src/main/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data-contracts-with-terraform/src/test/java/.gitkeep b/data-contracts-with-terraform/src/test/java/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data-contracts-with-terraform/src/test/resources/.gitkeep b/data-contracts-with-terraform/src/test/resources/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/data-contracts-with-terraform/terraform/.gitkeep b/data-contracts-with-terraform/terraform/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/settings.gradle b/settings.gradle index 94e04309..8be4a724 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include 'confluent-parallel-consumer-application:kafka' include 'common' include 'creating-first-apache-kafka-streams-application:kstreams' include 'cumulating-windows:flinksql' +include 'data-contracts-with-terraform' include 'deduplication:flinksql' include 'deduplication-windowed:flinksql' include 'deduplication-windowed:kstreams' From 791d8e72da832c2e4aef31c5a11b2d4e33dfc702 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Mon, 6 Jan 2025 13:00:31 -0500 Subject: [PATCH 02/13] terraform setup with v1 of schema. This includes a major_version compatibility group. --- data-contracts-with-terraform/cc/.gitignore | 6 + data-contracts-with-terraform/cc/kafka.tf | 127 ++++++++++++++++++ data-contracts-with-terraform/cc/main.tf | 28 ++++ .../cc/membership.tf | 66 +++++++++ data-contracts-with-terraform/cc/outputs.tf | 55 ++++++++ data-contracts-with-terraform/cc/variables.tf | 27 ++++ .../src/main/avro/membership_v1.avsc | 10 ++ .../src/main/avro/membership_v2.avsc | 20 +++ .../terraform/.gitkeep | 0 9 files changed, 339 insertions(+) create mode 100644 data-contracts-with-terraform/cc/.gitignore create mode 100644 data-contracts-with-terraform/cc/kafka.tf create mode 100644 data-contracts-with-terraform/cc/main.tf create mode 100644 data-contracts-with-terraform/cc/membership.tf create mode 100644 data-contracts-with-terraform/cc/outputs.tf create mode 100644 data-contracts-with-terraform/cc/variables.tf create mode 100644 data-contracts-with-terraform/src/main/avro/membership_v1.avsc create mode 100644 data-contracts-with-terraform/src/main/avro/membership_v2.avsc delete mode 100644 data-contracts-with-terraform/terraform/.gitkeep diff --git a/data-contracts-with-terraform/cc/.gitignore b/data-contracts-with-terraform/cc/.gitignore new file mode 100644 index 00000000..afd278b7 --- /dev/null +++ b/data-contracts-with-terraform/cc/.gitignore @@ -0,0 +1,6 @@ +.terraform.lock.hcl +.terraform/ +.tfplan +tfplan +terraform.tfstate +terraform.tfstate.backup \ No newline at end of file diff --git a/data-contracts-with-terraform/cc/kafka.tf b/data-contracts-with-terraform/cc/kafka.tf new file mode 100644 index 00000000..5384a8f8 --- /dev/null +++ b/data-contracts-with-terraform/cc/kafka.tf @@ -0,0 +1,127 @@ +# Update the config to use a cloud provider and region of your choice. +# https://registry.terraform.io/providers/confluentinc/confluent/latest/docs/resources/confluent_kafka_cluster +resource "confluent_kafka_cluster" "kafka_cluster" { + display_name = var.cc_cluster_name + availability = "SINGLE_ZONE" + cloud = var.cloud_provider + region = var.cloud_region + basic {} + environment { + id = confluent_environment.cc_env.id + } + + depends_on = [confluent_environment.cc_env] +} + +data "confluent_schema_registry_cluster" "advanced" { + environment { + id = confluent_environment.cc_env.id + } + depends_on = [confluent_kafka_cluster.kafka_cluster] +} + +// 'app-manager' service account is required in this configuration to create 'purchase' topic and grant ACLs +// to 'app-producer' and 'app-consumer' service accounts. +resource "confluent_service_account" "app-manager" { + display_name = "${var.cc_cluster_name}-app-manager" + description = "Service account to manage Kafka cluster" +} + +resource "confluent_role_binding" "app-manager-kafka-cluster-admin" { + principal = "User:${confluent_service_account.app-manager.id}" + role_name = "CloudClusterAdmin" + crn_pattern = confluent_kafka_cluster.kafka_cluster.rbac_crn +} + +resource "confluent_api_key" "app-manager-kafka-api-key" { + display_name = "app-manager-kafka-api-key" + description = "Kafka API Key that is owned by 'app-manager' service account" + owner { + id = confluent_service_account.app-manager.id + api_version = confluent_service_account.app-manager.api_version + kind = confluent_service_account.app-manager.kind + } + + managed_resource { + id = confluent_kafka_cluster.kafka_cluster.id + api_version = confluent_kafka_cluster.kafka_cluster.api_version + kind = confluent_kafka_cluster.kafka_cluster.kind + + environment { + id = confluent_environment.cc_env.id + } + } + + # The goal is to ensure that confluent_role_binding.app-manager-kafka-cluster-admin is created before + # confluent_api_key.app-manager-kafka-api-key is used to create instances of + # confluent_kafka_topic, confluent_kafka_acl resources. + + # 'depends_on' meta-argument is specified in confluent_api_key.app-manager-kafka-api-key to avoid having + # multiple copies of this definition in the configuration which would happen if we specify it in + # confluent_kafka_topic, confluent_kafka_acl resources instead. + depends_on = [ + confluent_role_binding.app-manager-kafka-cluster-admin + ] +} + +resource "confluent_service_account" "env-manager" { + display_name = "${var.cc_cluster_name}-env-manager" + description = "Service account to manage 'Staging' environment" +} + +resource "confluent_role_binding" "env-manager-environment-admin" { + principal = "User:${confluent_service_account.env-manager.id}" + role_name = "EnvironmentAdmin" + crn_pattern = confluent_environment.cc_env.resource_name +} + +resource "confluent_api_key" "env-manager-schema-registry-api-key" { + display_name = "env-manager-schema-registry-api-key" + description = "Schema Registry API Key that is owned by 'env-manager' service account" + owner { + id = confluent_service_account.env-manager.id + api_version = confluent_service_account.env-manager.api_version + kind = confluent_service_account.env-manager.kind + } + + managed_resource { + id = data.confluent_schema_registry_cluster.advanced.id + api_version = data.confluent_schema_registry_cluster.advanced.api_version + kind = data.confluent_schema_registry_cluster.advanced.kind + + environment { + id = confluent_environment.cc_env.id + } + } + + # The goal is to ensure that confluent_role_binding.env-manager-environment-admin is created before + # confluent_api_key.env-manager-schema-registry-api-key is used to create instances of + # confluent_schema resources. + + # 'depends_on' meta-argument is specified in confluent_api_key.env-manager-schema-registry-api-key to avoid having + # multiple copies of this definition in the configuration which would happen if we specify it in + # confluent_schema resources instead. + depends_on = [ + confluent_role_binding.env-manager-environment-admin, + data.confluent_schema_registry_cluster.advanced + ] +} + +resource "confluent_schema_registry_cluster_config" "schema_registry_cluster_config" { + schema_registry_cluster { + id = data.confluent_schema_registry_cluster.advanced.id + } + rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint + compatibility_level = "BACKWARD" + credentials { + key = confluent_api_key.env-manager-schema-registry-api-key.id + secret = confluent_api_key.env-manager-schema-registry-api-key.secret + } + + depends_on = [data.confluent_schema_registry_cluster.advanced, + confluent_api_key.env-manager-schema-registry-api-key] + + lifecycle { + prevent_destroy = false + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/cc/main.tf b/data-contracts-with-terraform/cc/main.tf new file mode 100644 index 00000000..9ef84610 --- /dev/null +++ b/data-contracts-with-terraform/cc/main.tf @@ -0,0 +1,28 @@ +# Configure the Confluent Provider +terraform { + backend "local" { + workspace_dir = ".tfstate/terraform.state" + } + + required_providers { + confluent = { + source = "confluentinc/confluent" + version = "2.12.0" + } + } +} + +provider "confluent" { +} + +resource "confluent_environment" "cc_env" { + display_name = var.cc_env_display_name + + stream_governance { + package = "ADVANCED" + } + + lifecycle { + prevent_destroy = false + } +} diff --git a/data-contracts-with-terraform/cc/membership.tf b/data-contracts-with-terraform/cc/membership.tf new file mode 100644 index 00000000..bf9ff1ea --- /dev/null +++ b/data-contracts-with-terraform/cc/membership.tf @@ -0,0 +1,66 @@ +resource "confluent_kafka_topic" "membership_avro" { + + topic_name = "membership-avro" + + kafka_cluster { + id = confluent_kafka_cluster.kafka_cluster.id + } + rest_endpoint = confluent_kafka_cluster.kafka_cluster.rest_endpoint + credentials { + key = confluent_api_key.app-manager-kafka-api-key.id + secret = confluent_api_key.app-manager-kafka-api-key.secret + } + + partitions_count = 10 + + lifecycle { + prevent_destroy = false + } +} + +resource "confluent_subject_config" "membership_value_avro" { + subject_name = "${confluent_kafka_topic.membership_avro.topic_name}-value" + + schema_registry_cluster { + id = data.confluent_schema_registry_cluster.advanced.id + } + rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint + + credentials { + key = confluent_api_key.env-manager-schema-registry-api-key.id + secret = confluent_api_key.env-manager-schema-registry-api-key.secret + } + + compatibility_level = "BACKWARD" + compatibility_group = "major_version" + + lifecycle { + prevent_destroy = false + } +} + +resource "confluent_schema" "membership_v1_avro" { + format = "AVRO" + subject_name = confluent_subject_config.membership_value_avro.subject_name + + schema_registry_cluster { + id = data.confluent_schema_registry_cluster.advanced.id + } + rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint + + credentials { + key = confluent_api_key.env-manager-schema-registry-api-key.id + secret = confluent_api_key.env-manager-schema-registry-api-key.secret + } + + schema = file("../src/main/avro/membership_v1.avsc") + metadata { + properties = { + "major_version" = "1" + } + } + + lifecycle { + prevent_destroy = false + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/cc/outputs.tf b/data-contracts-with-terraform/cc/outputs.tf new file mode 100644 index 00000000..96bd5f90 --- /dev/null +++ b/data-contracts-with-terraform/cc/outputs.tf @@ -0,0 +1,55 @@ +output "CC_ENV_DISPLAY_NAME" { + value = confluent_environment.cc_env.display_name +} + +output "CC_ENV_ID" { + value = confluent_environment.cc_env.id +} + +output "CC_KAFKA_CLUSTER_ID" { + value = confluent_kafka_cluster.kafka_cluster.id +} + +output "CC_BROKER" { + value = replace(confluent_kafka_cluster.kafka_cluster.bootstrap_endpoint, "SASL_SSL://", "") +} + +output "CC_BROKER_URL" { + value = confluent_kafka_cluster.kafka_cluster.rest_endpoint +} + +output "CC_SCHEMA_REGISTRY_ID" { + value = data.confluent_schema_registry_cluster.advanced.id +} + +output "CC_SCHEMA_REGISTRY_URL" { + value = data.confluent_schema_registry_cluster.advanced.rest_endpoint +} + +output "SCHEMA_REGISTRY_KEY_ID" { + value = confluent_api_key.env-manager-schema-registry-api-key.id + sensitive = false +} + +output "SCHEMA_REGISTRY_KEY_SECRET" { + value = nonsensitive(confluent_api_key.env-manager-schema-registry-api-key.secret) + # sensitive = false +} + +output "KAFKA_KEY_ID" { + value = confluent_api_key.app-manager-kafka-api-key.id + sensitive = false +} + +output "KAFKA_KEY_SECRET" { + value = nonsensitive(confluent_api_key.app-manager-kafka-api-key.secret) + # sensitive = false +} + +output "KAFKA_SASL_JAAS_CONFIG" { + value = "org.apache.kafka.common.security.plain.PlainLoginModule required username='${confluent_api_key.app-manager-kafka-api-key.id}' password='${nonsensitive(confluent_api_key.app-manager-kafka-api-key.secret)}';" +} + +output "SR_BASIC_AUTH_USER_INFO" { + value = "${confluent_api_key.env-manager-schema-registry-api-key.id}:${nonsensitive(confluent_api_key.env-manager-schema-registry-api-key.secret)}" +} diff --git a/data-contracts-with-terraform/cc/variables.tf b/data-contracts-with-terraform/cc/variables.tf new file mode 100644 index 00000000..33e386bc --- /dev/null +++ b/data-contracts-with-terraform/cc/variables.tf @@ -0,0 +1,27 @@ +variable "cloud_provider" { + type = string + description = "cloud provider for Confluent Cloud" + default = "AWS" +} + +variable "cloud_region" { + type = string + description = "cloud provider region" + default = "us-east-2" +} + +variable "cc_cluster_name" { + type = string + description = "name of kafka cluster" + default = "data-contracts-with-tf" +} + +variable "org_id" { + type = string +} + +variable "cc_env_display_name" { + type = string + description = "Name of Confluent Cloud Environment to Manage" + default = "tutorials-data-contracts-with-tf" +} diff --git a/data-contracts-with-terraform/src/main/avro/membership_v1.avsc b/data-contracts-with-terraform/src/main/avro/membership_v1.avsc new file mode 100644 index 00000000..7768156e --- /dev/null +++ b/data-contracts-with-terraform/src/main/avro/membership_v1.avsc @@ -0,0 +1,10 @@ +{ + "name": "Membership", + "namespace": "io.confluent.devrel", + "type": "record", + "fields": [ + {"name": "user_id", "type": "string"}, + {"name": "start_date", "type": {"type": "int", "logicalType": "date"}}, + {"name": "end_date", "type": {"type": "int", "logicalType": "date"}} + ] +} \ No newline at end of file diff --git a/data-contracts-with-terraform/src/main/avro/membership_v2.avsc b/data-contracts-with-terraform/src/main/avro/membership_v2.avsc new file mode 100644 index 00000000..6454cdd0 --- /dev/null +++ b/data-contracts-with-terraform/src/main/avro/membership_v2.avsc @@ -0,0 +1,20 @@ +{ + "name": "Membership", + "namespace": "io.confluent.devrel", + "type": "record", + "fields": [ + {"name": "user_id", "type": "string"}, + { + "name": "validity_period", + "type": { + "type": "record", + "name": "ValidityPeriod", + "fields": [ + {"name": "from", "type": {"type": "int", "logicalType": "date"}}, + {"name": "to", "type": {"type": "int", "logicalType": "date"} + } + ] + } + } + ] +} \ No newline at end of file diff --git a/data-contracts-with-terraform/terraform/.gitkeep b/data-contracts-with-terraform/terraform/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 4cb99865e99a21d69bb89c2e69845a3f78e9b204 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Mon, 6 Jan 2025 13:12:22 -0500 Subject: [PATCH 03/13] Adding v2 of membership schema. --- .../cc/membership.tf | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/data-contracts-with-terraform/cc/membership.tf b/data-contracts-with-terraform/cc/membership.tf index bf9ff1ea..8be3fff9 100644 --- a/data-contracts-with-terraform/cc/membership.tf +++ b/data-contracts-with-terraform/cc/membership.tf @@ -60,6 +60,32 @@ resource "confluent_schema" "membership_v1_avro" { } } + lifecycle { + prevent_destroy = false + } +} + +resource "confluent_schema" "membership_v2_avro" { + format = "AVRO" + subject_name = confluent_subject_config.membership_value_avro.subject_name + + schema_registry_cluster { + id = data.confluent_schema_registry_cluster.advanced.id + } + rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint + + credentials { + key = confluent_api_key.env-manager-schema-registry-api-key.id + secret = confluent_api_key.env-manager-schema-registry-api-key.secret + } + + schema = file("../src/main/avro/membership_v2.avsc") + metadata { + properties = { + "major_version" = "2" + } + } + lifecycle { prevent_destroy = false } From 466182fde3a89722b7d5d83af6d0cab9645ae854 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Tue, 14 Jan 2025 16:30:06 -0500 Subject: [PATCH 04/13] Managing schema registration with gradle. --- common/build.gradle | 12 +- .../kafka/build.gradle | 4 +- data-contracts-with-terraform/.gitignore | 2 +- data-contracts-with-terraform/README.md | 72 ++++- data-contracts-with-terraform/build.gradle | 24 -- .../cc/membership.tf | 52 ---- data-contracts-with-terraform/cc/outputs.tf | 54 ++-- .../gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - data-contracts-with-terraform/gradlew | 249 ------------------ data-contracts-with-terraform/gradlew.bat | 92 ------- .../producer-app-schema-v1/build.gradle.kts | 83 ++++++ .../producer-app-schema-v1/gradle.properties | 6 + .../settings.gradle.kts | 7 + .../src/main/avro/.gitignore | 1 + .../confluent/devrel/dc/v1/ApplicationMain.kt | 72 +++++ .../devrel/dc/v1/kafka/MembershipConsumer.kt | 24 ++ .../devrel/dc/v1/kafka/MembershipProducer.kt | 15 ++ .../src/main/resources/logback.xml | 25 ++ .../producer-app-schema-v2/build.gradle.kts | 83 ++++++ .../producer-app-schema-v2/gradle.properties | 6 + .../settings.gradle.kts | 7 + .../src/main/avro/.gitignore | 1 + .../confluent/devrel/v2/ApplicationV2Main.kt | 23 ++ .../devrel/v2/kafka/MembershipConsumer.kt | 20 ++ .../schemas/build.gradle.kts | 55 ++++ .../schemas/gradle.properties | 6 + .../schemas/settings.gradle.kts | 1 + .../src/main/avro/membership_v1.avsc | 0 .../src/main/avro/membership_v2.avsc | 0 .../metadata/membership_major_version_1.json | 5 + .../metadata/membership_major_version_2.json | 5 + .../avro => schemas/src/main/proto}/.gitkeep | 0 .../rulesets/membership_migration_rules.json | 19 ++ data-contracts-with-terraform/settings.gradle | 3 - .../shared/build.gradle.kts | 48 ++++ .../shared/gradle.properties | 6 + .../shared/settings.gradle.kts | 1 + .../datacontracts/shared/BaseConsumer.kt | 40 +++ .../datacontracts/shared/BaseProducer.kt | 42 +++ .../datacontracts/shared/ConfigLoader.kt | 23 ++ .../shared/src/main/resources/.gitignore | 1 + .../main/resources/confluent.properties.orig | 19 ++ .../src/main/protobuf/.gitkeep | 0 .../src/main/resources/.gitkeep | 0 .../src/test/java/.gitkeep | 0 .../src/test/resources/.gitkeep | 0 gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 184 ++++++------- over-aggregations/flinksql/build.gradle | 4 +- settings.gradle | 5 +- udf/ksql/build.gradle | 4 +- 53 files changed, 841 insertions(+), 574 deletions(-) delete mode 100644 data-contracts-with-terraform/build.gradle delete mode 100644 data-contracts-with-terraform/gradle.properties delete mode 100644 data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar delete mode 100644 data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties delete mode 100755 data-contracts-with-terraform/gradlew delete mode 100644 data-contracts-with-terraform/gradlew.bat create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/gradle.properties create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt create mode 100644 data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/gradle.properties create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt create mode 100644 data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt create mode 100644 data-contracts-with-terraform/schemas/build.gradle.kts create mode 100644 data-contracts-with-terraform/schemas/gradle.properties create mode 100644 data-contracts-with-terraform/schemas/settings.gradle.kts rename data-contracts-with-terraform/{ => schemas}/src/main/avro/membership_v1.avsc (100%) rename data-contracts-with-terraform/{ => schemas}/src/main/avro/membership_v2.avsc (100%) create mode 100644 data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_1.json create mode 100644 data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_2.json rename data-contracts-with-terraform/{src/main/avro => schemas/src/main/proto}/.gitkeep (100%) create mode 100644 data-contracts-with-terraform/schemas/src/main/rulesets/membership_migration_rules.json delete mode 100644 data-contracts-with-terraform/settings.gradle create mode 100644 data-contracts-with-terraform/shared/build.gradle.kts create mode 100644 data-contracts-with-terraform/shared/gradle.properties create mode 100644 data-contracts-with-terraform/shared/settings.gradle.kts create mode 100644 data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseConsumer.kt create mode 100644 data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseProducer.kt create mode 100644 data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/ConfigLoader.kt create mode 100644 data-contracts-with-terraform/shared/src/main/resources/.gitignore create mode 100644 data-contracts-with-terraform/shared/src/main/resources/confluent.properties.orig delete mode 100644 data-contracts-with-terraform/src/main/protobuf/.gitkeep delete mode 100644 data-contracts-with-terraform/src/main/resources/.gitkeep delete mode 100644 data-contracts-with-terraform/src/test/java/.gitkeep delete mode 100644 data-contracts-with-terraform/src/test/resources/.gitkeep diff --git a/common/build.gradle b/common/build.gradle index 463987ee..82df3944 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -25,8 +25,8 @@ artifacts { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { @@ -39,15 +39,15 @@ dependencies { implementation 'org.slf4j:slf4j-simple:2.0.7' implementation('org.apache.kafka:kafka-clients') { version { - strictly '3.7.0' + strictly '3.8.0' } } - implementation 'io.confluent:kafka-streams-avro-serde:7.5.1' + implementation 'io.confluent:kafka-streams-avro-serde:7.7.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testImplementation 'org.hamcrest:hamcrest:2.2' - testImplementation 'org.testcontainers:testcontainers:1.19.3' - testImplementation 'org.testcontainers:kafka:1.19.3' + testImplementation 'org.testcontainers:testcontainers:1.20.1' + testImplementation 'org.testcontainers:kafka:1.20.1' testImplementation 'commons-codec:commons-codec:1.17.0' testImplementation 'org.apache.flink:flink-sql-connector-kafka:3.2.0-1.19' testImplementation 'org.apache.flink:flink-connector-base:1.19.1' diff --git a/confluent-parallel-consumer-application/kafka/build.gradle b/confluent-parallel-consumer-application/kafka/build.gradle index 2741d61c..38e60c89 100644 --- a/confluent-parallel-consumer-application/kafka/build.gradle +++ b/confluent-parallel-consumer-application/kafka/build.gradle @@ -31,7 +31,7 @@ repositories { dependencies { implementation project(':common') implementation "org.slf4j:slf4j-simple:2.0.7" - implementation "io.confluent.parallelconsumer:parallel-consumer-core:0.5.2.4" + implementation "io.confluent.parallelconsumer:parallel-consumer-core:0.5.3.2" implementation "org.apache.commons:commons-lang3:3.12.0" implementation "me.tongfei:progressbar:0.9.3" implementation 'org.awaitility:awaitility:4.2.0' @@ -41,7 +41,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.awaitility:awaitility:4.2.0' - testImplementation "io.confluent.parallelconsumer:parallel-consumer-core:0.5.2.4:tests" // for LongPollingMockConsumer + testImplementation "io.confluent.parallelconsumer:parallel-consumer-core:0.5.3.2:tests" // for LongPollingMockConsumer testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' } diff --git a/data-contracts-with-terraform/.gitignore b/data-contracts-with-terraform/.gitignore index 9a7afb8c..4feefcdd 100644 --- a/data-contracts-with-terraform/.gitignore +++ b/data-contracts-with-terraform/.gitignore @@ -1,7 +1,7 @@ ### Gradle template .gradle **/build/ -!src/**/build/ +!producer-app-schema-v1/src/**/build/ # Ignore Gradle GUI config gradle-app.setting diff --git a/data-contracts-with-terraform/README.md b/data-contracts-with-terraform/README.md index 1add1b64..e4817bf5 100644 --- a/data-contracts-with-terraform/README.md +++ b/data-contracts-with-terraform/README.md @@ -7,13 +7,81 @@ Data contracts consists not only of the schemas to define the data, but also rul controls, and discovery.In this tutorial, we'll evolve a couple of schemas and add data quality and migration rules.We'll also explore tagging those schemas, fields, and rules for data discovery. -## Setup - ## Running the Example +In this tutorial we'll create Confluent Cloud infrastructure - including a Kafka cluster and Schema Registry. Then we'll create +a Kafka topic named `membership-avro` to store `Membership` events. The Apache Avro schema is maintained an managed in this repo +along with metadata and migration rules about those schemas. + +We will evolve the `membership` schema, refactoring the events to encapsulate the date-related fields of version 1 into its +own `record` type in version 2. Typically this would be a breaking change. However, data migration rules in the schema registry +allow us to perform this schema change without breaking producers or consumers. At the time this is written, this functionality +is only available to JVM-based Confluent client implementations. We'll update this example as our non-JVM clients evolve. + ### Prerequisites +Here are the tools needed to run this tutorial: +* [Confluent Cloud](http://confluent.cloud) +* [Confluent CLI](https://docs.confluent.io/confluent-cli/current/install.html) +* [Terraform](https://developer.hashicorp.com/terraform/install?product_intent=terraform) +* [jq](https://jqlang.github.io/jq/) +* JDK 17 +* IDE of choice + ### Executing Terraform +To create Confluent Cloud assets, change to the `cc` subdirectory. We'll step through the commands and what they do. + +Export the Confluent Cloud organization ID to a terraform environment variable: + +```shell +export TF_VAR_org_id=$(confluent organization list -o json | jq -c -r '.[] | select(.is_current)' | jq '.id') +``` + +#### Create Confluent Cloud Assets +Initialize the terraform environment: + +```shell +terraform init +``` + +Create a terraform "plan" - this may open a browser window, asking you to authenticate to Confluent Cloud: + +```shell +terraform plan -out "tfplan" +``` + +Apply the terraform plan, thus creating the needed Confluent Cloud infrastructure: + +```shell +terraform apply "tfplan" +``` + +#### Prepare Client Properties + +The output of `terraform apply` includes the properties needed to connect to Confluent Cloud. The command below will export +those outputs to a properties file in our project for later use by our client code: + +```shell +terraform output -json | \ + jq -r 'to_entries | map( {key: .key|tostring|split("_")|join("."), value: .value} ) | map("\(.key)=\(.value.value)")' | while read -r line ; do echo "$line"; \ + done > ../shared/src/main/resources/confluent.properties +``` + +For an example of this properties file, see [confluent.properties.orig](shared/src/main/resources/confluent.properties.orig). + ### Using Schemas + +## Teardown + +When you're done with the tutorial, issue this command from the `cc` directory to destroy the Confluent Cloud environment +we created: + +```shell +terraform destroy -auto-approve +``` + +Check the Confluent Cloud console to ensure this environment no longer exists. + + diff --git a/data-contracts-with-terraform/build.gradle b/data-contracts-with-terraform/build.gradle deleted file mode 100644 index 208ad718..00000000 --- a/data-contracts-with-terraform/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } -} - -plugins { - id 'java' - id 'idea' -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} -version = "0.0.1" - -repositories { - mavenCentral() -} - -dependencies { - testImplementation project(path: ':common', configuration: 'testArtifacts') -} diff --git a/data-contracts-with-terraform/cc/membership.tf b/data-contracts-with-terraform/cc/membership.tf index 8be3fff9..46676d44 100644 --- a/data-contracts-with-terraform/cc/membership.tf +++ b/data-contracts-with-terraform/cc/membership.tf @@ -38,55 +38,3 @@ resource "confluent_subject_config" "membership_value_avro" { prevent_destroy = false } } - -resource "confluent_schema" "membership_v1_avro" { - format = "AVRO" - subject_name = confluent_subject_config.membership_value_avro.subject_name - - schema_registry_cluster { - id = data.confluent_schema_registry_cluster.advanced.id - } - rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint - - credentials { - key = confluent_api_key.env-manager-schema-registry-api-key.id - secret = confluent_api_key.env-manager-schema-registry-api-key.secret - } - - schema = file("../src/main/avro/membership_v1.avsc") - metadata { - properties = { - "major_version" = "1" - } - } - - lifecycle { - prevent_destroy = false - } -} - -resource "confluent_schema" "membership_v2_avro" { - format = "AVRO" - subject_name = confluent_subject_config.membership_value_avro.subject_name - - schema_registry_cluster { - id = data.confluent_schema_registry_cluster.advanced.id - } - rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint - - credentials { - key = confluent_api_key.env-manager-schema-registry-api-key.id - secret = confluent_api_key.env-manager-schema-registry-api-key.secret - } - - schema = file("../src/main/avro/membership_v2.avsc") - metadata { - properties = { - "major_version" = "2" - } - } - - lifecycle { - prevent_destroy = false - } -} \ No newline at end of file diff --git a/data-contracts-with-terraform/cc/outputs.tf b/data-contracts-with-terraform/cc/outputs.tf index 96bd5f90..889caaf1 100644 --- a/data-contracts-with-terraform/cc/outputs.tf +++ b/data-contracts-with-terraform/cc/outputs.tf @@ -1,55 +1,35 @@ -output "CC_ENV_DISPLAY_NAME" { - value = confluent_environment.cc_env.display_name -} - -output "CC_ENV_ID" { - value = confluent_environment.cc_env.id -} - -output "CC_KAFKA_CLUSTER_ID" { - value = confluent_kafka_cluster.kafka_cluster.id -} - -output "CC_BROKER" { +output "bootstrap_servers" { value = replace(confluent_kafka_cluster.kafka_cluster.bootstrap_endpoint, "SASL_SSL://", "") } -output "CC_BROKER_URL" { - value = confluent_kafka_cluster.kafka_cluster.rest_endpoint -} - -output "CC_SCHEMA_REGISTRY_ID" { - value = data.confluent_schema_registry_cluster.advanced.id +output "security_protocol" { + value = "SASL_SSL" } -output "CC_SCHEMA_REGISTRY_URL" { - value = data.confluent_schema_registry_cluster.advanced.rest_endpoint +output "sasl_mechanism" { + value = "PLAIN" } -output "SCHEMA_REGISTRY_KEY_ID" { - value = confluent_api_key.env-manager-schema-registry-api-key.id - sensitive = false +output "sasl_jaas_config" { + value = "org.apache.kafka.common.security.plain.PlainLoginModule required username='${confluent_api_key.app-manager-kafka-api-key.id}' password='${nonsensitive(confluent_api_key.app-manager-kafka-api-key.secret)}';" } -output "SCHEMA_REGISTRY_KEY_SECRET" { - value = nonsensitive(confluent_api_key.env-manager-schema-registry-api-key.secret) - # sensitive = false +output "client_dns_lookup" { + value = "use_all_dns_ips" } -output "KAFKA_KEY_ID" { - value = confluent_api_key.app-manager-kafka-api-key.id - sensitive = false +output "schema_registry_url" { + value = data.confluent_schema_registry_cluster.advanced.rest_endpoint } -output "KAFKA_KEY_SECRET" { - value = nonsensitive(confluent_api_key.app-manager-kafka-api-key.secret) - # sensitive = false +output "basic_auth_credentials_source" { + value = "USER_INFO" } -output "KAFKA_SASL_JAAS_CONFIG" { - value = "org.apache.kafka.common.security.plain.PlainLoginModule required username='${confluent_api_key.app-manager-kafka-api-key.id}' password='${nonsensitive(confluent_api_key.app-manager-kafka-api-key.secret)}';" +output "basic_auth_user_info" { + value = "${confluent_api_key.env-manager-schema-registry-api-key.id}:${nonsensitive(confluent_api_key.env-manager-schema-registry-api-key.secret)}" } -output "SR_BASIC_AUTH_USER_INFO" { - value = "${confluent_api_key.env-manager-schema-registry-api-key.id}:${nonsensitive(confluent_api_key.env-manager-schema-registry-api-key.secret)}" +output "auto_register_schemas" { + value = false } diff --git a/data-contracts-with-terraform/gradle.properties b/data-contracts-with-terraform/gradle.properties deleted file mode 100644 index ae0a05bb..00000000 --- a/data-contracts-with-terraform/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -confluentVersion=7.7.0 \ No newline at end of file diff --git a/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar b/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index d64cd4917707c1f8861d8cb53dd15194d4248596..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43462 zcma&NWl&^owk(X(xVyW%ySuwf;qI=D6|RlDJ2cR^yEKh!@I- zp9QeisK*rlxC>+~7Dk4IxIRsKBHqdR9b3+fyL=ynHmIDe&|>O*VlvO+%z5;9Z$|DJ zb4dO}-R=MKr^6EKJiOrJdLnCJn>np?~vU-1sSFgPu;pthGwf}bG z(1db%xwr#x)r+`4AGu$j7~u2MpVs3VpLp|mx&;>`0p0vH6kF+D2CY0fVdQOZ@h;A` z{infNyvmFUiu*XG}RNMNwXrbec_*a3N=2zJ|Wh5z* z5rAX$JJR{#zP>KY**>xHTuw?|-Rg|o24V)74HcfVT;WtQHXlE+_4iPE8QE#DUm%x0 zEKr75ur~W%w#-My3Tj`hH6EuEW+8K-^5P62$7Sc5OK+22qj&Pd1;)1#4tKihi=~8C zHiQSst0cpri6%OeaR`PY>HH_;CPaRNty%WTm4{wDK8V6gCZlG@U3$~JQZ;HPvDJcT1V{ z?>H@13MJcCNe#5z+MecYNi@VT5|&UiN1D4ATT+%M+h4c$t;C#UAs3O_q=GxK0}8%8 z8J(_M9bayxN}69ex4dzM_P3oh@ZGREjVvn%%r7=xjkqxJP4kj}5tlf;QosR=%4L5y zWhgejO=vao5oX%mOHbhJ8V+SG&K5dABn6!WiKl{|oPkq(9z8l&Mm%(=qGcFzI=eLu zWc_oCLyf;hVlB@dnwY98?75B20=n$>u3b|NB28H0u-6Rpl((%KWEBOfElVWJx+5yg z#SGqwza7f}$z;n~g%4HDU{;V{gXIhft*q2=4zSezGK~nBgu9-Q*rZ#2f=Q}i2|qOp z!!y4p)4o=LVUNhlkp#JL{tfkhXNbB=Ox>M=n6soptJw-IDI|_$is2w}(XY>a=H52d z3zE$tjPUhWWS+5h=KVH&uqQS=$v3nRs&p$%11b%5qtF}S2#Pc`IiyBIF4%A!;AVoI zXU8-Rpv!DQNcF~(qQnyyMy=-AN~U>#&X1j5BLDP{?K!%h!;hfJI>$mdLSvktEr*89 zdJHvby^$xEX0^l9g$xW-d?J;L0#(`UT~zpL&*cEh$L|HPAu=P8`OQZV!-}l`noSp_ zQ-1$q$R-gDL)?6YaM!=8H=QGW$NT2SeZlb8PKJdc=F-cT@j7Xags+Pr*jPtlHFnf- zh?q<6;)27IdPc^Wdy-mX%2s84C1xZq9Xms+==F4);O`VUASmu3(RlgE#0+#giLh-& zcxm3_e}n4{%|X zJp{G_j+%`j_q5}k{eW&TlP}J2wtZ2^<^E(O)4OQX8FDp6RJq!F{(6eHWSD3=f~(h} zJXCf7=r<16X{pHkm%yzYI_=VDP&9bmI1*)YXZeB}F? z(%QsB5fo*FUZxK$oX~X^69;x~j7ms8xlzpt-T15e9}$4T-pC z6PFg@;B-j|Ywajpe4~bk#S6(fO^|mm1hKOPfA%8-_iGCfICE|=P_~e;Wz6my&)h_~ zkv&_xSAw7AZ%ThYF(4jADW4vg=oEdJGVOs>FqamoL3Np8>?!W#!R-0%2Bg4h?kz5I zKV-rKN2n(vUL%D<4oj@|`eJ>0i#TmYBtYmfla;c!ATW%;xGQ0*TW@PTlGG><@dxUI zg>+3SiGdZ%?5N=8uoLA|$4isK$aJ%i{hECP$bK{J#0W2gQ3YEa zZQ50Stn6hqdfxJ*9#NuSLwKFCUGk@c=(igyVL;;2^wi4o30YXSIb2g_ud$ zgpCr@H0qWtk2hK8Q|&wx)}4+hTYlf;$a4#oUM=V@Cw#!$(nOFFpZ;0lc!qd=c$S}Z zGGI-0jg~S~cgVT=4Vo)b)|4phjStD49*EqC)IPwyeKBLcN;Wu@Aeph;emROAwJ-0< z_#>wVm$)ygH|qyxZaet&(Vf%pVdnvKWJn9`%DAxj3ot;v>S$I}jJ$FLBF*~iZ!ZXE zkvui&p}fI0Y=IDX)mm0@tAd|fEHl~J&K}ZX(Mm3cm1UAuwJ42+AO5@HwYfDH7ipIc zmI;1J;J@+aCNG1M`Btf>YT>~c&3j~Qi@Py5JT6;zjx$cvOQW@3oQ>|}GH?TW-E z1R;q^QFjm5W~7f}c3Ww|awg1BAJ^slEV~Pk`Kd`PS$7;SqJZNj->it4DW2l15}xP6 zoCl$kyEF%yJni0(L!Z&14m!1urXh6Btj_5JYt1{#+H8w?5QI%% zo-$KYWNMJVH?Hh@1n7OSu~QhSswL8x0=$<8QG_zepi_`y_79=nK=_ZP_`Em2UI*tyQoB+r{1QYZCpb?2OrgUw#oRH$?^Tj!Req>XiE#~B|~ z+%HB;=ic+R@px4Ld8mwpY;W^A%8%l8$@B@1m5n`TlKI6bz2mp*^^^1mK$COW$HOfp zUGTz-cN9?BGEp}5A!mDFjaiWa2_J2Iq8qj0mXzk; z66JBKRP{p%wN7XobR0YjhAuW9T1Gw3FDvR5dWJ8ElNYF94eF3ebu+QwKjtvVu4L zI9ip#mQ@4uqVdkl-TUQMb^XBJVLW(-$s;Nq;@5gr4`UfLgF$adIhd?rHOa%D);whv z=;krPp~@I+-Z|r#s3yCH+c1US?dnm+C*)r{m+86sTJusLdNu^sqLrfWed^ndHXH`m zd3#cOe3>w-ga(Dus_^ppG9AC>Iq{y%%CK+Cro_sqLCs{VLuK=dev>OL1dis4(PQ5R zcz)>DjEkfV+MO;~>VUlYF00SgfUo~@(&9$Iy2|G0T9BSP?&T22>K46D zL*~j#yJ?)^*%J3!16f)@Y2Z^kS*BzwfAQ7K96rFRIh>#$*$_Io;z>ux@}G98!fWR@ zGTFxv4r~v)Gsd|pF91*-eaZ3Qw1MH$K^7JhWIdX%o$2kCbvGDXy)a?@8T&1dY4`;L z4Kn+f%SSFWE_rpEpL9bnlmYq`D!6F%di<&Hh=+!VI~j)2mfil03T#jJ_s?}VV0_hp z7T9bWxc>Jm2Z0WMU?`Z$xE74Gu~%s{mW!d4uvKCx@WD+gPUQ zV0vQS(Ig++z=EHN)BR44*EDSWIyT~R4$FcF*VEY*8@l=218Q05D2$|fXKFhRgBIEE zdDFB}1dKkoO^7}{5crKX!p?dZWNz$m>1icsXG2N+((x0OIST9Zo^DW_tytvlwXGpn zs8?pJXjEG;T@qrZi%#h93?FP$!&P4JA(&H61tqQi=opRzNpm zkrG}$^t9&XduK*Qa1?355wd8G2CI6QEh@Ua>AsD;7oRUNLPb76m4HG3K?)wF~IyS3`fXuNM>${?wmB zpVz;?6_(Fiadfd{vUCBM*_kt$+F3J+IojI;9L(gc9n3{sEZyzR9o!_mOwFC#tQ{Q~ zP3-`#uK#tP3Q7~Q;4H|wjZHO8h7e4IuBxl&vz2w~D8)w=Wtg31zpZhz%+kzSzL*dV zwp@{WU4i;hJ7c2f1O;7Mz6qRKeASoIv0_bV=i@NMG*l<#+;INk-^`5w@}Dj~;k=|}qM1vq_P z|GpBGe_IKq|LNy9SJhKOQ$c=5L{Dv|Q_lZl=-ky*BFBJLW9&y_C|!vyM~rQx=!vun z?rZJQB5t}Dctmui5i31C_;_}CEn}_W%>oSXtt>@kE1=JW*4*v4tPp;O6 zmAk{)m!)}34pTWg8{i>($%NQ(Tl;QC@J@FfBoc%Gr&m560^kgSfodAFrIjF}aIw)X zoXZ`@IsMkc8_=w%-7`D6Y4e*CG8k%Ud=GXhsTR50jUnm+R*0A(O3UKFg0`K;qp1bl z7``HN=?39ic_kR|^R^~w-*pa?Vj#7|e9F1iRx{GN2?wK!xR1GW!qa=~pjJb-#u1K8 zeR?Y2i-pt}yJq;SCiVHODIvQJX|ZJaT8nO+(?HXbLefulKKgM^B(UIO1r+S=7;kLJ zcH}1J=Px2jsh3Tec&v8Jcbng8;V-`#*UHt?hB(pmOipKwf3Lz8rG$heEB30Sg*2rx zV<|KN86$soN(I!BwO`1n^^uF2*x&vJ$2d$>+`(romzHP|)K_KkO6Hc>_dwMW-M(#S zK(~SiXT1@fvc#U+?|?PniDRm01)f^#55;nhM|wi?oG>yBsa?~?^xTU|fX-R(sTA+5 zaq}-8Tx7zrOy#3*JLIIVsBmHYLdD}!0NP!+ITW+Thn0)8SS!$@)HXwB3tY!fMxc#1 zMp3H?q3eD?u&Njx4;KQ5G>32+GRp1Ee5qMO0lZjaRRu&{W<&~DoJNGkcYF<5(Ab+J zgO>VhBl{okDPn78<%&e2mR{jwVCz5Og;*Z;;3%VvoGo_;HaGLWYF7q#jDX=Z#Ml`H z858YVV$%J|e<1n`%6Vsvq7GmnAV0wW4$5qQ3uR@1i>tW{xrl|ExywIc?fNgYlA?C5 zh$ezAFb5{rQu6i7BSS5*J-|9DQ{6^BVQ{b*lq`xS@RyrsJN?-t=MTMPY;WYeKBCNg z^2|pN!Q^WPJuuO4!|P@jzt&tY1Y8d%FNK5xK(!@`jO2aEA*4 zkO6b|UVBipci?){-Ke=+1;mGlND8)6+P;8sq}UXw2hn;fc7nM>g}GSMWu&v&fqh

iViYT=fZ(|3Ox^$aWPp4a8h24tD<|8-!aK0lHgL$N7Efw}J zVIB!7=T$U`ao1?upi5V4Et*-lTG0XvExbf!ya{cua==$WJyVG(CmA6Of*8E@DSE%L z`V^$qz&RU$7G5mg;8;=#`@rRG`-uS18$0WPN@!v2d{H2sOqP|!(cQ@ zUHo!d>>yFArLPf1q`uBvY32miqShLT1B@gDL4XoVTK&@owOoD)OIHXrYK-a1d$B{v zF^}8D3Y^g%^cnvScOSJR5QNH+BI%d|;J;wWM3~l>${fb8DNPg)wrf|GBP8p%LNGN# z3EaIiItgwtGgT&iYCFy9-LG}bMI|4LdmmJt@V@% zb6B)1kc=T)(|L@0;wr<>=?r04N;E&ef+7C^`wPWtyQe(*pD1pI_&XHy|0gIGHMekd zF_*M4yi6J&Z4LQj65)S zXwdM{SwUo%3SbPwFsHgqF@V|6afT|R6?&S;lw=8% z3}@9B=#JI3@B*#4s!O))~z zc>2_4Q_#&+5V`GFd?88^;c1i7;Vv_I*qt!_Yx*n=;rj!82rrR2rQ8u5(Ejlo{15P% zs~!{%XJ>FmJ})H^I9bn^Re&38H{xA!0l3^89k(oU;bZWXM@kn$#aoS&Y4l^-WEn-fH39Jb9lA%s*WsKJQl?n9B7_~P z-XM&WL7Z!PcoF6_D>V@$CvUIEy=+Z&0kt{szMk=f1|M+r*a43^$$B^MidrT0J;RI` z(?f!O<8UZkm$_Ny$Hth1J#^4ni+im8M9mr&k|3cIgwvjAgjH z8`N&h25xV#v*d$qBX5jkI|xOhQn!>IYZK7l5#^P4M&twe9&Ey@@GxYMxBZq2e7?`q z$~Szs0!g{2fGcp9PZEt|rdQ6bhAgpcLHPz?f-vB?$dc*!9OL?Q8mn7->bFD2Si60* z!O%y)fCdMSV|lkF9w%x~J*A&srMyYY3{=&$}H zGQ4VG_?$2X(0|vT0{=;W$~icCI{b6W{B!Q8xdGhF|D{25G_5_+%s(46lhvNLkik~R z>nr(&C#5wwOzJZQo9m|U<;&Wk!_#q|V>fsmj1g<6%hB{jGoNUPjgJslld>xmODzGjYc?7JSuA?A_QzjDw5AsRgi@Y|Z0{F{!1=!NES-#*f^s4l0Hu zz468))2IY5dmD9pa*(yT5{EyP^G>@ZWumealS-*WeRcZ}B%gxq{MiJ|RyX-^C1V=0 z@iKdrGi1jTe8Ya^x7yyH$kBNvM4R~`fbPq$BzHum-3Zo8C6=KW@||>zsA8-Y9uV5V z#oq-f5L5}V<&wF4@X@<3^C%ptp6+Ce)~hGl`kwj)bsAjmo_GU^r940Z-|`<)oGnh7 zFF0Tde3>ui?8Yj{sF-Z@)yQd~CGZ*w-6p2U<8}JO-sRsVI5dBji`01W8A&3$?}lxBaC&vn0E$c5tW* zX>5(zzZ=qn&!J~KdsPl;P@bmA-Pr8T*)eh_+Dv5=Ma|XSle6t(k8qcgNyar{*ReQ8 zTXwi=8vr>!3Ywr+BhggHDw8ke==NTQVMCK`$69fhzEFB*4+H9LIvdt-#IbhZvpS}} zO3lz;P?zr0*0$%-Rq_y^k(?I{Mk}h@w}cZpMUp|ucs55bcloL2)($u%mXQw({Wzc~ z;6nu5MkjP)0C(@%6Q_I_vsWrfhl7Zpoxw#WoE~r&GOSCz;_ro6i(^hM>I$8y>`!wW z*U^@?B!MMmb89I}2(hcE4zN2G^kwyWCZp5JG>$Ez7zP~D=J^LMjSM)27_0B_X^C(M z`fFT+%DcKlu?^)FCK>QzSnV%IsXVcUFhFdBP!6~se&xxrIxsvySAWu++IrH;FbcY$ z2DWTvSBRfLwdhr0nMx+URA$j3i7_*6BWv#DXfym?ZRDcX9C?cY9sD3q)uBDR3uWg= z(lUIzB)G$Hr!){>E{s4Dew+tb9kvToZp-1&c?y2wn@Z~(VBhqz`cB;{E4(P3N2*nJ z_>~g@;UF2iG{Kt(<1PyePTKahF8<)pozZ*xH~U-kfoAayCwJViIrnqwqO}7{0pHw$ zs2Kx?s#vQr7XZ264>5RNKSL8|Ty^=PsIx^}QqOOcfpGUU4tRkUc|kc7-!Ae6!+B{o~7nFpm3|G5^=0#Bnm6`V}oSQlrX(u%OWnC zoLPy&Q;1Jui&7ST0~#+}I^&?vcE*t47~Xq#YwvA^6^} z`WkC)$AkNub|t@S!$8CBlwbV~?yp&@9h{D|3z-vJXgzRC5^nYm+PyPcgRzAnEi6Q^gslXYRv4nycsy-SJu?lMps-? zV`U*#WnFsdPLL)Q$AmD|0`UaC4ND07+&UmOu!eHruzV|OUox<+Jl|Mr@6~C`T@P%s zW7sgXLF2SSe9Fl^O(I*{9wsFSYb2l%-;&Pi^dpv!{)C3d0AlNY6!4fgmSgj_wQ*7Am7&$z;Jg&wgR-Ih;lUvWS|KTSg!&s_E9_bXBkZvGiC6bFKDWZxsD$*NZ#_8bl zG1P-#@?OQzED7@jlMJTH@V!6k;W>auvft)}g zhoV{7$q=*;=l{O>Q4a@ ziMjf_u*o^PsO)#BjC%0^h>Xp@;5$p{JSYDt)zbb}s{Kbt!T*I@Pk@X0zds6wsefuU zW$XY%yyRGC94=6mf?x+bbA5CDQ2AgW1T-jVAJbm7K(gp+;v6E0WI#kuACgV$r}6L? zd|Tj?^%^*N&b>Dd{Wr$FS2qI#Ucs1yd4N+RBUQiSZGujH`#I)mG&VKoDh=KKFl4=G z&MagXl6*<)$6P}*Tiebpz5L=oMaPrN+caUXRJ`D?=K9!e0f{@D&cZLKN?iNP@X0aF zE(^pl+;*T5qt?1jRC=5PMgV!XNITRLS_=9{CJExaQj;lt!&pdzpK?8p>%Mb+D z?yO*uSung=-`QQ@yX@Hyd4@CI^r{2oiu`%^bNkz+Nkk!IunjwNC|WcqvX~k=><-I3 zDQdbdb|!v+Iz01$w@aMl!R)koD77Xp;eZwzSl-AT zr@Vu{=xvgfq9akRrrM)}=!=xcs+U1JO}{t(avgz`6RqiiX<|hGG1pmop8k6Q+G_mv zJv|RfDheUp2L3=^C=4aCBMBn0aRCU(DQwX-W(RkRwmLeuJYF<0urcaf(=7)JPg<3P zQs!~G)9CT18o!J4{zX{_e}4eS)U-E)0FAt}wEI(c0%HkxgggW;(1E=>J17_hsH^sP z%lT0LGgbUXHx-K*CI-MCrP66UP0PvGqM$MkeLyqHdbgP|_Cm!7te~b8p+e6sQ_3k| zVcwTh6d83ltdnR>D^)BYQpDKlLk3g0Hdcgz2}%qUs9~~Rie)A-BV1mS&naYai#xcZ z(d{8=-LVpTp}2*y)|gR~;qc7fp26}lPcLZ#=JpYcn3AT9(UIdOyg+d(P5T7D&*P}# zQCYplZO5|7+r19%9e`v^vfSS1sbX1c%=w1;oyruXB%Kl$ACgKQ6=qNWLsc=28xJjg zwvsI5-%SGU|3p>&zXVl^vVtQT3o-#$UT9LI@Npz~6=4!>mc431VRNN8od&Ul^+G_kHC`G=6WVWM z%9eWNyy(FTO|A+@x}Ou3CH)oi;t#7rAxdIXfNFwOj_@Y&TGz6P_sqiB`Q6Lxy|Q{`|fgmRG(k+!#b*M+Z9zFce)f-7;?Km5O=LHV9f9_87; zF7%R2B+$?@sH&&-$@tzaPYkw0;=i|;vWdI|Wl3q_Zu>l;XdIw2FjV=;Mq5t1Q0|f< zs08j54Bp`3RzqE=2enlkZxmX6OF+@|2<)A^RNQpBd6o@OXl+i)zO%D4iGiQNuXd+zIR{_lb96{lc~bxsBveIw6umhShTX+3@ZJ=YHh@ zWY3(d0azg;7oHn>H<>?4@*RQbi>SmM=JrHvIG(~BrvI)#W(EAeO6fS+}mxxcc+X~W6&YVl86W9WFSS}Vz-f9vS?XUDBk)3TcF z8V?$4Q)`uKFq>xT=)Y9mMFVTUk*NIA!0$?RP6Ig0TBmUFrq*Q-Agq~DzxjStQyJ({ zBeZ;o5qUUKg=4Hypm|}>>L=XKsZ!F$yNTDO)jt4H0gdQ5$f|d&bnVCMMXhNh)~mN z@_UV6D7MVlsWz+zM+inZZp&P4fj=tm6fX)SG5H>OsQf_I8c~uGCig$GzuwViK54bcgL;VN|FnyQl>Ed7(@>=8$a_UKIz|V6CeVSd2(P z0Uu>A8A+muM%HLFJQ9UZ5c)BSAv_zH#1f02x?h9C}@pN@6{>UiAp>({Fn(T9Q8B z^`zB;kJ5b`>%dLm+Ol}ty!3;8f1XDSVX0AUe5P#@I+FQ-`$(a;zNgz)4x5hz$Hfbg z!Q(z26wHLXko(1`;(BAOg_wShpX0ixfWq3ponndY+u%1gyX)_h=v1zR#V}#q{au6; z!3K=7fQwnRfg6FXtNQmP>`<;!N137paFS%y?;lb1@BEdbvQHYC{976l`cLqn;b8lp zIDY>~m{gDj(wfnK!lpW6pli)HyLEiUrNc%eXTil|F2s(AY+LW5hkKb>TQ3|Q4S9rr zpDs4uK_co6XPsn_z$LeS{K4jFF`2>U`tbgKdyDne`xmR<@6AA+_hPNKCOR-Zqv;xk zu5!HsBUb^!4uJ7v0RuH-7?l?}b=w5lzzXJ~gZcxRKOovSk@|#V+MuX%Y+=;14i*%{)_gSW9(#4%)AV#3__kac1|qUy!uyP{>?U#5wYNq}y$S9pCc zFc~4mgSC*G~j0u#qqp9 z${>3HV~@->GqEhr_Xwoxq?Hjn#=s2;i~g^&Hn|aDKpA>Oc%HlW(KA1?BXqpxB;Ydx)w;2z^MpjJ(Qi(X!$5RC z*P{~%JGDQqojV>2JbEeCE*OEu!$XJ>bWA9Oa_Hd;y)F%MhBRi*LPcdqR8X`NQ&1L# z5#9L*@qxrx8n}LfeB^J{%-?SU{FCwiWyHp682F+|pa+CQa3ZLzBqN1{)h4d6+vBbV zC#NEbQLC;}me3eeYnOG*nXOJZEU$xLZ1<1Y=7r0(-U0P6-AqwMAM`a(Ed#7vJkn6plb4eI4?2y3yOTGmmDQ!z9`wzbf z_OY#0@5=bnep;MV0X_;;SJJWEf^E6Bd^tVJ9znWx&Ks8t*B>AM@?;D4oWUGc z!H*`6d7Cxo6VuyS4Eye&L1ZRhrRmN6Lr`{NL(wDbif|y&z)JN>Fl5#Wi&mMIr5i;x zBx}3YfF>>8EC(fYnmpu~)CYHuHCyr5*`ECap%t@y=jD>!_%3iiE|LN$mK9>- zHdtpy8fGZtkZF?%TW~29JIAfi2jZT8>OA7=h;8T{{k?c2`nCEx9$r zS+*&vt~2o^^J+}RDG@+9&M^K*z4p{5#IEVbz`1%`m5c2};aGt=V?~vIM}ZdPECDI)47|CWBCfDWUbxBCnmYivQ*0Nu_xb*C>~C9(VjHM zxe<*D<#dQ8TlpMX2c@M<9$w!RP$hpG4cs%AI){jp*Sj|*`m)5(Bw*A0$*i-(CA5#%>a)$+jI2C9r6|(>J8InryENI z$NohnxDUB;wAYDwrb*!N3noBTKPpPN}~09SEL18tkG zxgz(RYU_;DPT{l?Q$+eaZaxnsWCA^ds^0PVRkIM%bOd|G2IEBBiz{&^JtNsODs;5z zICt_Zj8wo^KT$7Bg4H+y!Df#3mbl%%?|EXe!&(Vmac1DJ*y~3+kRKAD=Ovde4^^%~ zw<9av18HLyrf*_>Slp;^i`Uy~`mvBjZ|?Ad63yQa#YK`4+c6;pW4?XIY9G1(Xh9WO8{F-Aju+nS9Vmv=$Ac0ienZ+p9*O%NG zMZKy5?%Z6TAJTE?o5vEr0r>f>hb#2w2U3DL64*au_@P!J!TL`oH2r*{>ffu6|A7tv zL4juf$DZ1MW5ZPsG!5)`k8d8c$J$o;%EIL0va9&GzWvkS%ZsGb#S(?{!UFOZ9<$a| zY|a+5kmD5N&{vRqkgY>aHsBT&`rg|&kezoD)gP0fsNYHsO#TRc_$n6Lf1Z{?+DLziXlHrq4sf(!>O{?Tj;Eh@%)+nRE_2VxbN&&%%caU#JDU%vL3}Cb zsb4AazPI{>8H&d=jUaZDS$-0^AxE@utGs;-Ez_F(qC9T=UZX=>ok2k2 ziTn{K?y~a5reD2A)P${NoI^>JXn>`IeArow(41c-Wm~)wiryEP(OS{YXWi7;%dG9v zI?mwu1MxD{yp_rrk!j^cKM)dc4@p4Ezyo%lRN|XyD}}>v=Xoib0gOcdXrQ^*61HNj z=NP|pd>@yfvr-=m{8$3A8TQGMTE7g=z!%yt`8`Bk-0MMwW~h^++;qyUP!J~ykh1GO z(FZ59xuFR$(WE;F@UUyE@Sp>`aVNjyj=Ty>_Vo}xf`e7`F;j-IgL5`1~-#70$9_=uBMq!2&1l zomRgpD58@)YYfvLtPW}{C5B35R;ZVvB<<#)x%srmc_S=A7F@DW8>QOEGwD6suhwCg z>Pa+YyULhmw%BA*4yjDp|2{!T98~<6Yfd(wo1mQ!KWwq0eg+6)o1>W~f~kL<-S+P@$wx*zeI|1t7z#Sxr5 zt6w+;YblPQNplq4Z#T$GLX#j6yldXAqj>4gAnnWtBICUnA&-dtnlh=t0Ho_vEKwV` z)DlJi#!@nkYV#$!)@>udAU*hF?V`2$Hf=V&6PP_|r#Iv*J$9)pF@X3`k;5})9^o4y z&)~?EjX5yX12O(BsFy-l6}nYeuKkiq`u9145&3Ssg^y{5G3Pse z9w(YVa0)N-fLaBq1`P!_#>SS(8fh_5!f{UrgZ~uEdeMJIz7DzI5!NHHqQtm~#CPij z?=N|J>nPR6_sL7!f4hD_|KH`vf8(Wpnj-(gPWH+ZvID}%?~68SwhPTC3u1_cB`otq z)U?6qo!ZLi5b>*KnYHWW=3F!p%h1;h{L&(Q&{qY6)_qxNfbP6E3yYpW!EO+IW3?@J z);4>g4gnl^8klu7uA>eGF6rIGSynacogr)KUwE_R4E5Xzi*Qir@b-jy55-JPC8c~( zo!W8y9OGZ&`xmc8;=4-U9=h{vCqfCNzYirONmGbRQlR`WWlgnY+1wCXbMz&NT~9*| z6@FrzP!LX&{no2!Ln_3|I==_4`@}V?4a;YZKTdw;vT<+K+z=uWbW(&bXEaWJ^W8Td z-3&1bY^Z*oM<=M}LVt>_j+p=2Iu7pZmbXrhQ_k)ysE9yXKygFNw$5hwDn(M>H+e1&9BM5!|81vd%r%vEm zqxY3?F@fb6O#5UunwgAHR9jp_W2zZ}NGp2%mTW@(hz7$^+a`A?mb8|_G*GNMJ) zjqegXQio=i@AINre&%ofexAr95aop5C+0MZ0m-l=MeO8m3epm7U%vZB8+I+C*iNFM z#T3l`gknX;D$-`2XT^Cg*vrv=RH+P;_dfF++cP?B_msQI4j+lt&rX2)3GaJx%W*Nn zkML%D{z5tpHH=dksQ*gzc|}gzW;lwAbxoR07VNgS*-c3d&8J|;@3t^ zVUz*J*&r7DFRuFVDCJDK8V9NN5hvpgGjwx+5n)qa;YCKe8TKtdnh{I7NU9BCN!0dq zczrBk8pE{{@vJa9ywR@mq*J=v+PG;?fwqlJVhijG!3VmIKs>9T6r7MJpC)m!Tc#>g zMtVsU>wbwFJEfwZ{vB|ZlttNe83)$iz`~#8UJ^r)lJ@HA&G#}W&ZH*;k{=TavpjWE z7hdyLZPf*X%Gm}i`Y{OGeeu^~nB8=`{r#TUrM-`;1cBvEd#d!kPqIgYySYhN-*1;L z^byj%Yi}Gx)Wnkosi337BKs}+5H5dth1JA{Ir-JKN$7zC)*}hqeoD(WfaUDPT>0`- z(6sa0AoIqASwF`>hP}^|)a_j2s^PQn*qVC{Q}htR z5-)duBFXT_V56-+UohKXlq~^6uf!6sA#ttk1o~*QEy_Y-S$gAvq47J9Vtk$5oA$Ct zYhYJ@8{hsC^98${!#Ho?4y5MCa7iGnfz}b9jE~h%EAAv~Qxu)_rAV;^cygV~5r_~?l=B`zObj7S=H=~$W zPtI_m%g$`kL_fVUk9J@>EiBH zOO&jtn~&`hIFMS5S`g8w94R4H40mdNUH4W@@XQk1sr17b{@y|JB*G9z1|CrQjd+GX z6+KyURG3;!*BQrentw{B2R&@2&`2}n(z-2&X7#r!{yg@Soy}cRD~j zj9@UBW+N|4HW4AWapy4wfUI- zZ`gSL6DUlgj*f1hSOGXG0IVH8HxK?o2|3HZ;KW{K+yPAlxtb)NV_2AwJm|E)FRs&& z=c^e7bvUsztY|+f^k7NXs$o1EUq>cR7C0$UKi6IooHWlK_#?IWDkvywnzg&ThWo^? z2O_N{5X39#?eV9l)xI(>@!vSB{DLt*oY!K1R8}_?%+0^C{d9a%N4 zoxHVT1&Lm|uDX%$QrBun5e-F`HJ^T$ zmzv)p@4ZHd_w9!%Hf9UYNvGCw2TTTbrj9pl+T9%-_-}L(tES>Or-}Z4F*{##n3~L~TuxjirGuIY#H7{%$E${?p{Q01 zi6T`n;rbK1yIB9jmQNycD~yZq&mbIsFWHo|ZAChSFPQa<(%d8mGw*V3fh|yFoxOOiWJd(qvVb!Z$b88cg->N=qO*4k~6;R==|9ihg&riu#P~s4Oap9O7f%crSr^rljeIfXDEg>wi)&v*a%7zpz<9w z*r!3q9J|390x`Zk;g$&OeN&ctp)VKRpDSV@kU2Q>jtok($Y-*x8_$2piTxun81@vt z!Vj?COa0fg2RPXMSIo26T=~0d`{oGP*eV+$!0I<(4azk&Vj3SiG=Q!6mX0p$z7I}; z9BJUFgT-K9MQQ-0@Z=^7R<{bn2Fm48endsSs`V7_@%8?Bxkqv>BDoVcj?K#dV#uUP zL1ND~?D-|VGKe3Rw_7-Idpht>H6XRLh*U7epS6byiGvJpr%d}XwfusjH9g;Z98H`x zyde%%5mhGOiL4wljCaWCk-&uE4_OOccb9c!ZaWt4B(wYl!?vyzl%7n~QepN&eFUrw zFIOl9c({``6~QD+43*_tzP{f2x41h(?b43^y6=iwyB)2os5hBE!@YUS5?N_tXd=h( z)WE286Fbd>R4M^P{!G)f;h<3Q>Fipuy+d2q-)!RyTgt;wr$(?9ox3;q+{E*ZQHhOn;lM`cjnu9 zXa48ks-v(~b*;MAI<>YZH(^NV8vjb34beE<_cwKlJoR;k6lJNSP6v}uiyRD?|0w+X@o1ONrH8a$fCxXpf? z?$DL0)7|X}Oc%h^zrMKWc-NS9I0Utu@>*j}b@tJ=ixQSJ={4@854wzW@E>VSL+Y{i z#0b=WpbCZS>kUCO_iQz)LoE>P5LIG-hv9E+oG}DtlIDF>$tJ1aw9^LuhLEHt?BCj& z(O4I8v1s#HUi5A>nIS-JK{v!7dJx)^Yg%XjNmlkWAq2*cv#tHgz`Y(bETc6CuO1VkN^L-L3j_x<4NqYb5rzrLC-7uOv z!5e`GZt%B782C5-fGnn*GhDF$%(qP<74Z}3xx+{$4cYKy2ikxI7B2N+2r07DN;|-T->nU&!=Cm#rZt%O_5c&1Z%nlWq3TKAW0w zQqemZw_ue--2uKQsx+niCUou?HjD`xhEjjQd3%rrBi82crq*~#uA4+>vR<_S{~5ce z-2EIl?~s z1=GVL{NxP1N3%=AOaC}j_Fv=ur&THz zyO!d9kHq|c73kpq`$+t+8Bw7MgeR5~`d7ChYyGCBWSteTB>8WAU(NPYt2Dk`@#+}= zI4SvLlyk#pBgVigEe`?NG*vl7V6m+<}%FwPV=~PvvA)=#ths==DRTDEYh4V5}Cf$z@#;< zyWfLY_5sP$gc3LLl2x+Ii)#b2nhNXJ{R~vk`s5U7Nyu^3yFg&D%Txwj6QezMX`V(x z=C`{76*mNb!qHHs)#GgGZ_7|vkt9izl_&PBrsu@}L`X{95-2jf99K)0=*N)VxBX2q z((vkpP2RneSIiIUEnGb?VqbMb=Zia+rF~+iqslydE34cSLJ&BJW^3knX@M;t*b=EA zNvGzv41Ld_T+WT#XjDB840vovUU^FtN_)G}7v)1lPetgpEK9YS^OWFkPoE{ovj^=@ zO9N$S=G$1ecndT_=5ehth2Lmd1II-PuT~C9`XVePw$y8J#dpZ?Tss<6wtVglm(Ok7 z3?^oi@pPio6l&!z8JY(pJvG=*pI?GIOu}e^EB6QYk$#FJQ%^AIK$I4epJ+9t?KjqA+bkj&PQ*|vLttme+`9G=L% ziadyMw_7-M)hS(3E$QGNCu|o23|%O+VN7;Qggp?PB3K-iSeBa2b}V4_wY`G1Jsfz4 z9|SdB^;|I8E8gWqHKx!vj_@SMY^hLEIbSMCuE?WKq=c2mJK z8LoG-pnY!uhqFv&L?yEuxo{dpMTsmCn)95xanqBrNPTgXP((H$9N${Ow~Is-FBg%h z53;|Y5$MUN)9W2HBe2TD`ct^LHI<(xWrw}$qSoei?}s)&w$;&!14w6B6>Yr6Y8b)S z0r71`WmAvJJ`1h&poLftLUS6Ir zC$bG9!Im_4Zjse)#K=oJM9mHW1{%l8sz$1o?ltdKlLTxWWPB>Vk22czVt|1%^wnN@*!l)}?EgtvhC>vlHm^t+ogpgHI1_$1ox9e;>0!+b(tBrmXRB`PY1vp-R**8N7 zGP|QqI$m(Rdu#=(?!(N}G9QhQ%o!aXE=aN{&wtGP8|_qh+7a_j_sU5|J^)vxq;# zjvzLn%_QPHZZIWu1&mRAj;Sa_97p_lLq_{~j!M9N^1yp3U_SxRqK&JnR%6VI#^E12 z>CdOVI^_9aPK2eZ4h&^{pQs}xsijXgFYRIxJ~N7&BB9jUR1fm!(xl)mvy|3e6-B3j zJn#ajL;bFTYJ2+Q)tDjx=3IklO@Q+FFM}6UJr6km7hj7th9n_&JR7fnqC!hTZoM~T zBeaVFp%)0cbPhejX<8pf5HyRUj2>aXnXBqDJe73~J%P(2C?-RT{c3NjE`)om! zl$uewSgWkE66$Kb34+QZZvRn`fob~Cl9=cRk@Es}KQm=?E~CE%spXaMO6YmrMl%9Q zlA3Q$3|L1QJ4?->UjT&CBd!~ru{Ih^in&JXO=|<6J!&qp zRe*OZ*cj5bHYlz!!~iEKcuE|;U4vN1rk$xq6>bUWD*u(V@8sG^7>kVuo(QL@Ki;yL zWC!FT(q{E8#on>%1iAS0HMZDJg{Z{^!De(vSIq&;1$+b)oRMwA3nc3mdTSG#3uYO_ z>+x;7p4I;uHz?ZB>dA-BKl+t-3IB!jBRgdvAbW!aJ(Q{aT>+iz?91`C-xbe)IBoND z9_Xth{6?(y3rddwY$GD65IT#f3<(0o#`di{sh2gm{dw*#-Vnc3r=4==&PU^hCv$qd zjw;>i&?L*Wq#TxG$mFIUf>eK+170KG;~+o&1;Tom9}}mKo23KwdEM6UonXgc z!6N(@k8q@HPw{O8O!lAyi{rZv|DpgfU{py+j(X_cwpKqcalcqKIr0kM^%Br3SdeD> zHSKV94Yxw;pjzDHo!Q?8^0bb%L|wC;4U^9I#pd5O&eexX+Im{ z?jKnCcsE|H?{uGMqVie_C~w7GX)kYGWAg%-?8|N_1#W-|4F)3YTDC+QSq1s!DnOML3@d`mG%o2YbYd#jww|jD$gotpa)kntakp#K;+yo-_ZF9qrNZw<%#C zuPE@#3RocLgPyiBZ+R_-FJ_$xP!RzWm|aN)S+{$LY9vvN+IW~Kf3TsEIvP+B9Mtm! zpfNNxObWQpLoaO&cJh5>%slZnHl_Q~(-Tfh!DMz(dTWld@LG1VRF`9`DYKhyNv z2pU|UZ$#_yUx_B_|MxUq^glT}O5Xt(Vm4Mr02><%C)@v;vPb@pT$*yzJ4aPc_FZ3z z3}PLoMBIM>q_9U2rl^sGhk1VUJ89=*?7|v`{!Z{6bqFMq(mYiA?%KbsI~JwuqVA9$H5vDE+VocjX+G^%bieqx->s;XWlKcuv(s%y%D5Xbc9+ zc(_2nYS1&^yL*ey664&4`IoOeDIig}y-E~_GS?m;D!xv5-xwz+G`5l6V+}CpeJDi^ z%4ed$qowm88=iYG+(`ld5Uh&>Dgs4uPHSJ^TngXP_V6fPyl~>2bhi20QB%lSd#yYn zO05?KT1z@?^-bqO8Cg`;ft>ilejsw@2%RR7;`$Vs;FmO(Yr3Fp`pHGr@P2hC%QcA|X&N2Dn zYf`MqXdHi%cGR@%y7Rg7?d3?an){s$zA{!H;Ie5exE#c~@NhQUFG8V=SQh%UxUeiV zd7#UcYqD=lk-}sEwlpu&H^T_V0{#G?lZMxL7ih_&{(g)MWBnCZxtXg znr#}>U^6!jA%e}@Gj49LWG@*&t0V>Cxc3?oO7LSG%~)Y5}f7vqUUnQ;STjdDU}P9IF9d9<$;=QaXc zL1^X7>fa^jHBu_}9}J~#-oz3Oq^JmGR#?GO7b9a(=R@fw@}Q{{@`Wy1vIQ#Bw?>@X z-_RGG@wt|%u`XUc%W{J z>iSeiz8C3H7@St3mOr_mU+&bL#Uif;+Xw-aZdNYUpdf>Rvu0i0t6k*}vwU`XNO2he z%miH|1tQ8~ZK!zmL&wa3E;l?!!XzgV#%PMVU!0xrDsNNZUWKlbiOjzH-1Uoxm8E#r`#2Sz;-o&qcqB zC-O_R{QGuynW14@)7&@yw1U}uP(1cov)twxeLus0s|7ayrtT8c#`&2~Fiu2=R;1_4bCaD=*E@cYI>7YSnt)nQc zohw5CsK%m?8Ack)qNx`W0_v$5S}nO|(V|RZKBD+btO?JXe|~^Qqur%@eO~<8-L^9d z=GA3-V14ng9L29~XJ>a5k~xT2152zLhM*@zlp2P5Eu}bywkcqR;ISbas&#T#;HZSf z2m69qTV(V@EkY(1Dk3`}j)JMo%ZVJ*5eB zYOjIisi+igK0#yW*gBGj?@I{~mUOvRFQR^pJbEbzFxTubnrw(Muk%}jI+vXmJ;{Q6 zrSobKD>T%}jV4Ub?L1+MGOD~0Ir%-`iTnWZN^~YPrcP5y3VMAzQ+&en^VzKEb$K!Q z<7Dbg&DNXuow*eD5yMr+#08nF!;%4vGrJI++5HdCFcGLfMW!KS*Oi@=7hFwDG!h2< zPunUEAF+HncQkbfFj&pbzp|MU*~60Z(|Ik%Tn{BXMN!hZOosNIseT?R;A`W?=d?5X zK(FB=9mZusYahp|K-wyb={rOpdn=@;4YI2W0EcbMKyo~-#^?h`BA9~o285%oY zfifCh5Lk$SY@|2A@a!T2V+{^!psQkx4?x0HSV`(w9{l75QxMk!)U52Lbhn{8ol?S) zCKo*7R(z!uk<6*qO=wh!Pul{(qq6g6xW;X68GI_CXp`XwO zxuSgPRAtM8K7}5E#-GM!*ydOOG_{A{)hkCII<|2=ma*71ci_-}VPARm3crFQjLYV! z9zbz82$|l01mv`$WahE2$=fAGWkd^X2kY(J7iz}WGS z@%MyBEO=A?HB9=^?nX`@nh;7;laAjs+fbo!|K^mE!tOB>$2a_O0y-*uaIn8k^6Y zSbuv;5~##*4Y~+y7Z5O*3w4qgI5V^17u*ZeupVGH^nM&$qmAk|anf*>r zWc5CV;-JY-Z@Uq1Irpb^O`L_7AGiqd*YpGUShb==os$uN3yYvb`wm6d=?T*it&pDk zo`vhw)RZX|91^^Wa_ti2zBFyWy4cJu#g)_S6~jT}CC{DJ_kKpT`$oAL%b^!2M;JgT zM3ZNbUB?}kP(*YYvXDIH8^7LUxz5oE%kMhF!rnPqv!GiY0o}NR$OD=ITDo9r%4E>E0Y^R(rS^~XjWyVI6 zMOR5rPXhTp*G*M&X#NTL`Hu*R+u*QNoiOKg4CtNPrjgH>c?Hi4MUG#I917fx**+pJfOo!zFM&*da&G_x)L(`k&TPI*t3e^{crd zX<4I$5nBQ8Ax_lmNRa~E*zS-R0sxkz`|>7q_?*e%7bxqNm3_eRG#1ae3gtV9!fQpY z+!^a38o4ZGy9!J5sylDxZTx$JmG!wg7;>&5H1)>f4dXj;B+@6tMlL=)cLl={jLMxY zbbf1ax3S4>bwB9-$;SN2?+GULu;UA-35;VY*^9Blx)Jwyb$=U!D>HhB&=jSsd^6yw zL)?a|>GxU!W}ocTC(?-%z3!IUhw^uzc`Vz_g>-tv)(XA#JK^)ZnC|l1`@CdX1@|!| z_9gQ)7uOf?cR@KDp97*>6X|;t@Y`k_N@)aH7gY27)COv^P3ya9I{4z~vUjLR9~z1Z z5=G{mVtKH*&$*t0@}-i_v|3B$AHHYale7>E+jP`ClqG%L{u;*ff_h@)al?RuL7tOO z->;I}>%WI{;vbLP3VIQ^iA$4wl6@0sDj|~112Y4OFjMs`13!$JGkp%b&E8QzJw_L5 zOnw9joc0^;O%OpF$Qp)W1HI!$4BaXX84`%@#^dk^hFp^pQ@rx4g(8Xjy#!X%+X5Jd@fs3amGT`}mhq#L97R>OwT5-m|h#yT_-v@(k$q7P*9X~T*3)LTdzP!*B} z+SldbVWrrwQo9wX*%FyK+sRXTa@O?WM^FGWOE?S`R(0P{<6p#f?0NJvnBia?k^fX2 zNQs7K-?EijgHJY}&zsr;qJ<*PCZUd*x|dD=IQPUK_nn)@X4KWtqoJNHkT?ZWL_hF? zS8lp2(q>;RXR|F;1O}EE#}gCrY~#n^O`_I&?&z5~7N;zL0)3Tup`%)oHMK-^r$NT% zbFg|o?b9w(q@)6w5V%si<$!U<#}s#x@0aX-hP>zwS#9*75VXA4K*%gUc>+yzupTDBOKH8WR4V0pM(HrfbQ&eJ79>HdCvE=F z|J>s;;iDLB^3(9}?biKbxf1$lI!*Z%*0&8UUq}wMyPs_hclyQQi4;NUY+x2qy|0J; zhn8;5)4ED1oHwg+VZF|80<4MrL97tGGXc5Sw$wAI#|2*cvQ=jB5+{AjMiDHmhUC*a zlmiZ`LAuAn_}hftXh;`Kq0zblDk8?O-`tnilIh|;3lZp@F_osJUV9`*R29M?7H{Fy z`nfVEIDIWXmU&YW;NjU8)EJpXhxe5t+scf|VXM!^bBlwNh)~7|3?fWwo_~ZFk(22% zTMesYw+LNx3J-_|DM~`v93yXe=jPD{q;li;5PD?Dyk+b? zo21|XpT@)$BM$%F=P9J19Vi&1#{jM3!^Y&fr&_`toi`XB1!n>sbL%U9I5<7!@?t)~ z;&H%z>bAaQ4f$wIzkjH70;<8tpUoxzKrPhn#IQfS%9l5=Iu))^XC<58D!-O z{B+o5R^Z21H0T9JQ5gNJnqh#qH^na|z92=hONIM~@_iuOi|F>jBh-?aA20}Qx~EpDGElELNn~|7WRXRFnw+Wdo`|# zBpU=Cz3z%cUJ0mx_1($X<40XEIYz(`noWeO+x#yb_pwj6)R(__%@_Cf>txOQ74wSJ z0#F3(zWWaR-jMEY$7C*3HJrohc79>MCUu26mfYN)f4M~4gD`}EX4e}A!U}QV8!S47 z6y-U-%+h`1n`*pQuKE%Av0@)+wBZr9mH}@vH@i{v(m-6QK7Ncf17x_D=)32`FOjjo zg|^VPf5c6-!FxN{25dvVh#fog=NNpXz zfB$o+0jbRkHH{!TKhE709f+jI^$3#v1Nmf80w`@7-5$1Iv_`)W^px8P-({xwb;D0y z7LKDAHgX<84?l!I*Dvi2#D@oAE^J|g$3!)x1Ua;_;<@#l1fD}lqU2_tS^6Ht$1Wl} zBESo7o^)9-Tjuz$8YQSGhfs{BQV6zW7dA?0b(Dbt=UnQs&4zHfe_sj{RJ4uS-vQpC zX;Bbsuju4%!o8?&m4UZU@~ZZjeFF6ex2ss5_60_JS_|iNc+R0GIjH1@Z z=rLT9%B|WWgOrR7IiIwr2=T;Ne?30M!@{%Qf8o`!>=s<2CBpCK_TWc(DX51>e^xh8 z&@$^b6CgOd7KXQV&Y4%}_#uN*mbanXq(2=Nj`L7H7*k(6F8s6{FOw@(DzU`4-*77{ zF+dxpv}%mFpYK?>N_2*#Y?oB*qEKB}VoQ@bzm>ptmVS_EC(#}Lxxx730trt0G)#$b zE=wVvtqOct1%*9}U{q<)2?{+0TzZzP0jgf9*)arV)*e!f`|jgT{7_9iS@e)recI#z zbzolURQ+TOzE!ymqvBY7+5NnAbWxvMLsLTwEbFqW=CPyCsmJ}P1^V30|D5E|p3BC5 z)3|qgw@ra7aXb-wsa|l^in~1_fm{7bS9jhVRkYVO#U{qMp z)Wce+|DJ}4<2gp8r0_xfZpMo#{Hl2MfjLcZdRB9(B(A(f;+4s*FxV{1F|4d`*sRNd zp4#@sEY|?^FIJ;tmH{@keZ$P(sLh5IdOk@k^0uB^BWr@pk6mHy$qf&~rI>P*a;h0C{%oA*i!VjWn&D~O#MxN&f@1Po# zKN+ zrGrkSjcr?^R#nGl<#Q722^wbYcgW@{+6CBS<1@%dPA8HC!~a`jTz<`g_l5N1M@9wn9GOAZ>nqNgq!yOCbZ@1z`U_N`Z>}+1HIZxk*5RDc&rd5{3qjRh8QmT$VyS;jK z;AF+r6XnnCp=wQYoG|rT2@8&IvKq*IB_WvS%nt%e{MCFm`&W*#LXc|HrD?nVBo=(8*=Aq?u$sDA_sC_RPDUiQ+wnIJET8vx$&fxkW~kP9qXKt zozR)@xGC!P)CTkjeWvXW5&@2?)qt)jiYWWBU?AUtzAN}{JE1I)dfz~7$;}~BmQF`k zpn11qmObXwRB8&rnEG*#4Xax3XBkKlw(;tb?Np^i+H8m(Wyz9k{~ogba@laiEk;2! zV*QV^6g6(QG%vX5Um#^sT&_e`B1pBW5yVth~xUs#0}nv?~C#l?W+9Lsb_5)!71rirGvY zTIJ$OPOY516Y|_014sNv+Z8cc5t_V=i>lWV=vNu#!58y9Zl&GsMEW#pPYPYGHQ|;vFvd*9eM==$_=vc7xnyz0~ zY}r??$<`wAO?JQk@?RGvkWVJlq2dk9vB(yV^vm{=NVI8dhsX<)O(#nr9YD?I?(VmQ z^r7VfUBn<~p3()8yOBjm$#KWx!5hRW)5Jl7wY@ky9lNM^jaT##8QGVsYeaVywmpv>X|Xj7gWE1Ezai&wVLt3p)k4w~yrskT-!PR!kiyQlaxl(( zXhF%Q9x}1TMt3~u@|#wWm-Vq?ZerK={8@~&@9r5JW}r#45#rWii};t`{5#&3$W)|@ zbAf2yDNe0q}NEUvq_Quq3cTjcw z@H_;$hu&xllCI9CFDLuScEMg|x{S7GdV8<&Mq=ezDnRZAyX-8gv97YTm0bg=d)(>N z+B2FcqvI9>jGtnK%eO%y zoBPkJTk%y`8TLf4)IXPBn`U|9>O~WL2C~C$z~9|0m*YH<-vg2CD^SX#&)B4ngOSG$ zV^wmy_iQk>dfN@Pv(ckfy&#ak@MLC7&Q6Ro#!ezM*VEh`+b3Jt%m(^T&p&WJ2Oqvj zs-4nq0TW6cv~(YI$n0UkfwN}kg3_fp?(ijSV#tR9L0}l2qjc7W?i*q01=St0eZ=4h zyGQbEw`9OEH>NMuIe)hVwYHsGERWOD;JxEiO7cQv%pFCeR+IyhwQ|y@&^24k+|8fD zLiOWFNJ2&vu2&`Jv96_z-Cd5RLgmeY3*4rDOQo?Jm`;I_(+ejsPM03!ly!*Cu}Cco zrQSrEDHNyzT(D5s1rZq!8#?f6@v6dB7a-aWs(Qk>N?UGAo{gytlh$%_IhyL7h?DLXDGx zgxGEBQoCAWo-$LRvM=F5MTle`M})t3vVv;2j0HZY&G z22^iGhV@uaJh(XyyY%} zd4iH_UfdV#T=3n}(Lj^|n;O4|$;xhu*8T3hR1mc_A}fK}jfZ7LX~*n5+`8N2q#rI$ z@<_2VANlYF$vIH$ zl<)+*tIWW78IIINA7Rr7i{<;#^yzxoLNkXL)eSs=%|P>$YQIh+ea_3k z_s7r4%j7%&*NHSl?R4k%1>Z=M9o#zxY!n8sL5>BO-ZP;T3Gut>iLS@U%IBrX6BA3k z)&@q}V8a{X<5B}K5s(c(LQ=%v1ocr`t$EqqY0EqVjr65usa=0bkf|O#ky{j3)WBR(((L^wmyHRzoWuL2~WTC=`yZ zn%VX`L=|Ok0v7?s>IHg?yArBcync5rG#^+u)>a%qjES%dRZoIyA8gQ;StH z1Ao7{<&}6U=5}4v<)1T7t!J_CL%U}CKNs-0xWoTTeqj{5{?Be$L0_tk>M9o8 zo371}S#30rKZFM{`H_(L`EM9DGp+Mifk&IP|C2Zu_)Ghr4Qtpmkm1osCf@%Z$%t+7 zYH$Cr)Ro@3-QDeQJ8m+x6%;?YYT;k6Z0E-?kr>x33`H%*ueBD7Zx~3&HtWn0?2Wt} zTG}*|v?{$ajzt}xPzV%lL1t-URi8*Zn)YljXNGDb>;!905Td|mpa@mHjIH%VIiGx- zd@MqhpYFu4_?y5N4xiHn3vX&|e6r~Xt> zZG`aGq|yTNjv;9E+Txuoa@A(9V7g?1_T5FzRI;!=NP1Kqou1z5?%X~Wwb{trRfd>i z8&y^H)8YnKyA_Fyx>}RNmQIczT?w2J4SNvI{5J&}Wto|8FR(W;Qw#b1G<1%#tmYzQ zQ2mZA-PAdi%RQOhkHy9Ea#TPSw?WxwL@H@cbkZwIq0B!@ns}niALidmn&W?!Vd4Gj zO7FiuV4*6Mr^2xlFSvM;Cp_#r8UaqIzHJQg_z^rEJw&OMm_8NGAY2)rKvki|o1bH~ z$2IbfVeY2L(^*rMRU1lM5Y_sgrDS`Z??nR2lX;zyR=c%UyGb*%TC-Dil?SihkjrQy~TMv6;BMs7P8il`H7DmpVm@rJ;b)hW)BL)GjS154b*xq-NXq2cwE z^;VP7ua2pxvCmxrnqUYQMH%a%nHmwmI33nJM(>4LznvY*k&C0{8f*%?zggpDgkuz&JBx{9mfb@wegEl2v!=}Sq2Gaty0<)UrOT0{MZtZ~j5y&w zXlYa_jY)I_+VA-^#mEox#+G>UgvM!Ac8zI<%JRXM_73Q!#i3O|)lOP*qBeJG#BST0 zqohi)O!|$|2SeJQo(w6w7%*92S})XfnhrH_Z8qe!G5>CglP=nI7JAOW?(Z29;pXJ9 zR9`KzQ=WEhy*)WH>$;7Cdz|>*i>=##0bB)oU0OR>>N<21e4rMCHDemNi2LD>Nc$;& zQRFthpWniC1J6@Zh~iJCoLOxN`oCKD5Q4r%ynwgUKPlIEd#?QViIqovY|czyK8>6B zSP%{2-<;%;1`#0mG^B(8KbtXF;Nf>K#Di72UWE4gQ%(_26Koiad)q$xRL~?pN71ZZ zujaaCx~jXjygw;rI!WB=xrOJO6HJ!!w}7eiivtCg5K|F6$EXa)=xUC za^JXSX98W`7g-tm@uo|BKj39Dl;sg5ta;4qjo^pCh~{-HdLl6qI9Ix6f$+qiZ$}s= zNguKrU;u+T@ko(Vr1>)Q%h$?UKXCY>3se%&;h2osl2D zE4A9bd7_|^njDd)6cI*FupHpE3){4NQ*$k*cOWZ_?CZ>Z4_fl@n(mMnYK62Q1d@+I zr&O))G4hMihgBqRIAJkLdk(p(D~X{-oBUA+If@B}j& zsHbeJ3RzTq96lB7d($h$xTeZ^gP0c{t!Y0c)aQE;$FY2!mACg!GDEMKXFOPI^)nHZ z`aSPJpvV0|bbrzhWWkuPURlDeN%VT8tndV8?d)eN*i4I@u zVKl^6{?}A?P)Fsy?3oi#clf}L18t;TjNI2>eI&(ezDK7RyqFxcv%>?oxUlonv(px) z$vnPzRH`y5A(x!yOIfL0bmgeMQB$H5wenx~!ujQK*nUBW;@Em&6Xv2%s(~H5WcU2R z;%Nw<$tI)a`Ve!>x+qegJnQsN2N7HaKzrFqM>`6R*gvh%O*-%THt zrB$Nk;lE;z{s{r^PPm5qz(&lM{sO*g+W{sK+m3M_z=4=&CC>T`{X}1Vg2PEfSj2x_ zmT*(x;ov%3F?qoEeeM>dUn$a*?SIGyO8m806J1W1o+4HRhc2`9$s6hM#qAm zChQ87b~GEw{ADfs+5}FJ8+|bIlIv(jT$Ap#hSHoXdd9#w<#cA<1Rkq^*EEkknUd4& zoIWIY)sAswy6fSERVm&!SO~#iN$OgOX*{9@_BWFyJTvC%S++ilSfCrO(?u=Dc?CXZ zzCG&0yVR{Z`|ZF0eEApWEo#s9osV>F{uK{QA@BES#&;#KsScf>y zvs?vIbI>VrT<*!;XmQS=bhq%46-aambZ(8KU-wOO2=en~D}MCToB_u;Yz{)1ySrPZ z@=$}EvjTdzTWU7c0ZI6L8=yP+YRD_eMMos}b5vY^S*~VZysrkq<`cK3>>v%uy7jgq z0ilW9KjVDHLv0b<1K_`1IkbTOINs0=m-22c%M~l=^S}%hbli-3?BnNq?b`hx^HX2J zIe6ECljRL0uBWb`%{EA=%!i^4sMcj+U_TaTZRb+~GOk z^ZW!nky0n*Wb*r+Q|9H@ml@Z5gU&W`(z4-j!OzC1wOke`TRAYGZVl$PmQ16{3196( zO*?`--I}Qf(2HIwb2&1FB^!faPA2=sLg(@6P4mN)>Dc3i(B0;@O-y2;lM4akD>@^v z=u>*|!s&9zem70g7zfw9FXl1bpJW(C#5w#uy5!V?Q(U35A~$dR%LDVnq@}kQm13{} zd53q3N(s$Eu{R}k2esbftfjfOITCL;jWa$}(mmm}d(&7JZ6d3%IABCapFFYjdEjdK z&4Edqf$G^MNAtL=uCDRs&Fu@FXRgX{*0<(@c3|PNHa>L%zvxWS={L8%qw`STm+=Rd zA}FLspESSIpE_^41~#5yI2bJ=9`oc;GIL!JuW&7YetZ?0H}$$%8rW@*J37L-~Rsx!)8($nI4 zZhcZ2^=Y+p4YPl%j!nFJA|*M^gc(0o$i3nlphe+~-_m}jVkRN{spFs(o0ajW@f3K{ zDV!#BwL322CET$}Y}^0ixYj2w>&Xh12|R8&yEw|wLDvF!lZ#dOTHM9pK6@Nm-@9Lnng4ZHBgBSrr7KI8YCC9DX5Kg|`HsiwJHg2(7#nS;A{b3tVO?Z% za{m5b3rFV6EpX;=;n#wltDv1LE*|g5pQ+OY&*6qCJZc5oDS6Z6JD#6F)bWxZSF@q% z+1WV;m!lRB!n^PC>RgQCI#D1br_o^#iPk>;K2hB~0^<~)?p}LG%kigm@moD#q3PE+ zA^Qca)(xnqw6x>XFhV6ku9r$E>bWNrVH9fum0?4s?Rn2LG{Vm_+QJHse6xa%nzQ?k zKug4PW~#Gtb;#5+9!QBgyB@q=sk9=$S{4T>wjFICStOM?__fr+Kei1 z3j~xPqW;W@YkiUM;HngG!;>@AITg}vAE`M2Pj9Irl4w1fo4w<|Bu!%rh%a(Ai^Zhi zs92>v5;@Y(Zi#RI*ua*h`d_7;byQSa*v9E{2x$<-_=5Z<7{%)}4XExANcz@rK69T0x3%H<@frW>RA8^swA+^a(FxK| zFl3LD*ImHN=XDUkrRhp6RY5$rQ{bRgSO*(vEHYV)3Mo6Jy3puiLmU&g82p{qr0F?ohmbz)f2r{X2|T2 z$4fdQ=>0BeKbiVM!e-lIIs8wVTuC_m7}y4A_%ikI;Wm5$9j(^Y z(cD%U%k)X>_>9~t8;pGzL6L-fmQO@K; zo&vQzMlgY95;1BSkngY)e{`n0!NfVgf}2mB3t}D9@*N;FQ{HZ3Pb%BK6;5#-O|WI( zb6h@qTLU~AbVW#_6?c!?Dj65Now7*pU{h!1+eCV^KCuPAGs28~3k@ueL5+u|Z-7}t z9|lskE`4B7W8wMs@xJa{#bsCGDFoRSNSnmNYB&U7 zVGKWe%+kFB6kb)e;TyHfqtU6~fRg)f|>=5(N36)0+C z`hv65J<$B}WUc!wFAb^QtY31yNleq4dzmG`1wHTj=c*=hay9iD071Hc?oYoUk|M*_ zU1GihAMBsM@5rUJ(qS?9ZYJ6@{bNqJ`2Mr+5#hKf?doa?F|+^IR!8lq9)wS3tF_9n zW_?hm)G(M+MYb?V9YoX^_mu5h-LP^TL^!Q9Z7|@sO(rg_4+@=PdI)WL(B7`!K^ND- z-uIuVDCVEdH_C@c71YGYT^_Scf_dhB8Z2Xy6vGtBSlYud9vggOqv^L~F{BraSE_t} zIkP+Hp2&nH^-MNEs}^`oMLy11`PQW$T|K(`Bu*(f@)mv1-qY(_YG&J2M2<7k;;RK~ zL{Fqj9yCz8(S{}@c)S!65aF<=&eLI{hAMErCx&>i7OeDN>okvegO87OaG{Jmi<|}D zaT@b|0X{d@OIJ7zvT>r+eTzgLq~|Dpu)Z&db-P4z*`M$UL51lf>FLlq6rfG)%doyp z)3kk_YIM!03eQ8Vu_2fg{+osaEJPtJ-s36R+5_AEG12`NG)IQ#TF9c@$99%0iye+ zUzZ57=m2)$D(5Nx!n)=5Au&O0BBgwxIBaeI(mro$#&UGCr<;C{UjJVAbVi%|+WP(a zL$U@TYCxJ=1{Z~}rnW;7UVb7+ZnzgmrogDxhjLGo>c~MiJAWs&&;AGg@%U?Y^0JhL ze(x6Z74JG6FlOFK(T}SXQfhr}RIFl@QXKnIcXYF)5|V~e-}suHILKT-k|<*~Ij|VF zC;t@=uj=hot~*!C68G8hTA%8SzOfETOXQ|3FSaIEjvBJp(A)7SWUi5!Eu#yWgY+;n zlm<$+UDou*V+246_o#V4kMdto8hF%%Lki#zPh}KYXmMf?hrN0;>Mv%`@{0Qn`Ujp) z=lZe+13>^Q!9zT);H<(#bIeRWz%#*}sgUX9P|9($kexOyKIOc`dLux}c$7It4u|Rl z6SSkY*V~g_B-hMPo_ak>>z@AVQ(_N)VY2kB3IZ0G(iDUYw+2d7W^~(Jq}KY=JnWS( z#rzEa&0uNhJ>QE8iiyz;n2H|SV#Og+wEZv=f2%1ELX!SX-(d3tEj$5$1}70Mp<&eI zCkfbByL7af=qQE@5vDVxx1}FSGt_a1DoE3SDI+G)mBAna)KBG4p8Epxl9QZ4BfdAN zFnF|Y(umr;gRgG6NLQ$?ZWgllEeeq~z^ZS7L?<(~O&$5|y)Al^iMKy}&W+eMm1W z7EMU)u^ke(A1#XCV>CZ71}P}0x)4wtHO8#JRG3MA-6g=`ZM!FcICCZ{IEw8Dm2&LQ z1|r)BUG^0GzI6f946RrBlfB1Vs)~8toZf~7)+G;pv&XiUO(%5bm)pl=p>nV^o*;&T z;}@oZSibzto$arQgfkp|z4Z($P>dTXE{4O=vY0!)kDO* zGF8a4wq#VaFpLfK!iELy@?-SeRrdz%F*}hjKcA*y@mj~VD3!it9lhRhX}5YOaR9$} z3mS%$2Be7{l(+MVx3 z(4?h;P!jnRmX9J9sYN#7i=iyj_5q7n#X(!cdqI2lnr8T$IfOW<_v`eB!d9xY1P=2q&WtOXY=D9QYteP)De?S4}FK6#6Ma z=E*V+#s8>L;8aVroK^6iKo=MH{4yEZ_>N-N z`(|;aOATba1^asjxlILk<4}f~`39dBFlxj>Dw(hMYKPO3EEt1@S`1lxFNM+J@uB7T zZ8WKjz7HF1-5&2=l=fqF-*@>n5J}jIxdDwpT?oKM3s8Nr`x8JnN-kCE?~aM1H!hAE z%%w(3kHfGwMnMmNj(SU(w42OrC-euI>Dsjk&jz3ts}WHqmMpzQ3vZrsXrZ|}+MHA7 z068obeXZTsO*6RS@o3x80E4ok``rV^Y3hr&C1;|ZZ0|*EKO`$lECUYG2gVFtUTw)R z4Um<0ZzlON`zTdvVdL#KFoMFQX*a5wM0Czp%wTtfK4Sjs)P**RW&?lP$(<}q%r68Z zS53Y!d@&~ne9O)A^tNrXHhXBkj~$8j%pT1%%mypa9AW5E&s9)rjF4@O3ytH{0z6riz|@< zB~UPh*wRFg2^7EbQrHf0y?E~dHlkOxof_a?M{LqQ^C!i2dawHTPYUE=X@2(3<=OOxs8qn_(y>pU>u^}3y&df{JarR0@VJn0f+U%UiF=$Wyq zQvnVHESil@d|8&R<%}uidGh7@u^(%?$#|&J$pvFC-n8&A>utA=n3#)yMkz+qnG3wd zP7xCnF|$9Dif@N~L)Vde3hW8W!UY0BgT2v(wzp;tlLmyk2%N|0jfG$%<;A&IVrOI< z!L)o>j>;dFaqA3pL}b-Je(bB@VJ4%!JeX@3x!i{yIeIso^=n?fDX`3bU=eG7sTc%g%ye8$v8P@yKE^XD=NYxTb zbf!Mk=h|otpqjFaA-vs5YOF-*GwWPc7VbaOW&stlANnCN8iftFMMrUdYNJ_Bnn5Vt zxfz@Ah|+4&P;reZxp;MmEI7C|FOv8NKUm8njF7Wb6Gi7DeODLl&G~}G4be&*Hi0Qw z5}77vL0P+7-B%UL@3n1&JPxW^d@vVwp?u#gVcJqY9#@-3X{ok#UfW3<1fb%FT`|)V~ggq z(3AUoUS-;7)^hCjdT0Kf{i}h)mBg4qhtHHBti=~h^n^OTH5U*XMgDLIR@sre`AaB$ zg)IGBET_4??m@cx&c~bA80O7B8CHR7(LX7%HThkeC*@vi{-pL%e)yXp!B2InafbDF zjPXf1mko3h59{lT6EEbxKO1Z5GF71)WwowO6kY|6tjSVSWdQ}NsK2x{>i|MKZK8%Q zfu&_0D;CO-Jg0#YmyfctyJ!mRJp)e#@O0mYdp|8x;G1%OZQ3Q847YWTyy|%^cpA;m zze0(5p{tMu^lDkpe?HynyO?a1$_LJl2L&mpeKu%8YvgRNr=%2z${%WThHG=vrWY@4 zsA`OP#O&)TetZ>s%h!=+CE15lOOls&nvC~$Qz0Ph7tHiP;O$i|eDwpT{cp>+)0-|; zY$|bB+Gbel>5aRN3>c0x)4U=|X+z+{ zn*_p*EQoquRL+=+p;=lm`d71&1NqBz&_ph)MXu(Nv6&XE7(RsS)^MGj5Q?Fwude-(sq zjJ>aOq!7!EN>@(fK7EE#;i_BGvli`5U;r!YA{JRodLBc6-`n8K+Fjgwb%sX;j=qHQ z7&Tr!)!{HXoO<2BQrV9Sw?JRaLXV8HrsNevvnf>Y-6|{T!pYLl7jp$-nEE z#X!4G4L#K0qG_4Z;Cj6=;b|Be$hi4JvMH!-voxqx^@8cXp`B??eFBz2lLD8RRaRGh zn7kUfy!YV~p(R|p7iC1Rdgt$_24i0cd-S8HpG|`@my70g^y`gu%#Tf_L21-k?sRRZHK&at(*ED0P8iw{7?R$9~OF$Ko;Iu5)ur5<->x!m93Eb zFYpIx60s=Wxxw=`$aS-O&dCO_9?b1yKiPCQmSQb>T)963`*U+Ydj5kI(B(B?HNP8r z*bfSBpSu)w(Z3j7HQoRjUG(+d=IaE~tv}y14zHHs|0UcN52fT8V_<@2ep_ee{QgZG zmgp8iv4V{k;~8@I%M3<#B;2R>Ef(Gg_cQM7%}0s*^)SK6!Ym+~P^58*wnwV1BW@eG z4sZLqsUvBbFsr#8u7S1r4teQ;t)Y@jnn_m5jS$CsW1um!p&PqAcc8!zyiXHVta9QC zY~wCwCF0U%xiQPD_INKtTb;A|Zf29(mu9NI;E zc-e>*1%(LSXB`g}kd`#}O;veb<(sk~RWL|f3ljxCnEZDdNSTDV6#Td({6l&y4IjKF z^}lIUq*ZUqgTPumD)RrCN{M^jhY>E~1pn|KOZ5((%F)G|*ZQ|r4zIbrEiV%42hJV8 z3xS)=!X1+=olbdGJ=yZil?oXLct8FM{(6ikLL3E%=q#O6(H$p~gQu6T8N!plf!96| z&Q3=`L~>U0zZh;z(pGR2^S^{#PrPxTRHD1RQOON&f)Siaf`GLj#UOk&(|@0?zm;Sx ztsGt8=29-MZs5CSf1l1jNFtNt5rFNZxJPvkNu~2}7*9468TWm>nN9TP&^!;J{-h)_ z7WsHH9|F%I`Pb!>KAS3jQWKfGivTVkMJLO-HUGM_a4UQ_%RgL6WZvrW+Z4ujZn;y@ zz9$=oO!7qVTaQAA^BhX&ZxS*|5dj803M=k&2%QrXda`-Q#IoZL6E(g+tN!6CA!CP* zCpWtCujIea)ENl0liwVfj)Nc<9mV%+e@=d`haoZ*`B7+PNjEbXBkv=B+Pi^~L#EO$D$ZqTiD8f<5$eyb54-(=3 zh)6i8i|jp(@OnRrY5B8t|LFXFQVQ895n*P16cEKTrT*~yLH6Z4e*bZ5otpRDri&+A zfNbK1D5@O=sm`fN=WzWyse!za5n%^+6dHPGX#8DyIK>?9qyX}2XvBWVqbP%%D)7$= z=#$WulZlZR<{m#gU7lwqK4WS1Ne$#_P{b17qe$~UOXCl>5b|6WVh;5vVnR<%d+Lnp z$uEmML38}U4vaW8>shm6CzB(Wei3s#NAWE3)a2)z@i{4jTn;;aQS)O@l{rUM`J@K& l00vQ5JBs~;vo!vr%%-k{2_Fq1Mn4QF81S)AQ99zk{{c4yR+0b! diff --git a/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties b/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 1af9e093..00000000 --- a/data-contracts-with-terraform/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/data-contracts-with-terraform/gradlew b/data-contracts-with-terraform/gradlew deleted file mode 100755 index 1aa94a42..00000000 --- a/data-contracts-with-terraform/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/data-contracts-with-terraform/gradlew.bat b/data-contracts-with-terraform/gradlew.bat deleted file mode 100644 index 6689b85b..00000000 --- a/data-contracts-with-terraform/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts new file mode 100644 index 00000000..ede54dca --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts @@ -0,0 +1,83 @@ +import java.io.FileInputStream +import java.util.Properties + +buildscript { + repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") + gradlePluginPortal() + } +} + +plugins { + kotlin("jvm") version "2.0.21" + id("com.google.protobuf") version "0.9.4" + id("com.github.imflog.kafka-schema-registry-gradle-plugin") version "2.1.0" + id("com.bakdata.avro") version "1.2.1" +} + +group = "io.confluent.devrel" + +repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") +} + +sourceSets { + main { + kotlin.srcDirs("src/main/kotlin", "build/generated-main-avro-java") + } +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + implementation(project(":data-contracts-with-terraform:shared")) + + implementation("org.apache.kafka:kafka-clients:${project.property("kafkaVersion")}") + implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") + + implementation("io.github.serpro69:kotlin-faker:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-books:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-tech:${project.property("fakerVersion")}") + + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") +} + +kotlin { + jvmToolchain(17) +} + +schemaRegistry { + val srProperties = Properties() + srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + + url = srProperties.getProperty("schema.registry.url") + + val srCredTokens = srProperties.get("basic.auth.user.info").toString().split(":") + credentials { + username = srCredTokens[0] + password = srCredTokens[1] + } + outputDirectory = "${System.getProperty("user.home")}/tmp/schema-registry-plugin" + pretty = true + + download { + // download the membership avro schema, version 1 + subject("membership-avro-value", "${projectDir}/src/main/avro", 1) + } +} + +tasks.clean { + doFirst { + delete(fileTree("${projectDir}/src/main/avro/").include("**/*.avsc")) + } +} + +tasks.register("generateCode") { + group = "source generation" + description = "wrapper task for all source generation" + dependsOn("downloadSchemasTask", "generateAvroJava", "generateProto") +} diff --git a/data-contracts-with-terraform/producer-app-schema-v1/gradle.properties b/data-contracts-with-terraform/producer-app-schema-v1/gradle.properties new file mode 100644 index 00000000..3fd23b72 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/gradle.properties @@ -0,0 +1,6 @@ +confluentVersion=7.8.0 +fakerVersion=2.0.0-rc.3 +grpcVersion=1.15.1 +kafkaVersion=3.8.0 +protobufVersion=3.6.1 +slf4jVersion=2.0.11 diff --git a/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts new file mode 100644 index 00000000..7ee4de2d --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts @@ -0,0 +1,7 @@ +rootProject.name="producer-app-schema-v1" + +//include(":common") +//project(":common").projectDir("../../common") + +//include(":data-contracts-with-terraform:shared") +//project(":data-contracts-with-terraform:shared").projectDir("../shared") \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore b/data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore new file mode 100644 index 00000000..c5cab8d4 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore @@ -0,0 +1 @@ +*.avsc \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt new file mode 100644 index 00000000..4e4a0951 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt @@ -0,0 +1,72 @@ +package io.confluent.devrel.dc.v1 + +import io.confluent.devrel.Membership +import io.confluent.devrel.dc.v1.kafka.MembershipConsumer +import io.confluent.devrel.dc.v1.kafka.MembershipProducer +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import java.time.LocalDate +import java.util.* +import kotlin.concurrent.thread +import kotlin.random.Random +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class ApplicationMain { + + companion object { + + + @JvmStatic + fun main(args: Array) { + runBlocking { + println("Starting application main...") + println(args.joinToString(" ")) + val parser = ArgParser("schema-v1") + + val interval by parser.option(ArgType.Int, + shortName = "i", fullName = "interval", + description = "message send interval, seconds") + .default(1) + val duration by parser.option(ArgType.Int, + shortName = "d", fullName = "duration", + description = "how long to run, seconds") + .default(100) + parser.parse(args) + + val messageInterval = interval.toDuration(DurationUnit.SECONDS) + val sendDuration = duration.toDuration(DurationUnit.SECONDS) + + val producer = MembershipProducer() + val consumer = MembershipConsumer() + + thread { + consumer.start(listOf("membership-avro")) + } + + coroutineScope { + launch { + val until = Clock.System.now().plus(sendDuration) + while(Clock.System.now().compareTo(until) < 0) { + val userId = UUID.randomUUID().toString() + val membership = Membership.newBuilder() + .setUserId(userId) + .setStartDate(LocalDate.now().minusDays(Random.nextLong(100, 1000))) + .setEndDate(LocalDate.now().plusWeeks(Random.nextLong(1, 52))) + .build() + producer.send("membership-avro", userId, membership) + delay(messageInterval.inWholeSeconds) + } + } + } + producer.close() + } + } + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt new file mode 100644 index 00000000..6777230d --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt @@ -0,0 +1,24 @@ +package io.confluent.devrel.dc.v1.kafka + +import io.confluent.devrel.Membership +import io.confluent.devrel.datacontracts.shared.BaseConsumer +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig +import org.apache.kafka.clients.consumer.ConsumerConfig + +class MembershipConsumer: BaseConsumer(mapOf( + ConsumerConfig.GROUP_ID_CONFIG to "app-schema-v1", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringDeserializer", + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroDeserializer", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION to false, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=1" +)) { + + override fun consumeRecord( + key: String, + value: Membership + ) { + logger.info("Received Membership ${key}, ${value}") + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt new file mode 100644 index 00000000..0aeb53c3 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt @@ -0,0 +1,15 @@ +package io.confluent.devrel.dc.v1.kafka + +import io.confluent.devrel.Membership +import io.confluent.devrel.datacontracts.shared.BaseProducer +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig +import org.apache.kafka.clients.producer.ProducerConfig + +class MembershipProducer: BaseProducer(mapOf( + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringSerializer", + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroSerializer", + ProducerConfig.CLIENT_ID_CONFIG to "membership-producer-app-v1", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION to false, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=1" +)) \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml b/data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml new file mode 100644 index 00000000..b0d9c706 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml @@ -0,0 +1,25 @@ + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + diff --git a/data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts new file mode 100644 index 00000000..99c865c6 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts @@ -0,0 +1,83 @@ +import java.io.FileInputStream +import java.util.Properties + +buildscript { + repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") + gradlePluginPortal() + } +} + +plugins { + kotlin("jvm") version "2.0.21" + id("com.google.protobuf") version "0.9.4" + id("com.github.imflog.kafka-schema-registry-gradle-plugin") version "2.1.0" + id("com.bakdata.avro") version "1.2.1" +} + +group = "io.confluent.devrel" + +repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") +} + +sourceSets { + main { + kotlin.srcDirs("src/main/kotlin", "build/generated-main-avro-java") + } +} + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + implementation(project(":data-contracts-with-terraform:shared")) + + implementation("org.apache.kafka:kafka-clients:${project.property("kafkaVersion")}") + implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") + + implementation("io.github.serpro69:kotlin-faker:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-books:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-tech:${project.property("fakerVersion")}") + + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") +} + +kotlin { + jvmToolchain(17) +} + +schemaRegistry { + val srProperties = Properties() + srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + + url = srProperties.getProperty("schema.registry.url") + + val srCredTokens = srProperties.get("basic.auth.user.info").toString().split(":") + credentials { + username = srCredTokens[0] + password = srCredTokens[1] + } + outputDirectory = "${System.getProperty("user.home")}/tmp/schema-registry-plugin" + pretty = true + + download { + // download the membership avro schema, version 2 + subject("membership-avro-value", "${projectDir}/src/main/avro", 2) + } +} + +tasks.clean { + doFirst { + delete(fileTree("${projectDir}/src/main/avro/").include("**/*.avsc")) + } +} + +tasks.register("generateCode") { + group = "source generation" + description = "wrapper task for all source generation" + dependsOn("downloadSchemasTask", "generateAvroJava", "generateProto") +} diff --git a/data-contracts-with-terraform/producer-app-schema-v2/gradle.properties b/data-contracts-with-terraform/producer-app-schema-v2/gradle.properties new file mode 100644 index 00000000..3fd23b72 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/gradle.properties @@ -0,0 +1,6 @@ +confluentVersion=7.8.0 +fakerVersion=2.0.0-rc.3 +grpcVersion=1.15.1 +kafkaVersion=3.8.0 +protobufVersion=3.6.1 +slf4jVersion=2.0.11 diff --git a/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts new file mode 100644 index 00000000..ef39c1ee --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts @@ -0,0 +1,7 @@ +rootProject.name="producer-app-schema-v2" + +//include(":common") +//project(":common").projectDir("../../common") + +//include(":data-contracts-with-terraform:shared") +//project(":data-contracts-with-terraform:shared").projectDir("../shared") \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore b/data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore new file mode 100644 index 00000000..c5cab8d4 --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore @@ -0,0 +1 @@ +*.avsc \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt b/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt new file mode 100644 index 00000000..32ac74fd --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt @@ -0,0 +1,23 @@ +package io.confluent.devrel.v2 + +import io.confluent.devrel.v2.kafka.MembershipConsumer +import kotlinx.coroutines.runBlocking +import kotlin.concurrent.thread + +class ApplicationV2Main { + + companion object { + @JvmStatic + fun main(args: Array) { + runBlocking { + println("Starting application main...") + println(args.joinToString(" ")) + val consumer = MembershipConsumer() + + thread { + consumer.start(listOf("membership-avro")) + } + } + } + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt b/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt new file mode 100644 index 00000000..0c19a46b --- /dev/null +++ b/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt @@ -0,0 +1,20 @@ +package io.confluent.devrel.v2.kafka + +import io.confluent.devrel.Membership +import io.confluent.devrel.datacontracts.shared.BaseConsumer +import io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig +import org.apache.kafka.clients.consumer.ConsumerConfig + +class MembershipConsumer: BaseConsumer(mapOf( + ConsumerConfig.GROUP_ID_CONFIG to "app-schema-v2", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringDeserializer", + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroDeserializer", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=2" +) +) { + override fun consumeRecord(key: String, value: Membership) { + logger.info("v2 - Received Membership ${key}, ${value}") + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/schemas/build.gradle.kts b/data-contracts-with-terraform/schemas/build.gradle.kts new file mode 100644 index 00000000..50838caa --- /dev/null +++ b/data-contracts-with-terraform/schemas/build.gradle.kts @@ -0,0 +1,55 @@ +import java.io.FileInputStream +import java.util.Properties + +buildscript { + repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") + gradlePluginPortal() + } +} + +plugins { + kotlin("jvm") version "2.0.21" + id("com.google.protobuf") version "0.9.4" + id("com.github.imflog.kafka-schema-registry-gradle-plugin") version "2.1.0" + id("com.bakdata.avro") version "1.2.1" +} + +group = "io.confluent.devrel" + +repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") +} + +schemaRegistry { + val srProperties = Properties() + srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + + url = srProperties.getProperty("schema.registry.url") + + val srCredTokens = srProperties.get("basic.auth.user.info").toString().split(":") + println("***** $srCredTokens *****") + credentials { + username = srCredTokens[0] + password = srCredTokens[1] + } + outputDirectory = "${System.getProperty("user.home")}/tmp/schema-registry-plugin" + pretty = true + + val baseBuildDir = "${project.projectDir}/src/main" + val avroSchemaDir = "$baseBuildDir/avro" + val rulesetDir = "$baseBuildDir/rulesets" + val metadataDir = "$baseBuildDir/metadata" + + register { + subject(inputSubject = "membership-avro-value", type = "AVRO", file = "$avroSchemaDir/membership_v1.avsc") + .setMetadata("$metadataDir/membership_major_version_1.json") + subject(inputSubject = "membership-avro-value", type = "AVRO", file = "$avroSchemaDir/membership_v2.avsc") + .setMetadata("$metadataDir/membership_major_version_2.json") + .setRuleSet("$rulesetDir/membership_migration_rules.json") + } +} diff --git a/data-contracts-with-terraform/schemas/gradle.properties b/data-contracts-with-terraform/schemas/gradle.properties new file mode 100644 index 00000000..3fd23b72 --- /dev/null +++ b/data-contracts-with-terraform/schemas/gradle.properties @@ -0,0 +1,6 @@ +confluentVersion=7.8.0 +fakerVersion=2.0.0-rc.3 +grpcVersion=1.15.1 +kafkaVersion=3.8.0 +protobufVersion=3.6.1 +slf4jVersion=2.0.11 diff --git a/data-contracts-with-terraform/schemas/settings.gradle.kts b/data-contracts-with-terraform/schemas/settings.gradle.kts new file mode 100644 index 00000000..529990ae --- /dev/null +++ b/data-contracts-with-terraform/schemas/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name="schemas" diff --git a/data-contracts-with-terraform/src/main/avro/membership_v1.avsc b/data-contracts-with-terraform/schemas/src/main/avro/membership_v1.avsc similarity index 100% rename from data-contracts-with-terraform/src/main/avro/membership_v1.avsc rename to data-contracts-with-terraform/schemas/src/main/avro/membership_v1.avsc diff --git a/data-contracts-with-terraform/src/main/avro/membership_v2.avsc b/data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc similarity index 100% rename from data-contracts-with-terraform/src/main/avro/membership_v2.avsc rename to data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc diff --git a/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_1.json b/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_1.json new file mode 100644 index 00000000..c546283b --- /dev/null +++ b/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_1.json @@ -0,0 +1,5 @@ +{ + "properties": { + "major_version": 1 + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_2.json b/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_2.json new file mode 100644 index 00000000..8c043e30 --- /dev/null +++ b/data-contracts-with-terraform/schemas/src/main/metadata/membership_major_version_2.json @@ -0,0 +1,5 @@ +{ + "properties": { + "major_version": 2 + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/src/main/avro/.gitkeep b/data-contracts-with-terraform/schemas/src/main/proto/.gitkeep similarity index 100% rename from data-contracts-with-terraform/src/main/avro/.gitkeep rename to data-contracts-with-terraform/schemas/src/main/proto/.gitkeep diff --git a/data-contracts-with-terraform/schemas/src/main/rulesets/membership_migration_rules.json b/data-contracts-with-terraform/schemas/src/main/rulesets/membership_migration_rules.json new file mode 100644 index 00000000..1b55ad22 --- /dev/null +++ b/data-contracts-with-terraform/schemas/src/main/rulesets/membership_migration_rules.json @@ -0,0 +1,19 @@ +{ + "migrationRules": [ + { + "name": "move_start_and_end_date_to_validity_period", + "kind": "TRANSFORM", + "type": "JSONATA", + "mode": "UPGRADE", + "expr": "$merge([$sift($, function($v, $k) {$k != 'start_date' and $k != 'end_date'}), {'validity_period': {'from':start_date,'to':end_date}}])" + }, + { + "name": "move_validity_period_to_start_date_and_end_date", + "kind": "TRANSFORM", + "type": "JSONATA", + "mode": "DOWNGRADE", + "expr": "$merge([$sift($, function($v, $k) {$k != 'validity_period'}), {'start_date': validity_period.from, 'end_date': validity_period.to}])" + } + ] +} + diff --git a/data-contracts-with-terraform/settings.gradle b/data-contracts-with-terraform/settings.gradle deleted file mode 100644 index c0a46c4e..00000000 --- a/data-contracts-with-terraform/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -rootProject.name="data-contracts-terraform" -include ':common' -project(':common').projectDir = file('../../common') \ No newline at end of file diff --git a/data-contracts-with-terraform/shared/build.gradle.kts b/data-contracts-with-terraform/shared/build.gradle.kts new file mode 100644 index 00000000..e06da0cb --- /dev/null +++ b/data-contracts-with-terraform/shared/build.gradle.kts @@ -0,0 +1,48 @@ +buildscript { + repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") + gradlePluginPortal() + } +} + +plugins { + kotlin("jvm") version "2.0.21" +} + +group = "io.confluent.devrel" + +repositories { + mavenCentral() + maven("https://packages.confluent.io/maven/") + maven("https://jitpack.io") +} + +dependencies { + implementation("org.slf4j:slf4j-api:${project.property("slf4jVersion")}") + implementation("org.slf4j:slf4j-simple:${project.property("slf4jVersion")}") + implementation("ch.qos.logback:logback-core:1.4.14") + + implementation("io.github.serpro69:kotlin-faker:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-books:${project.property("fakerVersion")}") + implementation("io.github.serpro69:kotlin-faker-tech:${project.property("fakerVersion")}") + + implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.6") + implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") + + implementation("io.grpc:grpc-stub:${project.property("grpcVersion")}") + implementation("io.grpc:grpc-protobuf:${project.property("grpcVersion")}") + implementation("com.google.protobuf:protobuf-java:${project.property("protobufVersion")}") + + implementation("org.apache.kafka:kafka-clients:3.8.0") + implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") + implementation("io.confluent:kafka-protobuf-serializer:${project.property("confluentVersion")}") + implementation("io.confluent:kafka-schema-rules:${project.property("confluentVersion")}") + + testImplementation("org.jetbrains.kotlin:kotlin-test") +} + +kotlin { + jvmToolchain(17) +} diff --git a/data-contracts-with-terraform/shared/gradle.properties b/data-contracts-with-terraform/shared/gradle.properties new file mode 100644 index 00000000..3fd23b72 --- /dev/null +++ b/data-contracts-with-terraform/shared/gradle.properties @@ -0,0 +1,6 @@ +confluentVersion=7.8.0 +fakerVersion=2.0.0-rc.3 +grpcVersion=1.15.1 +kafkaVersion=3.8.0 +protobufVersion=3.6.1 +slf4jVersion=2.0.11 diff --git a/data-contracts-with-terraform/shared/settings.gradle.kts b/data-contracts-with-terraform/shared/settings.gradle.kts new file mode 100644 index 00000000..b63b95eb --- /dev/null +++ b/data-contracts-with-terraform/shared/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "shared" \ No newline at end of file diff --git a/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseConsumer.kt b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseConsumer.kt new file mode 100644 index 00000000..e738a1ca --- /dev/null +++ b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseConsumer.kt @@ -0,0 +1,40 @@ +package io.confluent.devrel.datacontracts.shared + +import org.apache.kafka.clients.consumer.KafkaConsumer +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.* + +abstract class BaseConsumer(propOverrides: Map = mapOf()) { + + companion object { + val logger = LoggerFactory.getLogger(BaseConsumer::class.java) + + fun getConsumerProperties(propOverrides: Map): Properties { + val props = Properties() + props.load(this::class.java.classLoader.getResourceAsStream("confluent.properties")) + props.put("specific.avro.reader", "true") + props.putAll(propOverrides) + return props + } + } + + val kafkaConsumer: KafkaConsumer = KafkaConsumer(getConsumerProperties(propOverrides)) + + fun start(topics: List) { + kafkaConsumer.subscribe(topics) + while (true) { + val records = kafkaConsumer.poll(Duration.ofSeconds(5)) + for (record in records) { + logger.trace("Record from ${record.topic()}, ${record.partition()}, ${record.offset()}") + consumeRecord(record.key(), record.value()) + } + } + } + + abstract fun consumeRecord(key: Key, value: Value) + + fun close() { + kafkaConsumer.close() + } +} \ No newline at end of file diff --git a/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseProducer.kt b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseProducer.kt new file mode 100644 index 00000000..4c04c45c --- /dev/null +++ b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseProducer.kt @@ -0,0 +1,42 @@ +package io.confluent.devrel.datacontracts.shared + +import org.apache.kafka.clients.producer.KafkaProducer +import org.apache.kafka.clients.producer.ProducerRecord +import org.apache.kafka.clients.producer.RecordMetadata +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* +import java.util.concurrent.Future + +abstract class BaseProducer(propOverrides: Map = mapOf()) { + + companion object { + fun getProducerProperties(propOverrides: Map): Properties { + val props = Properties() + props.load(this::class.java.classLoader.getResourceAsStream("confluent.properties")) + props.putAll(propOverrides) + return props + } + + val logger: Logger = LoggerFactory.getLogger(javaClass) + } + + val kafkaProducer: KafkaProducer = KafkaProducer(getProducerProperties(propOverrides)) + + open fun send(topicName: String, key: Key, value: Value): Future? { + val record = ProducerRecord(topicName, key, value) + return kafkaProducer.send(record) { metadata: RecordMetadata?, exception: Exception? -> + if (exception != null) { + logger.error("Failed to send message: ${exception.message}") + exception.printStackTrace(System.err) + } + else if (metadata != null) { + logger.debug("Message sent successfully! Topic: ${metadata.topic()}, Partition: ${metadata.partition()}, Offset: ${metadata.offset()}") + } + } + } + + fun close() { + kafkaProducer.close() + } +} diff --git a/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/ConfigLoader.kt b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/ConfigLoader.kt new file mode 100644 index 00000000..9ed4aa29 --- /dev/null +++ b/data-contracts-with-terraform/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/ConfigLoader.kt @@ -0,0 +1,23 @@ +package io.confluent.devrel.datacontracts.shared + +import java.io.File +import java.io.InputStream +import java.util.Properties + +object ConfigLoader { + + fun loadPropsFromFile(path: String): Properties { + val properties = Properties() + val stream: InputStream? = File(path).inputStream() + stream.use { + println("reading properties from $path") + properties.load(stream) + } + + return properties + .mapKeys { it.key.toString() } + .mapValues { it.value.toString().replace("\"", "") } + .toProperties() + } + +} \ No newline at end of file diff --git a/data-contracts-with-terraform/shared/src/main/resources/.gitignore b/data-contracts-with-terraform/shared/src/main/resources/.gitignore new file mode 100644 index 00000000..37ecdf22 --- /dev/null +++ b/data-contracts-with-terraform/shared/src/main/resources/.gitignore @@ -0,0 +1 @@ +confluent.properties \ No newline at end of file diff --git a/data-contracts-with-terraform/shared/src/main/resources/confluent.properties.orig b/data-contracts-with-terraform/shared/src/main/resources/confluent.properties.orig new file mode 100644 index 00000000..7257a098 --- /dev/null +++ b/data-contracts-with-terraform/shared/src/main/resources/confluent.properties.orig @@ -0,0 +1,19 @@ +# Required connection configs for Kafka Streams +bootstrap.servers= + +security.protocol=SASL_SSL +sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username='' password=''; +sasl.mechanism=PLAIN +# Required for correctness in Apache Kafka clients prior to 2.6 +client.dns.lookup=use_all_dns_ips + +# Best practice for higher availability in Apache Kafka clients prior to 3.0 +session.timeout.ms=45000 + +# Best practice for Kafka producer to prevent data loss +acks=all + +# Required connection configs for Confluent Cloud Schema Registry +schema.registry.url= +basic.auth.credentials.source=USER_INFO +basic.auth.user.info=: diff --git a/data-contracts-with-terraform/src/main/protobuf/.gitkeep b/data-contracts-with-terraform/src/main/protobuf/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/data-contracts-with-terraform/src/main/resources/.gitkeep b/data-contracts-with-terraform/src/main/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/data-contracts-with-terraform/src/test/java/.gitkeep b/data-contracts-with-terraform/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/data-contracts-with-terraform/src/test/resources/.gitkeep b/data-contracts-with-terraform/src/test/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e093..cea7a793 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/over-aggregations/flinksql/build.gradle b/over-aggregations/flinksql/build.gradle index 15d9a151..bcf54025 100644 --- a/over-aggregations/flinksql/build.gradle +++ b/over-aggregations/flinksql/build.gradle @@ -23,8 +23,8 @@ dependencies { testImplementation project(path: ':common', configuration: 'testArtifacts') testImplementation 'com.google.guava:guava:31.1-jre' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.testcontainers:testcontainers:1.19.3' - testImplementation 'org.testcontainers:kafka:1.19.3' + testImplementation 'org.testcontainers:testcontainers:1.20.1' + testImplementation 'org.testcontainers:kafka:1.20.1' testImplementation 'commons-codec:commons-codec:1.17.0' testImplementation 'org.apache.flink:flink-sql-connector-kafka:3.2.0-1.19' testImplementation 'org.apache.flink:flink-connector-base:1.19.1' diff --git a/settings.gradle b/settings.gradle index 8be4a724..ae94443b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,7 +21,10 @@ include 'confluent-parallel-consumer-application:kafka' include 'common' include 'creating-first-apache-kafka-streams-application:kstreams' include 'cumulating-windows:flinksql' -include 'data-contracts-with-terraform' +include 'data-contracts-with-terraform:shared' +include 'data-contracts-with-terraform:schemas' +include 'data-contracts-with-terraform:producer-app-schema-v1' +include 'data-contracts-with-terraform:producer-app-schema-v2' include 'deduplication:flinksql' include 'deduplication-windowed:flinksql' include 'deduplication-windowed:kstreams' diff --git a/udf/ksql/build.gradle b/udf/ksql/build.gradle index d0138dd6..0aeb25f6 100644 --- a/udf/ksql/build.gradle +++ b/udf/ksql/build.gradle @@ -10,8 +10,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } From 57ee5c16c9b66e885a6736cd8b13f30806fa7c1d Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Wed, 15 Jan 2025 09:20:14 -0500 Subject: [PATCH 05/13] Updates to documentation. --- data-contracts-with-terraform/README.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/data-contracts-with-terraform/README.md b/data-contracts-with-terraform/README.md index e4817bf5..4b420bda 100644 --- a/data-contracts-with-terraform/README.md +++ b/data-contracts-with-terraform/README.md @@ -32,35 +32,27 @@ Here are the tools needed to run this tutorial: To create Confluent Cloud assets, change to the `cc` subdirectory. We'll step through the commands and what they do. +#### Create Confluent Cloud Assets + Export the Confluent Cloud organization ID to a terraform environment variable: ```shell export TF_VAR_org_id=$(confluent organization list -o json | jq -c -r '.[] | select(.is_current)' | jq '.id') ``` -#### Create Confluent Cloud Assets -Initialize the terraform environment: +Now, we are ready to initialize the terraform environment, create a `plan` and `apply` said plan to create CC assets: ```shell terraform init -``` - -Create a terraform "plan" - this may open a browser window, asking you to authenticate to Confluent Cloud: - -```shell terraform plan -out "tfplan" -``` - -Apply the terraform plan, thus creating the needed Confluent Cloud infrastructure: - -```shell terraform apply "tfplan" ``` #### Prepare Client Properties -The output of `terraform apply` includes the properties needed to connect to Confluent Cloud. The command below will export -those outputs to a properties file in our project for later use by our client code: +This demo has tailored the output of `terraform apply` to return the properties needed to connect to Confluent Cloud. The command below will +reformat the names of those properties into the names used in Kafka Client configurations, then export those outputs to a properties file +in our project: ```shell terraform output -json | \ @@ -68,9 +60,10 @@ terraform output -json | \ done > ../shared/src/main/resources/confluent.properties ``` -For an example of this properties file, see [confluent.properties.orig](shared/src/main/resources/confluent.properties.orig). +All Kafka Client code in this project loads connection properties form `shared/src/main/resources/confluent.properties`. For an example of this +properties file, see [confluent.properties.orig](shared/src/main/resources/confluent.properties.orig). -### Using Schemas +### Schema Evolution ## Teardown From 0a50915539487ecaec1167883f9e67ca4fe77c40 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Thu, 16 Jan 2025 14:33:55 -0500 Subject: [PATCH 06/13] documenation, module rename, all gradle for schema registration and download. --- data-contracts-with-terraform/README.md | 80 ---- .../settings.gradle.kts | 7 - .../settings.gradle.kts | 7 - .../schemas/src/main/avro/membership_v2.avsc | 20 - .../.gitignore | 0 data-contracts/README.md | 432 ++++++++++++++++++ .../app-schema-v1}/build.gradle.kts | 4 +- .../app-schema-v1}/gradle.properties | 0 .../app-schema-v1/settings.gradle.kts | 1 + .../app-schema-v1}/src/main/avro/.gitignore | 0 .../confluent/devrel/dc/v1/ApplicationMain.kt | 0 .../devrel/dc/v1/kafka/MembershipConsumer.kt | 0 .../devrel/dc/v1/kafka/MembershipProducer.kt | 0 .../src/main/resources/logback.xml | 0 .../app-schema-v2}/build.gradle.kts | 4 +- .../app-schema-v2}/gradle.properties | 0 .../app-schema-v2/settings.gradle.kts | 1 + .../app-schema-v2}/src/main/avro/.gitignore | 0 .../confluent/devrel/v2/ApplicationV2Main.kt | 0 .../devrel/v2/kafka/MembershipConsumer.kt | 0 .../cc-terraform}/.gitignore | 0 .../cc-terraform}/kafka.tf | 0 .../cc-terraform}/main.tf | 0 .../cc-terraform}/membership.tf | 0 .../cc-terraform}/outputs.tf | 0 .../cc-terraform}/variables.tf | 0 data-contracts/images/overview.png | Bin 0 -> 140243 bytes .../schemas/build.gradle.kts | 4 +- .../schemas/gradle.properties | 0 .../schemas/settings.gradle.kts | 0 .../schemas/src/main/avro/membership_v1.avsc | 0 .../schemas/src/main/avro/membership_v2.avsc | 9 + .../src/main/avro/validity_period.avsc | 10 + .../metadata/membership_major_version_1.json | 0 .../metadata/membership_major_version_2.json | 0 .../schemas/src/main/proto/.gitkeep | 0 .../rulesets/membership_migration_rules.json | 0 .../shared/build.gradle.kts | 0 .../shared/gradle.properties | 0 .../shared/settings.gradle.kts | 0 .../datacontracts/shared/BaseConsumer.kt | 0 .../datacontracts/shared/BaseProducer.kt | 0 .../datacontracts/shared/ConfigLoader.kt | 0 .../shared/src/main/resources/.gitignore | 0 .../main/resources/confluent.properties.orig | 0 settings.gradle | 8 +- 46 files changed, 466 insertions(+), 121 deletions(-) delete mode 100644 data-contracts-with-terraform/README.md delete mode 100644 data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts delete mode 100644 data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts delete mode 100644 data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc rename {data-contracts-with-terraform => data-contracts}/.gitignore (100%) create mode 100644 data-contracts/README.md rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/build.gradle.kts (90%) rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/gradle.properties (100%) create mode 100644 data-contracts/app-schema-v1/settings.gradle.kts rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/src/main/avro/.gitignore (100%) rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt (100%) rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt (100%) rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt (100%) rename {data-contracts-with-terraform/producer-app-schema-v1 => data-contracts/app-schema-v1}/src/main/resources/logback.xml (100%) rename {data-contracts-with-terraform/producer-app-schema-v2 => data-contracts/app-schema-v2}/build.gradle.kts (90%) rename {data-contracts-with-terraform/producer-app-schema-v2 => data-contracts/app-schema-v2}/gradle.properties (100%) create mode 100644 data-contracts/app-schema-v2/settings.gradle.kts rename {data-contracts-with-terraform/producer-app-schema-v2 => data-contracts/app-schema-v2}/src/main/avro/.gitignore (100%) rename {data-contracts-with-terraform/producer-app-schema-v2 => data-contracts/app-schema-v2}/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt (100%) rename {data-contracts-with-terraform/producer-app-schema-v2 => data-contracts/app-schema-v2}/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/.gitignore (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/kafka.tf (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/main.tf (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/membership.tf (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/outputs.tf (100%) rename {data-contracts-with-terraform/cc => data-contracts/cc-terraform}/variables.tf (100%) create mode 100644 data-contracts/images/overview.png rename {data-contracts-with-terraform => data-contracts}/schemas/build.gradle.kts (86%) rename {data-contracts-with-terraform => data-contracts}/schemas/gradle.properties (100%) rename {data-contracts-with-terraform => data-contracts}/schemas/settings.gradle.kts (100%) rename {data-contracts-with-terraform => data-contracts}/schemas/src/main/avro/membership_v1.avsc (100%) create mode 100644 data-contracts/schemas/src/main/avro/membership_v2.avsc create mode 100644 data-contracts/schemas/src/main/avro/validity_period.avsc rename {data-contracts-with-terraform => data-contracts}/schemas/src/main/metadata/membership_major_version_1.json (100%) rename {data-contracts-with-terraform => data-contracts}/schemas/src/main/metadata/membership_major_version_2.json (100%) rename {data-contracts-with-terraform => data-contracts}/schemas/src/main/proto/.gitkeep (100%) rename {data-contracts-with-terraform => data-contracts}/schemas/src/main/rulesets/membership_migration_rules.json (100%) rename {data-contracts-with-terraform => data-contracts}/shared/build.gradle.kts (100%) rename {data-contracts-with-terraform => data-contracts}/shared/gradle.properties (100%) rename {data-contracts-with-terraform => data-contracts}/shared/settings.gradle.kts (100%) rename {data-contracts-with-terraform => data-contracts}/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseConsumer.kt (100%) rename {data-contracts-with-terraform => data-contracts}/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/BaseProducer.kt (100%) rename {data-contracts-with-terraform => data-contracts}/shared/src/main/kotlin/io/confluent/devrel/datacontracts/shared/ConfigLoader.kt (100%) rename {data-contracts-with-terraform => data-contracts}/shared/src/main/resources/.gitignore (100%) rename {data-contracts-with-terraform => data-contracts}/shared/src/main/resources/confluent.properties.orig (100%) diff --git a/data-contracts-with-terraform/README.md b/data-contracts-with-terraform/README.md deleted file mode 100644 index 4b420bda..00000000 --- a/data-contracts-with-terraform/README.md +++ /dev/null @@ -1,80 +0,0 @@ - - - -# Manage Data Contracts with Terraform - -Data contracts consists not only of the schemas to define the data, but also rulesets allowing for more fine-grained validations, -controls, and discovery.In this tutorial, we'll evolve a couple of schemas and add data quality and migration rules.We'll also -explore tagging those schemas, fields, and rules for data discovery. - -## Running the Example - -In this tutorial we'll create Confluent Cloud infrastructure - including a Kafka cluster and Schema Registry. Then we'll create -a Kafka topic named `membership-avro` to store `Membership` events. The Apache Avro schema is maintained an managed in this repo -along with metadata and migration rules about those schemas. - -We will evolve the `membership` schema, refactoring the events to encapsulate the date-related fields of version 1 into its -own `record` type in version 2. Typically this would be a breaking change. However, data migration rules in the schema registry -allow us to perform this schema change without breaking producers or consumers. At the time this is written, this functionality -is only available to JVM-based Confluent client implementations. We'll update this example as our non-JVM clients evolve. - -### Prerequisites - -Here are the tools needed to run this tutorial: -* [Confluent Cloud](http://confluent.cloud) -* [Confluent CLI](https://docs.confluent.io/confluent-cli/current/install.html) -* [Terraform](https://developer.hashicorp.com/terraform/install?product_intent=terraform) -* [jq](https://jqlang.github.io/jq/) -* JDK 17 -* IDE of choice - -### Executing Terraform - -To create Confluent Cloud assets, change to the `cc` subdirectory. We'll step through the commands and what they do. - -#### Create Confluent Cloud Assets - -Export the Confluent Cloud organization ID to a terraform environment variable: - -```shell -export TF_VAR_org_id=$(confluent organization list -o json | jq -c -r '.[] | select(.is_current)' | jq '.id') -``` - -Now, we are ready to initialize the terraform environment, create a `plan` and `apply` said plan to create CC assets: - -```shell -terraform init -terraform plan -out "tfplan" -terraform apply "tfplan" -``` - -#### Prepare Client Properties - -This demo has tailored the output of `terraform apply` to return the properties needed to connect to Confluent Cloud. The command below will -reformat the names of those properties into the names used in Kafka Client configurations, then export those outputs to a properties file -in our project: - -```shell -terraform output -json | \ - jq -r 'to_entries | map( {key: .key|tostring|split("_")|join("."), value: .value} ) | map("\(.key)=\(.value.value)")' | while read -r line ; do echo "$line"; \ - done > ../shared/src/main/resources/confluent.properties -``` - -All Kafka Client code in this project loads connection properties form `shared/src/main/resources/confluent.properties`. For an example of this -properties file, see [confluent.properties.orig](shared/src/main/resources/confluent.properties.orig). - -### Schema Evolution - - -## Teardown - -When you're done with the tutorial, issue this command from the `cc` directory to destroy the Confluent Cloud environment -we created: - -```shell -terraform destroy -auto-approve -``` - -Check the Confluent Cloud console to ensure this environment no longer exists. - - diff --git a/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts deleted file mode 100644 index 7ee4de2d..00000000 --- a/data-contracts-with-terraform/producer-app-schema-v1/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -rootProject.name="producer-app-schema-v1" - -//include(":common") -//project(":common").projectDir("../../common") - -//include(":data-contracts-with-terraform:shared") -//project(":data-contracts-with-terraform:shared").projectDir("../shared") \ No newline at end of file diff --git a/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts b/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts deleted file mode 100644 index ef39c1ee..00000000 --- a/data-contracts-with-terraform/producer-app-schema-v2/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -rootProject.name="producer-app-schema-v2" - -//include(":common") -//project(":common").projectDir("../../common") - -//include(":data-contracts-with-terraform:shared") -//project(":data-contracts-with-terraform:shared").projectDir("../shared") \ No newline at end of file diff --git a/data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc b/data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc deleted file mode 100644 index 6454cdd0..00000000 --- a/data-contracts-with-terraform/schemas/src/main/avro/membership_v2.avsc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "Membership", - "namespace": "io.confluent.devrel", - "type": "record", - "fields": [ - {"name": "user_id", "type": "string"}, - { - "name": "validity_period", - "type": { - "type": "record", - "name": "ValidityPeriod", - "fields": [ - {"name": "from", "type": {"type": "int", "logicalType": "date"}}, - {"name": "to", "type": {"type": "int", "logicalType": "date"} - } - ] - } - } - ] -} \ No newline at end of file diff --git a/data-contracts-with-terraform/.gitignore b/data-contracts/.gitignore similarity index 100% rename from data-contracts-with-terraform/.gitignore rename to data-contracts/.gitignore diff --git a/data-contracts/README.md b/data-contracts/README.md new file mode 100644 index 00000000..0d9f17b8 --- /dev/null +++ b/data-contracts/README.md @@ -0,0 +1,432 @@ +# Managing Data Contracts + +Data contracts consists not only of the schemas to define the data, but also rulesets allowing for more fine-grained validations, +controls, and discovery.In this tutorial, we'll evolve a couple of schemas and add data quality and migration rules. We'll also +explore tagging those schemas, fields, and rules for data discovery. + + + +In the workflow above, we see these tools in action: +* The [Confluent Terraform Provider](https://registry.terraform.io/providers/confluentinc/confluent/latest/docs) is used to define Confluent Cloud assets (Kafka cluster(s), Data Governance, Kafka Topics, and Schema Configurations). +* Using the newly created Schema Registry, data engineers and architects define the schema of the events that comprise the organization's canonical data model - i.e. entities, events, and commands that are shared across applications. - along with other parts of the data contract. This includes data quality rules, metadata, and migration rules. A gradle plugin is utilized to register the schemas and related elements of the data contract with the Schema Registry. +* Applications which producer and/or consume these event types can download the schemas from the Schema Registry. In our example, this is a JVM application built using Gradle. A gradle plugin is used to download the schemas, after which another gradle plugin is used to generate Java classes from those schemas - thus providing the application with compile-time type safety. + +We will cover these steps in detail. + +## Running the Example + +In this tutorial we'll create Confluent Cloud infrastructure - including a Kafka cluster and Schema Registry. Then we'll create +a Kafka topic named `membership-avro` to store `Membership` events. The Apache Avro schema is maintained an managed in this repo +along with metadata and migration rules about those schemas. + +We will evolve the `membership` schema, refactoring the events to encapsulate the date-related fields of version 1 into its +own `record` type in version 2. Typically this would be a breaking change. However, data migration rules in the schema registry +allow us to perform this schema change without breaking producers or consumers. + +At the time this is written, this data contract functionality is available to Java, GO, and .NET Confluent client implementations. We'll update this example as other clients evolve. + +### Prerequisites + +Here are the tools needed to run this tutorial: +* [Confluent Cloud](http://confluent.cloud) +* [Confluent CLI](https://docs.confluent.io/confluent-cli/current/install.html) +* [Terraform](https://developer.hashicorp.com/terraform/install?product_intent=terraform) +* [jq](https://jqlang.github.io/jq/) +* JDK 17 +* IDE of choice + +### Executing Terraform + +To create Confluent Cloud assets, change to the `cc-terraform` subdirectory. We'll step through the commands and what they do. + +#### Create Confluent Cloud Assets + +Terraform can use the value of any environment variable whose name begins with `TF_VAR_` as the value of a terraform variable of the same name. For more on this functionality, see the [terraform documentation](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name). + +Our example requires we set the value the `org_id` variable from Confluent Cloud. This denotes which organization will house the Confluent Cloud assets we are creating. So let's export the Confluent Cloud organization ID to a terraform environment variable. + +This command may open a browser window asking you to authenticate to Confluent Cloud. Once that's complete, the result of +`confluent organization list` is queried by `jq` to extract the `id` of the current organization to which you are authenticated: + +```shell +export TF_VAR_org_id=$(confluent organization list -o json | jq -c -r '.[] | select(.is_current)' | jq '.id') +``` + +Now, we are ready to initialize the terraform environment, create a `plan` and `apply` said plan to create CC assets: + +```shell +terraform init +terraform plan -out "tfplan" +terraform apply "tfplan" +``` + +Let's have a look at what we've created. We now have a Confluent Cloud environment, which includes a Kafka cluster and Data Governance platform with Confluent Schema Registry. Within the Kafka cluster, we create a Kafka topic named `membership-avro`. + +```terraform +resource "confluent_kafka_topic" "membership_avro" { + topic_name = "membership-avro" + kafka_cluster { + id = confluent_kafka_cluster.kafka_cluster.id + } + rest_endpoint = confluent_kafka_cluster.kafka_cluster.rest_endpoint +... + partitions_count = 10 +... +} +``` + +Then we configure the subject containing the value of events for this topic using `confluent_subject_config`. This tells the Schema Registry that `membership-avro-value` schemas will be `BACKWARD` compatible and defines an attribute called `major_version` to be used as the `compatibilityGroup`. + +```terraform +resource "confluent_subject_config" "membership_value_avro" { + subject_name = "${confluent_kafka_topic.membership_avro.topic_name}-value" + + schema_registry_cluster { + id = data.confluent_schema_registry_cluster.advanced.id + } + rest_endpoint = data.confluent_schema_registry_cluster.advanced.rest_endpoint +... + compatibility_level = "BACKWARD" + compatibility_group = "major_version" +... +} +``` + +For more on these data contract configuration enhancements, refer to the CC docs on [data contracts](https://docs.confluent.io/cloud/current/sr/fundamentals/data-contracts.html#configuration-enhancements). + + +#### Prepare Client Properties + +This demo has tailored the output of `terraform apply` to return the properties needed to connect to Confluent Cloud. The command below will +reformat the names of those properties into the names used in Kafka Client configurations, then export those outputs to a properties file +in our project: + +```shell +terraform output -json | \ + jq -r 'to_entries | map( {key: .key|tostring|split("_")|join("."), value: .value} ) | map("\(.key)=\(.value.value)")' | while read -r line ; do echo "$line"; \ + done > ../shared/src/main/resources/confluent.properties +``` + +All Kafka Client code in this project loads connection properties form `shared/src/main/resources/confluent.properties`. For an example of this +properties file, see [confluent.properties.orig](shared/src/main/resources/confluent.properties.orig). + +> [!NOTE] +> The file-based approach we're using here is NOT recommended for a production-quality application. Perhaps a secrets manager implementation would be better suited - which the major cloud providers all offer, or perhaps a tool like Hashicorp Vault. Such a tool would also have client libraries in a Maven repository for the JVM applications to access the secrets. + +### Producing and Consuming Events + +We'll create producer and consumer classes to configure and provide Kafka clients for our application(s). In the `shared` module there are implementations to encapsulate this behavior: + +```kotlin +abstract class BaseProducer(propOverrides: Map = mapOf()) { + ... + val kafkaProducer: KafkaProducer = KafkaProducer(getProducerProperties(propOverrides)) + + open fun send(topicName: String, key: Key, value: Value): Future? { + ... + } +} + +abstract class BaseConsumer(propOverrides: Map = mapOf()) { + ... + val kafkaConsumer: KafkaConsumer = KafkaConsumer(getConsumerProperties(propOverrides)) + + fun start(topics: List) { + kafkaConsumer.subscribe(topics) + while (true) { + val records = kafkaConsumer.poll(Duration.ofSeconds(5)) + for (record in records) { + logger.trace("Record from ${record.topic()}, ${record.partition()}, ${record.offset()}") + consumeRecord(record.key(), record.value()) + } + } + } + + abstract fun consumeRecord(key: Key, value: Value) +} +``` + +Build the `shared` module from the root of the `tutorials` repo: +```shell +./gradlew :data-contracts:shared:build +``` + +As the schemas evolve, we'll create implementations of these base classes to produce and consume events with specific versions of the schemas. + +### Schema Evolution + +Schemas are CODE! As such, they will evolve to meet new business requirements. In the upcoming sections, we'll create JVM applications - written in Kotlin and built with Gradle - using different versions of the schema. We will utilize the Data Governance migration rules, allowing us to make what would ordinarily be "breaking changes" to the schema version but with the caveat of a mapping between the versions based on Kafka client configurations. + +#### Working with Version 1 + +The `membership` schema begins as a fairly "flat" data model, where the fields to denote start and end dates are at the top-level of the object: + +```avroschema +{ + "name": "Membership", + "namespace": "io.confluent.devrel", + "type": "record", + "fields": [ + {"name": "user_id", "type": "string"}, + {"name": "start_date", "type": {"type": "int", "logicalType": "date"}}, + {"name": "end_date", "type": {"type": "int", "logicalType": "date"}} + ] +} +``` + +Using the [schema-registry-plugin](https://github.com/ImFlog/schema-registry-plugin) for gradle, we can register this schema with the Confluent Schema Registry. + +```kotlin +schemaRegistry { + ... + register { + subject(inputSubject = "membership-avro-value", type = "AVRO", file = "$avroSchemaDir/membership_v1.avsc") + .setMetadata("$metadataDir/membership_major_version_1.json") + } +} +``` + +Remember the `compatibility_group` parameter from the subject configuration? This now comes into play as we set the metadata on the schema to denote this version as `major_version=1`: + +```json +{ + "properties": { + "major_version": 1 + } +} +``` + +Schema registration is completed via the `registerSchemas` task of the `schemas` module: + +```shell +./gradlew :data-contracts:schemas:registerSchemasTask +``` + +To use this schema in an application - `app-schema-v1` - we utilize the same gradle plugin's `downloadSchemasTask`. First we define the subjects we want to download. In this example, we copy the schema to directory `src/main/avro`: + +```kotlin +schemaRegistry { + download { + // download the membership avro schema, version 1 + subject("membership-avro-value", "${projectDir}/src/main/avro", 1) + } +} +``` + +With the schema(s) downloaded, we next want to generate Java code to provide our application an SDK with compile-time bindings to serialize and deserialize events to and from the `membership-avro` topic. Let's use the [gradle-avro-dependency-plugin](https://github.com/bakdata/gradle-avro-dependency-plugin) to generate code, using the `generateAvroJava` task. + +For simplicity, I have encapsulated `downloadSchemasTask` with the `generateAvroJava` task into a custom gradle task named `generateCode`, executed as follows: + +```shell +./gradlew :data-contracts:app-schema-v1:generateCode +``` + +Now implement classes to produce and consume events with this schema. First the producer class, extending `BaseProducer`: + +```kotlin +class MembershipProducer: BaseProducer(mapOf( + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringSerializer", + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroSerializer", + ProducerConfig.CLIENT_ID_CONFIG to "membership-producer-app-v1", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION to false, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=1" +)) +``` + +This implementation must pass the configuration parameters required to serialize the key and value of events to the `membership-avro` topic. In this use case, the key is a `String` and the value is of type `Membership` - generated from the schema. The value uses the schema-registry-aware `Serializer` implementation `KafkaAvroSerializer`. The underlying serializer is also configured to NOT use the latest version of the schema (`use.latest.version`) and to relax the strict compatibility checks (`latest.compatibility.strict`). For more on these settings, see the CC doc's explanation of [the differences between preregistered and client-derived schemas](https://docs.confluent.io/platform/current/schema-registry/fundamentals/serdes-develop/index.html#handling-differences-between-preregistered-and-client-derived-schemas). + +Let's also implement the `BaseConsumer` class, configured to use this `major_version` of the schema: + +```kotlin +class MembershipConsumer: BaseConsumer(mapOf( + ConsumerConfig.GROUP_ID_CONFIG to "app-schema-v1", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringDeserializer", + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroDeserializer", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_VERSION to false, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=1" +)) { + + override fun consumeRecord( + key: String, + value: Membership + ) { + logger.info("Received Membership ${key}, ${value}") + } +} +``` + +The `consumeRecord` function is where we would typically start the business logic of actually "consuming" Kafka events. This implementation simply logs the consumed records to the provided `Logger` instance in the superclass. + +To exercise these producer and consumer implementations, we created a `main` function in the `ApplicationMain` class to start a consumer instance and a producer instance periodically send random events to the `membership-avro` topic. + +

+ Main function + +```kotlin +@JvmStatic +fun main(args: Array) { + runBlocking { + println("Starting application main...") + println(args.joinToString(" ")) + + val messageInterval = 1.toDuration(DurationUnit.SECONDS) + val sendDuration = 100.toDuration(DurationUnit.SECONDS) + + val producer = MembershipProducer() + val consumer = MembershipConsumer() + + // start a thread with a consumer instance + thread { + consumer.start(listOf("membership-avro")) + } + + // every 1 second for the next 100 seconds, send a randomly-generated event to the kafka topic + coroutineScope { + launch { + val until = Clock.System.now().plus(sendDuration) + while(Clock.System.now().compareTo(until) < 0) { + val userId = UUID.randomUUID().toString() + val membership = Membership.newBuilder() + .setUserId(userId) + .setStartDate(LocalDate.now().minusDays(Random.nextLong(100, 1000))) + .setEndDate(LocalDate.now().plusWeeks(Random.nextLong(1, 52))) + .build() + producer.send("membership-avro", userId, membership) + delay(messageInterval.inWholeSeconds) + } + } + } + producer.close() + } +} +``` + +
+ +Running this application will print the events being consumed from Kafka: + +
+ Console Output + +```shell +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 8e65d8e2-4ad8-475f-8f74-d724865ddbd8, {"user_id": "8e65d8e2-4ad8-475f-8f74-d724865ddbd8", "start_date": "2022-06-13", "end_date": "2025-03-25"} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 22781e70-8d2c-4d02-b325-9cdc8663ed19, {"user_id": "22781e70-8d2c-4d02-b325-9cdc8663ed19", "start_date": "2022-06-10", "end_date": "2025-11-11"} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 06ec5d95-d38c-4bac-9693-a01596eeedd7, {"user_id": "06ec5d95-d38c-4bac-9693-a01596eeedd7", "start_date": "2024-03-25", "end_date": "2025-02-25"} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 881bf838-1fe1-423e-9afc-0742e3080b5b, {"user_id": "881bf838-1fe1-423e-9afc-0742e3080b5b", "start_date": "2022-07-25", "end_date": "2025-07-08"} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 73254321-72f9-4783-949f-652f5ca9542e, {"user_id": "73254321-72f9-4783-949f-652f5ca9542e", "start_date": "2022-05-11", "end_date": "2025-03-11"} +``` + +
+ +#### Evolving to Version 2 + +The decision is made to refactor the `membership` schema to encapsulate the date fields into a type - `ValidityPeriod` - which can be reused in other event types. + +```avroschema +{ + "type": "record", + "name": "ValidityPeriod", + "fields": [ + {"name": "from", "type": {"type": "int", "logicalType": "date"}}, + {"name": "to", "type": {"type": "int", "logicalType": "date"} + } + ] +} +``` + +The refactored `membership` schema now references this new type: + +```avroschema +{ + "name": "Membership", + "namespace": "io.confluent.devrel", + "type": "record", + "fields": [ + { "name": "user_id", "type": "string" }, + { "name": "validity_period", "type": "io.confluent.devrel.ValidityPeriod" } + ] +} +``` + +When we register version 2 of the `membership-avro-value` subject, use `addLocalReference` to include the `validityPeriod` type: + +```kotlin +schemaRegistry{ + register { + subject(inputSubject = "membership-avro-value", type = "AVRO", file = "$avroSchemaDir/membership_v1.avsc") + .setMetadata("$metadataDir/membership_major_version_1.json") + + subject(inputSubject = "membership-avro-value", type = "AVRO", file = "$avroSchemaDir/membership_v2.avsc") + .addLocalReference("validity-period", "$avroSchemaDir/validity_period.avsc") + .setMetadata("$metadataDir/membership_major_version_2.json") + .setRuleSet("$rulesetDir/membership_migration_rules.json") + } +} +``` + +Register the new schema using our gradle plugin: + +```shell +./gradlew :data-contracts:schemas:registerSchemasTask +``` + +Download the schema to the version 2 application and generate the Java classes for this schema: + +```shell +./gradlew :data-contracts:app-schema-v2:generateCode +``` + +In the `app-schema-v2` module, we'll find a new implementation of a `MembershipConsumer`: + +```kotlin +class MembershipConsumer: BaseConsumer(mapOf( + ConsumerConfig.GROUP_ID_CONFIG to "app-schema-v2", + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to "earliest", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to "org.apache.kafka.common.serialization.StringDeserializer", + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to "io.confluent.kafka.serializers.KafkaAvroDeserializer", + AbstractKafkaSchemaSerDeConfig.LATEST_COMPATIBILITY_STRICT to true, + AbstractKafkaSchemaSerDeConfig.USE_LATEST_WITH_METADATA to "major_version=2" +) +) { + override fun consumeRecord(key: String, value: Membership) { + logger.info("v2 - Received Membership ${key}, ${value}") + } +} +``` + +A closer look at the configuration of this consumer shows we are now using `latest.compatibility.strict` and specifying `major_version=2` +in the `use.latest.with.metadata` configuration for the deserializer. + +Running the `main` function in `ApplicationV2Main` will consume the events from the `membership-avro` topic, but with a noticeable difference from the `app-schama-v1` application: + +
+ Console output version 2 + +```shell +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership af39d3a4-53b3-4d27-9bf0-3528965b6149, {"user_id": "af39d3a4-53b3-4d27-9bf0-3528965b6149", "validity_period": {"from": "2022-10-29", "to": "2025-08-28"}} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 4baddf48-6dda-40c0-8c4d-57222932c0b8, {"user_id": "4baddf48-6dda-40c0-8c4d-57222932c0b8", "validity_period": {"from": "2024-07-13", "to": "2025-08-14"}} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 6c385e77-4e50-4784-bf35-2e0be4aca6a6, {"user_id": "6c385e77-4e50-4784-bf35-2e0be4aca6a6", "validity_period": {"from": "2023-10-17", "to": "2025-03-13"}} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 79d36f24-4317-4f88-acb2-8f48ff88991b, {"user_id": "79d36f24-4317-4f88-acb2-8f48ff88991b", "validity_period": {"from": "2022-09-13", "to": "2025-09-25"}} +[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 2167c361-2202-41ce-b48e-35967b3fb8c7, {"user_id": "2167c361-2202-41ce-b48e-35967b3fb8c7", "validity_period": {"from": "2023-05-01", "to": "2025-05-01"}} +``` + +
+ +We're consuming the same events, but the deserialized with version 2 of the schema. + +## Teardown + + +When you're done with the tutorial, issue this command from the `cc-terraform` directory to destroy the Confluent Cloud environment +we created: + +```shell +terraform destroy -auto-approve +``` + +Check the Confluent Cloud console to ensure this environment no longer exists. + + diff --git a/data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts b/data-contracts/app-schema-v1/build.gradle.kts similarity index 90% rename from data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts rename to data-contracts/app-schema-v1/build.gradle.kts index ede54dca..a3a7aa64 100644 --- a/data-contracts-with-terraform/producer-app-schema-v1/build.gradle.kts +++ b/data-contracts/app-schema-v1/build.gradle.kts @@ -33,7 +33,7 @@ sourceSets { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") - implementation(project(":data-contracts-with-terraform:shared")) + implementation(project(":data-contracts:shared")) implementation("org.apache.kafka:kafka-clients:${project.property("kafkaVersion")}") implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") @@ -52,6 +52,8 @@ kotlin { schemaRegistry { val srProperties = Properties() + // At the moment, this is a file with which we are LOCALLY aware. + // In an ACTUAL CI/CD workflow, this would be externalized, perhaps provided from a base build image or other parameter. srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) url = srProperties.getProperty("schema.registry.url") diff --git a/data-contracts-with-terraform/producer-app-schema-v1/gradle.properties b/data-contracts/app-schema-v1/gradle.properties similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/gradle.properties rename to data-contracts/app-schema-v1/gradle.properties diff --git a/data-contracts/app-schema-v1/settings.gradle.kts b/data-contracts/app-schema-v1/settings.gradle.kts new file mode 100644 index 00000000..67734339 --- /dev/null +++ b/data-contracts/app-schema-v1/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name="app-schema-v1" diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore b/data-contracts/app-schema-v1/src/main/avro/.gitignore similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/src/main/avro/.gitignore rename to data-contracts/app-schema-v1/src/main/avro/.gitignore diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt b/data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt rename to data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt b/data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt rename to data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipConsumer.kt diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt b/data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt rename to data-contracts/app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/kafka/MembershipProducer.kt diff --git a/data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml b/data-contracts/app-schema-v1/src/main/resources/logback.xml similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v1/src/main/resources/logback.xml rename to data-contracts/app-schema-v1/src/main/resources/logback.xml diff --git a/data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts b/data-contracts/app-schema-v2/build.gradle.kts similarity index 90% rename from data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts rename to data-contracts/app-schema-v2/build.gradle.kts index 99c865c6..08d10e66 100644 --- a/data-contracts-with-terraform/producer-app-schema-v2/build.gradle.kts +++ b/data-contracts/app-schema-v2/build.gradle.kts @@ -33,7 +33,7 @@ sourceSets { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") - implementation(project(":data-contracts-with-terraform:shared")) + implementation(project(":data-contracts:shared")) implementation("org.apache.kafka:kafka-clients:${project.property("kafkaVersion")}") implementation("io.confluent:kafka-avro-serializer:${project.property("confluentVersion")}") @@ -52,6 +52,8 @@ kotlin { schemaRegistry { val srProperties = Properties() + // At the moment, this is a file with which we are LOCALLY aware. + // In an ACTUAL CI/CD workflow, this would be externalized, perhaps provided from a base build image or other parameter. srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) url = srProperties.getProperty("schema.registry.url") diff --git a/data-contracts-with-terraform/producer-app-schema-v2/gradle.properties b/data-contracts/app-schema-v2/gradle.properties similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v2/gradle.properties rename to data-contracts/app-schema-v2/gradle.properties diff --git a/data-contracts/app-schema-v2/settings.gradle.kts b/data-contracts/app-schema-v2/settings.gradle.kts new file mode 100644 index 00000000..f3f35313 --- /dev/null +++ b/data-contracts/app-schema-v2/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name="app-schema-v2" diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore b/data-contracts/app-schema-v2/src/main/avro/.gitignore similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v2/src/main/avro/.gitignore rename to data-contracts/app-schema-v2/src/main/avro/.gitignore diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt b/data-contracts/app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt rename to data-contracts/app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/ApplicationV2Main.kt diff --git a/data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt b/data-contracts/app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt similarity index 100% rename from data-contracts-with-terraform/producer-app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt rename to data-contracts/app-schema-v2/src/main/kotlin/io/confluent/devrel/v2/kafka/MembershipConsumer.kt diff --git a/data-contracts-with-terraform/cc/.gitignore b/data-contracts/cc-terraform/.gitignore similarity index 100% rename from data-contracts-with-terraform/cc/.gitignore rename to data-contracts/cc-terraform/.gitignore diff --git a/data-contracts-with-terraform/cc/kafka.tf b/data-contracts/cc-terraform/kafka.tf similarity index 100% rename from data-contracts-with-terraform/cc/kafka.tf rename to data-contracts/cc-terraform/kafka.tf diff --git a/data-contracts-with-terraform/cc/main.tf b/data-contracts/cc-terraform/main.tf similarity index 100% rename from data-contracts-with-terraform/cc/main.tf rename to data-contracts/cc-terraform/main.tf diff --git a/data-contracts-with-terraform/cc/membership.tf b/data-contracts/cc-terraform/membership.tf similarity index 100% rename from data-contracts-with-terraform/cc/membership.tf rename to data-contracts/cc-terraform/membership.tf diff --git a/data-contracts-with-terraform/cc/outputs.tf b/data-contracts/cc-terraform/outputs.tf similarity index 100% rename from data-contracts-with-terraform/cc/outputs.tf rename to data-contracts/cc-terraform/outputs.tf diff --git a/data-contracts-with-terraform/cc/variables.tf b/data-contracts/cc-terraform/variables.tf similarity index 100% rename from data-contracts-with-terraform/cc/variables.tf rename to data-contracts/cc-terraform/variables.tf diff --git a/data-contracts/images/overview.png b/data-contracts/images/overview.png new file mode 100644 index 0000000000000000000000000000000000000000..41843109e1b0dd76458e07cc3d6e791eca7a78dc GIT binary patch literal 140243 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

6gWPB(q=)kRsy1OH;F91wm%! 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

pPsPmv2R^ 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 o&X*8!Eh+zi+TU~xfEo| Date: Thu, 16 Jan 2025 15:17:43 -0500 Subject: [PATCH 07/13] typos and additional instructions. --- data-contracts/README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/data-contracts/README.md b/data-contracts/README.md index 0d9f17b8..0b89f2aa 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -1,8 +1,7 @@ # Managing Data Contracts -Data contracts consists not only of the schemas to define the data, but also rulesets allowing for more fine-grained validations, -controls, and discovery.In this tutorial, we'll evolve a couple of schemas and add data quality and migration rules. We'll also -explore tagging those schemas, fields, and rules for data discovery. +Data contracts consist not only of the schemas to define the structure of events, but also rulesets allowing for more fine-grained validations, +controls, and discovery. In this tutorial, we'll evolve a schemas and add data quality and migration rules. @@ -16,11 +15,11 @@ We will cover these steps in detail. ## Running the Example In this tutorial we'll create Confluent Cloud infrastructure - including a Kafka cluster and Schema Registry. Then we'll create -a Kafka topic named `membership-avro` to store `Membership` events. The Apache Avro schema is maintained an managed in this repo -along with metadata and migration rules about those schemas. +a Kafka topic named `membership-avro` to store `Membership` events. The Apache Avro schema is maintained and managed in this repo +along with metadata and migration rules. We will evolve the `membership` schema, refactoring the events to encapsulate the date-related fields of version 1 into its -own `record` type in version 2. Typically this would be a breaking change. However, data migration rules in the schema registry +own `record` type in version 2. Typically this would be a breaking change. However, [data migration rules](https://docs.confluent.io/cloud/current/sr/fundamentals/data-contracts.html#migration-rules) in the schema registry allow us to perform this schema change without breaking producers or consumers. At the time this is written, this data contract functionality is available to Java, GO, and .NET Confluent client implementations. We'll update this example as other clients evolve. @@ -35,6 +34,14 @@ Here are the tools needed to run this tutorial: * JDK 17 * IDE of choice +> [!Note] When installing and configuring the Confluent CLI, include the Confluent Cloud credentials as environment variables for future use. For instance with bash or zsh, include these export statements: +> +> ```shell +> export CONFLUENT_CLOUD_API_KEY= +> export CONFLUENT_CLOUD_API_SECRET +> ``` +> + ### Executing Terraform To create Confluent Cloud assets, change to the `cc-terraform` subdirectory. We'll step through the commands and what they do. From 6b567bdd0531d7285d368ca318276d77f96a2778 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 09:27:55 -0500 Subject: [PATCH 08/13] mkdir for schema-registry-plugin output. Addressing PR comment: https://github.com/confluentinc/tutorials/pull/80#discussion_r1919165653 --- data-contracts/app-schema-v1/build.gradle.kts | 10 +++++++++- data-contracts/app-schema-v2/build.gradle.kts | 10 +++++++++- data-contracts/schemas/build.gradle.kts | 13 +++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/data-contracts/app-schema-v1/build.gradle.kts b/data-contracts/app-schema-v1/build.gradle.kts index a3a7aa64..2047cfd3 100644 --- a/data-contracts/app-schema-v1/build.gradle.kts +++ b/data-contracts/app-schema-v1/build.gradle.kts @@ -50,11 +50,19 @@ kotlin { jvmToolchain(17) } +val schemaRegOutputDir = "${project.projectDir.absolutePath}/build/schema-registry-plugin" + +tasks.downloadSchemasTask { + doFirst { + mkdir(schemaRegOutputDir) + } +} + schemaRegistry { val srProperties = Properties() // At the moment, this is a file with which we are LOCALLY aware. // In an ACTUAL CI/CD workflow, this would be externalized, perhaps provided from a base build image or other parameter. - srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + srProperties.load(FileInputStream(File("${project.projectDir.absolutePath}/../shared/src/main/resources/confluent.properties"))) url = srProperties.getProperty("schema.registry.url") diff --git a/data-contracts/app-schema-v2/build.gradle.kts b/data-contracts/app-schema-v2/build.gradle.kts index 08d10e66..a07aa387 100644 --- a/data-contracts/app-schema-v2/build.gradle.kts +++ b/data-contracts/app-schema-v2/build.gradle.kts @@ -50,11 +50,19 @@ kotlin { jvmToolchain(17) } +val schemaRegOutputDir = "${project.projectDir.absolutePath}/build/schema-registry-plugin" + +tasks.downloadSchemasTask { + doFirst { + mkdir(schemaRegOutputDir) + } +} + schemaRegistry { val srProperties = Properties() // At the moment, this is a file with which we are LOCALLY aware. // In an ACTUAL CI/CD workflow, this would be externalized, perhaps provided from a base build image or other parameter. - srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + srProperties.load(FileInputStream(File("${project.projectDir.absolutePath}/../shared/src/main/resources/confluent.properties"))) url = srProperties.getProperty("schema.registry.url") diff --git a/data-contracts/schemas/build.gradle.kts b/data-contracts/schemas/build.gradle.kts index ddadd6a1..391c7b19 100644 --- a/data-contracts/schemas/build.gradle.kts +++ b/data-contracts/schemas/build.gradle.kts @@ -25,11 +25,19 @@ repositories { maven("https://jitpack.io") } +val schemaRegOutputDir = "${project.projectDir.absolutePath}/build/schema-registry-plugin" + +tasks.registerSchemasTask { + doFirst { + mkdir(schemaRegOutputDir) + } +} + schemaRegistry { val srProperties = Properties() // At the moment, this is a file with which we are LOCALLY aware. // In an ACTUAL CI/CD workflow, this would be externalized, perhaps provided from a base build image or other parameter. - srProperties.load(FileInputStream(File(project.projectDir.absolutePath + "/../shared/src/main/resources/confluent.properties"))) + srProperties.load(FileInputStream(File("${project.projectDir.absolutePath}/../shared/src/main/resources/confluent.properties"))) url = srProperties.getProperty("schema.registry.url") @@ -38,7 +46,8 @@ schemaRegistry { username = srCredTokens[0] password = srCredTokens[1] } - outputDirectory = "${System.getProperty("user.home")}/tmp/schema-registry-plugin" + + outputDirectory = schemaRegOutputDir pretty = true val baseBuildDir = "${project.projectDir}/src/main" From 478cca1d3af15614d475b806e8773c8a5eb5220c Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 09:29:30 -0500 Subject: [PATCH 09/13] Adding instructions to clone repo. Addresses PR comment: https://github.com/confluentinc/tutorials/pull/80#discussion_r1919165653 --- data-contracts/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data-contracts/README.md b/data-contracts/README.md index 0b89f2aa..c01f905d 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -26,6 +26,13 @@ At the time this is written, this data contract functionality is available to Ja ### Prerequisites +Clone the `confluentinc/tutorials` GitHub repository (if you haven't already) and navigate to the `tutorials` directory: + +```shell +git clone git@github.com:confluentinc/tutorials.git +cd tutorials +``` + Here are the tools needed to run this tutorial: * [Confluent Cloud](http://confluent.cloud) * [Confluent CLI](https://docs.confluent.io/confluent-cli/current/install.html) From 650195ed4b20b2c9df1eecafac242c5374128a57 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 09:34:25 -0500 Subject: [PATCH 10/13] Correction for terraform output command. Addressing https://github.com/confluentinc/tutorials/pull/80#discussion_r1919169371 --- data-contracts/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data-contracts/README.md b/data-contracts/README.md index c01f905d..2812c8ce 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -116,9 +116,9 @@ reformat the names of those properties into the names used in Kafka Client confi in our project: ```shell -terraform output -json | \ - jq -r 'to_entries | map( {key: .key|tostring|split("_")|join("."), value: .value} ) | map("\(.key)=\(.value.value)")' | while read -r line ; do echo "$line"; \ - done > ../shared/src/main/resources/confluent.properties +terraform output -json \ + | jq -r 'to_entries | map( {key: .key|tostring|split("_")|join("."), value: .value} ) | map("\(.key)=\(.value.value)") | .[]' \ + | while read -r line ; do echo "$line"; done > ../shared/src/main/resources/confluent.properties ``` All Kafka Client code in this project loads connection properties form `shared/src/main/resources/confluent.properties`. For an example of this From 9257bd3a9e7b8dd3f60f04325fac25d13ecd0831 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 10:37:18 -0500 Subject: [PATCH 11/13] Removed redundant registerSchemasTask command. --- data-contracts/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/data-contracts/README.md b/data-contracts/README.md index 2812c8ce..ac818c06 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -381,13 +381,7 @@ schemaRegistry{ } ``` -Register the new schema using our gradle plugin: - -```shell -./gradlew :data-contracts:schemas:registerSchemasTask -``` - -Download the schema to the version 2 application and generate the Java classes for this schema: +Download the version 2 schema to the version 2 application and generate the Java classes for this schema: ```shell ./gradlew :data-contracts:app-schema-v2:generateCode From 7ad071b5f0c9d8f000a46b9147ae2998498c1d77 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 10:43:57 -0500 Subject: [PATCH 12/13] Shortening README by removing some code samples. --- data-contracts/README.md | 66 +--------------------------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/data-contracts/README.md b/data-contracts/README.md index ac818c06..426a4503 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -274,67 +274,12 @@ class MembershipConsumer: BaseConsumer(mapOf( The `consumeRecord` function is where we would typically start the business logic of actually "consuming" Kafka events. This implementation simply logs the consumed records to the provided `Logger` instance in the superclass. -To exercise these producer and consumer implementations, we created a `main` function in the `ApplicationMain` class to start a consumer instance and a producer instance periodically send random events to the `membership-avro` topic. - -

- Main function - -```kotlin -@JvmStatic -fun main(args: Array) { - runBlocking { - println("Starting application main...") - println(args.joinToString(" ")) - - val messageInterval = 1.toDuration(DurationUnit.SECONDS) - val sendDuration = 100.toDuration(DurationUnit.SECONDS) - - val producer = MembershipProducer() - val consumer = MembershipConsumer() - - // start a thread with a consumer instance - thread { - consumer.start(listOf("membership-avro")) - } - - // every 1 second for the next 100 seconds, send a randomly-generated event to the kafka topic - coroutineScope { - launch { - val until = Clock.System.now().plus(sendDuration) - while(Clock.System.now().compareTo(until) < 0) { - val userId = UUID.randomUUID().toString() - val membership = Membership.newBuilder() - .setUserId(userId) - .setStartDate(LocalDate.now().minusDays(Random.nextLong(100, 1000))) - .setEndDate(LocalDate.now().plusWeeks(Random.nextLong(1, 52))) - .build() - producer.send("membership-avro", userId, membership) - delay(messageInterval.inWholeSeconds) - } - } - } - producer.close() - } -} -``` - -
- -Running this application will print the events being consumed from Kafka: - -
- Console Output +To exercise these producer and consumer implementations, we created a `main` function in the `ApplicationMain` class to start a consumer instance and a producer instance periodically send random events to the `membership-avro` topic. Running [this application](./app-schema-v1/src/main/kotlin/io/confluent/devrel/dc/v1/ApplicationMain.kt) will print the events being consumed from Kafka: ```shell [Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 8e65d8e2-4ad8-475f-8f74-d724865ddbd8, {"user_id": "8e65d8e2-4ad8-475f-8f74-d724865ddbd8", "start_date": "2022-06-13", "end_date": "2025-03-25"} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 22781e70-8d2c-4d02-b325-9cdc8663ed19, {"user_id": "22781e70-8d2c-4d02-b325-9cdc8663ed19", "start_date": "2022-06-10", "end_date": "2025-11-11"} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 06ec5d95-d38c-4bac-9693-a01596eeedd7, {"user_id": "06ec5d95-d38c-4bac-9693-a01596eeedd7", "start_date": "2024-03-25", "end_date": "2025-02-25"} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 881bf838-1fe1-423e-9afc-0742e3080b5b, {"user_id": "881bf838-1fe1-423e-9afc-0742e3080b5b", "start_date": "2022-07-25", "end_date": "2025-07-08"} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - Received Membership 73254321-72f9-4783-949f-652f5ca9542e, {"user_id": "73254321-72f9-4783-949f-652f5ca9542e", "start_date": "2022-05-11", "end_date": "2025-03-11"} ``` -
- #### Evolving to Version 2 The decision is made to refactor the `membership` schema to encapsulate the date fields into a type - `ValidityPeriod` - which can be reused in other event types. @@ -410,19 +355,10 @@ in the `use.latest.with.metadata` configuration for the deserializer. Running the `main` function in `ApplicationV2Main` will consume the events from the `membership-avro` topic, but with a noticeable difference from the `app-schama-v1` application: -
- Console output version 2 - ```shell [Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership af39d3a4-53b3-4d27-9bf0-3528965b6149, {"user_id": "af39d3a4-53b3-4d27-9bf0-3528965b6149", "validity_period": {"from": "2022-10-29", "to": "2025-08-28"}} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 4baddf48-6dda-40c0-8c4d-57222932c0b8, {"user_id": "4baddf48-6dda-40c0-8c4d-57222932c0b8", "validity_period": {"from": "2024-07-13", "to": "2025-08-14"}} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 6c385e77-4e50-4784-bf35-2e0be4aca6a6, {"user_id": "6c385e77-4e50-4784-bf35-2e0be4aca6a6", "validity_period": {"from": "2023-10-17", "to": "2025-03-13"}} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 79d36f24-4317-4f88-acb2-8f48ff88991b, {"user_id": "79d36f24-4317-4f88-acb2-8f48ff88991b", "validity_period": {"from": "2022-09-13", "to": "2025-09-25"}} -[Thread-0] INFO io.confluent.devrel.datacontracts.shared.BaseConsumer - v2 - Received Membership 2167c361-2202-41ce-b48e-35967b3fb8c7, {"user_id": "2167c361-2202-41ce-b48e-35967b3fb8c7", "validity_period": {"from": "2023-05-01", "to": "2025-05-01"}} ``` -
- We're consuming the same events, but the deserialized with version 2 of the schema. ## Teardown From c5b7a6d0ffeaa0b7f29345a5abcdd0acf74c7336 Mon Sep 17 00:00:00 2001 From: Sandon Jacobs Date: Fri, 17 Jan 2025 10:46:41 -0500 Subject: [PATCH 13/13] title change. --- data-contracts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data-contracts/README.md b/data-contracts/README.md index 426a4503..00bf0f64 100644 --- a/data-contracts/README.md +++ b/data-contracts/README.md @@ -1,4 +1,4 @@ -# Managing Data Contracts +# Managing Data Contracts in Confluent Cloud Data contracts consist not only of the schemas to define the structure of events, but also rulesets allowing for more fine-grained validations, controls, and discovery. In this tutorial, we'll evolve a schemas and add data quality and migration rules.